MCP 教程 2026:2026-07-28 新规范实战,写一个 MCP Server 我踩了 4 个坑

MCP 教程 2026:2026-07-28 新规范实战,写一个 MCP Server 我踩了 4 个坑

先说背景。我维护 7027A 这个 AI 工具导航站,每天早上 9 点有个定时任务要写文章入库。动笔之前得先查清楚库里已经写过什么、哪些关键词用过、哪些文章流量还行。以前这事儿我就在脚本里连数据库查一下,很土但能用。

上周我想把它接到 AI 客户端上,让 Cursor 或者 Claude 直接问"我站上写过哪些 Claude 相关的文章",而不是每次都让我手动查库贴结果。顺着这条线摸下去,我才算真的把 MCP 用起来了——然后发现 2026 年 7 月 28 日发布的那版新规范,把我原来写教程时用的那套写法推翻了一大半。

这篇是我这一周实际搭出来的东西,包括能跑的完整代码、我在本地抓到的真实 HTTP 报文,还有 4 个让我卡住超过半小时的坑。篇幅有点长,但代码部分我都本地跑过,不是抄文档。

先搞明白 MCP 到底在解决什么

我一开始的理解很朴素:MCP 就是给 AI 装了个 USB-C。以前每个 AI 客户端要接我的数据库,就得单独写一套胶水代码——Cursor 一套、Claude 一套,以后再来个新客户端又是一套。写到最后我自己都忘了到底有几个客户端在连。

MCP 的做法是把"我有什么能力"写成一份带 JSON Schema 的说明书放在服务端,客户端启动的时候读一遍,就知道该给我什么参数、返回什么格式,剩下的模型自己决定要不要调。

所以 MCP 服务器本质上就是个普通进程,只不过它对外暴露的不是 HTTP 接口,而是"工具列表 + 工具调用"。这个理解对了,后面所有事情都顺。

顺带一提,很多教程一上来就讲 host / client / server 三层结构还有协议版本演进,我一开始看得很劝退。这部分概念官方文档写得很清楚,我这里只说用下来的体感:写 MCP 服务器这件事本身比想象的简单,难的是"怎么让模型正确地调用你的工具"。

2026-07-28 这版到底改了什么(我抓包看到的)

7 月 28 日发布的 2026-07-28 规范,是 MCP 发布以来最大的一次改版。官方博客说 Tier 1 SDK 每月下载量接近 5 亿次,TypeScript 和 Python 两个 SDK 的累计下载都过了 10 亿。体量这么大还做破坏性变更,说明改的方向大家都认。

我关心的就一件事:它到底长什么样。所以我起了个本地 HTTP 服务,用 curl 打了一遍,把报文抄在这儿。

会话没了,握手也没了

老规范(2025-11-25)是这样:客户端先发一个 initialize 请求握手,服务端回一个 Mcp-Session-Id 头,之后每个请求都得带着这个 ID,相当于把客户端钉死在某个实例上。

新规范把 initialize 和 Mcp-Session-Id 全删了。每个请求自己带版本号和客户端信息,一个请求一个请求地独立发出去。我在服务端加了一行日志打印入站 header,modern 客户端打过来是这样:

--> INBOUND: {"mcp-protocol-version":"2026-07-28","mcp-method":"server/discover"}
--> INBOUND: {"mcp-protocol-version":"2026-07-28","mcp-method":"tools/call","mcp-name":"search_articles"}

注意第二个请求的 mcp-method 是 tools/call,mcp-name 是具体工具名 search_articles。这两个 header 是新规范强制的(SEP-2243),好处很实在:你的网关、限流器、WAF 不用解开 JSON body 就能知道这是哪个工具的调用,直接按 header 路由和限流。

另外新规范把 server/discover 做成了一个正式的 RPC,客户端想提前知道服务器支持什么版本、有什么能力,可以先调它。不过它是可选的,不调也行。

一个端点同时服务两代客户端

这个我实测了两次。同一个 HTTP 端点、同一个进程,我拿两个客户端连:

MODERN -> negotiated: 2026-07-28  era: modern
MODERN -> call: [{"type":"text","text":"hit:modern"}]
LEGACY -> negotiated: 2025-11-25  era: legacy
LEGACY -> call: [{"type":"text","text":"hit:legacy"}]

新客户端走无状态路径,老客户端还是走老的 initialize 握手,两边都正常。这是官方一直强调的"不抛弃存量客户端"——2026 新客户端连老服务器也会自动降级回握手,反过来老客户端连新服务器也认,因为服务器默认两个协议都应答。

这也为下面第一个坑埋了伏笔。

代码:我最后跑通的那一版

先说安装。TypeScript 这边 v2 拆包了,server 和 client 分开装:

npm install @modelcontextprotocol/server @modelcontextprotocol/client zod

我的服务器文件长这样,完整的,能跑:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod/v4";

const server = new McpServer({ name: "7027a-demo", version: "1.0.0" });

server.registerTool(
  "search_articles",
  {
    title: "Search 7027A articles",
    description: "Search the 7027A article database by keyword. Use this when the user asks about AI tool articles, tutorials, or reviews on 7027A.",
    inputSchema: {
      keyword: z.string().describe("Keyword to search for in article titles"),
      limit: z.number().int().min(1).max(10).default(5).describe("Max results to return"),
    },
  },
  async ({ keyword, limit }) => {
    const hits = await db.query(
      "SELECT id, title FROM articles WHERE title LIKE ? LIMIT ?",
      [`%${keyword}%`, limit]
    );
    return {
      content: [{ type: "text", text: hits.map(h => `#${h.id} ${h.title}`).join("\n") }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("7027a-demo MCP server running on stdio");

验证工具列表我用的是官方 Inspector 的 CLI 模式,拿到的输出是这样的(真跑):

npx @modelcontextprotocol/inspector --cli node server.js --method tools/list

返回的 inputSchema 里 keyword 是 required,limit 带默认值和上下界。我故意把 limit 卡在 1 到 10 之间,是因为 schema 越紧模型越不容易传错参数——这一点后面还会展开。

我踩的坑

坑一:v2 默认还在说老协议,三处设置缺一不可

这个最坑。我以为装上 v2 SDK 就自动切到新规范了,结果连上去一看:

negotiated: 2025-11-25
era: legacy

查了半天才发现,v2 的 Client 默认 versionNegotiation.mode 是 'legacy',压根不去探测。要真正跑 2026-07-28,三处都得改:

位置配置不改会怎样
客户端 transportprotocolVersion: "2026-07-28"发出去的 header 还是老版本号
客户端 ClientversionNegotiation: { mode: "auto" }不去探测 server/discover
服务端createMcpHandler(factory, { legacy: "reject" })服务端两代都接,测不出差异

只改一处的后果我挨个试了一遍。服务端开 legacy: "reject"、客户端没改的时候,客户端直接撞墙:

{"code":-32022,"message":"Unsupported protocol version: 2025-11-25",
 "data":{"supported":["2026-07-28"],"requested":"2025-11-25"}}

反过来只给 transport 设了新版本号、但 Client 还在发老握手,就变成另一个错:

{"code":-32020,"message":"Bad Request: the request headers and body disagree:
 an initialize request (legacy handshake) was sent with a modern MCP-Protocol-Version header"}

这两个错误码都是新规范预留的:-32020 是 header 和 body 不一致,-32021 是缺必要能力,-32022 是协议版本不支持。我第一次看到 -32020 的时候完全懵了,字面意思是"你发的 header 和 body 打架了",它其实就是把我"版本号设对了但握手方式没跟上"这件事说得特别直白而已。

坑二:console.log 会污染 stdio 协议流

这个是老坑了,但我还是踩了。stdio 传输用子进程的 stdin/stdout 传 JSON-RPC,我在工具里加了一行调试输出:

console.log("DEBUG: about to reply to", msg);

然后我用 raw socket 把 stdout 原文抓出来看:

{"result":{"protocolVersion":"2025-11-25",...},"jsonrpc":"2.0","id":1}
DEBUG: about to reply to hello
{"result":{"content":[{"type":"text","text":"pong"}]},"jsonrpc":"2.0","id":2}

中间那行 DEBUG 就横在两个 JSON-RPC 消息中间。更有意思的是,我拿 v2 的官方客户端去调,居然没报错,结果正常返回,大概是客户端跳过了不认识的行。但协议流已经被污染了——换成别的客户端、或者中间过一层网关,谁知道会出什么事。

解法很简单:调试信息一律走 stderr,用 console.error()。我最后一行代码里保留的 console.error("7027a-demo MCP server running on stdio") 就是这个原因,不是随便写的。

坑三:ESM only,package.json 要加 type

v2 是纯 ESM。我第一次直接 node server.js 报的是:

SyntaxError: Cannot use import statement outside a module

不是代码写错了,是 package.json 里缺 "type": "module"。加上就好。

坑四:Python 那边的类改名了,旧 import 直接报错

如果你用 Python,官方 SDK v2 把 FastMCP 改名成了 MCPServer,而且不是废弃警告,是直接删掉旧路径:

from mcp.server import MCPServer          # v2 新的
# from mcp.server.fastmcp import FastMCP  # v1 的,v2 环境里 ModuleNotFoundError

装饰器用法不变,@mcp.tool() 还是那样,但路径变了。官方 SDK 文档写得很明确:v1.x 转维护模式,继续收安全补丁。想稳着不动可以先钉版本:

uv add "mcp[cli]<2"

另外 v2 里 MCPServer("x", port=9000) 会直接 TypeError,端口配置搬到 run() 里去了。这几条我是照官方 SDK 文档核的,没在本地跑 Python——这台机器上没装可用的 Python 解释器,所以不敢说"我实测过",但改动本身文档写得清楚。

工具描述怎么写,比协议本身更影响效果

这一节是我的个人体感,不是 spec 规定的东西。

MCP 里模型是靠工具描述来决定"要不要调这个工具、参数怎么传"的。我一开始写的是:

description: "搜索文章"

结果模型经常不调,或者传一堆奇怪的参数。改成这样之后稳定多了:

description: "Search the 7027A article database by keyword. Use this when the user asks about AI tool articles, tutorials, or reviews on 7027A."

差别在两处:一是说清楚什么时候该用("when the user asks about..."),二是把搜索对象限定死。参数上用 zod 的 .describe() 写清楚每个参数的含义,给数值参数设默认值和上下界,模型就不容易传出离谱的值。

顺带说一个官方文档里反复强调的点:工具描述本身就是模型会读的指令,不是给人看的注释。所以第三方服务器的工具描述要当成不可信输入来看,这直接关系到下面这个安全问题。

安全:stdio 不等于沙箱

这一段主要是官方安全指南的结论,加上我自己的理解。

最反直觉的一点是:stdio 传输的 MCP 服务器不是沙箱。它就是个普通子进程,用你的权限跑,能读你账号能读的文件,能看到环境变量里的 token,能访问网络。你在配置文件里加一条 MCP 服务器条目,本质上等于授权执行一条命令。

所以几条硬规矩:

  • 文件类服务器只指向一个专用练习目录,别指到整个 home 目录
  • 第三方服务器的 token 用窄权限的,别复用你的个人大权限 token
  • 测试新服务器时,Claude Code 用 claude --permission-mode manual,每个工具调用自己点确认
  • 别把本地 MCP 服务器暴露到公网上,绑 127.0.0.1 就好,绑 0.0.0.0 等于开门
  • 用容器跑你不信任的第三方服务器,除了必需的目录别的都别挂载

还有一类叫 tool poisoning 的攻击:攻击者在工具描述里藏一段指令,模型读了会照做,但你在界面上看不见。变种叫 rug pull——你批准之后服务器偷偷改了描述。所以工具描述要版本化,装之前先读一遍。

另外新规范要求 HTTP 传输的服务器校验 Origin 头防止 DNS rebinding。官方给的 Node 挂载方式里也带了这个校验函数,别图省事跳过。

什么时候用 MCP,什么时候直接调 API

这个问题官方也明确回答过:流程固定、只有一个调用方,就直接调 API。MCP 的价值在于"多个 AI 客户端要用同一套工具",或者"调用方是模型、工具集会动态变化"。

就我自己的例子:7027A 的定时写文任务是固定流程,脚本直连数据库其实更省事,套 MCP 反而是过度设计。真正值得套的是"我想在 Cursor / Claude / ChatGPT 里都能直接问站上文章库"这个需求——同一套能力,多个客户端。

所以别一上来就写 MCP。先问自己两句:这个工具是不是要给多个 AI 客户端用?调用它的会不会是模型自己决定要不要调?如果两个都是否,直接写个 API 或者脚本更划算。

如果我也从头再来一次,会注意的几件事

装完 v2 SDK 别以为协议自动升级了,versionNegotiation.mode 默认是 legacy,得手动开。调试输出全部走 stderr,stdout 是协议通道。ESM 项目记得在 package.json 里加 "type": "module"。Python 用户注意 FastMCP 已经改名成 MCPServer,旧 import 路径没了。

工具描述不是注释,是给模型看的说明书,值得多花十分钟写。测试阶段用 Inspector 的 CLI 模式先把工具列表和参数 schema 打出来看一眼,比反复在客户端里试快得多。

我搭完之后最大的感受是,MCP 把"给 AI 加工具"这件事从写胶水代码变成了写说明书,这个转变对独立开发者来说太重要了——你不用再为每个客户端写一遍适配。

具体到写代码这一步,我更关心工具描述的质量。模型不调你的工具,八成是描述没写清楚,而不是协议出了问题。这块我还在琢磨,如果你有更好的写法,可以在 关于 7027A 里留的联系方式找我聊。也可以先翻翻 7027A 的 AI 工具库,或者看这篇 Vibe Coding 完整指南,里面聊的是 MCP 背后的那套 Agent 架构怎么落到具体项目里。

🛠 本文提到的 AI 工具

C
ChatGPT 通用 AI 助手与多模态创作
访问 →
C
Claude Anthropic Claude 对话、写作和智能协作入口
访问 →
G
Gemini Google Gemini 官方 AI 助手入口
访问 →
D
DeepSeek 中文推理、写作、代码问答
访问 →

📚 相关文章

Claude 5 迁移实录:7 个 400 报错我全踩了一遍(附官方破坏性变更对照)

Claude 5 全型号价格/上下文对照表 + 7 个 400 报错的官方成因与改法:关思考、tool_choice、采样参数、思考块绑定、新分词器 +30% token、computer 工具集改名,附可照抄的 JSON 对照和迁移检查清单。

👁 14
AI 工具站 100 天挑战完整路线图(D14-D100 2026 夏季版):200 篇文章 + 月入 $3000 终极挑战

AI 工具站 100 天挑战完整路线图(D14-D100)

👁 203
30 天挑战 30 个具体选题清单(2026 夏季版):5 大主题各 6 个 + 每天 1 篇可执行

30 天挑战 30 个具体选题清单(2026 夏季版)

👁 180
Vibe Coding 完整指南 2026:Cursor 3 / Devin 2 / Claude Agent SDK 谁最强?(30 个 prompt + 5 场景实战)

Vibe Coding 完整指南 2026:Cursor 3 / Devin 2 / Claude Agent SDK 谁最强?

👁 256