/** * Tool name namespacing + ToolContract adapter. * * Namespaces each MCP tool as __ (double underscore). * Adapts an MCP tool definition into a Dispatch ToolContract whose execute() * proxies to the MCP server via the injected McpToolCaller (client.callTool()). * * PURE: depends only on the McpToolCaller port — never on the concrete client. */ import type { ToolContract, ToolExecuteContext, ToolParameterSchema, ToolResult, } from "@dispatch/kernel"; import type { McpContentItem, McpToolCaller, McpToolInfo } from "./types.js"; const NAMESPACE_SEP = "__"; export function namespace(serverId: string, toolName: string): string { return `${serverId}${NAMESPACE_SEP}${toolName}`; } export function adaptTool( serverId: string, mcpTool: McpToolInfo, caller: McpToolCaller, ): ToolContract { const parameters: ToolParameterSchema = { type: "object", ...(mcpTool.inputSchema.properties !== undefined && { properties: mcpTool.inputSchema.properties, }), ...(mcpTool.inputSchema.required !== undefined && { required: mcpTool.inputSchema.required, }), ...(mcpTool.inputSchema.additionalProperties !== undefined && { additionalProperties: mcpTool.inputSchema.additionalProperties, }), }; return { name: namespace(serverId, mcpTool.name), description: `[${serverId}] ${mcpTool.description}`, parameters, concurrencySafe: false, execute: async (args: unknown, ctx: ToolExecuteContext): Promise => { const result = await caller.callTool(mcpTool.name, args, ctx.signal); const toolResult: ToolResult = { content: flattenContent(result.content), }; if (result.isError !== undefined) { (toolResult as { isError?: boolean }).isError = result.isError; } return toolResult; }, }; } export function flattenContent(content: readonly McpContentItem[]): string { if (content.length === 0) return ""; const parts: string[] = []; for (const item of content) { if (item.type === "text" && item.text !== undefined) { parts.push(item.text); } else if (item.type === "image" && item.mimeType !== undefined) { parts.push(`[image: ${item.mimeType}]`); } else if (item.type === "resource" && item.resource !== undefined) { if (item.resource.text !== undefined) { parts.push(item.resource.text); } else { parts.push(`[resource: ${item.resource.uri}]`); } } } return parts.join("\n"); }