Skip to content

所在组:互操作 | 上一组出口:能把模型输出接入会话与状态 | 本组出口:能实现/调试一个 MCP server(含路径安全与错误语义),并按 2026-07-28 版本意识做迁移决策 前置:工具调用契约、工具执行工程 | 下一步:A2A(Agent↔Agent 方向)、协议地图

1. 概述 ​

结论先讲:没有 MCP 时,M 个 AI 应用接 N 个外部系统要写 M×N 套集成;MCP 把它变成 M+N——应用实现一次 client,系统实现一次 server,中间是开放协议。MCP(Model Context Protocol,模型上下文协议)是连接 LLM 应用与外部数据源和工具的开放标准:JSON-RPC 2.0 消息、三角色(host/client/server)、服务端三原语(tools/resources/prompts)、两种官方传输(stdio、Streamable HTTP)。

心智模型:三角色 ​

  • Host:发起连接的 LLM 应用;内含一个或多个 client。
  • Client:host 内的连接器,维护与一个 server 的专属连接,把 tools/resources/prompts 呈现给模型。
  • Server:提供上下文与能力的服务程序——本地子进程或远程 HTTP 服务。

一个 server 挂多个 client,一个 host 挂多个 server;每个 client 与一个 server 一一对应。

何时使用 / 何时不用 ​

  • 用:Agent 需要接外部工具/数据(文件、数据库、API、浏览器);工具集需要跨应用复用;需要统一授权与审计边界。
  • 不用:
    • 同 host 内的普通函数调用——直接函数即可(见复杂度决策阶梯梯级 3);
    • Agent↔Agent 协作——那是 A2A 的方向;
    • 只是想复用流程知识——Skills 不是协议。

决策表:与相邻协议/机制对比 ​

机制方向控制权状态信任域最低复杂度
直接函数调用进程内代码即边界无进程内写一个函数
MCP(本页)Agent ↔ 工具/数据server 声明 + client/host 授权确认2026-07-28 起无状态请求跨进程/跨网络边界一个 server + 工具 schema
A2AAgent ↔ Agent双方自治 + 能力发现(Agent Card)异步 Task跨组织双方实现协议端点
Skills知识 → 上下文description 触发静态文件宿主进程内一个文件夹

历史版本里程碑(均核验于 2026-09-01) ​

修订关键变化
2024-11-05早期修订;定义了 HTTP+SSE 传输(后被弃用)
2025-03-26HTTP+SSE 传输在此版本被弃用,由 Streamable HTTP 取代
2025-11-25最后一个 legacy 代修订(initialize 握手代)
2026-07-28当前修订(modern 代):协议改为无状态,移除 initialize 握手与 Mcp-Session-Id,新增 server/discover,请求经 _meta 携带版本与能力;ping、logging/setLevel 移除;Roots/Sampling/Logging 进入弃用流程

版本意识是本页主线:网上大量教程(含本仓旧笔记)描述的是 legacy 代行为,读时先对代次再对细节。

2. 使用 ​

最小实战:零依赖实现一个 echo server + 驱动它的 client——不用任何 SDK,Node 内置模块手写 JSON-RPC over stdio,15 分钟内看到完整生命周期(握手→发现→调用→错误→优雅关闭)。

步骤 1:创建 server.mjs ​

javascript
// server.mjs — 零依赖最小 MCP server(node 内置模块)
// 实现 legacy 代交互面(initialize 握手, <=2025-11-25):
// 双代(dual-era)server 必须继续应答 legacy client,这是当前互操作基线。
// 从 stdin 逐行读 JSON-RPC 2.0;每条回复一行写到 stdout。
// 日志只写 stderr(stdout 是协议通道)。
import { createInterface } from "node:readline";

const PROTOCOL_VERSION = "2025-11-25"; // 最新的 legacy 代修订

function result(id, payload) {
  return { jsonrpc: "2.0", id, result: payload };
}
function error(id, code, message) {
  return { jsonrpc: "2.0", id, error: { code, message } };
}

function handle(msg) {
  if (msg.method === "initialize") {
    return result(msg.id, {
      protocolVersion: PROTOCOL_VERSION,
      capabilities: { tools: {} },
      serverInfo: { name: "echo-server", version: "1.0.0" },
    });
  }
  if (msg.method === "notifications/initialized") return null; // 通知:不回复
  if (msg.method === "tools/list") {
    return result(msg.id, {
      tools: [{
        name: "echo",
        description: "Echoes the input text back, prefixed with 'echo:'.",
        inputSchema: {
          type: "object",
          properties: { text: { type: "string" } },
          required: ["text"],
        },
      }],
    });
  }
  if (msg.method === "tools/call") {
    const { name, arguments: args } = msg.params ?? {};
    if (name !== "echo") {
      return error(msg.id, -32602, `Unknown tool: ${name}`); // Invalid params
    }
    if (typeof args?.text !== "string") {
      return error(msg.id, -32602, "arguments.text must be a string");
    }
    return result(msg.id, { content: [{ type: "text", text: `echo: ${args.text}` }] });
  }
  // 其余一律 JSON-RPC "Method not found"
  return error(msg.id, -32601, `Method not found: ${msg.method}`);
}

const rl = createInterface({ input: process.stdin });
rl.on("line", (line) => {
  if (!line.trim()) return;
  let msg;
  try {
    msg = JSON.parse(line);
  } catch {
    console.error(`[server] non-JSON line ignored: ${line.slice(0, 40)}`);
    return;
  }
  const reply = handle(msg);
  if (reply) process.stdout.write(JSON.stringify(reply) + "\n");
});
rl.on("close", () => process.exit(0)); // client 关闭 stdin = 优雅关闭信号
console.error("[server] echo-server ready on stdio");

步骤 2:创建 client.mjs ​

javascript
// client.mjs — 零依赖最小 MCP client:拉起 server, 走完一次完整交互,
// 打印所有 JSON-RPC 消息;任一预期步骤失败则退出码 1。
import { spawn } from "node:child_process";

const child = spawn(process.execPath, ["server.mjs"], { stdio: ["pipe", "pipe", "inherit"] });

let nextId = 1;
const pending = new Map(); // id -> { method, resolve }
let buf = "";

child.stdout.on("data", (chunk) => {
  buf += chunk;
  let nl;
  while ((nl = buf.indexOf("\n")) !== -1) {
    const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
    if (!line.trim()) continue;
    const msg = JSON.parse(line);
    console.log(`<-- ${JSON.stringify(msg)}`);
    pending.get(msg.id)?.resolve(msg);
    pending.delete(msg.id);
  }
});

function request(method, params) {
  const id = nextId++;
  return new Promise((resolve) => {
    pending.set(id, { method, resolve });
    const msg = { jsonrpc: "2.0", id, method, ...(params ? { params } : {}) };
    console.log(`--> ${JSON.stringify(msg)}`);
    child.stdin.write(JSON.stringify(msg) + "\n");
  });
}
function notify(method) {
  const msg = { jsonrpc: "2.0", method };
  console.log(`--> ${JSON.stringify(msg)}`);
  child.stdin.write(JSON.stringify(msg) + "\n");
}

const failures = [];
const expect = (ok, label) => { console.log(`${ok ? "PASS" : "FAIL"}  ${label}`); if (!ok) failures.push(label); };

// 1. legacy 生命周期:initialize -> notifications/initialized
const init = await request("initialize", {
  protocolVersion: "2025-11-25",
  capabilities: {},
  clientInfo: { name: "minimal-client", version: "1.0.0" },
});
expect(init.result?.serverInfo?.name === "echo-server", "initialize returns serverInfo");
expect(init.result?.capabilities?.tools !== undefined, "server advertises tools capability");
notify("notifications/initialized");

// 2. 发现工具
const list = await request("tools/list", {});
expect(list.result?.tools?.[0]?.name === "echo", "tools/list returns the echo tool");

// 3. 调用 echo 工具(正常路径)
const call = await request("tools/call", { name: "echo", arguments: { text: "hello mcp" } });
expect(call.result?.content?.[0]?.text === "echo: hello mcp", "tools/call echoes the text");

// 4. 负例:未知工具 + 未知方法
const badTool = await request("tools/call", { name: "nope", arguments: {} });
expect(badTool.error?.code === -32602, "unknown tool -> JSON-RPC -32602");
const badMethod = await request("debug/poke", {});
expect(badMethod.error?.code === -32601, "unknown method -> JSON-RPC -32601");

// 5. 优雅关闭:关 stdin, 等 server 退出
child.stdin.end();
const code = await new Promise((r) => child.on("exit", r));
expect(code === 0, `server exits 0 on stdin close (got ${code})`);
console.log(failures.length ? `result: ${failures.length} failure(s)` : "result: all checks passed");
process.exit(failures.length ? 1 : 0);

步骤 3:运行(正常路径) ​

bash
node client.mjs; echo "exit=$?"

正常输出节选(实测,Node 24;完整输出含全部消息逐行打印):

text
--> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"minimal-client","version":"1.0.0"}}}
<-- {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"echo-server","version":"1.0.0"}}}
PASS  initialize returns serverInfo
PASS  server advertises tools capability
--> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello mcp"}}}
<-- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"echo: hello mcp"}]}}
PASS  tools/call echoes the text
PASS  unknown tool -> JSON-RPC -32602
PASS  unknown method -> JSON-RPC -32601
PASS  server exits 0 on stdin close (got 0)
result: all checks passed
exit=0

步骤 4:负例(错误语义长什么样) ​

client fixture 内置两个负例,单独看原始响应:

text
<-- {"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"Unknown tool: nope"}}
<-- {"jsonrpc":"2.0","id":5,"error":{"code":-32601,"message":"Method not found: debug/poke"}}

-32602 是 JSON-RPC 的 Invalid params(工具名属于参数错误);-32601 是 Method not found(方法层未知)。MCP 规范在 -32020 至 -32099 区间保留自己的错误码(如版本不支持的 -32022),不要挪用。

验收与清理 ​

  • 验收:node client.mjs 退出码 0,七项 PASS;exit=$? 为 0。
  • 清理:rm server.mjs client.mjs。
  • 旧 fixture:仓库的 examples/mcp-lab/ 是基于官方 SDK 的旧教学示例;本页零依赖版是协议层最小实现,两者并存,读源码时以本页的代次叙述为准。

步骤 5:场景变体——数据查询型 server(零 key mock) ​

echo 只演示了「原样返回」。真实 server 的三类典型结构——数据查询(查库/记事)、发消息(桥接外部 API,见场景矩阵第二行)、跑代码(执行型工具,安全边界见 §4 runbook 3)——在协议面上是同一套 tools/list + tools/call,差别全在 handler。本步把步骤 1 的 server 换成数据查询型:内存数据集 + 过滤参数的查询工具,并第一次演示 resources 原语(只读数据源)。骨架(readline 循环、错误码、stdin 关闭退出)与步骤 1 完全相同。

db-server.mjs(= 步骤 1 的 server.mjs,只替换数据与 handle()):

javascript
// db-server.mjs — 数据查询型 MCP server(零依赖,内存数据集)
// 仅列出与步骤 1 不同的部分:NOTES 数据、handle()、capabilities 加 resources
const NOTES = [
  { id: 1, tag: "release", text: "v2.1 shipped with streaming fixes" },
  { id: 2, tag: "incident", text: "gateway 502 spike traced to upstream pool" },
  { id: 3, tag: "release", text: "v2.2 rolled out canary to 5%" },
];

function handle(msg) {
  if (msg.method === "initialize") {
    return result(msg.id, {
      protocolVersion: PROTOCOL_VERSION,
      capabilities: { tools: {}, resources: {} }, // 声明两个 server 原语
      serverInfo: { name: "notes-server", version: "1.0.0" },
    });
  }
  if (msg.method === "notifications/initialized") return null;
  if (msg.method === "tools/list") {
    return result(msg.id, {
      tools: [{
        name: "notes_query",
        description: "Queries the in-memory notes store. Optional tag filters by exact tag match.",
        inputSchema: {
          type: "object",
          properties: { tag: { type: "string" } }, // 可选过滤参数
        },
      }],
    });
  }
  if (msg.method === "tools/call") {
    const { name, arguments: args } = msg.params ?? {};
    if (name !== "notes_query") return error(msg.id, -32602, `Unknown tool: ${name}`);
    if (args?.tag !== undefined && typeof args.tag !== "string") {
      return error(msg.id, -32602, "arguments.tag must be a string");
    }
    const hits = args?.tag ? NOTES.filter((n) => n.tag === args.tag) : NOTES;
    return result(msg.id, { content: [{ type: "text", text: JSON.stringify(hits) }] });
  }
  if (msg.method === "resources/list") {
    return result(msg.id, {
      resources: [{
        uri: "notes://all",
        name: "all-notes",
        description: "The full read-only notes dataset.",
        mimeType: "application/json",
      }],
    });
  }
  if (msg.method === "resources/read") {
    if (msg.params?.uri !== "notes://all") {
      return error(msg.id, -32602, `Unknown resource: ${msg.params?.uri}`); // uri 属参数错误,用 Invalid params
    }
    return result(msg.id, {
      contents: [{ uri: "notes://all", mimeType: "application/json", text: JSON.stringify(NOTES) }],
    });
  }
  return error(msg.id, -32601, `Method not found: ${msg.method}`);
}

db-client.mjs(驱动:initialize → 查全部 → 按 tag 过滤 → 类型负例 → 读 resource → 未知 resource 负例):

javascript
// db-client.mjs — 驱动 notes-server(Node ≥ 18,零依赖)
// spawn/按行解析的骨架与步骤 2 的 client.mjs 相同,这里只保留请求序列。
import { spawn } from "node:child_process";
const child = spawn(process.execPath, ["db-server.mjs"], { stdio: ["pipe", "pipe", "inherit"] });
let nextId = 1; const pending = new Map(); let buf = "";
child.stdout.on("data", (chunk) => {
  buf += chunk; let nl;
  while ((nl = buf.indexOf("\n")) !== -1) {
    const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
    if (!line.trim()) continue;
    const msg = JSON.parse(line);
    pending.get(msg.id)?.resolve(msg); pending.delete(msg.id);
  }
});
function request(method, params) {
  const id = nextId++;
  return new Promise((resolve) => {
    pending.set(id, { resolve });
    child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, ...(params ? { params } : {}) }) + "\n");
  });
}
function notify(method) { child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method }) + "\n"); }

const init = await request("initialize", { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "db-client", version: "1.0.0" } });
console.log("PASS  initialize:", init.result.serverInfo.name, "caps:", Object.keys(init.result.capabilities).join("+"));
notify("notifications/initialized");
const list = await request("tools/list", {});
console.log("PASS  tools:", list.result.tools.map((t) => t.name).join(","));
const all = await request("tools/call", { name: "notes_query", arguments: {} });
console.log("PASS  query all ->", all.result.content[0].text);
const rel = await request("tools/call", { name: "notes_query", arguments: { tag: "release" } });
console.log("PASS  query tag=release ->", rel.result.content[0].text);
const badTag = await request("tools/call", { name: "notes_query", arguments: { tag: 42 } });
console.log("PASS  bad tag type ->", badTag.error.code, badTag.error.message);
const rlist = await request("resources/list", {});
console.log("PASS  resources:", rlist.result.resources.map((r) => r.uri).join(","));
const rread = await request("resources/read", { uri: "notes://all" });
console.log("PASS  read notes://all ->", rread.result.contents[0].text.slice(0, 60), "…");
const badUri = await request("resources/read", { uri: "notes://nope" });
console.log("PASS  unknown resource ->", badUri.error.code);
child.stdin.end();
const code = await new Promise((r) => child.on("exit", r));
console.log("PASS  server exits", code);
bash
node db-client.mjs; echo "exit=$?"

实测输出(Node 24,节选):

text
PASS  initialize: notes-server caps: tools+resources
PASS  tools: notes_query
PASS  query all -> [{"id":1,"tag":"release","text":"v2.1 shipped with streaming fixes"},{"id":2,"tag":"incident","text":"gateway 502 spike traced to upstream pool"},{"id":3,"tag":"release","text":"v2.2 rolled out canary to 5%"}]
PASS  query tag=release -> [{"id":1,"tag":"release","text":"v2.1 shipped with streaming fixes"},{"id":3,"tag":"release","text":"v2.2 rolled out canary to 5%"}]
PASS  bad tag type -> -32602 arguments.tag must be a string
PASS  resources: notes://all
PASS  read notes://all -> [{"id":1,"tag":"release","text":"v2.1 shipped with streaming fixes","…
PASS  unknown resource -> -32602
PASS  server exits 0
exit=0

读法:查询走 tools(模型控制、参数可过滤),整表只读走 resources(应用控制)——同一份数据的两种暴露方式,对应第 3 节原语表的控制权分界。真实落地时把 NOTES 换成数据库只读视图或内部 API 的缓存即可,协议面不变。本步验收:node db-client.mjs 退出码 0、九项 PASS;清理时删除两个脚本。

场景矩阵 ​

场景输入动作输出适用不适用
基础:数据查询本地/内部只读数据集(步骤 5 的 notes mock,或真实 DB 的只读视图)tools 带过滤参数查询 + resources 暴露只读源结构化查询结果记事本/查库/内部数据检索写入型操作
常见:桥接外部 API(发消息类)第三方 REST/邮件/通知服务server 做协议转换(对上是 MCP,对外是普通 HTTP client)统一成 MCP 工具聚合多 API、集中鉴权审计单一简单调用(直接 HTTP 更省)
执行型(跑代码)文件写入、命令或代码执行工具执行 + 显式边界约束受控执行结果需要副作用的能力无边界约束的生产环境(先做 §4 runbook 3)
组合:MCP + Skills「连上之后怎么用」MCP 供连接,Skill 供流程连接与步骤分工工具使用规范互替思维

3. 原理 ​

两代生命周期(版本意识的根) ​

MCP 在 2026-07-28 完成了一次代际切换,读任何 MCP 资料先判代:

legacy 代(≤2025-11-25)modern 代(2026-07-28 起)
会话有状态:initialize 握手建会话(HTTP 有 Mcp-Session-Id)无状态:无握手、无会话头
版本与能力握手时一次协商(protocolVersion/capabilities)每个请求经 _meta 携带(io.modelcontextprotocol/protocolVersion、clientCapabilities、clientInfo)
探测无server 必须实现 server/discover,广播支持版本与能力;client 可先探测再调用,也可直接调用并处理版本错误
版本不匹配握手失败UnsupportedProtocolVersionError(-32022),响应带 supported 列表,client 选交集重试
取消notifications/cancelled(通知)同左(沿用)
兼容—双代(dual-era)实现同时支持两代:收到带 _meta 的请求按 modern 处理,收到 initialize 按 legacy 处理

为什么这样设计:无状态让 server 可以水平扩缩、请求可独立重试(连接断了在途请求丢失后直接换新请求重发),也删掉了会话粘性与恢复这类最贵的状态机。

原语:server 给什么、client 给什么(2026-07-28 状态) ​

方原语控制权状态
servertools(可执行函数,模型控制)模型决定调用,host 必须先取得用户同意活跃
serverresources(只读上下文数据)应用控制活跃
serverprompts(可复用消息模板)用户控制活跃
clientelicitation(server 向用户请求补充信息)用户确认活跃
clientsampling / roots / logging—已进入弃用流程(2026-07-28;迁移建议:直接对接 LLM 供应商 API、经参数传目录、日志走 stderr/OTel)

注意第三行的代际变化:旧教程把 sampling/roots/logging 讲成 client 标配能力;2026-07-28 起它们是弃用项,新实现不应新增依赖。另:异步长任务不再是核心方法,移入扩展 io.modelcontextprotocol/tasks;会话内 UI 是扩展 io.modelcontextprotocol/ui(MCP Apps)。

传输:stdio 与 Streamable HTTP ​

stdioStreamable HTTP
形态client 把 server 拉起为子进程远程 HTTP 端点
帧换行分隔的 JSON-RPC,一行一条,禁止内嵌换行HTTP POST 携带 JSON-RPC;响应可流式
协议通道stdin/stdout;stdout 只准写协议消息请求/响应体
日志stderr(client 不应假定 stderr 输出等于出错)常规 HTTP 语义 + OTel
关闭client 关 stdin → server 应立即退出(EOF 是唯一可移植的优雅关闭信号)连接/请求生命周期
授权进程信任 + 宿主授权确认OAuth(见下)
已弃用—HTTP+SSE(2024-11-05 定义、2025-03-26 起弃用)

规范明确 stdio 的帧格式与子进程生命周期是两件事:同样的「一行一条 JSON-RPC」可直接跑在 Unix socket/TCP 上。

授权(authorization) ​

远程 MCP server 的授权建立在 OAuth 系模型上:MCP server 扮演 OAuth 资源服务器。2026-07-28 的关键变化(核验):动态客户端注册(RFC 7591)进入弃用流程,改用 Client ID Metadata Documents;授权服务器应在响应中带 iss(RFC 9207),client 兑换 code 前必须校验;凭据必须按签发方隔离,不得跨授权服务器复用。安全三原则(规范原文要点):用户同意与控制、数据隐私、工具安全——工具即任意代码执行,其描述与注解在不可信来源下应视为不可信输入。

规范要求 vs 本地实测 ​

规范要求(2026-07-28,2026-09-01 核验)本地实测(第 2 节 fixture)
stdout 不得写入任何非 MCP 消息的内容server 全部日志走 stderr,实测输出无污染
消息换行分隔、单行单条client 按行切分解析,全部通过
client 关闭 stdin 后 server 应及时退出rl.on("close") → process.exit(0),实测退出码 0
未知方法 → JSON-RPC 错误(legacy server 常用 -32601)debug/poke 收到 -32601,实测一致
modern 代每个请求带 _meta 版本;server 必须实现 server/discoverfixture 未实现(它刻意实现 legacy 面以便与最大存量的双代 client 互操作);补 modern 面时按上表加 _meta 与 server/discover
Roots/Sampling/Logging 弃用fixture 不依赖这三者

控制流全景(一次工具调用) ​

关键边界:模型输出的是调用意图,host 解析后构建 JSON-RPC——模型不直接说协议。授权发生在 host 侧(工具=任意代码执行)。

4. 开发 ​

调试工具链 ​

  • MCP Inspector(官方,https://github.com/modelcontextprotocol/inspector):交互式连接 server,可视化查看 initialize/tools/list/tools/call 与原始消息,是「先看真实消息再改代码」的第一站。
  • 日志:stdio server 日志只进 stderr;在 host 侧打开 server stderr 转发即可看到生命周期日志。
  • 超时:host 对 tools/call 设超时;server 对外部依赖(文件、HTTP)设内层超时,超时返回 isError 结果而不是挂死。

症状 → 证据 → 处理 → 完成标准 ​

症状:client 报 JSON parse error / 消息流错乱,工具时好时坏。 证据:server 把日志或 banner 打到了 stdout——stdout 混入非协议内容,破坏「一行一条 JSON-RPC」的帧。 处理:所有输出改走 console.error(stderr)或日志库默认 stderr;CI 加一条断言:stdout 每行都能 JSON.parse 成功。 完成标准:Inspector 连接该 server 无解析错误;CI 断言常绿。

症状 → 证据 → 处理 → 完成标准 ​

症状:client 连上后长时间无响应或直接报错,双方都「正常启动」。 证据:代次不匹配——modern client 遇到 legacy-only server(或反之);用 server/discover 探测:返回 DiscoverResult 或版本错误(-32022)= 对端是 modern;返回其他错误或不响应 = legacy。 处理:client 侧实现双代回退(探测失败即退回 initialize 握手);或升级 server 同时应答两代(收到带 _meta 的请求按 modern 处理,收到 initialize 按 legacy 处理)。 完成标准:用 Inspector 与目标 host 各连一次,两种打开方式都能完成 tools/list。

症状 → 证据 → 处理 → 完成标准 ​

症状:文件类工具能读到服务器上任意文件(如 .env、密钥)。 证据:工具的路径参数未做根目录约束。本仓真实教训:examples/mcp-lab 的 read_file_summary 工具接受绝对路径直接 fs.readFile——模型(或诱导模型的输入)可以让它读宿主机器上任何文件。参数 schema 只验证类型,不验证边界。 处理:root allowlist 模式——server 启动时显式声明允许访问的根目录,所有路径参数先解析再校验包含关系,越界返回错误:

javascript
// 路径安全:root allowlist(修复 examples/mcp-lab read_file_summary 的模式)
import { readFile } from "node:fs/promises";
import { resolve, relative, isAbsolute } from "node:path";

// 1. 显式声明允许访问的根(来自配置/环境, 不是来自模型输入)
const ALLOWED_ROOTS = [resolve(process.env.DATA_ROOT ?? "./data")];

function assertWithinRoots(p) {
  const abs = isAbsolute(p) ? resolve(p) : resolve(process.cwd(), p);
  const ok = ALLOWED_ROOTS.some((root) => {
    const rel = relative(root, abs);
    return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
  });
  if (!ok) throw new Error(`path outside allowed roots: ${p}`);
  return abs;
}

// 2. 工具实现里先校验再读
export async function readFileSummary(filePath) {
  const abs = assertWithinRoots(filePath);       // 越界在这里抛错
  const content = await readFile(abs, "utf-8");  // 校验通过才碰文件系统
  return content.slice(0, 100);
}

完成标准:负例测试(../../etc/passwd、绝对路径指向允许根之外)全部返回错误而非内容;正例(允许根内相对路径)不受影响;测试进 CI。

症状 → 证据 → 处理 → 完成标准 ​

症状:升级 SDK/规范版本后 server 行为异常(ping 失效、日志级别不生效、握手报错)。 证据:对照 2026-07-28 变更清单:ping 与 logging/setLevel 已移除(日志级别改为请求级 _meta);initialize 握手在 modern 代不存在;结果对象新增必填 resultType(legacy server 省略时 client 按 "complete" 处理)。 处理:按迁移清单逐项替换——ping→业务层心跳或删除;logging→stderr/OTel;握手→per-request _meta 或保留双代应答;错误码若用到 -32001/-32003/-32004 改为重编号后的 -32020/-32021/-32022。 完成标准:目标 host 与 Inspector 双代各连一次通过;版本升级 PR 附迁移对照表。

反模式清单 ​

  • 「一次挂五个 server」:工具定义先吃掉上下文预算;按当期任务最小化启用。
  • stdout 当日志:见 runbook 1,必炸。
  • 路径参数无边界:见 runbook 3;schema 只管类型不管权限。
  • 把工具描述当可信输入:规范明确注解来自不可信 server 时应视为不可信。
  • 拿 MCP 做 Agent↔Agent 通信:方向错了,用 A2A。

5. 资料库 ​

四级阅读路线:

  • Beginner:跑通第 2 节 fixture;用 Inspector 重连一次看原始消息。
  • Builder:读 2026-07-28 规范的 Architecture 与 Base Protocol 两页;给自己的 server 补 modern 代 _meta 与 server/discover。
  • Operator:把「stdout 纯净性」与「路径越界」两测进 CI;跟踪官方 SDK 版本与 CHANGELOG。
  • Researcher:读版本页的兼容矩阵与 SEP 记录(无状态化、MRTR、subscriptions/listen 的取舍)。

资源表 ​

名称证据层级canonical URL用途支持的断言下一步
MCP 规范(latest=2026-07-28)L0(官方规范)https://modelcontextprotocol.io/specification/latest协议契约唯一来源代次划分、原语、传输、弃用清单(retrievedAt 2026-09-01)读 Architecture / Base Protocol
版本与兼容页L0(官方规范)https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecyclemodern/legacy 与兼容矩阵_meta 字段名、server/discover、-32022(retrievedAt 2026-09-01)对照第 3 节两代表
Key Changes(changelog)L0(官方规范)https://modelcontextprotocol.io/specification/latest/changelog修订间差异2026-07-28 全部变更与弃用(retrievedAt 2026-09-01)迁移清单来源
MCP InspectorL0(官方工具)https://github.com/modelcontextprotocol/inspector交互式调试官方调试入口(retrievedAt 2026-09-01)第 4 节 runbook 实操
MCP SDK(TypeScript/Python 等)L0(官方工具)https://modelcontextprotocol.io/sdk生产实现官方多语言 SDK(retrievedAt 2026-09-01)替换手写实现

主动证伪与未决问题 ​

  • 证伪入口:若你的 server 在两种代次下与规范行为不符(如 server/discover 返回形状不对),以规范 schema 为准并修订本页「规范 vs 实测」表。
  • 未决:2026-07-28 之后的新修订(watch specVersion);弃用项(Roots/Sampling/Logging、HTTP+SSE、RFC 7591 注册)的最终移除时间表;扩展生态(tasks/ui)的稳定性——均需按核验日复查。

learn-ai 到此为止 / 继续去哪 ​

  • 工具调用的模型侧契约(schema、选择、参数验证):工具调用契约。
  • 幂等/超时/重试/审批的执行工程:工具执行工程。
  • Agent↔Agent 方向:A2A;全景:协议地图。
  • 具体第三方 MCP server 的用法(如 chrome-devtools-mcp):Products 区。

为前端工程师打造 · 基于 VitePress 构建