Appearance
Q59 · MCP 服务器开发流程是什么?
客服应用要回答“订单 A123 到哪了”,数据却在原有订单系统中。写一个 MCP Server 的目标,是把这个既有业务能力以统一的接口提供给支持 MCP 的 AI 应用:让应用能发现“查订单”工具,传入订单号,收到可解释的结果。真正难的地方还包括该暴露什么、谁能查、查错或查不到时返回什么、上线后怎样确认它仍可靠。只让程序启动并显示一个工具名,并不算完成开发。
以下 U9、A123、B456 都是教学用假设值:U9 是已获授权的用户,A123 属于 U9;B456 属于另一个用户 U8。模拟结果设为:A123“运输中,9 月 26 日 10:00 到达苏州中转站”。我们只演示读物流,不执行退款或修改订单。MCP 的官方架构把提供能力的程序称为 Server,把 AI 应用中连接它的组件称为 Client;Server 能暴露 Tools、Resources 和 Prompts。MCP 官方架构 · MCP 官方:Understanding servers
开始前先认清术语和代码里的名字
| 名称 | 通俗解释 | 在订单例子里 |
|---|---|---|
| MCP Server | 依照 MCP 协议对外提供能力的程序;可以在本机,也可以部署在远端 | 包装订单系统的查询能力 |
| Tool / 工具 | 可执行的一个操作,具有名称、说明、输入规则和返回结果 | get_order_status 查询一笔订单 |
| Resource / 资源 | 可被客户端读取、作为上下文的资料 | “运输中”等物流状态的说明页 |
| Prompt / 提示模板 | 可复用的交互模板,可带参数;有需要才设计 | “整理物流异常说明”的模板,本例先不实现 |
| Schema / 输入规则 | 描述参数名称、类型、格式、必填条件的约定 | order_id 是形如 A123 的字符串 |
| URI / 资源标识 | 指向一份资源的唯一地址样式,不一定是网页 URL | guide://shipping/status |
| stdio | 标准输入 / 标准输出流;本机 Host 启动 Server 子进程并通过这两条流通信 | 本文的本地演示方式 |
| Streamable HTTP | 通过 HTTP 提供远端 MCP 端点的传输方式 | 将来给多个远端客户端接入时可选 |
| SDK / Zod | SDK 是帮助实现协议的开发包;Zod 是 TypeScript 的输入规则库 | SDK 注册工具,Zod 检查订单号格式 |
| Handler / 处理函数 | 真正运行工具业务逻辑的函数 | 查模拟订单表并返回结果 |
| Inspector | MCP 官方提供的交互式调试工具 | 在接入客服 Host 前列出并调用工具 |
U9 / A123 / B456 | 假设用户与订单标识,不是认证凭证 | A123 可读、B456 不可读 |
isError | 工具结果中标记业务调用失败的字段 | B456 返回“订单不可访问” |
2026 年的官方 TypeScript SDK v2 文档已覆盖 2026-07-28 规范。本文代码按这套 SDK 写;看到旧教程的 @modelcontextprotocol/sdk、server.tool() 或旧的连接握手时,先核对其版本,不要把不同版 API 拼在一起。MCP TypeScript SDK v2 · 2026-07-28 支持说明
第一步:从用户问题反推最小能力
先写下业务输入和输出,再决定暴露形式。用户问的是“一笔订单的最新物流状态”,所以工具名选择 get_order_status,输入只要 order_id。描述写明只读、查当前有权访问的订单、返回最新状态。若工具描述只写“订单工具”,Client 和模型就难判断何时用它;若让模型传 user_id,还容易误把调用参数当认证身份。业务身份应从经过验证的连接或服务端上下文取得,再由后端核验订单归属。
| 候选能力 | 放在哪里 | 为什么 |
|---|---|---|
| 查 A123 最新物流 | Tool:get_order_status(order_id) | 要执行一次动态查询,结果可能随时间变化 |
| 查看“运输中”的含义 | Resource:guide://shipping/status | 可读取的说明资料,客户端决定何时提供给模型 |
| 帮客服写物流异常说明 | 可选 Prompt | 这是复用的写作模板,不是查订单必需步骤 |
| 取消订单 | 暂不暴露 | 会改变业务状态,需另外设计权限、确认和防重复执行 |
图示的是从原订单系统挑出有限能力、包装为 MCP Server,然后先用 Inspector 检查再接入 Host 的顺序。锁表示权限边界,不表示示例代码已经具备真实身份系统。

Tool 和 Resource 的区别并不只在“可读还是可写”:查订单本身也是只读操作,但它需要携带参数、运行查询、处理业务错误,因此适合 Tool;固定状态说明更像可读取资料,适合 Resource。官方 SDK 分别提供 registerTool 与 registerResource,Resource 的读取由客户端发起。MCP SDK v2:Tools · MCP SDK v2:Resources
第二步:确定运行位置与传输方式
若客服 Host 在同一机器启动 Server 子进程,本地开发可先用 stdio。Client 往 Server 的标准输入写协议请求,Server 往标准输出写协议响应。此时 console.log() 会把调试文字混进协议流,导致 Client 解析失败;日志要写到标准错误流,例如 console.error()。官方 SDK v2 的 serveStdio 会处理这条连接。MCP SDK v2:Serve over stdio
若订单能力要部署成供多个远端 Client 使用的端点,则选 Streamable HTTP,并设计 HTTPS、认证、网关、部署和容量。SDK v2 用 createMcpHandler 创建 HTTP 处理器;它的工厂函数会为每次请求构建 Server 实例,可从可信请求上下文取得认证信息。不能只把下方本地演示的 serveStdio 改成一个公开端口就算上线,更不能把固定演示用户 U9 留在生产代码里。MCP SDK v2:Serve over HTTP · MCP SDK v2:Require authorization
第三步:实现一个可以本地运行的最小 Server
下面程序使用 Node.js 20+、TypeScript 和官方 SDK v2。新建一个独立练习目录,执行:
bash
mkdir order-mcp-demo
cd order-mcp-demo
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx把下面内容存为 server.ts,再执行 npx tsx server.ts。程序会等待 Client 连接;直接运行后“没有聊天回答”是正常的,因为它只是 Server。Map 是这里模拟订单系统的内存表;demoUserId 被故意固定为 U9,只用于演示不同订单的分支,没有真实身份认证。orders.get(order_id) 按订单号取记录,取不到得到 undefined。createServer() 每次构建并返回一个 McpServer;registerTool 把名称、描述、Zod 输入规则和异步 Handler 绑在一起。z.object 要求输入是对象,z.string().regex(...) 要求订单号满足字母加三位数字;正则表达式 /^[A-Z][0-9]{3}$/ 中 ^ 和 $ 限定整串,[A-Z] 是大写字母,[0-9]{3} 是三位数字。
ts
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const demoUserId = 'U9';
const orders = new Map([
['A123', { owner: 'U9', status: '运输中,9 月 26 日 10:00 到达苏州中转站' }],
['B456', { owner: 'U8', status: '已签收' }],
]);
function createServer(): McpServer {
const server = new McpServer({ name: 'demo-orders', version: '1.0.0' });
server.registerTool(
'get_order_status',
{
description: '查询当前已授权用户的订单物流;只读,不修改订单。',
inputSchema: z.object({
order_id: z.string().regex(/^[A-Z][0-9]{3}$/, '订单号格式应如 A123'),
}),
},
async ({ order_id }) => {
const order = orders.get(order_id);
if (!order || order.owner !== demoUserId) {
return { content: [{ type: 'text', text: '订单不可访问' }], isError: true };
}
return { content: [{ type: 'text', text: `订单 ${order_id}:${order.status}` }] };
},
);
server.registerResource(
'shipping-status-guide',
'guide://shipping/status',
{ title: '物流状态说明', description: '客服使用的状态解释示例', mimeType: 'text/plain' },
async (uri) => ({
contents: [{ uri: uri.href, mimeType: 'text/plain', text: '运输中:包裹仍在配送链路中。' }],
}),
);
return server;
}
void serveStdio(createServer);async 表示 Handler 可做异步工作,真实场景里会等待订单 API。content 是返回给 Client 的内容块数组;type: 'text' 说明这一块是文本,isError: true 让 Host 和模型知道这是工具业务失败。Resource 的 uri 是客户端要读的资源地址,回调返回 contents 数组;mimeType: 'text/plain' 表示普通文本。末尾的 serveStdio(createServer) 开始监听标准输入输出,void 只是表明这个简短示例不保留它返回的关闭句柄;实际服务应在退出时清理连接和外部资源。SDK 会用 Zod 规则生成对外公布的输入 Schema,并在 Handler 执行前拒绝无效参数。MCP SDK v2:Build your first server · MCP SDK v2:Errors
这段代码可以在本地跑通协议行为,但其授权逻辑只是用固定 U9 做模拟。真实 Server 需要从已验证的身份信息得到当前用户,把查询限定到该用户或租户,并由订单后端再次校验。仅传入 order_id,甚至让模型再传一个 user_id,都不足以证明订单归属。MCP 官方安全建议:State handle / 标识符边界
第四步:用真实 Client 测成功和失败路径
官方 MCP Inspector 可以在不接入完整聊天应用的情况下启动 stdio Server、列出能力并调用工具。下面两条命令在 order-mcp-demo 目录执行;首次运行 Inspector 的 npx 会下载工具。--method 选择协议方法,--tool-name 指定工具名,--tool-arg 传订单号,--format json 让结果方便检查。MCP 官方:Inspector
bash
npx -y @modelcontextprotocol/inspector --cli npx tsx server.ts --method tools/list --format json
npx -y @modelcontextprotocol/inspector --cli npx tsx server.ts --method tools/call --tool-name get_order_status --tool-arg order_id=A123 --format json我用官方 SDK v2 Client 和 Inspector CLI 对上述完整 server.ts 做了实际调用。tools/list 能找到 get_order_status,resources/list 能找到 guide://shipping/status,resources/read 返回状态说明。三种输入的区别如下:
| 输入 | 预期与实测的关键结果 | 为什么 |
|---|---|---|
A123 | 返回“订单 A123:运输中,9 月 26 日 10:00 到达苏州中转站” | 格式合法,模拟记录归 U9 |
B456 | 返回 isError: true 和“订单不可访问” | 格式合法,但模拟记录归 U8;不向 U9 泄露详情 |
abc | 返回 isError: true 和输入校验错误 | 不符合 order_id 的 Schema;Handler 不运行 |
这样测比只看“进程启动了”更有用:tools/list 测发现,A123 测正常调用,B456 测业务权限分支,abc 测输入层,resources/read 测资料内容。还应加真实订单后端的超时、无记录、错误返回,以及 Host 是否能正确呈现错误;必要时用测试用户验证跨用户、跨租户隔离。官方把 Handler 返回的 isError: true 视为模型可读的工具错误,协议级错误则是另一层,不能把所有失败都伪装成“运输中”。MCP SDK v2:Test a server · MCP SDK v2:Errors
第五步:接入、发布和持续维护
本地验证后,再让目标 Host 配置启动命令或远端地址,确认它确实能发现工具与资源。上生产前应按同一业务问题逐项核对:
- 真实数据接入:把内存
Map换成订单 API;限定查询范围、超时和重试,避免把网络失败伪装成“订单不存在”。设置可审计的请求标识,但日志里不写完整个人资料或密钥。 - 身份与权限:远端 HTTP 端点应按部署环境验证访问令牌及所需权限,再把可信身份传给业务层;订单归属在 Server 或后端再次验证。官方 SDK v2 提供 HTTP 鉴权接入方式,MCP 安全文档也明确不能把可猜的标识符当身份凭证。MCP SDK v2:Require authorization · MCP 官方安全建议
- 错误与副作用:区分输入错误、无权限、订单系统超时、内部故障;向模型返回足够处理问题但不泄露内部栈和他人数据的消息。以后若增加取消订单等写工具,还要做明确授权、必要的用户确认、幂等键和审计。
- 兼容与发布:记录 Server 与 SDK 版本、协议版本、工具名称和 Schema 的变化。改参数或删除工具前,检查已有 Host 是否依赖它;发布说明写清可用工具、权限范围、配置方式和示例输入。官方 2026-07-28 规范对发现与请求元数据的处理已不同于旧版初始化流程,接入时应实际测试目标 Client 的协议版本。MCP 官方架构:Discovery · MCP SDK v2:Protocol versions
- 观察与回归:持续记录调用成功率、拒绝率、超时、延迟,以及工具列表变化;保留正常、格式错误、越权和依赖故障的自动化用例。发现工具描述误导模型时改描述与示例,并重新测调用选择。stdio 部署还要确认调试日志不会写入 stdout。
图里的“测试再接入”也包括最后这轮运行维护:Inspector 证明协议交互可用,目标 Host 端到端验证才证明业务体验可用。若工具在 Inspector 中可调用、在 Host 中却不可见,优先检查启动命令、客户端版本、工具暴露配置和鉴权;若工具可见但 A123 查询失败,则看参数校验、授权与订单后端响应。
面试时可以这样说
我会先从业务任务确定最小能力:动态订单查询做成有清楚名称、描述和输入 Schema 的 Tool,固定状态说明做成 Resource,写操作单独审查。然后选运行方式:本机 Host 启动进程用 stdio,远端多人接入用 Streamable HTTP。用当前官方 SDK 注册工具和资源,Handler 调原业务系统;输入 Schema 先挡格式错误,业务层再用可信身份核对订单归属。接着用 Inspector 和真实 Client 测发现、正常调用、越权、无效参数、超时及资源读取。发布时配置鉴权和日志、标记版本变化,持续监控错误与延迟。MCP 统一的是接口和通信,具体业务权限与可靠性仍由 Server 和后端负责。
若追问“为什么示例中的 z.string() 还不够”,可以回答:字符串类型只证明参数形状,不能证明订单属于当前用户;还要核验可信身份和订单记录。若追问“stdio Server 为什么不能 console.log”,可以回答:stdout 正被 Client 当作协议通道解析,普通日志会破坏消息;日志写 stderr。若追问“什么时候加 Prompt”,可以回答:当使用者需要一个可复用的交互模板时再加,查订单这条工具链不必为了凑齐三类能力而加模板。
参考资料
- MCP 官方架构:Server、Client、能力和传输层。
- MCP 官方:Build an MCP server:从创建到测试的官方教程。
- MCP TypeScript SDK v2:Build your first server:当前 SDK 的
McpServer、registerTool、serveStdio示例。 - MCP TypeScript SDK v2:Resources:
registerResource和读取回调。 - MCP TypeScript SDK v2:HTTP 与 Authorization:远端部署及认证入口。
- MCP 官方:Inspector:本地调试与 CLI 调用。
- MCP 官方安全建议:访问控制与标识符风险。