你将构建什么
这是一个刻意保持很小的本地项目:
stdio Client
|
v
MCP Server
|
v
一个只读 Tool:text.stats
Tool 接收文本并返回有界的字符数和单词数,不读取文件、不访问网络、不执行代码,也不修改外部状态。这样的边界更适合第一次协议测试。
本文覆盖构建和检查流程,不声称所有 Client、SDK 版本或平台都使用相同的配置文件。
前置准备
检查运行时:
node --version
npm --version
使用所选 SDK 支持的 Node.js 版本。不要直接把本文的版本判断写进生产支持政策;应在自己的仓库中记录 Runtime 和依赖版本。
1. 创建项目
mkdir mcp-text-stats
cd mcp-text-stats
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install --save-dev typescript @types/node
mkdir src
这些命令只是创建实验环境的示例,不是推荐使用未固定的“latest”版本。应根据当前 SDK 文档解析版本,提交 Lockfile,并记录经过测试的 Runtime。
将解析出的 SDK 与 Zod 版本固定在 package-lock.json。为了可复现,提交 Lockfile 并使用 npm ci。
更新 package.json:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
SDK 的导入路径可能随主版本变化。如果编译器拒绝某个导入,应查阅固定版本的 Release Notes,以版本化方式更新示例,不要静默混用 API。
2. 实现一个有界 Tool
创建 src/index.ts:
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: "mcp-text-stats",
version: "0.1.0",
});
server.tool(
"text.stats",
"Return bounded character and word counts for caller-provided text. This tool does not read files or modify external state.",
{
text: z.string().max(20_000),
},
async ({ text }) => {
const trimmed = text.trim();
const words = trimmed === "" ? 0 : trimmed.split(/\s+/u).length;
return {
content: [
{
type: "text",
text: JSON.stringify({
characters: text.length,
words,
}),
},
],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-text-stats is ready on stdio");
这个示例有四个刻意设计:
- Zod 在 Handler 执行前拒绝超过本地结果预算的输入;
- 操作是确定性的只读行为;
- 描述明确说明 Tool 不做什么;
- 诊断信息使用
console.error,因为 stdout 属于协议流。
Schema 校验不是授权。如果后续 Tool 接收发票 ID、路径、Tenant 或外部目标,Server 必须从可信上下文派生身份,并授权精确对象和副作用。
3. 编译并静态检查
连接 Client 前先编译:
npm run build
预期产物是 dist/index.js。TypeScript 编译可以检查导入和本地类型,但不能证明协议兼容、授权或业务正确性。
手动运行进程:
npm start
进程应保持运行并等待 stdio 输入。不要在终端输入任意文本后就认为它是合法 MCP 请求。使用 Ctrl-C 退出。
4. 使用 Inspector 检查
使用 SDK 版本文档中对应的 MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.js
在 Inspector 中确认:
- Initialization 和 Capability 协商完成;
- 能看到
text.stats; - 普通输入返回 JSON 文本;
- 超过 20,000 字符的输入被拒绝;
- 诊断日志没有出现在协议响应中;
- 失败请求后 Server 仍保持运行。
将 Node.js、SDK、Inspector 和操作系统版本与测试结果一起保存。“Inspector 通过”但没有版本信息,复现证据很弱。
5. 谨慎连接 Client
许多桌面 Client 接受类似下面的配置,但文件路径、Key、重载方式和支持的 Command 取决于具体 Client:
{
"mcpServers": {
"mcp-text-stats": {
"command": "/absolute/path/to/node",
"args": ["/absolute/path/to/mcp-text-stats/dist/index.js"]
}
}
}
Client 启动进程时使用绝对路径。不要把 Secret 放进配置文件。修改配置后,按照 Client 文档执行重载或重启,并检查日志。
Client 界面显示 Tool,不代表模型一定会正确选择,也不代表未来的 Tool 已获得授权。增加副作用前,先用无害输入测试真实 Client 流程。
MCP 能力原语
本项目只暴露一个 Tool。MCP 还定义 Resources 和 Prompts,但只有在契约清晰时才应添加:
| 原语 | 用途 | 需要定义的边界 |
|---|---|---|
| Tool | 请求工作或副作用 | 身份、对象、Purpose、幂等、超时 |
| Resource | 提供数据或上下文 | 访问、时效、大小、敏感性、删除 |
| Prompt | 可复用交互模板 | 调用者、不可信内容、审批、输出处理 |
不要直接暴露类似 env://{name} 的任意环境变量 Resource。没有 Allowlist 时,它可能泄露 Token 和连接字符串。
常见故障
进程立即退出
确认 server.connect(transport) 已等待、编译产物存在,并且启动命令使用与构建相同的 Node.js Runtime。
Client 看不到 Tool
直接运行编译后的文件,查看 stderr,确认 Tool 在 connect 前注册,并检查 Client 配置是否指向正确文件。是否需要重启取决于具体 Client。
协议流损坏
搜索源码和依赖对 stdout 的写入。诊断信息使用 stderr,并正确配置第三方日志库。
Tool 结果异常
先脱离模型测试 Handler。覆盖 Schema 边界、结果大小、错误分类,以及空字符串、Unicode 和对抗性输入。
Handler 抛出异常
不要捕获所有异常后报告成功。区分预期校验、授权、依赖、超时、取消和未知错误;返回不泄露 Secret 或内部路径的错误,并记录有界诊断证据。
远程或有状态部署前
本地 stdio 示例刻意省略远程问题。切换到 HTTP 或共享 Server 前:
- 选择并固定当前 Transport Profile,不要默认使用旧式 SSE;
- 按拓扑认证调用者;
- 校验 Issuer、Audience/Resource、Scope、Expiry 和 Key Rotation;
- 有 Session 时绑定 Principal、Tenant 和 Transport;
- 每次调用授权精确对象和副作用;
- 限制请求/结果字节、执行时间、并发和成本;
- 实现取消、重试分类和幂等;
- 从 Telemetry 中脱敏 Token、原始参数和敏感结果;
- 测试跨 Tenant、Tool Result Injection、重连、重复投递和回滚。
认证用于识别调用者,不会自动授予其访问所有对象或 Tool 的权限。
生产准备清单
- [ ] 固定 Node.js、SDK、Zod 和 Inspector 版本。
- [ ] 保存 Lockfile 并记录构建命令。
- [ ] 保持 stdout 只承载协议,stderr 承载诊断。
- [ ] 定义窄化 Schema 和有界结果。
- [ ] 将 Schema、描述、Annotation、Prompt 和 Result 视为不可信数据。
- [ ] 从可信应用状态派生身份、Tenant、所有权、价格和 Role。
- [ ] 在引入副作用前增加授权、超时、取消、幂等和配额。
- [ ] 测试准确的 Client 和 Transport Profile。
- [ ] 规划凭证轮换、撤销、删除、回滚和事件响应。
总结
有用的第一个 MCP Server 应足够小,能够编译、检查和解释。先从一个有界 Tool 开始,保持 stdio 流干净,固定版本并测试真实 Client 路径。只有在每个新边界都有明确契约和对应测试后,再增加 Resources、Prompts、远程 Transport、认证或副作用。