import type { McpServerConfig } from "../../config/schemas/mcp.js"; import type { McpToolAnnotations } from "../../mcp/client.js"; import type { McpServerManager } from "../../mcp/manager.js"; import { RiskLevel } from "../../permissions/types.js"; import { toProviderSafeToolName } from "../provider-tool-name.js"; import { schemaDefinesProperty } from "../schema-transforms.js"; import type { Tool, ToolContext, ToolExecutionResult } from "../types.js"; const riskMap: Record = { low: RiskLevel.Low, medium: RiskLevel.Medium, high: RiskLevel.High, }; /** * Create a namespaced tool name to prevent collisions across MCP servers * and with core/skill tools. */ function mcpToolName(serverId: string, toolName: string): string { return toProviderSafeToolName(`mcp__${serverId}__${toolName}`); } export interface McpToolMetadata { name: string; description: string; inputSchema: Record; annotations?: McpToolAnnotations; } /** * Resolve the risk level a tool carries. * * `readOnlyHint` is self-reported by the server, so it only refines risk * downward and only for servers the user has already configured below the * high-trust ceiling. Servers left at the default "high" are unaffected, and * the hint never raises risk above the server default. */ function resolveRiskLevel( metadata: McpToolMetadata, serverConfig: McpServerConfig, ): RiskLevel { const serverRisk = riskMap[serverConfig.defaultRiskLevel] ?? RiskLevel.High; if ( metadata.annotations?.readOnlyHint === true && serverRisk !== RiskLevel.High ) { return RiskLevel.Low; } return serverRisk; } /** * Create a Tool object from MCP tool metadata. * The tool delegates execution to the McpServerManager. */ export function createMcpTool( metadata: McpToolMetadata, serverId: string, serverConfig: McpServerConfig, manager: McpServerManager, ): Tool { const namespacedName = mcpToolName(serverId, metadata.name); const riskLevel = resolveRiskLevel(metadata, serverConfig); const serverDefinesActivity = schemaDefinesProperty( metadata.inputSchema, "activity", { refBehavior: "assume-defined" }, ); return { name: namespacedName, description: metadata.description, category: "mcp", defaultRiskLevel: riskLevel, executionTarget: "host", input_schema: metadata.inputSchema as object, async execute( input: Record, context: ToolContext, ): Promise { try { // Strip injected activity before sending to MCP server const { activity: _activity, ...mcpInput } = input as Record< string, unknown > & { activity?: unknown; }; const forwardInput = serverDefinesActivity ? input : mcpInput; const result = await manager.callTool( serverId, metadata.name, forwardInput, context.signal, ); return { content: result.content, isError: result.isError, }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { content: `MCP tool execution failed: ${message}`, isError: true, }; } }, }; } /** * Create Tool objects from all tools provided by an MCP server. */ export function createMcpToolsFromServer( tools: McpToolMetadata[], serverId: string, serverConfig: McpServerConfig, manager: McpServerManager, ): Tool[] { return tools.map((tool) => createMcpTool(tool, serverId, serverConfig, manager), ); }