@langchain/mcp-adapters
Version:
LangChain.js adapters for Model Context Protocol (MCP)
341 lines (339 loc) • 14.9 kB
JavaScript
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
const require_types = require('./types.cjs');
const require_logging = require('./logging.cjs');
const zod_v3 = require_rolldown_runtime.__toESM(require("zod/v3"));
const zod_v4 = require_rolldown_runtime.__toESM(require("zod/v4"));
const __langchain_core_tools = require_rolldown_runtime.__toESM(require("@langchain/core/tools"));
const __langchain_core_messages = require_rolldown_runtime.__toESM(require("@langchain/core/messages"));
const __langchain_langgraph = require_rolldown_runtime.__toESM(require("@langchain/langgraph"));
//#region src/tools.ts
const debugLog = require_logging.getDebugLog("tools");
/**
* Dereferences $ref pointers in a JSON Schema by inlining the definitions from $defs.
* This is necessary because some JSON Schema validators (like @cfworker/json-schema)
* don't automatically resolve $ref references to $defs.
*
* @param schema - The JSON Schema to dereference
* @returns A new schema with all $ref pointers resolved
*/
function dereferenceJsonSchema(schema) {
const definitions = schema.$defs ?? schema.definitions ?? {};
/**
* Recursively resolve $ref pointers in the schema.
* Tracks visited refs to prevent infinite recursion with circular references.
*/
function resolveRefs(obj, visitedRefs = /* @__PURE__ */ new Set()) {
if (typeof obj !== "object" || obj === null) return obj;
if (obj.$ref && typeof obj.$ref === "string") {
const refPath = obj.$ref;
const defsMatch = refPath.match(/^#\/\$defs\/(.+)$/);
const definitionsMatch = refPath.match(/^#\/definitions\/(.+)$/);
const match = defsMatch || definitionsMatch;
if (match) {
const defName = match[1];
const definition = definitions[defName];
if (definition) {
if (visitedRefs.has(refPath)) {
debugLog(`WARNING: Circular reference detected for ${refPath}, using empty object`);
return { type: "object" };
}
const newVisitedRefs = new Set(visitedRefs);
newVisitedRefs.add(refPath);
const { $ref: _,...restOfObj } = obj;
const resolvedDef = resolveRefs(definition, newVisitedRefs);
return {
...resolvedDef,
...restOfObj
};
} else debugLog(`WARNING: Could not resolve $ref: ${refPath}`);
}
return obj;
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "$defs" || key === "definitions") continue;
if (Array.isArray(value)) result[key] = value.map((item) => typeof item === "object" && item !== null ? resolveRefs(item, visitedRefs) : item);
else if (typeof value === "object" && value !== null) result[key] = resolveRefs(value, visitedRefs);
else result[key] = value;
}
return result;
}
return resolveRefs(schema);
}
/**
* Custom error class for tool exceptions
*/
var ToolException = class extends Error {
constructor(message, cause) {
super(message);
this.name = "ToolException";
/**
* don't display the large ZodError stack trace
*/
if (cause && (cause instanceof zod_v4.ZodError || cause instanceof zod_v3.ZodError)) {
const minifiedZodError = new Error(zod_v4.z.prettifyError(cause));
const stackByLine = cause.stack?.split("\n") || [];
minifiedZodError.stack = cause.stack?.split("\n").slice(stackByLine.findIndex((l) => l.includes(" at"))).join("\n");
this.cause = minifiedZodError;
} else if (cause) this.cause = cause;
}
};
function isToolException(error) {
return typeof error === "object" && error !== null && "name" in error && error.name === "ToolException";
}
function isResourceReference(resource) {
return typeof resource === "object" && resource !== null && "uri" in resource && typeof resource.uri === "string" && (!("blob" in resource) || resource.blob == null) && (!("text" in resource) || 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 ("blob" in resource && resource.blob != null) yield {
type: "file",
source_type: "base64",
data: resource.blob,
mime_type: resource.mimeType,
...resource.uri != null ? { metadata: { uri: resource.uri } } : {}
};
if ("text" in resource && 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": 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 ((!("blob" in resource) || resource.blob == null) && (!("text" in resource) || resource.text == null) && "uri" in resource && typeof resource.uri === "string") {
const response = await client.readResource({ uri: 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 = require_types._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.type === "text" ? 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();
const artifacts = (await Promise.all(result.content.filter((content) => _getOutputTypeForContentType(content.type, outputHandling) === "artifact").map((content) => {
return _embeddedResourceToArtifact(content, useStandardContentBlocks, client, toolName, serverName);
}))).flat();
const structuredContent = result.structuredContent;
const meta = result._meta;
const enhancedArtifacts = [...artifacts];
if (structuredContent) enhancedArtifacts.push({
type: "mcp_structured_content",
data: structuredContent
});
if (meta) enhancedArtifacts.push({
type: "mcp_meta",
data: meta
});
if (convertedContent.length === 1 && convertedContent[0].type === "text") {
const textBlock = convertedContent[0];
const textContent = textBlock.text;
if (structuredContent || meta) return [{
...textBlock,
...structuredContent ? { structuredContent } : {},
...meta ? { meta } : {}
}, enhancedArtifacts];
return [textContent, enhancedArtifacts];
}
return [convertedContent, enhancedArtifacts];
}
/**
* 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, onProgress, beforeToolCall, afterToolCall }) {
try {
debugLog(`INFO: Calling tool ${toolName}(${JSON.stringify(args)})`);
const numericTimeout = config?.metadata?.timeoutMs ?? config?.timeout;
const requestOptions = {
...numericTimeout ? { timeout: numericTimeout } : {},
...config?.signal ? { signal: config.signal } : {},
...onProgress ? { onprogress: (progress) => {
onProgress?.(progress, {
type: "tool",
name: toolName,
args,
server: serverName
});
} } : {}
};
let state = {};
try {
state = (0, __langchain_langgraph.getCurrentTaskInput)(config);
} catch (error) {
debugLog(`State can't be derrived as LangGraph is not used: ${String(error)}`);
}
const beforeToolCallInterception = await beforeToolCall?.({
name: toolName,
args,
serverName
}, state, config ?? {});
const finalArgs = Object.assign(args, beforeToolCallInterception?.args || {});
const headers = beforeToolCallInterception?.headers || {};
const hasHeaderChanges = Object.entries(headers).length > 0;
if (hasHeaderChanges && typeof client.fork !== "function") throw new ToolException(`MCP client for server "${serverName}" does not support header changes`);
const finalClient = hasHeaderChanges && typeof client.fork === "function" ? await client.fork(headers) : client;
const callToolArgs = [{
name: toolName,
arguments: finalArgs
}];
if (Object.keys(requestOptions).length > 0) {
callToolArgs.push(void 0);
callToolArgs.push(requestOptions);
}
const result = await finalClient.callTool(...callToolArgs);
const [content, artifacts] = await _convertCallToolResult({
serverName,
toolName,
result,
client: finalClient,
useStandardContentBlocks,
outputHandling
});
const normalizedContent = typeof content === "string" ? content : Array.isArray(content) ? content : [content];
const normalizedArtifacts = artifacts.filter((artifact) => artifact.type === "resource" || artifact.type !== "mcp_structured_content" && artifact.type !== "mcp_meta" && typeof artifact === "object" && artifact !== null && "source_type" in artifact);
const interceptedResult = await afterToolCall?.({
name: toolName,
args: finalArgs,
result: [normalizedContent, normalizedArtifacts],
serverName
}, state, config ?? {});
if (!interceptedResult) return [content, artifacts];
if (typeof interceptedResult.result === "string") return [interceptedResult.result, []];
if (Array.isArray(interceptedResult.result)) return interceptedResult.result;
if (__langchain_core_messages.ToolMessage.isInstance(interceptedResult.result)) return [interceptedResult.result.contentBlocks, []];
if (interceptedResult?.result instanceof __langchain_langgraph.Command) return interceptedResult.result;
throw new Error(`Unexpected result value type from afterToolCall: expected either a Command, a ToolMessage or a tuple of ContentBlock and Artifact, but got ${interceptedResult.result}`);
} catch (error) {
if (error instanceof zod_v4.ZodError || error instanceof zod_v3.ZodError) throw new ToolException(zod_v4.z.prettifyError(error), error);
debugLog(`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 = [];
let toolsResponse;
do {
toolsResponse = await client.listTools({ ...toolsResponse?.nextCursor ? { cursor: toolsResponse.nextCursor } : {} });
mcpTools.push(...toolsResponse.tools || []);
} while (toolsResponse.nextCursor);
debugLog(`INFO: Found ${mcpTools.length} MCP tools`);
const initialPrefix = additionalToolNamePrefix ? `${additionalToolNamePrefix}__` : "";
const serverPrefix = prefixToolNameWithServerName ? `${serverName}__` : "";
const toolNamePrefix = `${initialPrefix}${serverPrefix}`;
return (await Promise.all(mcpTools.filter((tool) => !!tool.name).map(async (tool) => {
try {
if (!tool.inputSchema.properties) tool.inputSchema.properties = {};
const dereferencedSchema = dereferenceJsonSchema(tool.inputSchema);
const dst = new __langchain_core_tools.DynamicStructuredTool({
name: `${toolNamePrefix}${tool.name}`,
description: tool.description || "",
schema: dereferencedSchema,
responseFormat: "content_and_artifact",
metadata: { annotations: tool.annotations },
defaultConfig: defaultToolTimeout ? { timeout: defaultToolTimeout } : void 0,
func: async (args, _runManager, config) => {
return _callTool({
serverName,
toolName: tool.name,
client,
args,
config,
useStandardContentBlocks,
outputHandling,
onProgress: options?.onProgress,
beforeToolCall: options?.beforeToolCall,
afterToolCall: options?.afterToolCall
});
}
});
debugLog(`INFO: Successfully loaded tool: ${dst.name}`);
return dst;
} catch (error) {
debugLog(`ERROR: Failed to load tool "${tool.name}":`, error);
if (throwOnLoadError) throw error;
return null;
}
}))).filter(Boolean);
}
//#endregion
exports.loadMcpTools = loadMcpTools;
//# sourceMappingURL=tools.cjs.map