之前学 Function Calling 时,我弄明白了一件事:模型能提出"调用这个函数,参数是这些",真正执行函数的是程序。再往前走一步,如果我写了一个工具,怎么让不同的 AI 应用都能找到它、读懂它需要哪些参数,并按同一种方式调用它?
MCP(Model Context Protocol,模型上下文协议)约定了 AI 应用与外部程序交换信息的方式。拿后端开发里一个很小的需求做例子:日志里写着接口耗时 3250 ms,我想让助手调用一个本地工具,得到 3.250 s。换算只有一行逻辑,却足够看清 MCP 帮我们做了哪些事。
一次调用里有三个角色
| 角色 | 在耗时换算这个例子里做什么 |
|---|---|
| Host(宿主应用) | 用户输入问题的地方,例如支持 MCP 的桌面助手。它管理模型、工具权限和对话,决定是否允许执行一次调用。 |
| Client(客户端) | 宿主内部负责 MCP 通信的部分。它向某个服务端查询工具清单,发送调用请求,再把结果交回宿主。 |
| Server(服务端) | 提供能力的程序。下面由 Node.js 运行的 TypeScript 程序就可以充当服务端,在本机执行换算并返回结果。 |
一个宿主可以连接多个服务端。假设助手同时接入日志工具和文档查询工具,它可以为两个服务端分别维护客户端。模型看到的是宿主提供的工具说明,开发者编写的函数则在对应服务端运行。MCP 官方架构说明
服务端可以提供什么
**Tools(工具)**适合把一项操作交给应用调用。每个工具有名字、用途说明和参数格式。比如 format_duration 接收整数毫秒,返回格式化后的秒数。客户端用 tools/list 发现工具,用 tools/call 发起调用。
**Resources(资源)**适合提供可读取的数据。比如服务端可以用 docs://logging/fields 标识一份日志字段说明,客户端通过 resources/read 读取,宿主再决定是否把它加入模型上下文。
**Prompts(提示模板)**适合提供可复用的任务说明。比如服务端提供一个"分析慢请求"模板,用户选中后,客户端用 prompts/get 取回消息内容,宿主再用它组织对话。这三类能力可以按需实现,下面的小工具只用到 Tools。MCP 官方服务端开发指南
客户端怎样找到并调用函数
MCP 的数据层 规定消息怎么写,使用 JSON-RPC 2.0。请求用 method 表示要做什么,params 装参数,id 用来匹配响应。传输层 负责把消息送到对方。本地工具常用 stdio,宿主启动服务端子进程,通过标准输入和标准输出交换消息。远程服务则可以通过 Streamable HTTP 暴露 MCP 端点。协议架构与传输
以一次换算为例,完整过程可以顺着这几步读:
- 发现服务能力。 在
2026-07-28版协议中,客户端可以先调用server/discover查看服务端支持的版本和能力,也可以直接发带版本信息的请求。较早的教程中,2025-11-25等版本使用initialize握手,协商完成后再发送notifications/initialized。官方版本说明 - 读取工具清单。 客户端调用
tools/list。服务端返回format_duration的名字、说明和inputSchema,其中写明milliseconds是必填整数。有了这份说明,宿主就能把工具提供给模型。 - 发送一次调用。 用户问"把 3250 毫秒换算成秒"。模型提出使用该工具,宿主按自身策略允许后,客户端发送
tools/call。业务参数包含工具名format_duration和arguments: {"milliseconds": 3250}。 - 执行并返回。 服务端检查参数,执行 TypeScript 函数,把
3.250 s放进工具结果。客户端按请求 ID 收到它,宿主可以直接展示结果,也可以交给模型组织回答。
对写后端的人来说,这里面有熟悉的 RPC 调用。MCP 额外约定了"去哪里拿工具清单"和"怎样描述参数",所以宿主可以在运行时认识新工具。
用 TypeScript 写一个本地 MCP 工具
本例使用 Node.js 24.15 和官方 TypeScript SDK 2.4.0。在一个空目录打开 PowerShell,初始化项目并安装依赖。
powershell
npm init -y
npm install --save-exact @modelcontextprotocol/server@2.4.0 zod@4.6.7
把示例源码保存到刚才安装依赖的目录,文件名为 server.mts。.mts 表示这是使用 ES 模块的 TypeScript 文件。Node.js 24.15 可以直接运行下面这段代码。完整源码
typescript
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
function formatDuration(milliseconds: number): string {
const remainder = milliseconds % 1000;
const seconds = (milliseconds - remainder) / 1000;
return `${seconds}.${String(remainder).padStart(3, "0")} s`;
}
function createServer() {
const server = new McpServer({ name: "log-tools", version: "1.0.0" });
server.registerTool(
"format_duration",
{
description: "把非负的整数毫秒换算成秒,保留三位小数。",
inputSchema: z.object({ milliseconds: z.int().nonnegative() }),
},
({ milliseconds }) => ({
content: [{ type: "text", text: formatDuration(milliseconds) }],
}),
);
return server;
}
serveStdio(createServer, { onerror: (error) => console.error(error) });
formatDuration 只负责计算。传入 3250 时,整秒部分是 3,余数是 250。传入 5 时,补零后得到 0.005 s。server.registerTool 把函数接到 MCP 上。客户端通过 tools/list 看到的工具名是 format_duration,调用时 SDK 会按照 inputSchema 检查参数。TypeScript 类型帮助编写代码,Zod 则在程序运行时检查来自客户端的数据。
最后的 serveStdio 负责通过标准输入输出收发协议消息。调试日志用 console.error 输出到标准错误,避免日志混入返回给客户端的消息。官方 TypeScript 开发示例
先试工具,再接入助手
在项目目录运行下面两条命令。第一条查询工具清单,第二条用 Inspector 发起一次真实调用。MCP Inspector
powershell
npx -y @modelcontextprotocol/inspector --cli node ./server.mts --method tools/list
npx -y @modelcontextprotocol/inspector --cli node ./server.mts --method tools/call --tool-name format_duration --tool-arg milliseconds=3250
清单里应当出现 format_duration,第二条命令的文本结果应为 3.250 s。把参数改为 0,预期为 0.000 s。再传 -1,可以观察输入校验返回的错误。
接入支持本地 MCP 的助手时,在宿主配置里选择标准输入输出传输。command 填 node.exe 的绝对路径,可用 (Get-Command node.exe).Source 查看。args 填只含 server.mts 绝对路径的数组。这样即使宿主从其他目录启动,也能找到脚本。加载配置后,要求助手"调用 format_duration 换算 3250 毫秒",在调用记录中核对工具名、参数和结果。