UNPKG

@langchain/mcp-adapters

Version:
309 lines (308 loc) 11.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ToolException = void 0; exports.isToolException = isToolException; exports.loadMcpTools = loadMcpTools; const tools_1 = require("@langchain/core/tools"); const debug_1 = __importDefault(require("debug")); const types_js_1 = require("./types.cjs"); // Replace direct initialization with lazy initialization let debugLog; function getDebugLog() { if (!debugLog) { debugLog = (0, debug_1.default)("@langchain/mcp-adapters:tools"); } return debugLog; } /** * Custom error class for tool exceptions */ class ToolException extends Error { constructor(message) { super(message); this.name = "ToolException"; } } exports.ToolException = ToolException; function isToolException(error) { return (typeof error === "object" && error !== null && "name" in error && error.name === "ToolException"); } function isResourceReference(resource) { return (typeof resource === "object" && resource !== null && resource.uri != null && resource.blob == null && resource.text == null); } async function* _embeddedResourceToStandardFileBlocks(resource, client) { if (isResourceReference(resource)) { const response = await client.readResource({ uri: resource.uri, }); for (const content of response.contents) { yield* _embeddedResourceToStandardFileBlocks(content, client); } return; } if (resource.blob != null) { yield { type: "file", source_type: "base64", data: resource.blob, mime_type: resource.mimeType, ...(resource.uri != null ? { metadata: { uri: resource.uri } } : {}), }; } if (resource.text != null) { yield { type: "file", source_type: "text", mime_type: resource.mimeType, text: resource.text, ...(resource.uri != null ? { metadata: { uri: resource.uri } } : {}), }; } } async function _toolOutputToContentBlocks(content, useStandardContentBlocks, client, toolName, serverName) { const blocks = []; switch (content.type) { case "text": return [ { type: "text", ...(useStandardContentBlocks ? { source_type: "text", } : {}), text: content.text, }, ]; case "image": if (useStandardContentBlocks) { return [ { type: "image", source_type: "base64", data: content.data, mime_type: content.mimeType, }, ]; } return [ { type: "image_url", image_url: { url: `data:${content.mimeType};base64,${content.data}`, }, }, ]; case "audio": // We don't check `useStandardContentBlocks` here because we only support audio via // standard content blocks return [ { type: "audio", source_type: "base64", data: content.data, mime_type: content.mimeType, }, ]; case "resource": for await (const block of _embeddedResourceToStandardFileBlocks(content.resource, client)) { blocks.push(block); } return blocks; default: throw new ToolException(`MCP tool '${toolName}' on server '${serverName}' returned a content block with unexpected type "${content.type}." Expected one of "text", "image", or "audio".`); } } async function _embeddedResourceToArtifact(resource, useStandardContentBlocks, client, toolName, serverName) { if (useStandardContentBlocks) { return _toolOutputToContentBlocks(resource, useStandardContentBlocks, client, toolName, serverName); } if (!resource.blob && !resource.text && resource.uri) { const response = await client.readResource({ uri: resource.resource.uri, }); return response.contents.map((content) => ({ type: "resource", resource: { ...content, }, })); } return [resource]; } function _getOutputTypeForContentType(contentType, outputHandling) { if (outputHandling === "content" || outputHandling === "artifact") { return outputHandling; } const resolved = (0, types_js_1._resolveDetailedOutputHandling)(outputHandling); return (resolved[contentType] ?? (contentType === "resource" ? "artifact" : "content")); } /** * Process the result from calling an MCP tool. * Extracts text content and non-text content for better agent compatibility. * * @internal * * @param args - The arguments to pass to the tool * @returns A tuple of [textContent, nonTextContent] */ async function _convertCallToolResult({ serverName, toolName, result, client, useStandardContentBlocks, outputHandling, }) { if (!result) { throw new ToolException(`MCP tool '${toolName}' on server '${serverName}' returned an invalid result - tool call response was undefined`); } if (!Array.isArray(result.content)) { throw new ToolException(`MCP tool '${toolName}' on server '${serverName}' returned an invalid result - expected an array of content, but was ${typeof result.content}`); } if (result.isError) { throw new ToolException(`MCP tool '${toolName}' on server '${serverName}' returned an error: ${result.content .map((content) => content.text) .join("\n")}`); } const convertedContent = (await Promise.all(result.content .filter((content) => _getOutputTypeForContentType(content.type, outputHandling) === "content") .map((content) => _toolOutputToContentBlocks(content, useStandardContentBlocks, client, toolName, serverName)))).flat(); // Create the text content output const artifacts = (await Promise.all(result.content.filter((content) => _getOutputTypeForContentType(content.type, outputHandling) === "artifact").map((content) => { return _embeddedResourceToArtifact(content, useStandardContentBlocks, client, toolName, serverName); }))).flat(); if (convertedContent.length === 1 && convertedContent[0].type === "text") { return [convertedContent[0].text, artifacts]; } return [convertedContent, artifacts]; } /** * Call an MCP tool. * * Use this with `.bind` to capture the fist three arguments, then pass to the constructor of DynamicStructuredTool. * * @internal * @param args - The arguments to pass to the tool * @returns A tuple of [textContent, nonTextContent] */ async function _callTool({ serverName, toolName, client, args, config, useStandardContentBlocks, outputHandling, }) { try { getDebugLog()(`INFO: Calling tool ${toolName}(${JSON.stringify(args)})`); // Extract timeout from RunnableConfig and pass to MCP SDK const requestOptions = { ...(config?.timeout ? { timeout: config.timeout } : {}), ...(config?.signal ? { signal: config.signal } : {}), }; const callToolArgs = [ { name: toolName, arguments: args, }, ]; if (Object.keys(requestOptions).length > 0) { callToolArgs.push(undefined); // optional output schema arg callToolArgs.push(requestOptions); } const result = await client.callTool(...callToolArgs); return _convertCallToolResult({ serverName, toolName, result: result, client, useStandardContentBlocks, outputHandling, }); } catch (error) { getDebugLog()(`Error calling tool ${toolName}: ${String(error)}`); if (isToolException(error)) { throw error; } throw new ToolException(`Error calling tool ${toolName}: ${String(error)}`); } } const defaultLoadMcpToolsOptions = { throwOnLoadError: true, prefixToolNameWithServerName: false, additionalToolNamePrefix: "", useStandardContentBlocks: false, }; /** * Load all tools from an MCP client. * * @param serverName - The name of the server to load tools from * @param client - The MCP client * @returns A list of LangChain tools */ async function loadMcpTools(serverName, client, options) { const { throwOnLoadError, prefixToolNameWithServerName, additionalToolNamePrefix, useStandardContentBlocks, outputHandling, defaultToolTimeout, } = { ...defaultLoadMcpToolsOptions, ...(options ?? {}), }; const mcpTools = []; // Get tools in a single operation let toolsResponse; do { toolsResponse = await client.listTools({ ...(toolsResponse?.nextCursor ? { cursor: toolsResponse.nextCursor } : {}), }); mcpTools.push(...(toolsResponse.tools || [])); } while (toolsResponse.nextCursor); getDebugLog()(`INFO: Found ${mcpTools.length} MCP tools`); const initialPrefix = additionalToolNamePrefix ? `${additionalToolNamePrefix}__` : ""; const serverPrefix = prefixToolNameWithServerName ? `${serverName}__` : ""; const toolNamePrefix = `${initialPrefix}${serverPrefix}`; // Filter out tools without names and convert in a single map operation return (await Promise.all(mcpTools .filter((tool) => !!tool.name) .map(async (tool) => { try { if (!tool.inputSchema.properties) { // Workaround for MCP SDK not consistently providing properties // eslint-disable-next-line no-param-reassign tool.inputSchema.properties = {}; } const dst = new tools_1.DynamicStructuredTool({ name: `${toolNamePrefix}${tool.name}`, description: tool.description || "", schema: tool.inputSchema, responseFormat: "content_and_artifact", metadata: { annotations: tool.annotations }, defaultConfig: defaultToolTimeout ? { timeout: defaultToolTimeout } : undefined, func: async (args, _runManager, config) => { return _callTool({ serverName, toolName: tool.name, client, args, config, useStandardContentBlocks, outputHandling, }); }, }); getDebugLog()(`INFO: Successfully loaded tool: ${dst.name}`); return dst; } catch (error) { getDebugLog()(`ERROR: Failed to load tool "${tool.name}":`, error); if (throwOnLoadError) { throw error; } return null; } }))).filter(Boolean); }