|
| 1 | +/** |
| 2 | + * MCP Tool for MyCoder Agent |
| 3 | + * |
| 4 | + * This tool allows the agent to interact with Model Context Protocol (MCP) servers |
| 5 | + * to retrieve resources and use tools provided by those servers. |
| 6 | + * |
| 7 | + * Uses the official MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk |
| 8 | + */ |
| 9 | + |
| 10 | +import { z } from 'zod'; |
| 11 | +import { zodToJsonSchema } from 'zod-to-json-schema'; |
| 12 | + |
| 13 | +import { McpConfig } from '../core/mcp/index.js'; |
| 14 | +import { Tool } from '../core/types.js'; |
| 15 | + |
| 16 | +// Parameters for listResources method |
| 17 | +const listResourcesSchema = z.object({ |
| 18 | + server: z |
| 19 | + .string() |
| 20 | + .optional() |
| 21 | + .describe('Optional server name to filter resources by'), |
| 22 | +}); |
| 23 | + |
| 24 | +// Parameters for getResource method |
| 25 | +const getResourceSchema = z.object({ |
| 26 | + uri: z |
| 27 | + .string() |
| 28 | + .describe('URI of the resource to fetch in the format "scheme://path"'), |
| 29 | +}); |
| 30 | + |
| 31 | +// Return type for listResources |
| 32 | +const listResourcesReturnSchema = z.array( |
| 33 | + z.object({ |
| 34 | + uri: z.string(), |
| 35 | + metadata: z.record(z.unknown()).optional(), |
| 36 | + }), |
| 37 | +); |
| 38 | + |
| 39 | +// Return type for getResource |
| 40 | +const getResourceReturnSchema = z.string(); |
| 41 | + |
| 42 | +// Map to store MCP clients |
| 43 | +const mcpClients = new Map<string, any>(); |
| 44 | + |
| 45 | +/** |
| 46 | + * Create a new MCP tool with the specified configuration |
| 47 | + * @param config MCP configuration |
| 48 | + * @returns The MCP tool |
| 49 | + */ |
| 50 | +export function createMcpTool(config: McpConfig): Tool { |
| 51 | + // We'll import the MCP SDK dynamically to avoid TypeScript errors |
| 52 | + // This is a temporary solution until we can properly add type declarations |
| 53 | + const mcpSdk = require('@modelcontextprotocol/sdk'); |
| 54 | + |
| 55 | + // Initialize MCP clients for each configured server |
| 56 | + mcpClients.clear(); |
| 57 | + |
| 58 | + if (config.servers && config.servers.length > 0) { |
| 59 | + for (const server of config.servers) { |
| 60 | + try { |
| 61 | + let clientOptions: any = { |
| 62 | + baseURL: server.url, |
| 63 | + }; |
| 64 | + |
| 65 | + // Add authentication if configured |
| 66 | + if (server.auth && server.auth.type === 'bearer') { |
| 67 | + clientOptions = { |
| 68 | + ...clientOptions, |
| 69 | + headers: { |
| 70 | + Authorization: `Bearer ${server.auth.token}`, |
| 71 | + }, |
| 72 | + }; |
| 73 | + } |
| 74 | + |
| 75 | + const client = new mcpSdk.Client(clientOptions); |
| 76 | + mcpClients.set(server.name, client); |
| 77 | + } catch (error) { |
| 78 | + console.error( |
| 79 | + `Failed to initialize MCP client for server ${server.name}:`, |
| 80 | + error, |
| 81 | + ); |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + // Define the MCP tool |
| 87 | + return { |
| 88 | + name: 'mcp', |
| 89 | + description: |
| 90 | + 'Interact with Model Context Protocol (MCP) servers to retrieve resources', |
| 91 | + parameters: z.discriminatedUnion('method', [ |
| 92 | + z.object({ |
| 93 | + method: z.literal('listResources'), |
| 94 | + params: listResourcesSchema.optional(), |
| 95 | + }), |
| 96 | + z.object({ |
| 97 | + method: z.literal('getResource'), |
| 98 | + params: getResourceSchema, |
| 99 | + }), |
| 100 | + ]), |
| 101 | + parametersJsonSchema: zodToJsonSchema( |
| 102 | + z.discriminatedUnion('method', [ |
| 103 | + z.object({ |
| 104 | + method: z.literal('listResources'), |
| 105 | + params: listResourcesSchema.optional(), |
| 106 | + }), |
| 107 | + z.object({ |
| 108 | + method: z.literal('getResource'), |
| 109 | + params: getResourceSchema, |
| 110 | + }), |
| 111 | + ]), |
| 112 | + ), |
| 113 | + returns: z.union([listResourcesReturnSchema, getResourceReturnSchema]), |
| 114 | + returnsJsonSchema: zodToJsonSchema( |
| 115 | + z.union([listResourcesReturnSchema, getResourceReturnSchema]), |
| 116 | + ), |
| 117 | + |
| 118 | + execute: async ({ method, params }, { logger }) => { |
| 119 | + // Extract the server name from a resource URI |
| 120 | + function getServerNameFromUri(uri: string): string | undefined { |
| 121 | + const match = uri.match(/^([^:]+):\/\//); |
| 122 | + return match ? match[1] : undefined; |
| 123 | + } |
| 124 | + |
| 125 | + if (method === 'listResources') { |
| 126 | + // List available resources from MCP servers |
| 127 | + const resources: any[] = []; |
| 128 | + const serverFilter = params?.server; |
| 129 | + |
| 130 | + // If a specific server is requested, only check that server |
| 131 | + if (serverFilter) { |
| 132 | + const client = mcpClients.get(serverFilter); |
| 133 | + if (client) { |
| 134 | + try { |
| 135 | + logger.verbose(`Fetching resources from server: ${serverFilter}`); |
| 136 | + const serverResources = await client.resources(); |
| 137 | + resources.push(...(serverResources as any[])); |
| 138 | + } catch (error) { |
| 139 | + logger.error( |
| 140 | + `Failed to fetch resources from server ${serverFilter}:`, |
| 141 | + error, |
| 142 | + ); |
| 143 | + } |
| 144 | + } else { |
| 145 | + logger.warn(`Server not found: ${serverFilter}`); |
| 146 | + } |
| 147 | + } else { |
| 148 | + // Otherwise, check all servers |
| 149 | + for (const [serverName, client] of mcpClients.entries()) { |
| 150 | + try { |
| 151 | + logger.verbose(`Fetching resources from server: ${serverName}`); |
| 152 | + const serverResources = await client.resources(); |
| 153 | + resources.push(...(serverResources as any[])); |
| 154 | + } catch (error) { |
| 155 | + logger.error( |
| 156 | + `Failed to fetch resources from server ${serverName}:`, |
| 157 | + error, |
| 158 | + ); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + return resources; |
| 164 | + } else if (method === 'getResource') { |
| 165 | + // Fetch a resource from an MCP server |
| 166 | + const uri = params.uri; |
| 167 | + |
| 168 | + // Parse the URI to determine which server to use |
| 169 | + const serverName = getServerNameFromUri(uri); |
| 170 | + if (!serverName) { |
| 171 | + throw new Error(`Could not determine server from URI: ${uri}`); |
| 172 | + } |
| 173 | + |
| 174 | + const client = mcpClients.get(serverName); |
| 175 | + if (!client) { |
| 176 | + throw new Error(`Server not found: ${serverName}`); |
| 177 | + } |
| 178 | + |
| 179 | + // Use the MCP SDK to fetch the resource |
| 180 | + logger.verbose(`Fetching resource: ${uri}`); |
| 181 | + const resource = await client.resource(uri); |
| 182 | + return resource.content; |
| 183 | + } |
| 184 | + |
| 185 | + throw new Error(`Unknown method: ${method}`); |
| 186 | + }, |
| 187 | + |
| 188 | + logParameters: (params, { logger }) => { |
| 189 | + if (params.method === 'listResources') { |
| 190 | + logger.verbose( |
| 191 | + `Listing MCP resources${params.params?.server ? ` from server: ${params.params.server}` : ''}`, |
| 192 | + ); |
| 193 | + } else if (params.method === 'getResource') { |
| 194 | + logger.verbose(`Fetching MCP resource: ${params.params.uri}`); |
| 195 | + } |
| 196 | + }, |
| 197 | + |
| 198 | + logReturns: (result, { logger }) => { |
| 199 | + if (Array.isArray(result)) { |
| 200 | + logger.verbose(`Found ${result.length} MCP resources`); |
| 201 | + } else { |
| 202 | + logger.verbose( |
| 203 | + `Retrieved MCP resource content (${result.length} characters)`, |
| 204 | + ); |
| 205 | + } |
| 206 | + }, |
| 207 | + }; |
| 208 | +} |
0 commit comments