/** * MCP Client. * * Handles connection initialization, tool listing, and tool calling. */ import { withTimeout } from "@oh-my-pi/pi-utils"; import { createHttpTransport } from "./transports/http"; import { createStdioTransport } from "./transports/stdio"; import type { MCPHttpServerConfig, MCPInitializeParams, MCPInitializeResult, MCPRequestOptions, MCPServerCapabilities, MCPServerConfig, MCPServerConnection, MCPSseServerConfig, MCPStdioServerConfig, MCPToolCallParams, MCPToolCallResult, MCPToolDefinition, MCPToolsListResult, MCPTransport, } from "./types"; /** MCP protocol version we support */ const PROTOCOL_VERSION = "2025-03-26"; /** Default connection timeout in ms */ const CONNECTION_TIMEOUT_MS = 30_000; /** Client info sent during initialization */ const CLIENT_INFO = { name: "omp-coding-agent", version: "1.0.0", }; /** * Create a transport for the given server config. */ async function createTransport(config: MCPServerConfig): Promise { const serverType = config.type ?? "stdio"; switch (serverType) { case "stdio": return createStdioTransport(config as MCPStdioServerConfig); case "http": case "sse": return createHttpTransport(config as MCPHttpServerConfig | MCPSseServerConfig); default: throw new Error(`Unknown server type: ${serverType}`); } } /** * Initialize connection with MCP server. */ async function initializeConnection( transport: MCPTransport, options?: { signal?: AbortSignal }, ): Promise { const params: MCPInitializeParams = { protocolVersion: PROTOCOL_VERSION, capabilities: { roots: { listChanged: false }, }, clientInfo: CLIENT_INFO, }; const result = await transport.request( "initialize", params as unknown as Record, options, ); if (options?.signal?.aborted) { throw options.signal.reason instanceof Error ? options.signal.reason : new Error("Aborted"); } // Send initialized notification await transport.notify("notifications/initialized"); return result; } /** * Connect to an MCP server. * Has a 30 second timeout to prevent blocking startup. */ export async function connectToServer( name: string, config: MCPServerConfig, options?: { signal?: AbortSignal }, ): Promise { const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS; const connect = async (): Promise => { const transport = await createTransport(config); try { const initResult = await initializeConnection(transport, options); return { name, config, transport, serverInfo: initResult.serverInfo, capabilities: initResult.capabilities, }; } catch (error) { await transport.close(); throw error; } }; return withTimeout( connect(), timeoutMs, `Connection to MCP server "${name}" timed out after ${timeoutMs}ms`, options?.signal, ); } /** * List tools from a connected server. */ export async function listTools( connection: MCPServerConnection, options?: { signal?: AbortSignal }, ): Promise { // Check if server supports tools if (!connection.capabilities.tools) { return []; } // Return cached tools if available if (connection.tools) { return connection.tools; } const allTools: MCPToolDefinition[] = []; let cursor: string | undefined; do { const params: Record = {}; if (cursor) { params.cursor = cursor; } const result = await connection.transport.request("tools/list", params, options); allTools.push(...result.tools); cursor = result.nextCursor; } while (cursor); // Cache tools connection.tools = allTools; return allTools; } /** * Call a tool on a connected server. */ export async function callTool( connection: MCPServerConnection, toolName: string, args: Record = {}, options?: MCPRequestOptions, ): Promise { const params: MCPToolCallParams = { name: toolName, arguments: args, }; return connection.transport.request( "tools/call", params as unknown as Record, options, ); } /** * Disconnect from a server. */ export async function disconnectServer(connection: MCPServerConnection): Promise { await connection.transport.close(); } /** * Check if a server supports tools. */ export function serverSupportsTools(capabilities: MCPServerCapabilities): boolean { return capabilities.tools !== undefined; }