你将构建什么

这是一个刻意保持很小的本地项目:

text
stdio Client
    |
    v
MCP Server
    |
    v
一个只读 Tool:text.stats

Tool 接收文本并返回有界的字符数和单词数,不读取文件、不访问网络、不执行代码,也不修改外部状态。这样的边界更适合第一次协议测试。

本文覆盖构建和检查流程,不声称所有 Client、SDK 版本或平台都使用相同的配置文件。

前置准备

检查运行时:

bash
node --version
npm --version

使用所选 SDK 支持的 Node.js 版本。不要直接把本文的版本判断写进生产支持政策;应在自己的仓库中记录 Runtime 和依赖版本。

1. 创建项目

bash
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

json
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

创建 tsconfig.json

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

typescript
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 前先编译:

bash
npm run build

预期产物是 dist/index.js。TypeScript 编译可以检查导入和本地类型,但不能证明协议兼容、授权或业务正确性。

手动运行进程:

bash
npm start

进程应保持运行并等待 stdio 输入。不要在终端输入任意文本后就认为它是合法 MCP 请求。使用 Ctrl-C 退出。

4. 使用 Inspector 检查

使用 SDK 版本文档中对应的 MCP Inspector:

bash
npx @modelcontextprotocol/inspector node dist/index.js

在 Inspector 中确认:

  1. Initialization 和 Capability 协商完成;
  2. 能看到 text.stats
  3. 普通输入返回 JSON 文本;
  4. 超过 20,000 字符的输入被拒绝;
  5. 诊断日志没有出现在协议响应中;
  6. 失败请求后 Server 仍保持运行。

将 Node.js、SDK、Inspector 和操作系统版本与测试结果一起保存。“Inspector 通过”但没有版本信息,复现证据很弱。

5. 谨慎连接 Client

许多桌面 Client 接受类似下面的配置,但文件路径、Key、重载方式和支持的 Command 取决于具体 Client:

json
{
  "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、认证或副作用。

一手来源