Files
prowler/ui/lib/lighthouse/tools/meta-tool.ts
Chandrapal Badshah b9bfdc1a5a feat: Integrate Prowler MCP to Lighthouse AI (#9255)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-12-17 10:10:43 +01:00

205 lines
5.7 KiB
TypeScript

import "server-only";
import type { StructuredTool } from "@langchain/core/tools";
import { tool } from "@langchain/core/tools";
import { addBreadcrumb, captureException } from "@sentry/nextjs";
import { z } from "zod";
import { getMCPTools, isMCPAvailable } from "@/lib/lighthouse/mcp-client";
/** Input type for describe_tool */
interface DescribeToolInput {
toolName: string;
}
/** Input type for execute_tool */
interface ExecuteToolInput {
toolName: string;
toolInput: Record<string, unknown>;
}
/**
* Get all available tools (MCP only)
*/
function getAllTools(): StructuredTool[] {
if (!isMCPAvailable()) {
return [];
}
return getMCPTools();
}
/**
* Describe a tool by getting its full schema
*/
export const describeTool = tool(
async ({ toolName }: DescribeToolInput) => {
const allTools = getAllTools();
if (allTools.length === 0) {
addBreadcrumb({
category: "meta-tool",
message: "describe_tool called but no tools available",
level: "warning",
data: { toolName },
});
return {
found: false,
message: "No tools available. MCP server may not be connected.",
};
}
// Find exact tool by name
const targetTool = allTools.find((t) => t.name === toolName);
if (!targetTool) {
addBreadcrumb({
category: "meta-tool",
message: `Tool not found: ${toolName}`,
level: "info",
data: { toolName, availableCount: allTools.length },
});
return {
found: false,
message: `Tool '${toolName}' not found.`,
hint: "Check the tool list in the system prompt for exact tool names.",
availableToolsCount: allTools.length,
};
}
return {
found: true,
name: targetTool.name,
description: targetTool.description || "No description available",
schema: targetTool.schema
? JSON.stringify(targetTool.schema, null, 2)
: "{}",
message: "Tool schema retrieved. Use execute_tool to run it.",
};
},
{
name: "describe_tool",
description: `Get the full schema and parameter details for a specific Prowler Hub tool.
Use this to understand what parameters a tool requires before executing it.
Tool names are listed in your system prompt - use the exact name.
You must always provide the toolName key in the JSON object.
Example: describe_tool({ "toolName": "prowler_hub_list_providers" })
Returns:
- Full parameter schema with types and descriptions
- Tool description
- Required vs optional parameters`,
schema: z.object({
toolName: z
.string()
.describe(
"Exact name of the tool to describe (e.g., 'prowler_hub_list_providers'). You must always provide the toolName key in the JSON object.",
),
}),
},
);
/**
* Execute a tool with parameters
*/
export const executeTool = tool(
async ({ toolName, toolInput }: ExecuteToolInput) => {
const allTools = getAllTools();
const targetTool = allTools.find((t) => t.name === toolName);
if (!targetTool) {
addBreadcrumb({
category: "meta-tool",
message: `execute_tool: Tool not found: ${toolName}`,
level: "warning",
data: { toolName, toolInput },
});
return {
error: `Tool '${toolName}' not found. Use describe_tool to check available tools.`,
suggestion:
"Check the tool list in your system prompt for exact tool names. You must always provide the toolName key in the JSON object.",
};
}
try {
// Use empty object for empty inputs, otherwise use the provided input
const input =
!toolInput || Object.keys(toolInput).length === 0 ? {} : toolInput;
addBreadcrumb({
category: "meta-tool",
message: `Executing tool: ${toolName}`,
level: "info",
data: { toolName, hasInput: !!input },
});
// Execute the tool directly - let errors propagate so LLM can handle retries
const result = await targetTool.invoke(input);
return {
success: true,
toolName,
result,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
captureException(error, {
tags: {
component: "meta-tool",
tool_name: toolName,
error_type: "tool_execution_failed",
},
level: "error",
contexts: {
tool_execution: {
tool_name: toolName,
tool_input: JSON.stringify(toolInput),
},
},
});
return {
error: `Failed to execute '${toolName}': ${errorMessage}`,
toolName,
toolInput,
};
}
},
{
name: "execute_tool",
description: `Execute a Prowler Hub MCP tool with the specified parameters.
Provide the exact tool name and its input parameters as specified in the tool's schema.
You must always provide the toolName and toolInput keys in the JSON object.
Example: execute_tool({ "toolName": "prowler_hub_list_providers", "toolInput": {} })
All input to the tool must be provided in the toolInput key as a JSON object.
Example: execute_tool({ "toolName": "prowler_hub_list_providers", "toolInput": { "query": "value1", "page": 1, "pageSize": 10 } })
Always describe the tool first to understand:
1. What parameters it requires
2. The expected input format
3. Required vs optional parameters`,
schema: z.object({
toolName: z
.string()
.describe(
"Exact name of the tool to execute (from system prompt tool list)",
),
toolInput: z
.record(z.string(), z.unknown())
.default({})
.describe(
"Input parameters for the tool as a JSON object. Use empty object {} if tool requires no parameters.",
),
}),
},
);