/** * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { CallToolResult, Tool, } from '@modelcontextprotocol/sdk/types.js' with { 'resolution-mode': 'import' }; import { JSONSchema7, tool as genkitTool, z, type Genkit, type MultipartToolAction, type Part, type ToolAction, } from 'genkit'; import { logger } from 'genkit/logging'; const toText = (c: CallToolResult['content']) => c.map((p) => (p.type === 'text' ? p.text : '')).join(''); function processResult(result: CallToolResult) { if (result.isError) return { error: toText(result.content) }; if (result.structuredContent !== undefined) { return result.structuredContent; } if (result.content.every((c) => c.type === 'text' && !!c.text)) { const text = toText(result.content); if (text.trim().startsWith('{') || text.trim().startsWith('[')) { try { return JSON.parse(text); } catch (e) { return text; } } return text; } if (result.content.length === 1) return result.content[0]; return result; } function processMultipartResult(result: CallToolResult) { if (result.isError) { return { output: { error: toText(result.content) }, metadata: result._meta, }; } const content: Part[] = []; let textOutput = ''; for (const c of result.content) { if (c.type === 'text') { if (c.text) { textOutput += c.text; } } else if (c.type === 'image' || c.type === 'audio') { if (c.data) { content.push({ media: { url: `data:${c.mimeType};base64,${c.data}`, contentType: c.mimeType, }, }); } } else if (c.type === 'resource_link') { if (c.uri) { content.push({ resource: { uri: c.uri, }, }); } } else if (c.type === 'resource') { if (c.resource) { if ('text' in c.resource && c.resource.text) { textOutput += (textOutput ? '\n\n' : '') + `Resource (${c.resource.uri}):\n${c.resource.text}`; } else if ('blob' in c.resource && c.resource.blob) { content.push({ media: { url: `data:${c.resource.mimeType};base64,${c.resource.blob}`, contentType: c.resource.mimeType, }, }); } } } } let output: unknown = result.structuredContent; if (output === undefined && textOutput) { if ( textOutput.trim().startsWith('{') || textOutput.trim().startsWith('[') ) { try { output = JSON.parse(textOutput); } catch (e) { output = textOutput; } } else { output = textOutput; } } return { ...(output !== undefined ? { output } : {}), ...(content.length > 0 ? { content } : {}), metadata: result._meta, }; } /** * Registers a single MCP tool as a Genkit tool. * It defines a new Genkit tool action that, when called, will * interact with the MCP client to execute the corresponding MCP tool. * * @param ai The Genkit instance to define the tool on. * @param client The MCP client instance used to interact with the MCP server. * @param tool The MCP Tool object to register. * @param params Contains the Genkit client name, MCP server name for namespacing, * and a flag for raw tool responses. */ function registerTool( ai: Genkit, client: Client, tool: Tool, params: { serverName: string; name: string; rawToolResponses?: boolean; multipart?: boolean; } ) { logger.debug( `[MCP] Registering tool '${params.name}/${tool.name}' from server '${params.serverName}'` ); if (params.multipart && params.rawToolResponses) { logger.warn( `[MCP] Tool '${params.serverName}/${tool.name}' is configured with both multipart and rawToolResponses. Genkit will return the raw MCP CallToolResult in the output field, and media parts will not be natively parsed.` ); } if (params.multipart) { ai.defineTool( { name: `${params.serverName}/${tool.name}`, description: tool.description || '', inputJsonSchema: tool.inputSchema as JSONSchema7, outputSchema: z.any(), metadata: { mcp: { _meta: tool._meta || {} } }, multipart: true as const, }, async (args, { context }) => { logger.debug( `[MCP] Calling MCP tool '${params.serverName}/${tool.name}' with arguments`, JSON.stringify(args) ); const result = await client.callTool({ name: tool.name, arguments: args, _meta: context?.mcp?._meta, }); if (params.rawToolResponses) return { output: result }; return processMultipartResult(result as CallToolResult); } ); } else { ai.defineTool( { name: `${params.serverName}/${tool.name}`, description: tool.description || '', inputJsonSchema: tool.inputSchema as JSONSchema7, outputSchema: z.any(), metadata: { mcp: { _meta: tool._meta || {} } }, }, async (args, { context }) => { logger.debug( `[MCP] Calling MCP tool '${params.serverName}/${tool.name}' with arguments`, JSON.stringify(args) ); const result = await client.callTool({ name: tool.name, arguments: args, _meta: context?.mcp?._meta, }); if (params.rawToolResponses) return result; return processResult(result as CallToolResult); } ); } } /** * Creates a Genkit dynamic tool action for a given MCP tool. * This is similar to `registerTool` but returns the `ToolAction` directly * instead of defining it on the Genkit instance. * * @param ai The Genkit instance, used for creating the dynamic tool. * @param client The MCP client instance. * @param tool The MCP Tool object. * @param params Configuration parameters including namespacing and raw response flag. * @returns A Genkit `ToolAction` representing the MCP tool. */ function createDynamicTool( ai: Genkit, client: Client, tool: Tool, params: { serverName: string; name: string; rawToolResponses?: boolean; multipart?: Multipart; } ): Multipart extends true ? MultipartToolAction : ToolAction { if (params.multipart && params.rawToolResponses) { logger.warn( `[MCP] Tool '${params.serverName}/${tool.name}' is configured with both multipart and rawToolResponses. Genkit will return the raw MCP CallToolResult in the output field, and media parts will not be natively parsed.` ); } if (params.multipart) { return genkitTool( { name: `${params.serverName}/${tool.name}`, description: tool.description || '', inputJsonSchema: tool.inputSchema as JSONSchema7, outputSchema: z.any(), metadata: { mcp: { _meta: tool._meta || {} } }, multipart: true as const, }, async (args, { context }) => { logger.debug( `[MCP] calling tool '${params.serverName}/${tool.name}' in host '${params.name}'` ); const result = await client.callTool({ name: tool.name, arguments: args, _meta: context?.mcp?._meta, }); if (params.rawToolResponses) return { output: result }; return processMultipartResult(result as CallToolResult); } ) as unknown as Multipart extends true ? MultipartToolAction : ToolAction; } return genkitTool( { name: `${params.serverName}/${tool.name}`, description: tool.description || '', inputJsonSchema: tool.inputSchema as JSONSchema7, outputSchema: z.any(), metadata: { mcp: { _meta: tool._meta || {} } }, }, async (args, { context }) => { logger.debug( `[MCP] calling tool '${params.serverName}/${tool.name}' in host '${params.name}'` ); const result = await client.callTool({ name: tool.name, arguments: args, _meta: context?.mcp?._meta, }); if (params.rawToolResponses) return result as CallToolResult; return processResult(result as CallToolResult); } ) as unknown as Multipart extends true ? MultipartToolAction : ToolAction; } /** * Lookup all tools available in the server and register each as a Genkit tool. */ export async function registerAllTools( ai: Genkit, client: Client, params: { name: string; serverName: string; rawToolResponses?: boolean; multipart?: boolean; } ): Promise { let cursor: string | undefined; while (true) { const { nextCursor, tools } = await client.listTools({ cursor }); tools.forEach((t) => registerTool(ai, client, t, params)); cursor = nextCursor; if (!cursor) break; } } /** * Lookup all tools available in the server and fetches as a Genkit dynamic tool. */ export async function fetchDynamicTools( ai: Genkit, client: Client, params: { name: string; serverName: string; rawToolResponses?: boolean; multipart?: Multipart; } ): Promise<(Multipart extends true ? MultipartToolAction : ToolAction)[]> { let cursor: string | undefined; let allTools: (Multipart extends true ? MultipartToolAction : ToolAction)[] = []; while (true) { const { nextCursor, tools } = await client.listTools({ cursor }); allTools.push( ...tools.map((t) => createDynamicTool(ai, client, t, params)) ); cursor = nextCursor; if (!cursor) break; } return allTools; }