前言

大模型本身擅长理解语言、分析问题和生成内容,但它并不天然具备访问外部世界的能力。比如让模型查询附近酒店、打开浏览器、截图页面、读取本地文件、把结果写入 Markdown 文档,这些事情都不是单靠模型参数就能完成的。

要让模型真正“做事”,就需要给它接入外部工具。传统做法是为每一个工具单独写函数,然后把函数描述提供给模型。这样虽然可行,但当工具越来越多时,接入方式、调用协议、参数结构、运行环境都会变得很分散。

MCP 的出现,就是为了解决这个问题。MCP 全称是 Model Context Protocol,可以理解为一套面向大模型应用的工具接入协议。它的本质仍然是 tool,只不过把工具封装成了标准化的服务。大模型应用通过统一方式连接 MCP Server,再由 MCP Server 去访问地图、浏览器、文件系统或者业务接口。

本文通过一个完整项目讲解远程 MCP 的使用方式:使用 LangChain 连接多个 MCP Server,让模型能够调用 高德地图 MCP 查询地点和路线,调用 Chrome DevTools MCP 控制浏览器,调用 FileSystem MCP 写入本地文件,并通过一个简单 Agent 循环把这些工具串成完整 AI 工作流。

1. MCP 基础知识

MCP 可以先从一句话理解:

MCP 是让大模型应用用统一协议调用外部工具的一种标准。

在没有 MCP 的情况下,如果要让模型调用地图,需要写一套地图工具;要让模型操作浏览器,需要再写一套浏览器工具;要让模型写文件,还要继续封装文件系统工具。每一种工具的接入方式都不一样,后期维护成本会越来越高。

有了 MCP 后,外部能力可以统一封装成 MCP Server。大模型应用只需要连接这些 Server,就能获取它们暴露出来的工具列表。

整体关系可以这样理解:

用户问题
  ↓
大模型应用 / Agent
  ↓
MCP Client
  ↓
MCP Server
  ↓
地图、浏览器、文件系统、数据库、业务 API

MCP Client 通常运行在大模型应用里,负责连接一个或多个 MCP Server,读取工具列表,并把工具转换成模型可以调用的形式。

MCP Server 负责真正提供能力。比如高德地图 MCP Server 提供地点搜索、路线规划;FileSystem MCP Server 提供文件读取和写入;Chrome DevTools MCP Server 提供浏览器控制能力。

模型并不是直接访问外部系统。 模型只是根据工具描述生成工具调用请求,例如“调用地图工具查询北京南站附近酒店”。真正执行调用的是 MCP Client 和 MCP Server。工具返回结果后,结果再被放回对话上下文,让模型继续分析和总结。

从开发角度看,MCP 的工作过程可以拆成几个关键点。

第一,工具发现。 MCP Client 连接 MCP Server 后,会先获取 Server 暴露的工具列表。每个工具通常会包含名称、描述、参数结构等信息。模型正是根据这些描述判断“什么时候该调用哪个工具”。

第二,参数约束。 工具不是随便传字符串调用的,MCP Server 会描述工具需要什么参数。例如地图搜索工具可能需要关键词、城市、经纬度;文件写入工具可能需要文件路径和文件内容。参数结构越清楚,模型调用工具时越稳定。

第三,结果回传。 工具执行完成后,返回结果并不会直接变成最终答案,而是会进入模型上下文。模型会继续阅读工具结果,再决定是继续调用下一个工具,还是把结果整理成自然语言回复。

第四,能力边界。 MCP 让模型能调用外部工具,但也必须控制权限。比如 FileSystem MCP 只能开放指定目录,高德地图 MCP 只能使用授权 Key,Chrome DevTools MCP 只能控制允许连接的浏览器实例。MCP 提供的是连接能力,不代表可以无边界地开放所有系统权限。

因此,MCP 不只是“把函数给模型用”,它更像是给 AI 应用建立了一套标准工具层:工具如何声明、如何连接、如何调用、如何返回结果,都有统一方式。

MCP 常见的连接方式主要有两种。

第一种是 stdio 这种方式通常用于本地 MCP Server。应用通过命令启动一个本地进程,然后通过标准输入输出通信。例如文件系统 MCP、Chrome DevTools MCP、自定义 Node.js MCP Server,都很适合用这种方式运行。

'filesystem': {
    command: 'npx',
    args: [
        '-y',
        '@modelcontextprotocol/server-filesystem',
        '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
    ]
}

这里的 command 表示要启动什么命令,args 表示启动命令时传入的参数。上面这段配置的意思是通过 npx 启动官方文件系统 MCP Server,并且只允许它访问指定目录。

第二种是 HTTP / Streamable HTTP。 这种方式通常用于远程 MCP Server。比如高德地图 MCP 不需要在本地启动地图服务,而是通过一个远程地址访问。

{
  "mcpServers": {
    "amap-maps-http": {
      "url": "https://mcp.amap.com/mcp?key=高德地图Key"
    }
  }
}

这种配置方式非常适合云服务、第三方 API、企业内部平台等场景。只要远程服务按照 MCP 协议暴露工具,大模型应用就可以通过 URL 连接它。

除了在代码中配置 MCP,也可以在支持 MCP 的编辑器或 AI IDE 中配置。比如在 Trae 中,可以进入设置里的 MCP 配置区域,选择手动配置,然后粘贴类似下面的 JSON:

{
  "mcpServers": {
    "amap-maps-http": {
      "url": "https://mcp.amap.com/mcp?key=高德地图Key"
    }
  }
}

这类 JSON 的核心结构就是 mcpServers。里面每一个字段名都是一个 MCP Server 的自定义名称,对应的配置可以是远程 url,也可以是本地 command + args。在 Trae 里手动配置后,编辑器就能识别这个 MCP Server 暴露的工具,后续 AI 助手就可以在需要时调用这些能力。

MCP 的核心价值不只是“多了几个工具”,而是让工具生态变得标准化。任何人都可以开发符合协议的 MCP Server,其他 AI 应用可以直接复用。地图、浏览器、文件系统、数据库、内部业务平台,都可以通过同一套思路接入 Agent。

2. 项目目标与整体流程

这个项目要完成的任务是:用户输入一句自然语言需求,模型自动判断需要调用哪些工具,查询北京南站附近的 2 个酒店,规划路线,并把路线规划结果生成一个 Markdown 文件保存到本地目录。

示例用户输入如下:

北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件

这句话里面其实包含了多个子任务:

  • 需要理解“北京南站”这个地点
  • 需要搜索北京南站附近的酒店
  • 需要筛选出 2 个酒店
  • 需要规划从北京南站到酒店的路线
  • 需要把最终内容整理成 Markdown 文档
  • 需要调用文件系统工具写入本地文件

如果靠普通代码手写流程,就要依次调用地图接口、处理结果、拼接文档、写入文件。使用 Agent + MCP 后,模型可以根据工具描述自动拆解任务,并在多轮循环中逐步调用工具。

项目整体流程如下:

用户输入任务
  ↓
LangChain 把问题交给模型
  ↓
模型判断是否需要调用工具
  ↓
如果需要工具,生成 tool_calls
  ↓
代码找到对应 MCP 工具并执行
  ↓
把工具结果包装成 ToolMessage 放回上下文
  ↓
模型继续分析工具结果
  ↓
直到模型不再请求工具调用,输出最终结果

这里最关键的思想是:Agent 不是一次调用模型就结束,而是模型、工具、工具结果之间不断循环,直到任务完成。

3. MCP Server 配置讲解

项目中同时连接多个 MCP Server。每一个 MCP Server 都代表一类外部能力。

下面这段是写在代码中的配置方式。如果是在 Trae 这类工具里使用 MCP,可以把同类型的 JSON 配置放到设置里的 MCP 手动配置中;如果是在自己的 Node.js 项目里开发,就可以像下面这样交给 MultiServerMCPClient 管理。

const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'amap-mcp-streamableHTTP': {
            url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
        },
        'my-mcp-server': {
            command: 'node',
            args: [
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
            ]
        },
        'filesystem': {
            command: 'npx',
            args: [
                '-y',
                '@modelcontextprotocol/server-filesystem',
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
            ]
        },
        'chrome-devtools': {
            command: 'npx',
            args: [
                '-y',
                'chrome-devtools-mcp@latest',
            ]
        }
    }
});

高德地图 MCP 使用的是远程 HTTP 方式:

'amap-mcp-streamableHTTP': {
    url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
}

它负责提供地图相关能力,比如地点搜索、周边搜索、路径规划等。对于“北京南站附近酒店”“西安大唐不夜城附近酒店”“从 A 到 B 怎么走”这类问题,模型都可以借助它拿到更真实的地理信息。

这里推荐把高德 Key 放在环境变量中:

AMAP_MAPS_API_KEY=高德地图Key

不要把真实 Key 直接写进代码。代码一旦上传到仓库,密钥就有泄漏风险。

FileSystem MCP 使用的是本地 stdio 方式:

'filesystem': {
    command: 'npx',
    args: [
        '-y',
        '@modelcontextprotocol/server-filesystem',
        '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
    ]
}

它负责文件读写。最后一个路径参数表示允许访问的目录。这个参数非常重要,因为文件系统工具有真实读写能力,不能随便开放整个电脑目录。实际项目里建议给 AI 单独准备一个工作目录,只允许它在这个目录下创建和修改文件。

Chrome DevTools MCP 也是本地工具:

'chrome-devtools': {
    command: 'npx',
    args: [
        '-y',
        'chrome-devtools-mcp@latest',
    ]
}

它可以让模型通过 DevTools 控制浏览器,例如打开网页、点击元素、读取页面结构、截图等。浏览器自动化、前端页面检查、网页信息采集,都可以基于这个能力扩展。

自定义 MCP Server 代表项目自己的业务工具:

'my-mcp-server': {
    command: 'node',
    args: [
        '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
    ]
}

这类 Server 可以封装自己的业务逻辑,比如查询内部系统、访问数据库、调用公司接口、执行固定业务流程。只要它符合 MCP 协议,就能和其他工具一样被 Agent 调用。

4. 完整代码

下面是一份完整示例代码。为了更适合实际项目使用,代码中把模型 Key 和高德 Key 都放到了环境变量中,并修正了关闭客户端时的变量名。

// 读取 .env 文件中的环境变量,例如模型 Key、高德地图 Key
import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
    HumanMessage,
    ToolMessage
} from '@langchain/core/messages';

// 创建模型实例。这里使用 DeepSeek 的 OpenAI 兼容接口,所以可以通过 ChatOpenAI 调用
const model = new ChatOpenAI({
    modelName: 'deepseek-v4-pro',
    apiKey: process.env.DEEPSEEK_API_KEY,
    // 工具调用场景更关注稳定性,temperature 设置为 0 可以减少随机性
    temperature: 0,
    configuration: {
        baseURL: 'https://api.deepseek.com/v1',
    },
});

// 创建 MCP Client,并在一个客户端中统一管理多个 MCP Server
const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        // 远程 MCP:通过 HTTP 连接高德地图 MCP,用于地点查询、路线规划等
        'amap-mcp-streamableHTTP': {
            url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
        },
        // 自定义本地 MCP:通过 node 启动自己实现的 MCP Server
        'my-mcp-server': {
            command: 'node',
            args: [
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
            ]
        },
        // 文件系统 MCP:只允许访问指定目录,用于读取、写入、创建文件
        'filesystem': {
            command: 'npx',
            args: [
                '-y',
                '@modelcontextprotocol/server-filesystem',
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
            ]
        },
        // Chrome DevTools MCP:用于控制浏览器,例如打开页面、点击元素、截图
        'chrome-devtools': {
            command: 'npx',
            args: [
                '-y',
                'chrome-devtools-mcp@latest',
            ]
        }
    }
});

// 从所有 MCP Server 中获取工具列表,并转换成 LangChain 可识别的 tools
const tools = await mcpClient.getTools();
console.log(tools);

// 把工具绑定到模型上。绑定后,模型才可以生成 tool_calls
const modelWithTools = model.bindTools(tools);

// 不同 MCP 工具返回的数据结构可能不同,这里统一转成字符串交给模型继续理解
function stringifyToolResult(toolResult) {
    if (typeof toolResult === 'string') {
        return toolResult;
    }

    if (toolResult?.content) {
        return JSON.stringify(toolResult.content);
    }

    return JSON.stringify(toolResult);
}

async function runAgentWithTools(query, maxIterations = 30) {
    // messages 保存完整上下文:用户问题、模型回复、工具执行结果都会放进来
    const messages = [
        new HumanMessage(query)
    ];

    // Agent 循环:模型可以在多轮中反复判断是否需要继续调用工具
    for (let i = 0; i < maxIterations; i++) {
        console.log(chalk.bgGreen(`${i + 1}轮对话`));

        // 调用绑定工具后的模型。模型可能直接回答,也可能返回 tool_calls
        const response = await modelWithTools.invoke(messages);
        messages.push(response);

        // 如果没有工具调用,说明模型已经得到最终答案,可以结束循环
        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(chalk.bgRed(`AI 回答:${response.content}`));
            return response.content;
        }

        console.log(chalk.bgBlue(
            `工具调用: ${response.tool_calls.map(t => t.name).join(', ')}`
        ));

        for (const toolCall of response.tool_calls) {
            // 根据模型返回的工具名,在 tools 列表中找到真正可执行的工具
            const foundTool = tools.find(t => t.name === toolCall.name);

            if (!foundTool) {
                continue;
            }

            // 执行工具。toolCall.args 是模型为该工具生成的入参
            const toolResult = await foundTool.invoke(toolCall.args);
            const contentStr = stringifyToolResult(toolResult);

            // 把工具结果放回上下文,并用 tool_call_id 对应模型刚才的工具调用
            messages.push(new ToolMessage({
                content: contentStr,
                tool_call_id: toolCall.id
            }));
        }
    }

    // 如果达到最大轮数仍未结束,返回最后一条消息内容
    return messages[messages.length - 1].content;
}

// 启动一次完整任务:查询酒店、规划路线,并让文件系统 MCP 保存 Markdown 文档
await runAgentWithTools(
    '北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件'
);

// 任务结束后关闭 MCP Client,释放本地进程或远程连接资源
await mcpClient.close();

5. 核心代码分块讲解

这一部分是整篇文章的重点。理解这段代码,关键不是只看每一行做了什么,而是要理解它如何把“模型推理”和“工具执行”连接起来。

5.1 导入依赖:准备模型、MCP 客户端和消息类型

import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
    HumanMessage,
    ToolMessage
} from '@langchain/core/messages';

import 'dotenv/config' 是一个副作用导入。它不需要手动调用函数,导入后会自动读取项目根目录下的 .env 文件,并把里面的配置加载到 process.env 中。

例如 .env 可以这样写:

DEEPSEEK_API_KEY=DeepSeekKey
AMAP_MAPS_API_KEY=高德地图Key

这样代码里就可以通过 process.env.DEEPSEEK_API_KEYprocess.env.AMAP_MAPS_API_KEY 读取密钥。对于 AI 项目来说,这一点很重要,因为模型 Key、地图 Key 都属于敏感信息,不应该硬编码在源码里。

  • MultiServerMCPClient 来自 @langchain/mcp-adapters,它的作用是把 MCP Server 暴露出来的工具转换成 LangChain 能识别的工具。这里用的是 MultiServer,意思是可以同时管理多个 MCP Server,而不是只能连接一个工具服务。

  • ChatOpenAI 是 LangChain 中用于调用 OpenAI 兼容聊天模型的类。虽然示例里接入的是 DeepSeek,但只要服务接口兼容 OpenAI 格式,就可以通过 ChatOpenAI 加上自定义 baseURL 来调用。

  • HumanMessageToolMessage 是 LangChain 的消息类型。HumanMessage 表示用户输入,ToolMessage 表示工具执行结果。Agent 的多轮循环本质上就是不断往 messages 数组里追加这些消息,让模型看到完整上下文。

  • chalk 只是用来美化终端日志,不影响 Agent 核心逻辑。

5.2 创建模型:让 LangChain 连接 DeepSeek 接口

const model = new ChatOpenAI({
    modelName: 'deepseek-v4-pro',
    apiKey: process.env.DEEPSEEK_API_KEY,
    temperature: 0,
    configuration: {
        baseURL: 'https://api.deepseek.com/v1',
    },
});

这段代码创建了一个聊天模型对象。后续所有模型推理、工具调用判断,都会通过这个 model 完成。

modelName 表示要调用的模型名称。实际开发中要根据模型服务商支持的模型名填写,如果模型名称写错,接口通常会返回模型不存在或请求失败。

apiKey 是访问模型服务的凭证。这里从环境变量读取,避免密钥泄漏。

temperature 控制模型输出的随机性。值越高,模型回答越发散;值越低,输出越稳定。工具调用类 Agent 通常建议把它设置得低一些,例如 0。原因是这类任务更看重稳定执行,而不是创意表达。

configuration.baseURL 用来指定接口地址。ChatOpenAI 默认会访问 OpenAI 的接口,如果要调用 DeepSeek 这类 OpenAI 兼容接口,就需要把 baseURL 改成对应服务商地址。

这里可以把模型对象理解为一个“推理引擎”。但此时它还不会使用 MCP 工具,因为工具还没有绑定进去。

5.3 创建 MCP Client:同时接入多个工具服务

const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'amap-mcp-streamableHTTP': {
            url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
        },
        'my-mcp-server': {
            command: 'node',
            args: [
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
            ]
        },
        'filesystem': {
            command: 'npx',
            args: [
                '-y',
                '@modelcontextprotocol/server-filesystem',
                '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
            ]
        },
        'chrome-devtools': {
            command: 'npx',
            args: [
                '-y',
                'chrome-devtools-mcp@latest',
            ]
        }
    }
});

这段代码是项目的工具接入中心。mcpServers 对象里的每一个 key,都是一个 MCP Server 的名字;每一个 value,都是这个 Server 的连接方式。

对于远程 MCP Server,通常使用 url

'amap-mcp-streamableHTTP': {
    url: `https://mcp.amap.com/mcp?key=${process.env.AMAP_MAPS_API_KEY}`
}

这个配置告诉 MCP Client:高德地图工具不需要本地启动进程,直接通过这个 HTTP 地址连接即可。模型后面如果需要地图能力,最终会通过这个 Server 获取工具并执行。

对于本地 MCP Server,通常使用 command + args

'filesystem': {
    command: 'npx',
    args: [
        '-y',
        '@modelcontextprotocol/server-filesystem',
        '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/remote-mcp'
    ]
}

这表示 MCP Client 会启动一个本地命令。command 是命令名,args 是命令参数。对文件系统 MCP 来说,最后一个参数就是允许访问的目录。

这里最容易忽略的是权限边界。FileSystem MCP 不是模拟写文件,而是真的有可能在本地创建、修改文件。所以实际使用时,不要把访问目录配置成 /、用户主目录或者包含敏感信息的目录。更推荐创建一个专门的工作目录,给 AI 一个明确的活动范围。

自定义 MCP Server 的配置方式也类似:

'my-mcp-server': {
    command: 'node',
    args: [
        '/Users/moss/study/workspace/moss_ai/ai/agent_in_action/hello-langchain/src/my-mcp-server.mjs'
    ]
}

这表示通过 node 运行一个本地 .mjs 文件。只要这个文件实现了 MCP Server 规范,它暴露出来的工具就可以和高德地图、文件系统、浏览器工具放在一起使用。

这也是 MCP 设计上很灵活的地方:远程服务和本地服务可以同时存在,最终都会被统一转换成工具列表。

5.4 获取工具并绑定模型:让模型知道自己能调用什么

const tools = await mcpClient.getTools();
console.log(tools);

const modelWithTools = model.bindTools(tools);

mcpClient.getTools() 会连接前面配置的所有 MCP Server,读取它们提供的工具,并转换成 LangChain Tool。

这些工具一般会包含几个关键信息:

  • 工具名称
  • 工具描述
  • 参数结构
  • 调用方法

模型能不能正确调用工具,很大程度上取决于这些工具描述是否清楚。比如一个地图工具如果描述了“可以根据关键词搜索地点”,模型就知道遇到地点搜索任务时可以使用它;一个文件工具如果描述了“可以写入文件”,模型就知道保存 Markdown 时可以调用它。

model.bindTools(tools) 的作用是把工具列表绑定到模型上。绑定之后,模型在生成回复时就不只是输出普通文本,还可以输出结构化的 tool_calls

可以这样理解:

绑定前:模型只能回答文本
绑定后:模型可以回答文本,也可以请求调用工具

当用户问“北京南站附近的 2 个酒店,并保存路线规划到 md 文件”时,模型不会直接编造结果,而是可以先请求调用地图工具,再请求调用文件系统工具。

5.5 Agent 循环:让模型和工具多轮协作

async function runAgentWithTools(query, maxIterations = 30) {
    const messages = [
        new HumanMessage(query)
    ];

    for (let i = 0; i < maxIterations; i++) {
        console.log(chalk.bgGreen(`${i + 1}轮对话`));

        const response = await modelWithTools.invoke(messages);
        messages.push(response);

        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(chalk.bgRed(`AI 回答:${response.content}`));
            return response.content;
        }

        ...
    }
}

这是整个项目的核心函数。

函数接收两个参数:query 是用户输入的问题,maxIterations 是最大循环次数。设置最大循环次数是为了防止 Agent 无限调用工具。比如模型一直认为信息不够,不断请求工具调用,如果没有上限,程序就可能一直运行下去。

messages 是对话上下文数组。第一条消息是:

new HumanMessage(query)

它代表用户输入。后面每一轮模型回复、每一次工具结果,都会继续追加到这个数组里。

这行代码会调用绑定工具后的模型:

const response = await modelWithTools.invoke(messages);

此时模型会根据完整上下文做判断。如果它认为可以直接回答,就返回普通文本;如果它认为需要工具,就返回带有 tool_calls 的响应。

判断是否还有工具调用的代码如下:

if (!response.tool_calls || response.tool_calls.length === 0) {
    console.log(chalk.bgRed(`AI 回答:${response.content}`));
    return response.content;
}

这就是 Agent 停止循环的条件。只要模型不再请求工具调用,就说明它认为任务已经可以完成,于是返回最终回答。

5.6 执行工具调用:把模型的 tool_calls 变成真实动作

for (const toolCall of response.tool_calls) {
    const foundTool = tools.find(t => t.name === toolCall.name);

    if (!foundTool) {
        continue;
    }

    const toolResult = await foundTool.invoke(toolCall.args);
    const contentStr = stringifyToolResult(toolResult);

    messages.push(new ToolMessage({
        content: contentStr,
        tool_call_id: toolCall.id
    }));
}

模型返回的 tool_calls 不是工具执行结果,而是“需要调用某个工具”的请求。它通常包含:

  • name:要调用的工具名称
  • args:调用工具时传入的参数
  • id:本次工具调用的唯一标识

这行代码根据工具名找到真正的工具对象:

const foundTool = tools.find(t => t.name === toolCall.name);

如果找不到对应工具,说明模型请求了一个不存在的工具。示例里直接 continue 跳过,实际生产环境可以记录日志,或者把错误信息也包装成 ToolMessage 返回给模型,让模型尝试修正。

真正执行工具的是这一行:

const toolResult = await foundTool.invoke(toolCall.args);

invoke 是 LangChain Tool 的调用方法。这里传入的是模型生成的参数 toolCall.args。比如地图工具可能接收地点关键词、城市、经纬度;文件系统工具可能接收文件路径和文件内容。

执行后,工具会返回结果。这个结果可能是字符串,也可能是对象,所以示例中封装了一个转换函数:

function stringifyToolResult(toolResult) {
    if (typeof toolResult === 'string') {
        return toolResult;
    }

    if (toolResult?.content) {
        return JSON.stringify(toolResult.content);
    }

    return JSON.stringify(toolResult);
}

为什么要把工具结果转成字符串?因为 ToolMessagecontent 需要是可以放进对话上下文的内容。无论工具原始返回什么结构,最终都要变成模型可以继续阅读和分析的信息。

最后创建 ToolMessage

messages.push(new ToolMessage({
    content: contentStr,
    tool_call_id: toolCall.id
}));

这里的 tool_call_id 很重要,它用来告诉模型:这条工具结果对应的是前面哪一次工具调用。尤其当模型一次请求多个工具时,tool_call_id 可以保证结果和请求对应起来。

这一步完成后,循环会进入下一轮:

工具结果进入 messages
  ↓
模型再次读取上下文
  ↓
模型决定继续调用工具,或者输出最终答案

这就是 Agent 工具调用的基本闭环。

5.7 执行任务并关闭客户端

await runAgentWithTools(
    '北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件'
);

await mcpClient.close();

第一段代码启动整个任务。模型会根据这句话自动拆解动作:先找地点和酒店,再规划路线,最后调用文件系统工具写入 Markdown 文件。

第二段代码关闭 MCP Client。因为 MCP Client 可能启动了本地子进程,也可能维持着远程连接,所以任务结束后需要关闭资源。

这里要注意变量名要一致。如果前面创建的是:

const mcpClient = new MultiServerMCPClient(...)

最后就应该写:

await mcpClient.close();

不要写成:

await client.close();

否则会出现 client is not defined 之类的错误。

6. 从一次运行看懂完整链路

把前面的知识串起来,可以用一次任务运行来理解整个项目。

用户输入:

北京南站附近的2个酒店,以及去的路线,路线规划生成文档保存到 当前目录 的一个 md 文件

模型第一轮看到这个问题后,会发现自己没有实时地图数据,也不能凭空确认附近酒店,所以它会倾向于调用高德地图 MCP。高德地图 MCP 返回地点、酒店或路线相关信息后,代码会把这些结果包装成 ToolMessage 放回 messages

模型第二轮读取上下文时,就能看到真实工具返回的信息。此时它可能继续调用路线规划工具,分别获取北京南站到两个酒店的路线。路线结果再次通过 ToolMessage 返回给模型。

当地图信息足够后,模型会开始整理 Markdown 文档内容。因为用户要求“保存到当前目录的一个 md 文件”,模型还需要调用 FileSystem MCP,把整理好的内容写入指定目录。

文件写入成功后,文件系统工具会返回执行结果。模型再次读取这个结果,如果确认任务已经完成,就不再发起工具调用,而是输出最终回答。

整个过程可以总结为:

自然语言需求
  ↓
模型判断任务需要哪些工具
  ↓
调用地图 MCP 获取真实信息
  ↓
调用文件系统 MCP 写入文档
  ↓
模型根据工具结果生成最终回复

这也是 MCP + Agent 最有价值的地方:开发者不用把所有步骤写死在业务代码里,而是提供工具能力和执行循环,让模型根据任务动态决定下一步该做什么。

7. 常见问题与优化建议

API Key 建议放到环境变量中。 地图 Key、模型 Key 都不应该直接写进代码。推荐使用 .env 管理,并把 .env 加入 .gitignore

.env

文件系统 MCP 要严格限制目录。 文件系统工具拥有真实读写能力,只给它开放项目工作目录即可,不要开放系统目录、用户主目录或者包含隐私数据的目录。

工具返回结果要做兼容处理。 不同 MCP Server 返回的数据结构可能不一样,有的返回字符串,有的返回对象。统一转换成字符串后再放入 ToolMessage,可以减少模型处理上下文时的问题。

Agent 循环必须设置上限。 maxIterations = 30 的意义是防止模型反复调用工具。如果做生产级应用,还可以在达到上限时抛出错误,或者返回更清晰的失败信息。

生产环境要补充异常处理。 示例代码为了突出主流程,没有展开大量错误处理。真实项目中建议为模型调用、MCP 连接、工具执行、文件写入都增加 try...catch,并记录日志。

try {
    const toolResult = await foundTool.invoke(toolCall.args);
} catch (error) {
    console.error('工具调用失败:', error);
}

总结

MCP 本质上是工具调用协议,但它把工具从“项目里的一个函数”提升成了“可复用的标准服务”。通过 MCP,大模型应用可以统一接入地图、浏览器、文件系统、自定义业务服务等能力。

本文项目的核心链路是:

MultiServerMCPClient 连接多个 MCP Server
  ↓
getTools() 获取工具列表
  ↓
bindTools() 把工具绑定到模型
  ↓
模型根据用户问题生成 tool_calls
  ↓
代码执行对应工具
  ↓
ToolMessage 把工具结果交还给模型
  ↓
模型继续推理,直到任务完成

理解这条链路后,就能看懂大多数基于 MCP 的 Agent 项目。高德地图 MCP 负责提供位置和路线能力,Chrome DevTools MCP 负责浏览器控制,FileSystem MCP 负责本地文件读写,自定义 MCP Server 则可以扩展自己的业务能力。

当这些工具被统一接入后,AI 不再只是回答问题,而是可以在真实环境中查询、操作、整理和保存结果。这正是 MCP 在 AI Agent 开发中最重要的意义。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐