逐日AI
第 1 周 · D2约 5 小时

写第一个 MCP server:stdio 传输、官方 SDK、参数 schema、工具注解与 Inspector 调试

用官方 SDK 写出第一个能被真实客户端加载的服务端,把参数 schema、输出 schema、工具注解和错误分类一次配齐,并学会用 Inspector 排查它。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能用官方 SDK 起一个 stdio 服务端,并在客户端里看到它注册的工具
  2. 能为工具写出可用的输入 schema、输出 schema 与描述,并说明描述写给谁看
  3. 能区分协议错误与工具执行错误,并各举一个该用哪一种的例子

昨天你把一条 tools/call 报文逐字段拆开了。今天反过来:写一个真的会发出那种报文的进程。 代码量不大,难的是那几个决定——描述写多细、schema 怎么划、错误算哪一类。读完回来把上面三条勾掉。

小白版讲解

stdio 传输:一行一条消息,多打一个字就全崩

昨天说协议是插头标准。那传输就是电线,而 stdio 是屋里现拉的一根线:客户端把服务端当子进程拉起来,两个进程之间用标准输入和标准输出直接对讲。没有端口、没有域名、没有鉴权,因为它们本来就在同一台机器、同一个用户下。

规矩少得可以一口气说完。消息是 JSON-RPC,一行一条,用换行分隔,消息内部不许有裸换行。客户端把请求写进服务端的标准输入,服务端把响应写回标准输出。服务端不得往标准输出写任何不是 MCP 消息的东西;客户端也不得往服务端的标准输入写 JSON-RPC 响应(客户端只发请求和通知)。要打日志就写标准错误——规范明确允许服务端往标准错误输出任意 UTF-8 文本,也提醒客户端不要把标准错误上有输出当成出错的信号。

关停也简单:客户端关掉输入流,服务端读到文件结束就该尽快退出。规范说这是主要的、也是唯一可移植的优雅停机信号,所以老老实实处理它,能省掉一堆强杀进程的麻烦。

工程代价这么看:stdio 的好处是零配置、零网络攻击面、进程隔离天然存在;坏处是它只能给本机用,没法多副本、没法给团队共享,而且服务端的生命周期完全被客户端捏着——客户端崩了,你的服务端跟着变孤儿进程。要给团队用就得换传输,那是第 4 天的事。

用官方 SDK 起服务端:注册一个工具需要哪几件东西

昨天那段手拼 _meta 的代码是为了让你看清底层。真写服务端时没人这么干,因为 SDK 已经把传输、报文封装、schema 校验全包了。

注册一个工具需要四件东西:名字、给模型看的描述、输入 schema、执行体。输出 schema 和注解是可选的,但生产里都该写。

server.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: 'weather-fx-server', version: '0.1.0' })
 
server.registerTool(
  'get_weather',
  {
    title: '天气查询', // 给人看的,客户端拿它渲染界面
    description: '查询某个城市当前的天气,返回摄氏温度与天气状况。city 传城市名,中文或英文均可,例如「上海」。',
    inputSchema: {
      city: z.string().min(1).describe('城市名,中文或英文,例如「上海」或「Seattle」'),
    },
  },
  async ({ city }) => {
    const hit = await fetchWeather(city)
    return { content: [{ type: 'text', text: JSON.stringify(hit) }] }
  }
)
 
// 传输是最后一步,也是唯一一处「换传输只改这一行」的地方
await server.connect(new StdioServerTransport())

两边的形状很像:都是「声明元信息 + 写一个普通函数」。Python 侧更省事,因为它能从类型注解和 docstring 反推出 schema;TypeScript 侧用 zod 显式声明,换来的是编译期就能查出参数名写错。

现在必须说一件让人不太舒服的事,否则你照着网上教程走会一路困惑。2026-09-06 实测:JavaScript 的 @modelcontextprotocol/sdk 最新版是 1.30.0,它内部的最新协议版本常量仍然是 2025-11-25,包里搜不到 resultType,也没有 server/discover。Python 的 mcp 包 2.1.1 已经跟上了 2026-07-28,类型里能找到发现请求、输入待补结果、订阅监听请求这些新东西。

口径就一句话:规范这么定,当前 SDK 这样实现。 用 JS SDK 写出来的服务端,线上真实发出的是上一版协议的报文;昨天那份手写报文才是规范的样子。这不影响你今天学到的任何一个概念——工具、schema、注解、错误分类在两版之间没变——但它会影响你抓包时看到什么。判断一份材料新不新,最快的办法是搜它有没有提 server/discoverresultType

输入 schema 是给模型看的合同

这一节是全天最值钱的部分,因为它决定了你的工具会不会被调用、以及会不会被错误地调用

先说描述。工具描述是给模型看的,不是给同事看的。模型看不到你的 wiki、看不到代码注释、看不到需求文档——它只有你写的那一句。所以「查询订单,详见文档」这种写法等于什么都没写。一句合格的描述要回答三个问题:这个工具做什么、参数长什么样(最好给一个例子)、什么情况下才该用它。 最后一条最常被漏掉,也最要命:没写清适用条件,模型就会在不该调的时候调它。

再说 schema。MCP 的 schema 默认是 JSON Schema 2020-12(不写 $schema 就按这个走)。规范对工具名有一组建议:长度 1 到 128 个字符,只用大小写字母、数字、下划线、连字符和点,区分大小写,在同一个服务端内唯一,不要有空格和逗号。 无参工具的写法值得单独记,推荐这一种:

JSONJSON
{
  "name": "get_current_time",
  "description": "Returns the current server time",
  "inputSchema": { "type": "object", "additionalProperties": false }
}

additionalProperties: false 表示「只接受空对象」,比光写 { "type": "object" }(接受任何对象)更明确。输入 schema 必须是一个合法的 JSON Schema 对象,不能是 null

参数命名也算合同的一部分。cityq 好,start_dated1 好,因为模型是靠名字和描述一起猜语义的。每个字段都写上 description,这一条的投入产出比高得离谱——一个字段的一句描述,往往比你在工具总描述里再加三行更有效。

输出 schema 与结构化内容:让下游代码不用再解析自然语言

工具返回分两种。非结构化内容放在 content 里,是给模型读的文本、图片、音频;结构化内容放在 structuredContent 里,是给代码用的 JSON。

如果你声明了 outputSchema,规范的要求是硬的:服务端必须返回符合这个 schema 的结构化结果,客户端应当拿它做校验。好处很直接——下游代码不用再写正则去从「纽约当前 22 度,多云」里抠出数字。

还有一条容易漏的兼容规则:返回结构化内容的工具,应当同时在 content 里回一份序列化后的 JSON 文本。 因为老客户端只认 content,你只给 structuredContent 它就什么都拿不到。

structured.ts
server.registerTool(
  'get_weather',
  {
    description: '查询某个城市当前的天气……',
    inputSchema: { city: z.string().describe('城市名') },
    // 声明了输出形状,客户端就能校验,下游代码也不用解析自然语言
    outputSchema: {
      celsius: z.number().describe('当前气温,摄氏度'),
      conditions: z.string().describe('天气状况的中文描述'),
    },
  },
  async ({ city }) => {
    const hit = await fetchWeather(city)
    return {
      // 两个都给:content 兼容老客户端,structuredContent 给代码用
      content: [{ type: 'text', text: JSON.stringify(hit) }],
      structuredContent: hit,
    }
  }
)

工程代价:输出 schema 一旦发布就是对外契约,改字段名等于破坏性变更。所以别一上来把内部数据结构原样吐出去,只暴露你愿意长期维护的那几个字段。第 7 天讲版本化时会回到这一点。

工具注解的四个提示位,以及为什么它们不可信

注解(annotations)是挂在工具定义上的一组行为提示。实测四个字段名是:readOnlyHint(只读,不改变外部状态)、destructiveHint(可能造成破坏性更新)、idempotentHint(同样参数重复调用等价于调一次)、openWorldHint(作用于开放的外部世界,而不是一个封闭集合),另外还有一个 title 用于显示。

客户端拿这些干什么?主要是决定要不要弹确认框、以及界面上怎么标记。一个只读且幂等的查询可以静默执行;一个带破坏性提示的删除操作就该让用户点一下确认。

但规范在这里加了一条非常重的警告:客户端必须把工具注解当成不可信输入,除非它们来自可信的服务端。

这句话值得多想一层。注解是服务端自己写的,一个恶意服务端完全可以给 delete_all_files 打上 readOnlyHint: true,骗过那些「只读就不用确认」的客户端。所以:

注解是界面提示,不是权限控制。 真正的权限必须由宿主按服务端来源来定,不能由服务端自述决定。

这和昨天 clientInfo 那条规则是同一个道理——凡是对方自报的东西,都不能拿来做安全判断。第 6 天会把这条线索完整地展开成一套攻击面分析。

两类错误:一类让程序修,一类让模型自己改

新手最容易写错的地方。MCP 的工具有两套错误上报机制,用错了会让模型陷入无意义的重试。

协议错误走 JSON-RPC 的 error 字段。它表示「这个请求本身就不对」:工具名不存在、请求不满足调用工具的 schema、服务端内部炸了。

JSONJSON
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": { "code": -32602, "message": "Unknown tool: invalid_tool_name" }
}

工具执行错误走结果里的 isError: true。它表示「工具跑了,但业务上没成」:下游 API 失败、入参业务校验不过、业务逻辑拒绝。这仍然是一个成功的 JSON-RPC 响应。

JSONJSON
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "出发日期必须晚于今天。当前日期是 2026-09-06。" }],
    "isError": true
  }
}

区别在于谁能修好它。规范说客户端应当把执行错误交给模型,让它自我纠正后重试;协议错误也可以给模型看,但基本没用。所以判据只有一句:

模型换个参数有没有可能成功?有就用 isError,没有就回 JSON-RPC 的 error

推论是执行错误的文案写给模型看:要把可选值、正确格式、边界条件列出来。上面那句「当前日期是 2026-09-06」就是范例——它直接告诉模型该往哪个方向改。写成「参数错误」则等于让模型瞎猜。

今天的实验里有一个更阴险的反例:不做参数校验,让非法输入算出一个 NaN 然后静默返回。没有报错、没有 isError,模型会把这个错误结果当成正确答案用下去。这比崩掉危险得多,因为它不会留下任何痕迹。

用 Inspector 调试:看得见报文,才不用猜

写服务端最痛苦的时刻是「模型就是不调我的工具」。这时候有两条路:一条是改改描述再试试,纯靠玄学;另一条是把报文调出来看。

官方提供了 MCP Inspector,一个可以把服务端接进来、逐条查看双向报文、手动触发工具调用的调试工具。具体的启动方式和界面在官方文档里,随版本会变,这里不抄命令。要记住的是思路:先确认工具真的出现在 tools/list 里、schema 真的是你以为的形状,再去怀疑模型。 一大半的「模型不调我的工具」最后发现是工具压根没注册上,或者参数名拼错了。

今天的实验自带一个更轻的替代品:用 SDK 的内存传输把客户端和服务端接在同一个进程里,逐项跑断言并打印现象。它比 Inspector 更适合放进持续集成,因为它有明确的退出码。两者是互补的:Inspector 适合探索,自测入口适合回归。

最后补一条容易忽略的规范建议:tools/list 应当以确定的顺序返回工具(底层工具集没变时,每次返回的顺序都一样)。原因不是强迫症——顺序稳定,客户端才能可靠地缓存工具列表,模型侧的提示缓存命中率也会更高。随手用一个哈希表遍历来生成列表,就会把这个优化毁掉。

源码导读

动手实验

🧪 D2 实验:一个含天气与汇率两个工具的 stdio MCP 服务端

代码位置:labs/mcp-7days/day-02-weather-fx-server

验收标准:

  1. tools/list 返回 get_weatherconvert_currency 两个工具,且都带输入与输出 schema
  2. get_weather 正常调用返回 structuredContent,未知城市返回 isError 且文案里列出了可用城市
  3. convert_currency 把 100 USD 正确换成 712 CNY,金额为负或币种未知时返回对模型有用的 isError
  4. 两个工具都声明了 readOnlyHintidempotentHint 注解
  5. MOCK=1 SELFTEST=1 pnpm start 在 solution 下 7 项全 ✅ 并以退出码 0 结束

这个实验完全不联网、也不需要任何 API key,天气和汇率都来自内置的假数据表。starter 里挖了 5 个练习点,原样跑会看到 5 项 ❌——每做完一个就有一项变 ✅,这就是你的进度条。 卡住了先看那一项打印出来的现象,它会直接告诉你差在哪。

  1. 先读 solution 的 src/server.ts,找到工具注册、schema 声明与传输启动三处,确认每个工具都是「元信息 + 一个普通函数」的形状。
  2. 在 starter 里补全 get_weather:把敷衍的描述改写成模型看得懂的话,补上输出 schema 与 structuredContent,跑一遍看第 1 和第 3 项变绿。
  3. 把未知城市的 throw 改成主动返回 isError,并把可用城市列进文案里,看第 4 项变绿——顺便体会一下 throw 出来的文案对模型有多没用。
  4. 给 convert_currency 补上参数校验,让金额为负和币种未知都返回执行错误;注意 starter 原本会静默返回一个负数结果,这才是最危险的情况。
  5. 给两个工具各写一组注解,想清楚只读、幂等、开放世界三个提示各该填什么,跑到 7 项全 ✅。

面试题

今天 3 道题在下方题库区,侧重工具描述的写法、schema 设计、两类错误的分工、stdio 的边界。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。

检查清单与明日预告

  • 能用官方 SDK 起一个 stdio 服务端,并在客户端里看到它注册的工具
  • 能为工具写出可用的输入 schema、输出 schema 与描述,并说明描述写给谁看
  • 能区分协议错误与工具执行错误,并各举一个该用哪一种的例子
  • 能说清为什么工具注解必须被当成不可信输入
  • 实验的 5 条验收标准全部通过
  • 3 道面试题不看要点也能答出至少 2 道

明天(D3)我们把另外两种原语补上:资源与提示模板。顺序仍然是有意的——工具是模型自己挑的,资源和提示模板是应用与用户挑的,控制方不同,设计方法也就完全不同。 而且一旦数据量上来(比如一个几千篇笔记的目录),你会立刻撞上分页、缓存和变更通知这三件今天完全没碰到的事。

面试题库

  • 工具的 description 到底写给谁看?写得太泛,在生产里会造成什么具体后果?Who is a tool's description actually written for, and what concretely goes wrong in production when it is too vague?
    国内高频海外高频基础#tool-design#prompt-surface

    分析过程 · 先想清楚再作答

    1. 这题在筛「有没有真的排查过模型不调工具」。答成「写清楚一点,方便别人理解」就落到文档思维了;面试官想听的是描述是模型唯一的判断依据这件事。
    2. 拆法:先问自己「模型做这个决定时手上有什么」。它看不到你的 wiki、代码注释、需求文档,只有工具名加这一句描述加参数 schema。所以描述不是文档,是决策依据。
    3. 把「太泛」拆成两个方向的后果:一是**漏调**,模型不知道这个工具能解决当前问题,任务默默做不成,而且不会报错;二是**误调**,描述边界不清,模型在不该调的时候调它——如果这个工具有副作用,那就是一次真实的线上事故。
    4. 结论:一句合格的描述要回答三件事——做什么、参数长什么样(给例子)、什么情况下才该用。第三条最常被漏掉,也最要命,因为它才是防误调的那道闸。
    5. 补一条工程视角:描述是对外契约,改它等于改行为。同一段描述在不同模型上表现还不一样,所以描述要进版本管理、要有评估集,不能靠上线后人肉观察。
    6. 可预期的追问:那把描述写得越长越好吗?不是。描述会占上下文预算,工具一多就挤掉真正的对话内容;正确做法是短而准,把细节放进每个参数各自的 description 里。

    How to reason about it · think before answering

    1. The screen is whether you have ever debugged a tool the model refuses to call. Answering 'write it clearly so colleagues understand' reveals doc-thinking; the point is that the description is the model's only evidence.
    2. Ask what the model has when it makes the decision: the tool name, this one description, and the parameter schema. It cannot see your wiki, comments, or spec. The description is a decision input, not documentation.
    3. Split vagueness into two failure directions. Under-calling: the model never realizes the tool solves the current problem, so the task silently fails with no error. Over-calling: fuzzy boundaries make the model invoke it when it should not, which is a real incident if the tool has side effects.
    4. Conclusion: a usable description answers three things — what it does, what the parameters look like with an example, and when it should be used. The third is the one people omit, and it is the gate that prevents over-calling.
    5. Add the engineering view: a description is an external contract, so changing it changes behavior, and the same wording performs differently across models. It belongs in version control with an eval set, not in post-launch eyeballing.
    6. Likely follow-up: is longer always better? No. Descriptions consume context budget and crowd out the actual conversation once you have many tools. Keep the summary short and push detail into each parameter's own description.

    答题要点

    • 描述是给模型看的,是它决定调不调这个工具的唯一依据,不是给同事看的文档
    • 写得太泛有两类后果:漏调导致任务静默失败,误调则可能触发有副作用的操作
    • 合格描述回答三件事:做什么、参数长什么样并给例子、什么情况下才该用
    • 描述是对外契约,要进版本管理并配评估集;细节放进每个参数的 description,总描述保持短而准

    Key points

    • The description is read by the model and is its only basis for deciding whether to call the tool
    • Vagueness causes silent under-calling or dangerous over-calling of side-effecting tools
    • A good description states what it does, what the parameters look like with an example, and when it applies
    • Treat it as an external contract with version control and evals; push detail into per-parameter descriptions to save context
  • 什么时候该返回 JSON-RPC 的 error,什么时候该返回 isError 为真的工具结果?给我一个判据。When should a tool return a JSON-RPC error versus a result with isError set to true? Give me a decision rule.
    国内高频海外高频进阶#error-handling#tool-design

    分析过程 · 先想清楚再作答

    1. 这题几乎是 MCP 服务端的入门分水岭。能背出「两类错误」只算及格,区分度在于能不能给出一条可执行的判据,以及知不知道错误文案是写给谁的。
    2. 拆法:问「谁能修好这个错」。请求本身不合法——工具名不存在、参数不满足调用工具的 schema、服务端内部异常——模型再怎么改参数都没用,这类走 JSON-RPC 的 error,典型是 -32602。工具跑了但业务没成——下游 API 失败、日期格式不对、金额越界——模型换个参数就可能成功,这类走 result 里的 isError。
    3. 判据一句话:**模型换个参数有没有可能成功?有就用 isError,没有就用 error。** 注意 isError 仍然是一个成功的 JSON-RPC 响应,resultType 照样是 complete。
    4. 结论要带上文案要求:规范说客户端应当把执行错误交给模型自我纠正,所以文案是写给模型看的,要列出可选值、正确格式、边界条件。写「参数错误」等于让模型瞎猜。
    5. 生产视角的坑:最危险的不是分错类,而是**两类都不返回**——不做校验,让非法输入算出 NaN 或空结果静默返回。模型会把错误答案当正确答案用下去,且不留痕迹。靠输出 schema 校验去兜底也不算处理,因为模型拿到的是一段 schema 堆栈。
    6. 可预期的追问:客户端要不要把协议错误也喂给模型?规范说可以,但基本没用,因为模型改不了;更该做的是记日志报警,那是你的 bug 不是模型的。

    How to reason about it · think before answering

    1. This is close to a pass/fail line for MCP server work. Reciting 'two kinds of errors' is baseline; the discriminator is producing an actionable rule and knowing who the error text is written for.
    2. Ask who can fix it. If the request itself is invalid — unknown tool, arguments failing the call-tool schema, an internal server fault — no amount of parameter tweaking helps, so return a JSON-RPC error, typically -32602. If the tool ran but the business case failed — downstream API error, bad date format, amount out of range — a different argument might work, so return isError in the result.
    3. The rule in one line: could the model succeed by changing an argument? If yes use isError, if no use error. Note that isError is still a successful JSON-RPC response with resultType complete.
    4. Carry the text requirement into the conclusion: the spec says clients should hand execution errors to the model for self-correction, so the message is written for the model. List allowed values, the correct format, the boundary. 'Invalid parameter' just makes it guess.
    5. The production trap is not misclassifying but returning neither — skipping validation so an illegal input yields NaN or an empty result that is silently returned. The model then uses a wrong answer with no trace. Leaning on output-schema validation is not handling it either, since the model receives a schema stack trace.
    6. Likely follow-up: should clients feed protocol errors to the model too? The spec permits it but it rarely helps, because the model cannot fix them. Log and alert instead — that one is your bug.

    答题要点

    • 协议错误走 JSON-RPC 的 error:未知工具、请求不满足 schema、服务端内部错,模型改参数也无济于事
    • 执行错误走结果里的 isError 为真:下游失败、业务校验不过,它仍是成功的 JSON-RPC 响应
    • 判据是模型换个参数有没有可能成功,有就 isError,没有就 error
    • 执行错误的文案写给模型看,要列出可选值与正确格式;最危险的是两类都不返回、静默给出错误结果

    Key points

    • Protocol errors use the JSON-RPC error field: unknown tool, schema-invalid request, internal fault — unfixable by the model
    • Execution errors use isError true in the result and remain a successful JSON-RPC response
    • The rule: if a different argument could succeed, use isError; otherwise use error
    • Write execution-error text for the model with allowed values and formats; the worst case is neither, silently returning a wrong result
  • 一个 stdio 的 MCP 服务端最常见的翻车原因是什么?你会在代码和流程上分别怎么堵住它?What is the most common way a stdio MCP server breaks, and how do you prevent it in code and in process?
    国内高频海外高频深入#stdio-transport#debugging

    分析过程 · 先想清楚再作答

    1. 这题在验有没有真跑过。没实际接过的人会答「进程没起来」「路径不对」这类泛泛的,真踩过的人第一句就会说标准输出被污染。
    2. 拆法:先复述 stdio 的硬规矩——消息是一行一条换行分隔的 JSON、内部不许有裸换行,服务端不得往标准输出写任何不是 MCP 消息的东西,日志一律走标准错误。规矩一说完,翻车原因就自明了。
    3. 现场特征值得单独说,因为它是这题的区分点:客户端只会报一句 JSON 解析失败,指不到你哪一行 console.log;而且污染源常常不是你自己的代码,而是某个第三方库在启动时打的横幅或弃用警告。
    4. 结论分两层。代码上:封一个只写标准错误的日志函数并全局禁用直接打印,接第三方库之前先确认它不往标准输出写东西,把 JSON 序列化后确保不含裸换行。流程上:加一个自测入口,用内存传输在同一个进程里把客户端和服务端接起来跑断言,有明确退出码,进持续集成——这样污染一出现就会在合并前被拦下。
    5. 再补一条相关的:优雅停机。规范说客户端关掉输入流、服务端读到文件结束就应尽快退出,这是主要且唯一可移植的停机信号;不处理它就会留下孤儿进程,本机开发时表现为端口和文件锁莫名被占。
    6. 可预期的追问:既然这么脆,为什么还用 stdio?因为它零配置、零网络攻击面、进程隔离天生就有,本机场景收益远大于代价;要给团队共享或多副本才需要换成远程传输。

    How to reason about it · think before answering

    1. This checks whether you have actually run one. People who have not will say 'the process did not start' or 'wrong path'; anyone who has been bitten leads with stdout contamination.
    2. Restate the hard rules first: messages are newline-delimited JSON, one per line, with no embedded newlines, and the server must not write anything to stdout that is not an MCP message. Logging goes to stderr. Once the rules are stated the failure mode is obvious.
    3. Call out the symptom, because that is the discriminator: the client only reports a JSON parse failure and cannot point at your console.log, and the polluter is often a third-party library printing a banner or deprecation warning at import time rather than your own code.
    4. Conclusion in two layers. In code: wrap a stderr-only logger, ban direct printing, vet third-party libraries for stdout writes, and ensure serialized JSON carries no raw newlines. In process: add a self-test entry point that links a client and server over an in-memory transport in one process, asserts, and exits with a real status code, then run it in CI so contamination is caught before merge.
    5. Add the adjacent one: graceful shutdown. The spec makes closing stdin and exiting on EOF the primary and only portable shutdown signal; ignoring it leaves orphan processes that show up locally as mysteriously held ports and file locks.
    6. Likely follow-up: if it is this fragile, why use stdio? Zero configuration, zero network attack surface, and process isolation for free — the tradeoff is clearly worth it locally. You switch transports when you need team sharing or multiple replicas.

    答题要点

    • 最常见的是标准输出被污染:stdio 规定 stdout 只能有 MCP 消息,一行 console.log 就让客户端解析失败
    • 现场只报 JSON 解析失败,指不到具体行,污染源常常是第三方库启动时打的横幅或警告
    • 代码上封一个只写标准错误的日志函数并禁用直接打印,接库之前先验它不写 stdout
    • 流程上加一个用内存传输的自测入口,有明确退出码并进持续集成;同时处理 stdin 关闭时的优雅退出

    Key points

    • Stdout contamination: stdio reserves stdout for MCP messages, so a single console.log breaks the client's parser
    • The symptom is only a JSON parse failure with no line number, and the culprit is often a third-party library's startup banner
    • In code, use a stderr-only logger, ban direct printing, and vet dependencies for stdout writes
    • In process, add an in-memory-transport self-test with a real exit code in CI, and exit promptly on stdin EOF to avoid orphan processes

评论

登录后即可参与讨论

还没有评论,来说第一句。