Skip to content

工具 (Tools) ​

将 MCP 工具加载到 LangChain 智能体中,控制其执行并处理服务器结果与请求。

MCPAdapter 搭建了 MCP 服务器与 LangChain 智能体之间的桥梁:它能够发现服务器所宣告的工具,并将它们适配为标准的 LangChain 工具。你可以像使用任何其他 LangChain 工具一样,将 listTools() 返回的工具传递给 createAgent。

本页面介绍了该桥接特有的内容:识别 MCP 工具、控制其执行、处理其输出,以及在调用期间服务器需要输入时进行响应。有关可运行的工具发现与智能体示例,请参阅 MCP 快速入门。

在智能体中使用 MCP 工具 ​

使用 MCPAdapter.listTools 发现服务器的目录,然后将返回的工具提供给 createAgent。从智能体的角度来看,它们的行为与 LangChain 工具无异:模型选择工具,LangChain 调用该工具,生成的 ToolMessage 返回给模型。

ts
import { MCPAdapter } from "@langchain/mcp-adapters";
import { createAgent } from "langchain";

async function main(server: string) {
  const adapter = new MCPAdapter({
    servers: { weather: { url: server } },
  });

  try {
    // 发现服务器的工具,然后像使用任何其他 LangChain 工具一样将它们交给智能体。
    // 在智能体调用其工具期间,请保持适配器处于开启状态。
    const tools = await adapter.listTools();
    const agent = createAgent({ model: "claude-sonnet-5", tools });

    await agent.invoke({
      messages: [{ role: "user", content: "What is the forecast for Oslo?" }],
    });
  } finally {
    await adapter.close();
  }
}

查看示例 Trace

打开此示例的公开 LangSmith 运行记录。

在智能体可以调用其工具期间保持适配器处于开启状态,包括在恢复被中断的运行(run)时。在智能体使用完毕后,调用 await adapter.close()。

有关定义、绑定和使用 LangChain 工具的通用指南,请参阅工具。有关多个 MCP 服务器及其带命名空间的工具目录,请参阅连接。

处理工具输出 ​

MCP 工具结果会转换为 ToolMessage 对象,其中包含模型可读取的内容、用于应用程序数据的工件(artifact),以及指示成功或失败的状态。

多模态内容 ​

适配器将模型可见的 MCP 内容转换为 LangChain 内容块。例如,返回带有屏幕截图的 MCP 图像块的工具,会以 image 块的形式到达模型。

ts
import { MCPAdapter } from "@langchain/mcp-adapters";
import { createAgent } from "langchain";

async function accessMultimodalToolContent(serverUrl: string) {
  const adapter = new MCPAdapter({
    servers: { browser: { url: serverUrl } },
  });

  try {
    const tools = await adapter.listTools();

    const agent = createAgent({ model: "claude-sonnet-5", tools });

    const result = await agent.invoke({
      messages: [
        { role: "user", content: "Take a screenshot of the current page" },
      ],
    });

    // MCP 结果以 LangChain 内容块的形式到达。图像内容
    // 会与 `text` 一起转换为标准化的 `image` 块。
    for (const message of result.messages) {
      if (message.type === "tool") {
        for (const block of message.contentBlocks) { 
          if (block.type === "text") { 
            console.log(`Text: ${block.text}`); 
          } else if (block.type === "image") { 
            console.log(`Image MIME type: ${block.mimeType}`); 
            console.log(`Image data: ${String(block.data).slice(0, 50)}...`); 
          } 
        } 
      }
    }
  } finally {
    await adapter.close();
  }
}

查看示例 Trace

打开此示例的公开 LangSmith 运行记录。

结构化内容 ​

当工具返回结构化内容时,适配器会将其作为工件(artifact)附加到 ToolMessage 上,而不是折叠到模型可见的文本中。运行智能体,然后从结果中的 ToolMessage 实例中读取 artifact:

ts
import { MCPAdapter } from "@langchain/mcp-adapters";
import { createAgent, ToolMessage } from "langchain";

async function readStructuredContent(serverUrl: string) {
  const adapter = new MCPAdapter({
    servers: { crm: { url: serverUrl } },
  });

  try {
    const tools = await adapter.listTools();
    const agent = createAgent({ model: "claude-sonnet-5", tools });
    const result = await agent.invoke({
      messages: [{ role: "user", content: "Look up user 42." }],
    });

    // 适配器将 MCP 结果条目存储在 `artifact` 数组中。
    // 找到 `mcp_structured_content` 条目,然后读取其 `data` 字段。
    for (const message of result.messages) {
      if (ToolMessage.isInstance(message) && Array.isArray(message.artifact)) {
        const structured = message.artifact.find(
          (entry) => entry.type === "mcp_structured_content",
        );
        if (structured) {
          console.log(`Structured content: ${JSON.stringify(structured.data)}`); 
        }
      }
    }
  } finally {
    await adapter.close();
  }
}

查看示例 Trace

打开此示例的公开 LangSmith 运行记录。

artifact 是一个 MCP 结果条目数组。结构化内容出现在 type: "mcp_structured_content" 的条目中,其 data 字段保存工具结果的 structuredContent。

错误处理 ​

当服务器在智能体工具调用期间返回 isError: true 时,适配器会返回一个带有 status: "error" 和服务器自身消息的 ToolMessage。模型可以读取该错误并重新尝试。

适配器会抛出传输故障。当工具在 createAgent 内部运行时,智能体的默认错误处理也会将这些故障转换为错误工具消息。仅凭消息的 status 无法区分导致错误的原因:

ts
import { MCPAdapter } from "@langchain/mcp-adapters";
import { createAgent, ToolMessage } from "langchain";

async function divideByZero(serverUrl: string) {
  const adapter = new MCPAdapter({
    servers: { calculator: { url: serverUrl } },
  });

  try {
    const tools = await adapter.listTools();

    const agent = createAgent({ model: "claude-sonnet-5", tools });
    const result = await agent.invoke({
      messages: [{ role: "user", content: "What is 10 divided by 0?" }],
    });

    // 服务器错误(isError: true)会作为失败的 ToolMessage 传递给模型,
    // 因此智能体可以读取服务器自身的消息并进行恢复。默认情况下,
    // createAgent 也会将传输失败转换为错误工具消息。
    for (const message of result.messages) {
      if (ToolMessage.isInstance(message) && message.status === "error") {
        console.log(`Tool error: ${message.text}`); 
      }
    }
  } finally {
    await adapter.close();
  }
}

查看示例 Trace

打开此示例的公开 LangSmith 运行记录。

如果你直接使用普通参数调用适配后的工具,返回 isError: true 的服务器结果会抛出 ToolException,并在 error.result 中包含 MCP 结果。

工具元数据 ​

适配器将 MCP 工具注解存储在 tool.metadata.annotations 中:

typescript
tool.metadata;
// {
//   annotations: {
//     destructiveHint: true,
//     readOnlyHint: false,
//   },
// }

annotations 保存了服务器的 MCP 工具注解,例如 readOnlyHint 和 destructiveHint。服务器可能会省略它们,因此该字段可能为 undefined。

防御性地读取可选元数据,以便缺失字段时返回默认值而不是失败:

本示例以及人在回路示例从 @modelcontextprotocol/client 导入类型;请将其添加为直接依赖项。

ts
import type { DynamicStructuredTool } from "@langchain/core/tools";
import type { ToolAnnotations } from "@modelcontextprotocol/client";

/** 从适配器的工具元数据中读取 MCP destructive 提示。 */
function isDestructive(tool: DynamicStructuredTool): boolean {
  // 使用可选链和默认值,以便缺少任何字段的工具返回 false 而不是抛出异常。
  const annotations = tool.metadata?.annotations as ToolAnnotations | undefined;
  return annotations?.destructiveHint ?? false;
}

人在回路 (Human-in-the-loop) ​

通过读取注解,你可以根据服务器对工具的声明来对工具进行门控,而不是硬编码工具名称。MCP 注解对工具进行分类,而 LangChain 的人在回路中间件负责执行审批策略。

在工具发现期间从元数据中读取一次 destructive 提示。然后为人在回路配置提供一个 when 谓词函数,该函数接收每个待处理的工具调用并返回该调用是否需要审批:

ts
import type { DynamicStructuredTool } from "@langchain/core/tools";
import { MemorySaver } from "@langchain/langgraph";
import { MCPAdapter } from "@langchain/mcp-adapters";
import type { ToolAnnotations } from "@modelcontextprotocol/client";
import {
  createAgent,
  humanInTheLoopMiddleware,
  type InterruptOnConfig,
  type WhenPredicate,
} from "langchain";

function isDestructive(tool: DynamicStructuredTool): boolean {
  const annotations = tool.metadata?.annotations as ToolAnnotations | undefined;
  return annotations?.destructiveHint ?? false;
}

async function gateDestructiveTools(serverUrl: string) {
  const adapter = new MCPAdapter({
    servers: { crm: { url: serverUrl } },
  });
  const tools = await adapter.listTools();

  // 读取一次提示,然后将相同的谓词应用于每次工具调用。
  const destructive = new Set(
    tools.filter(isDestructive).map((tool) => tool.name),
  );

  const needsApproval: WhenPredicate = (request) =>
    destructive.has(request.toolCall.name);

  const gate: InterruptOnConfig = {
    allowedDecisions: ["approve", "reject"],
    when: needsApproval,
  };
  const interruptOn = Object.fromEntries(
    tools.map((tool) => [tool.name, gate]),
  );

  const agent = createAgent({
    model: "claude-sonnet-5",
    tools,
    middleware: [humanInTheLoopMiddleware({ interruptOn })],
    checkpointer: new MemorySaver(),
  });
  return { agent, adapter };
}

WARNING

interruptOn 的键必须是适配器的工具名称,其中包含服务器前缀(crm_delete_file)。未带前缀的键永远无法匹配,因此工具将在未经批准的情况下运行。

当智能体调用受谓词门控的工具时,运行会暂停。批准该调用以运行它,或拒绝它以跳过该工具并通知模型:

typescript
import { Command } from "@langchain/langgraph";

// 批准待处理的破坏性调用并恢复执行。
const resumed = await agent.invoke(
  new Command({ resume: { decisions: [{ type: "approve" }] } }),
  config
);

该谓词还可以检查调用的参数。这样可以在输入安全时让工具自由运行,仅在存在风险时暂停,例如针对受保护路径的 delete_file 调用。

通过 request.toolCall.args 访问这些参数。

将元数据分类与参数检查相结合,仅在工具的类型和输入确实需要审批时才对其进行门控。有关完整的审批工作流程,请参阅人在回路。

工具执行期间的服务器请求 ​

大多数工具在执行完成时不会在调用中途向客户端请求任何内容。当服务器需要输入时,MCPAdapter 会将启发式输入 (elicitation) 作为 LangGraph 的 interrupt 呈现出来。

启发式输入 (Elicitation) ​

启发式输入 (Elicitation) 允许 MCP 服务器在工具调用期间请求输入。适配器使用 LangGraph 中断暂停运行。你的应用程序向用户展示该请求,并使用用户的回答恢复运行。

为智能体附加一个检查点保存器 (checkpointer),并在调用和恢复它时使用相同的 thread_id。

本示例假定预订工具请求一个日期并暂停一次。请将示例日期替换为用户的回答。

中断驱动的启发式输入需要现代 MCP 服务器并默认启用。在服务器上设置 elicitation: false 可以选择退出。对于旧版服务器,请配置带有 mode: "legacy" 的 onElicitation 处理程序。如果没有检查点保存器,请求输入的工具将失败并返回错误的 ToolMessage,而不是暂停。

ts
import { Command, MemorySaver } from "@langchain/langgraph";
import {
  MCPAdapter,
  createMCPElicitationResume,
  type MCPElicitationInterrupt,
} from "@langchain/mcp-adapters";
import { createAgent } from "langchain";

async function bookWithElicitation(serverUrl: string) {
  // 当服务器在调用过程中需要输入时,适配器会将问题作为 LangGraph 中断暴露出来。
  const adapter = new MCPAdapter({
    servers: { booking: { url: serverUrl } },
  });

  try {
    const tools = await adapter.listTools();
    // 检查点保存器会保存被中断的运行,以便其可以恢复。
    const agent = createAgent({
      model: "claude-sonnet-5",
      tools,
      checkpointer: new MemorySaver(),
    });
    const config = { configurable: { thread_id: "booking-1" } };

    const paused = await agent.invoke(
      { messages: [{ role: "user", content: "Book a table for 4." }] },
      config,
    );
    const [pending] = paused.__interrupt__!;
    const question = pending.value as MCPElicitationInterrupt;
    const [key] = Object.keys(question.requests);

    // 答复以服务器自身的请求键(key)为索引。
    // 使用 `decline` 或 `cancel` 可以拒绝该请求。
    const answer = {
      action: "accept" as const,
      content: { date: "2026-09-14" },
    };
    // 将答复定向到请求它的中断。
    const resume = createMCPElicitationResume(pending, { [key]: answer });
    return await agent.invoke(new Command({ resume }), config);
  } finally {
    await adapter.close();
  }
}

查看示例 Trace

打开此示例的公开 LangSmith 运行记录。

createMCPElicitationResume 将答复定向到请求它的中断。响应使用服务器的请求键(request key)。请始终使用 createMCPElicitationResume 构建每个恢复值,而不是使用纯粹的 { responses } 对象,因为后者并未指明它响应的是哪个中断。

每个中断的 value 都是一个 MCPElicitationInterrupt:type: "mcp_elicitation"、server、tool(服务器自身的工具名称,不带前缀)、arguments(生效的工具参数)以及 requests。当同一个智能体也可以引发人在回路中断时,请检查 type。

当多个调用同时暂停时,请答复每个中断。将 createMCPElicitationResume 返回的对象合并为一个 Command({ resume }),例如 Object.assign({}, ...paused.__interrupt__!.map((q) => createMCPElicitationResume(q, answers))),或者一直恢复直到 __interrupt__ 为空。仅答复第一个中断的恢复操作会让其他中断保持暂停状态。

NOTE

恢复执行会从头重新运行该工具。服务器请求输入之前所执行的任何工作都可能会重复进行。请确保这些工作能够安全重复执行,且不会产生重复的副作用。

每个答复使用以下操作之一:

  • accept:提供符合请求模式(schema)的表单 content,或者在没有 content 的情况下确认 URL 交互已完成。
  • decline:拒绝提供所请求的信息。
  • cancel:表明用户取消了该交互。

适配器将答复转发给服务器,由服务器决定工具调用如何结束。

只有启发式输入以此方式进行响应。适配器在每次调用时仅声明启发式输入功能,因此需要采样 (sampling) 或根目录 (roots) 的工具将失败并抛出 ToolException,createAgent 会将其作为错误的 ToolMessage 返回给模型。请参阅采样与根目录。

NOTE

中断驱动的启发式输入用于响应以 InputRequiredResult 形式返回其请求的服务器。如果服务器仅通过旧版握手会话推送启发式输入,则无法通过这种方式进行响应。

另请参阅 ​