第 3 章 — 服务器原语:暴露上下文与能力
LLM Primer IV: Designing AI Cognition with MCP 章节走读的第三篇。这一章里我们学一个 MCP server 到底能说什么 — 三个名词、三种生命周期、三种错误模型 — 以及挑对那一个原语的纪律,为什么它决定了一个 server 是规模化得起来,还是一点点堆积成累赘。
这一章为什么存在
协议有多有用,取决于它让你说什么。第 2 章把 host、client、server 的心智模型搭起来,看着一个会话靠能力协商活过来。握手完成、双方知道对方能干什么之后,具体桌上还能放什么?MCP 用三个名词回答:Resources、Prompts、Tools。它们表面上很像 — 每一个都是 server 能产出或运行的、命过名、带 schema 描述的东西 — 但它们对应三种不同的意图。Resources 是读状态。Prompts 是可复用的脚手架。Tools 是写操作。这一章一个一个走:schema、生命周期、错误模型,还有工程师在哪些地方习惯搞错。
3.1 Resources:只读的上下文数据
Resource 是 server 能作为上下文递给模型的数据。决定性的那个词是 只读。Resource 不改世界;它描述世界。如果一次取值在别处引起了状态变化 — 审计日志多了一行、某个计数器加了一、某个 webhook 被触发了 — 它就不是 resource,它是穿着 resource 衣服的 tool。把这条线划得锐利,是 MCP server 设计的第一份纪律。
每一个 resource 有一个稳定 URI,scheme 由 server 自己选(file://、postgres://、linear://issue/ENG-1234),加一组元数据:name、可选的 description、MIME 类型、可选的 size。这些字段没有一个是装饰用的。一个没有描述的 resource,模型没法评估。生命周期很直白:resources/list 枚举,resources/read 取值。没有 resources/write — server 想要写,就必须暴露一个 tool。这个不对称把信任边界画得看得见。
两种模式让 resource 在规模上能用。URI 模板让 server 暴露一个参数化的读端点(db://orders/{order_id}),而不是把上百万行都列出来。订阅让 host 对一个 URI 登记兴趣,在底层数据变化时收到 notifications/resources/updated — 替代方案是按 LLM 的节奏轮询,这两边都浪费。要躲的坑是会话开始时就把每一个内部对象都塞进 resource 列表:三千个 resource 的列表,用户还没敲一个字就吃掉了几万个 token。暴露一个精选的顶层加上模板,把长尾留给模型去搜。
3.2 Prompts:可复用模板与工作流
MCP 里的 Prompts 跟 LLM 的 system prompt 没有任何关系。它们是可复用的、由 server 定义的模板,用户 — 或者 host 代用户 — 可以触发它们,开启一段特定的交互。Prompt 更接近一条 slash 命令,而不是一种人设。重点是让 server 跟它暴露的 tool 和 resource 一起,出货一批已知好用的交互模式,这样键盘前面的人不用记怎么措辞才能让请求生效。
一个 prompt 有 name、可选 description、一组参数。Host 调 prompts/list 来发现,然后调 prompts/get 把 prompt 展开成一串消息,host 拿这串消息喂进模型循环。Prompt 里可以按 URI 引用 resource,这种情况下 host 在发送前把它们 inline 进去。Server 写好开局几轮;模型从那里接手。
区分 prompt 和 system message 很重要。一个用户调 /review_pr,在对话记录里看到 assistant 开始做 review — 他知道开始的是什么、能打断、能审计。如果 server 反过来悄悄往 host 的 system prompt 上追加指令,用户根本不知道 assistant 为什么突然变了。Prompt 是用户看得见的脚手架;system prompt 是 host 的框架。两个不要混淆。纪律是:任何 prompt 里的内容,如果显式给用户看用户会不同意,那就是缺陷。
3.3 Tools:动作、结构化输出、幂等性
Tools 是 MCP 变得有意思也同等危险的地方。Resources 是读;prompts 是脚手架;tools 是写 — 它们建行、发消息、部署服务、扣信用卡。每一个 tool 有 name、description、一份用 JSON Schema 写的输入 schema。Description 是整个 MCP 表面上最重要的字段,因为它是模型决定要不要调、什么时候调、用什么参数调时读的那段文字。一个描述写着 "发邮件" 的 tool,凡是出现 "发邮件" 形状的意图模型就会去抓它。一个描述写着 "通过营销平台发交易类邮件,仅对已验证的客户地址;不用于私人通信" 的 tool,模型用起来就有分寸得多。
错误模型有两条通道。一个协议错误(参数畸形、未知工具)返回 JSON-RPC 错误,这是一个成帧失败。一个 tool 错误(收件人弹回、磁盘满)返回一个 成功 响应,里面 isError: true,带一个内容块解释发生了什么。这个区分很重要:模型应该看到 tool 错误并适应;host 应该看到协议错误并恢复传输。两者混在一起,就抢走了模型要用的信息。
现代 MCP 在散文风的 content 数组旁边支持 structuredContent,符合声明的 outputSchema。这件事在一条长 trace 上的复利是实打实的:简短、结构化的 tool 输出给模型留下做真正推理的余地;长长的散文输出吃掉上下文预算。还有两条纪律值得起名。极简:十二个描述清楚的 tool 在几乎所有 benchmark 上都打得过六十个;暴露 find_users 配一个结构化 filter,而不是 list_users、get_user、search_users、count_users 四件套。幂等性:重试风暴是必然的;一个接 idempotency key 或用确定性 id 方案的 tool,把网络抖动变成无操作,而不是数据一致性事故。
3.4 组合:三种原语怎么合作
这些原语在合作的时候最有用。一个设计良好的 server 通常每种都暴露一些:几个 prompt 引发常见交互,一小组 tool 处理重要的动作,可能一大组 resource 提供上下文。一个客服 server 可能把客户档案和工单历史暴露成 resource,把 reply_to_ticket 和 escalate_ticket 暴露成 tool,把 /triage_ticket 做成 prompt,加载对应 resource 再让模型分类。Prompts 起头;Resources 把场景填进来;Tools 改变世界。
看到这些原语有多灵活之后,诱惑是把一切都从一个里面推过去。你可以用只读 tool 假冒 resource,用 prompt 推模型吐出命令来假冒 tool,用塞了指令的 resource 假冒 prompt。每一种都压缩了设计空间,丢掉了一些东西。被你假冒成 tool 的 resource 没法缓存、订阅、被 inline 到 prompt 展开里。被你假冒成 prompt 的 tool 没法在调用点授权,也没有结构化输出通道。原语不是随便取的;它们被分开是为了让 host 知道怎么安全地对待每一种。尊重这个分割是协议合约的一部分。
第 3 章接下去会怎么走
你已经走过了 server 这一侧。Server 暴露 Resources、Prompts、Tools,各有 schema、生命周期、错误模型。纪律是给每一件 server 能做的事挑对原语,起名描述都仔细做,抗住串台的诱惑。但 MCP 不是单向的。Host 也能反过来给 server 暴露能力 — 协议里最有意思也最敏感的设计选择就住在那里。
明天 — 第 4 章:客户端原语 — Sampling、Roots、Elicitation。反过来那一面 — host 还给 server 的东西 — 以及每一项能力跨过信任边界时的安全含义。