Skip to content

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 / 资源标识指向一份资源的唯一地址样式,不一定是网页 URLguide://shipping/status
stdio标准输入 / 标准输出流;本机 Host 启动 Server 子进程并通过这两条流通信本文的本地演示方式
Streamable HTTP通过 HTTP 提供远端 MCP 端点的传输方式将来给多个远端客户端接入时可选
SDK / ZodSDK 是帮助实现协议的开发包;Zod 是 TypeScript 的输入规则库SDK 注册工具,Zod 检查订单号格式
Handler / 处理函数真正运行工具业务逻辑的函数查模拟订单表并返回结果
InspectorMCP 官方提供的交互式调试工具在接入客服 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 的顺序。锁表示权限边界,不表示示例代码已经具备真实身份系统。

从订单业务接口定义工具和资源,加入权限边界后测试并接入 MCP 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 配置启动命令或远端地址,确认它确实能发现工具与资源。上生产前应按同一业务问题逐项核对:

  1. 真实数据接入:把内存 Map 换成订单 API;限定查询范围、超时和重试,避免把网络失败伪装成“订单不存在”。设置可审计的请求标识,但日志里不写完整个人资料或密钥。
  2. 身份与权限:远端 HTTP 端点应按部署环境验证访问令牌及所需权限,再把可信身份传给业务层;订单归属在 Server 或后端再次验证。官方 SDK v2 提供 HTTP 鉴权接入方式,MCP 安全文档也明确不能把可猜的标识符当身份凭证。MCP SDK v2:Require authorization · MCP 官方安全建议
  3. 错误与副作用:区分输入错误、无权限、订单系统超时、内部故障;向模型返回足够处理问题但不泄露内部栈和他人数据的消息。以后若增加取消订单等写工具,还要做明确授权、必要的用户确认、幂等键和审计。
  4. 兼容与发布:记录 Server 与 SDK 版本、协议版本、工具名称和 Schema 的变化。改参数或删除工具前,检查已有 Host 是否依赖它;发布说明写清可用工具、权限范围、配置方式和示例输入。官方 2026-07-28 规范对发现与请求元数据的处理已不同于旧版初始化流程,接入时应实际测试目标 Client 的协议版本。MCP 官方架构:Discovery · MCP SDK v2:Protocol versions
  5. 观察与回归:持续记录调用成功率、拒绝率、超时、延迟,以及工具列表变化;保留正常、格式错误、越权和依赖故障的自动化用例。发现工具描述误导模型时改描述与示例,并重新测调用选择。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”,可以回答:当使用者需要一个可复用的交互模板时再加,查订单这条工具链不必为了凑齐三类能力而加模板。

参考资料 ​

最后更新2026-09-26
难度P1
频率high
阅读20 min
主题mcp / server / tools
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题