MCP协议详解:AI工具调用的标准接口与Server搭建实践
2026/9/15 23:19:39 网站建设 项目流程

1. 为什么需要 MCP:AI 工具调用从一开始就缺一张“标准插座”

1.1 大模型时代的工具调用困境

过去两年我一直在做 LLM Agent 相关的东西,最头疼的往往不是模型推理能力,而是“如何让模型可靠地调用外部能力”。举个例子:你做了一个智能客服,模型需要查订单、查库存、退换货,每次对接一个新业务系统,都要单独写一套适配逻辑。业务 API 一变,适配代码跟着改,模型 prompt 里的工具描述也要同步改。这套流程做三四个系统还能忍,做到第十个,基本就变成维护灾难了。

MCP(Model Context Protocol)解决的就是这个问题。它的目标不是给某个模型做私有接口,而是像 USB-C 一样,形成一套“AI 工具接口标准”。MCP Server 暴露工具,MCP Client 负责连接,任何支持 MCP 的 AI 应用都能直接发现并调用这些工具,不需要关心服务端是 Python 写的还是 Node 写的,也不用关心协议细节是 REST 还是 SDK 封装。你可以把它理解成“AI 世界的 USB 接口”:鼠标、键盘、显示器都按同一套标准接入,即插即用。

这个需求的本质在于,大模型本身没有“执行能力”,它只能生成文本和结构化参数。真正的动作,比如查数据库、发 HTTP 请求、操作文件系统,必须由外部程序完成。过去这些外部程序是零散的自定义函数,互相之间没有统一描述方式,模型天然难以泛化。MCP 把这些外部能力变成了可被发现、可被描述、可被调用的标准资源,模型只需要按协议读取工具列表,然后按规范传参。

1.2 MCP 与 Function Calling 的本质区别

很多人会把 MCP 和 Function Calling 混为一谈,这其实是两个层面的东西,最好分清楚。

Function Calling 是模型侧的一种推理能力。模型根据用户输入和工具描述,输出一段结构化的调用指令,比如“调用 get_weather,参数 city=beijing”。它解决的是“模型如何决策去调用工具”,本质上是模型输出格式的约定。不同厂商有各自的实现,OpenAI 有 function calling,Anthropic 有 tool use,Google Gemini 也有自己的 function declaration。它们互不兼容,但解决的是同一个问题。

MCP 是应用侧的协议。它解决的是“工具如何被描述、如何被发现、如何被调用”。MCP 不关心模型内部怎么生成调用参数,它定义的是客户端和服务端之间的通信规则。比如客户端向 MCP Server 请求一个工具列表,拿到 JSON Schema 格式的工具描述,然后当模型决定调用某个工具时,客户端再通过 MCP 协议把调用请求发给 Server 执行。

两者之间的关系是互补的:MCP 负责工具标准化,Function Calling 负责模型决策。你在实际项目中完全可以两者都用:模型侧用厂商的 Function Calling 能力做决策,工具侧通过 MCP 统一接入,这样换模型厂商时,工具层完全不用动。

1.3 核心角色:Host、Client、Server 各管什么

MCP 协议里定义了三个清晰的角色,理解它们是你上手的第一步。

角色作用常见实例
Host用户直接交互的应用程序,负责调度模型和工具Claude Desktop、Cursor、自研 Agent 应用
Client嵌在 Host 内部,负责与 MCP Server 建立连接、维护会话SDK 中的 Client 实例,每个 Server 一个
Server暴露工具、资源、提示的独立进程或服务文件系统 MCP Server、Figma MCP Server、自研工具服务

Host 可以连接多个 Server,每个 Server 对应一个 Client。实际开发中,我们通常在自己的 Agent 应用里创建多个 MCP Client,每个 Client 连接一个远程 Server,然后把这些客户端收集到的工具统一喂给模型。这个过程很像 IDE 连接多个插件:每个插件是一个独立进程,通过协议与 IDE 通信。

这里想强调一个重要概念:MCP 的核心价值在于标准化了 Host 与 Server 之间的交互。Server 不需要知道用户用的是什么模型,Host 也不需要知道工具服务端的实现语言。只要双方都遵守 MCP 协议,就能完成工具赋能。所以哪怕你当前只做一个很小的 Agent 项目,也值得按这套结构抽象,避免未来重复造轮子。

2. MCP 协议原理拆解:一次带 initialize 的握手,定义了 AI 访问外部世界的边界

2.1 传输层:stdio 与 Streamable HTTP

MCP 的传输层经历了几个阶段。早期主要支持 stdio 和 SSE,后来官方又引入了 Streamable HTTP,逐渐把远程传输统一起来。

stdio 模式指 MCP Server 作为子进程启动,客户端通过标准输入和标准输出与 Server 通信。这种模式最适合本地工具,比如文件系统操作、代码分析、数据库迁移脚本。好处是进程隔离,Server 崩溃不会拖垮主应用,也不存在端口占用问题,安全性相对好控制。坏处是只能本机用,不方便跨网络部署。

Streamable HTTP 模式则是把 MCP Server 跑成一个 HTTP 服务,客户端通过 POST 发送 JSON-RPC 请求,服务端可以返回单次响应,也可以返回 SSE 流。这解决了远程调用和跨设备协作的问题,比如团队内部共享一个代码审查 MCP Server,所有人通过内网访问。

官方 SDK 对两种模式都有支持,但底层消息格式完全一致。也就是说,同一个 Server 逻辑,只需要换一层 Transport,就能从本地进程变成远程服务。这也是 MCP 设计得比较聪明的地方:传输层可插拔,上层协议稳定。

2.2 消息格式:JSON-RPC 2.0 的三类消息

MCP 的消息格式基于 JSON-RPC 2.0,这是一种非常轻量的协议规范。它只有三类消息:请求、响应和通知。

请求消息必须包含 id、method、params,服务端处理后必须返回带相同 id 的响应。通知消息则不需要 id,也不需要响应,是单向传递的。响应消息里要么是 result,要么是 error,不会两个同时出现。

下面是一个典型的请求示例:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "city": "beijing" } } }

对应的响应:

{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "北京:晴,25℃" } ], "isError": false } }

为什么用 JSON-RPC 而不是 HTTP REST?因为 MCP 会话是长连接的,双向通信需求多。JSON-RPC 天然支持请求-响应模式,也支持服务端主动发通知,语义更统一。而 REST 要区分 resource、action、callback 一堆概念,反而复杂。实际阅读源码时你会发现,几乎所有 MCP SDK 的核心层都在处理 JSON-RPC 消息的路由,理解这一点后看源码会顺畅很多。

2.3 建立会话的完整握手流程

MCP 会话建立不是直接开始调用工具,而是必须先走一轮握手协商。这个设计非常像 SSH 或者 TLS 握手,目的是确认双方能力,避免后续请求版本不兼容。

具体流程如下:

  1. 客户端发送initialize请求,带上客户端信息、协议版本、期望的能力标签。
  2. 服务端返回支持的最高协议版本、服务端信息、能力标签。
  3. 客户端发送notifications/initialized通知,表示初始化完成。
  4. 双方进入就绪状态,开始正常通信。

这里的能力协商是关键。比如客户端声明自己支持采样能力,服务端就会知道自己可以主动向客户端请求模型生成结果。如果服务端声明自己支持工具列表变更通知,客户端就会监听notifications/tools/list_changed,以便动态刷新工具列表。

用代码看更清晰。在 TypeScript SDK 中,客户端 connect 时会执行:

const result = await this.request({ method: "initialize", params: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: this.capabilities, clientInfo: clientInfo } }, InitializeRequestSchema);

然后根据返回的 capabilities 设置本地方便后续使用。这一步做不好,后面就容易出现“工具列表为空”或“方法不存在”这类问题。

2.4 工具、资源、提示三类原语

MCP 并不仅仅支持工具调用,它还定义了三个互补的原语:Tools、Resources、Prompts。

Tools 用于执行动作,是“动态的、会改变状态”的操作,比如发送邮件、创建订单。模型通过tools/call触发。Resources 用于提供上下文,是“静态或半静态的数据”,比如文件内容、数据库 schema、设计稿标注。客户端可以用resources/read读取。Prompts 则是一段可复用的提示词模板,用于规范交互方式,比如“按项目规范生成提交信息”。

原语用途典型方法
Tools执行动作tools/list, tools/call
Resources提供上下文resources/list, resources/read
Prompts复用提示逻辑prompts/list, prompts/get

为什么需要区分?因为模型天然需要两种信息:一是“可以做什么动作”,二是“有什么知识可用”。如果全堆在工具里,模型容易被工具数量淹没。把可读资源单独拆出来,客户端可以根据用户问题主动拉取相关资源塞入上下文,这样更高效。

像我们常听到的 Figma MCP、蓝湖 MCP,其实都是资源和工具的混合体:资源端暴露设计稿节点的结构化数据,工具端提供“获取选中图层”“导出切图”等操作。MCP 标准化了这类插件的接口,让 AI 可以直接读取设计稿上下文,而不需要每个设计工具都开发一套私有 AI 插件。

3. 从零到一:快速搭建一个 MCP Server(TypeScript 篇)

3.1 先从脚手架开始:依赖与最小工程结构

理论讲再多,不如动手写一个 MCP Server。我习惯用 TypeScript + 官方 SDK,因为类型提示完整,注册工具时不容易写错 schema。

先初始化项目:

mkdir my-mcp-server cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript tsx @types/node

zod 是参数校验库,MCP SDK 用它来声明工具参数 schema,比手写 JSON Schema 舒服很多。然后创建tsconfig.json,开启 NodeNext 模块解析:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", "strict": true }, "include": ["src"] }

最小工程结构只需要两个文件:src/index.ts作为入口,src/server.ts放 Server 逻辑。对于本地工具,入口就是启动一个 stdio Server。

3.2 写一个支持加减乘除的计算器工具

我们用McpServer这个高层封装来快速写一个计算器工具。先看代码:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "calculator-mcp", version: "1.0.0" }); server.registerTool( "calculate", { description: "执行加、减、乘、除运算", inputSchema: { a: z.number().describe("第一个操作数"), b: z.number().describe("第二个操作数"), op: z.enum(["add", "sub", "mul", "div"]).describe("运算符") } }, async ({ a, b, op }) => { let result: number; switch (op) { case "add": result = a + b; break; case "sub": result = a - b; break; case "mul": result = a * b; break; case "div": if (b === 0) throw new Error("除数不能为 0"); result = a / b; break; } return { content: [{ type: "text", text: String(result) }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);

注意registerTool的三个参数:第一个是工具名,第二个是描述和参数 schema,第三个是真正的执行函数。模型看到的工具描述就是description这个字段,写得越清楚,模型调用准确率越高。

这里的inputSchema会被 SDK 自动转换成 JSON Schema,最终通过tools/list返回给客户端。这也是我推荐用 SDK 的原因:不需要手写 JSON Schema,zod 定义就是单一事实来源。

3.3 用 MCP Inspector 本地验证

写完后直接接客户端容易到处踩坑,我建议先用官方调试工具 MCP Inspector 验证。启动命令很简单:

npx @modelcontextprotocol/inspector node dist/index.js

Inspector 会启动一个本地 Web 页面,你可以看到 MCP Server 暴露的所有工具,手动传参调用,观察返回结果。它本质上是把 MCP Host、Client、Server 三者的交互可视化,非常适合排查“工具注册了但调用失败”这类问题。

我在调试时会重点看三块:

  • Tools列表里工具名和 schema 是否符合预期。
  • 调用时参数校验是否正确,错误信息是否友好。
  • 资源列表(如果有)能否正常读取。

这一套流程熟练之后,新写的 MCP Server 基本五分钟就能完成验证,不用反复去重启客户端。

3.4 接入客户端:以支持 MCP 的桌面端为例

本地验证通过后,就可以把 Server 接入真实客户端。以 Claude Desktop 为例,在它的配置文件中增加一个mcpServers节点:

{ "mcpServers": { "calculator": { "command": "node", "args": ["/absolute/path/to/dist/index.js"] } } }

注意这里必须写绝对路径,相对路径在部分客户端中经常失效。配置完成后重启客户端,你应该能看到计算器工具已经挂载成功。如果你用的是 Cursor 或其他支持 MCP 的 IDE,配置方式大同小异,核心都是告诉客户端“启动哪个命令来拉起 Server”。

4. 源码走读:MCP SDK 的核心机制

4.1 Protocol 类是如何路由消息的

进入源码部分,我们先看所有 MCP 通信的底层核心:Protocol类。它负责发送请求、接收响应、分发通知,是所有 Server 和 Client 的公共基类。

在 SDK 源码里,请求发送大概长这样:

protected request<T>(message, schema) { return new Promise((resolve, reject) => { const id = this._requestId++; this._pendingRequests.set(id, { resolve, reject, schema }); this._transport.send({ ...message, id }); }); }

这里的_pendingRequests是一个 Map,key 是请求 id,value 是 promise 的 resolve/reject 函数。当 transport 收到响应时,Protocol 根据响应里的 id 找到对应的 pending 请求,调用 resolve 或 reject。如果超时还没收到响应,就会触发拒绝。

这个设计其实和 HTTP 请求库很像。好处是协议层不关心消息内容是什么,只要 id 对得上就能路由。所以你在写自定义 MCP Server 时,不需要自己管理请求映射,SDK 已经帮你做好了。

4.2 Server 层的 registerTool 到底做了什么

很多人好奇registerTool内部是不是直接存一个函数,然后tools/call来了就调用。其实没那么简单。

McpServer实现里,registerTool会做三件事:

  1. 把工具定义存到一个内部的_toolsMap 中,key 是工具名。
  2. 把 zod schema 通过zodToJsonSchema转换成 JSON Schema,存为工具的描述字段。
  3. 给底层的Server实例注册tools/listtools/call两个请求处理器。

也就是说,tools/list不是 SDK 自动从注册表生成,而是每次请求时遍历_toolsMap 动态构造响应。tools/call则是从请求参数里取出 name,查 Map 得到对应的 handler,再调用它。

关键点在参数校验。SDK 会用原始 zod schema 对传入的 arguments 做一次校验,如果模型传参给错了,你会收到 Invalid Params 错误,而不会直接跑进业务函数里。这种设计避免了很多运行时错误。

4.3 Client 端如何维护会话状态

Client 端的核心工作是维护与 Server 的会话状态。SDK 里有一个Client类,它的connect方法会传入 transport,然后执行握手流程。

过程中会保存服务端返回的 capabilities 和 serverInfo,之后客户端的请求都基于这些能力变化。比如如果服务端声明支持采样,客户端就会允许服务端反向调用sampling/createMessage。这个过程在源码里的注释写得很直白:"If the server announces that it supports sampling, then we should stfu and let it do so."

实际开发中,你如果用自己的代码实现 MCP Client,需要特别注意两点:初始化握手必须严格等待initialize响应之后再发initialized通知;任何工具调用都要在握手完成后才允许发起。顺序乱了,Server 会直接拒绝请求。

4.4 错误协议与取消机制

MCP 协议错误码基本沿用 JSON-RPC。常见的有:

错误码含义
-32700解析错误,JSON 格式不对
-32600无效请求,消息结构不符合标准
-32601方法不存在,Server 没实现该 method
-32602参数无效,schema 校验不过
-32603内部错误,业务逻辑抛异常
-32000Server 未初始化完成
-32001未知方法

源码里Protocol类捕获到异常后,会统一包装成McpError,带上错误码和消息。如果你的 Server 内部抛出普通 Error,SDK 默认会把它包装成-32603。如果想给模型更友好的错误信息,建议在业务代码里主动 catch,然后抛McpError或返回带isError的 result。

另外 MCP 支持超时和取消。客户端发送请求时会开启超时计时,默认大约 60 秒,超时后 pending 请求会被 reject。如果模型中途反悔了,客户端可以发送取消通知。理解这一点后,再处理长时间运行的工具任务时,你就能想到用进度通知 + 调整超时时间,而不是干等。

5. 实际落地中的常见问题与排查技巧

5.1 stdio 模式 Server 启动失败的几个原因

本地跑 MCP Server 最经典的问题是“客户端里看不到工具”。大概率不是代码问题,而是 Server 没启动成功。我在实践中总结了一套排查顺序:

先手动启动一次 Server,看有没有报错:

node dist/index.js

如果手动启动正常,再看配置文件。常见错误包括路径写错、加了多余引号、command 写成了相对路径。JSON 配置里不允许注释,如果从网上拷的模板带注释,会导致整个配置解析失败。最后确认 Node 版本,MCP SDK 要求 Node 18 以上,老版本跑不起来。

用 Inspector 先验证是很好的习惯,它能帮你区分是 Server 本身挂了,还是客户端配置问题。

5.2 tools/call 容易踩的超时与流式坑

做 MCP Server 时,最容易被忽略的是工具执行时间。默认超时通常是一分钟,但真实业务里查个大报表、等个外部 API,很容易超过。如果 Server 端是同步阻塞模型,请求会长期挂起,直到超时。

解决方案有三种:

  • 如果是快速任务,直接把超时时间调大,比如 120 秒。
  • 如果是长任务,把工具设计成“提交任务 -> 返回任务 ID -> 轮询状态”两步模式。
  • 使用 MCP 进度通知,在长任务执行过程中持续发送notifications/progress,让客户端知道任务还在进行。

还有一点需要注意:工具返回内容过大时,不要全部塞进content字段。模型上下文窗口有限,你返回一个 500KB 的 JSON,模型根本吃不消。更好的做法是把大文件写入临时文件或对象存储,然后通过resources暴露一个只读资源,再在工具返回里给出资源链接和摘要。

5.3 多 Server 场景:上下文越大越容易“工具爆炸”

当你接入的 MCP Server 越来越多,会发现模型选择工具的准确率在下降。因为每个工具描述都会占据上下文,工具一旦多到上百个,模型会挑花眼,还会在无关工具上浪费 token。

我的经验是做分组和按需加载。比如按业务域拆多个 Server,只把当前会话需要的 Server 装载进来。或者在一个 Server 里给工具名加前缀,让模型更容易区分。MCP 的notifications/tools/list_changed通知也很有用,Server 可以主动告诉客户端“我的工具列表变了”,客户端重新拉取后再决定是否更新暂存区。

还有一点值得提:很多 MCP 客户端会把工具 schema 缓存起来,如果你在 Server 端改了工具定义但没有发给客户端,模型用的还是旧 schema,调用自然失败。遇到这种问题,先检查是不是缓存没刷新。

5.4 安全边界:给模型开一扇门,也要装好锁

MCP Server 本质上给模型开放了执行能力,这意味着安全边界必须提前设计。我见过一个团队做了一个“数据库通用查询工具”,字段传进去直接拼接 SQL,结果模型被诱导输出了全部用户数据。这不是模型的问题,是工具设计的问题。

建议从几个层面做限制:

  • 白名单机制:只暴露业务必要的操作,不该暴露的绝不注册成工具。
  • 参数强校验:不是 schema 层面校验完了就够,还要做业务权限判断。
  • 人工审批流:对高风险的写操作,要求用户在客户端上点击确认后再执行。
  • 网络隔离:把 MCP Server 部署在与核心业务隔离的环境中,尽量不开放到公网。远程 MCP 必须走鉴权,比如 OAuth 授权,不能裸奔。

记住,MCP 工具是给模型用的 API,它的攻击面比普通 API 更大,因为输入是模型生成的,更容易出现不可预测的边界场景。

6. 生态观察与选型建议

6.1 MCP 生态正在成为 AI Agent 的“接口标准”

最近两年,MCP 生态肉眼可见地在膨胀。设计领域有 Figma MCP、蓝湖 MCP,开发领域有 GitHub MCP Server、数据库 MCP,办公领域也有各种文档、表格、日历的 MCP 适配器。你会发现大家不再纠结于“如何把工具接进 AI”,而是直接问“这个系统有没有 MCP Server”。

这个趋势背后的逻辑很清晰:AI Agent 需要一套跨厂商、跨语言、跨部署环境的标准化接口。早期各家都在做私有插件协议,开发者接入一个平台要学一套 API,严重阻碍了工具生态的复用。MCP 把“插件系统”抽成了一层公共协议,工具作者写一遍,所有兼容 MCP 的客户端都能用。

对我们做工程的人来说,这意味着选型上可以更乐观。当你需要给某个系统做 AI 化改造时,先查一下它有没有现成 MCP Server,有就直接接入,没有就按 MCP 标准封装。这套投入不是一次性买卖,未来任何支持 MCP 的应用都能复用。

6.2 什么时候该上 MCP,什么时候不用

虽然 MCP 很火,但也不是所有场景都必须上。我自己的判断标准是这样的:

如果只是给模型暴露一两个内部 HTTP API,比如自己的后端查订单接口,那么直接在代码里写一个工具函数就够了。因为这时候不存在“多个客户端复用工具”的需求,引入 MCP 反而多了一层进程通信和配置复杂度。

但如果你的工具要服务多个 AI 应用,或者要开放给团队其他成员、甚至外部生态使用,那就应该上 MCP。举个例子,公司内部有一个代码规范检查服务,之前只给 A 团队用,后来 B 团队也想用,再后来想接进 IDE 插件,这时候统一包装成一个 MCP Server 最划算。

另外,资源的标准化也值得考量。如果你除了工具,还想把知识库文档、设计稿上下文、数据库元数据开放给模型用,MCP 的 Resources 和 Prompts 体系比你自己设计一套上下文装载方案要成熟得多。这也是选择 MCP 的重要理由之一。

最后再分享一个小经验:刚开始做 MCP 时,不要贪多求全,先从一个高频工具开始,跑通流程后再逐步扩展。我自己实战下来发现,MCP 最难的部分不是写 Server,而是设计出“模型真正用得顺手”的工具描述与参数 schema。好的工具描述能让模型调用准确率从不到 60% 提升到 90% 以上。这背后没有太多技巧,就是反复调描述语言、观察失败案例、自然迭代。

MCP 这套协议已经逐渐成为 AI Agent 时代的“基础设施”。如果你正准备做智能体或者工具集成,值得从今天开始动手试一试。毕竟,标准只有被用了,才会创造价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询