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

resources 与 prompts:URI 模板、变更通知、进度与日志、分页,以及客户端能力

把只读数据做成资源、把重复问法做成提示模板,再补齐 URI 模板、分页、进度、订阅与缓存这几件让服务端在真实数据量下还能用的事。

今日目标 0/3

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

今日目标

  1. 能判断一份数据该做成资源还是做成工具,并说出判断依据
  2. 能用 URI 模板暴露一组参数化资源,并为列表接口实现游标分页
  3. 能说清订阅流与请求内通知的区别,并各举一个使用场景

昨天你的服务端只有工具,一共两个,谁调都一样快。今天换一份真实体量的数据进来——几千篇笔记——你会立刻撞上分页、缓存和通知这三堵墙。完整的报文骨架 D1 已经拆过一遍,今天只看新增的那几个字段。读完回来把上面三条勾掉。

小白版讲解

资源是应用驱动的:谁决定它进不进上下文

先回到插座那个类比。工具像是电器上的开关:模型看着说明书自己按。资源不一样,它更像插排上贴的标签——宿主应用或用户扫一眼,决定这一路要不要通电。

规范把这件事说得很直白:资源是"应用驱动"(application-driven)的,由宿主应用决定怎么把它并进上下文。典型的界面形态是一个文件选择器、一棵目录树,或者一个搜索框;也可以由应用按启发式规则自动带上。而工具是"模型控制"的,模型看着描述自己挑。

所以同一份数据做成哪一种,判据不是"它是什么",而是这一次由谁来决定用不用它

情况做成
只读、可枚举、希望用户在界面上挑资源
会产生副作用,或要由模型自己判断何时使用工具
需要一次检索出结果,而不是先列后读工具(哪怕它是只读的)

最后一行是最容易搞错的地方。"搜索代码"看起来是只读的,像资源,但它没法枚举——你不可能把所有可能的查询词列成一张清单让用户挑。能不能被列出来,是资源和工具之间那条最实用的分界线。

这条分界线还有一个说不出口但很现实的好处:资源不进上下文就不花钱。工具的定义无论用不用,每一轮都要塞进请求里;资源只是一条清单项,宿主不选它就一个 token 都不占。一个有三千篇文档的知识库,做成三千个工具会直接把上下文撑爆,做成资源则只在用户点中那一篇时才付费。

问题也就跟着来了:三千篇笔记,resources/list 一次性全吐回去吗?

URI 模板:一条定义顶一整组资源

答案当然是不能全吐,但在讲分页之前,先解决一个更前置的问题:有些资源你根本列不完,但它们又确实是可枚举的一族。

比如"按标签读笔记"。标签有几十个,每个标签下有若干篇,组合起来几百上千条,全列出来既慢又没人看。规范给的办法是资源模板(resource template):用 resources/templates/list 返回一条 RFC 6570 格式的 URI 模板,把变化的部分留成占位符。

JSONJSON
{
  "resourceTemplates": [
    {
      "uriTemplate": "notes:///{tag}/{slug}",
      "name": "note-by-tag",
      "title": "按标签读一篇笔记",
      "mimeType": "text/markdown"
    }
  ],
  "ttlMs": 300000,
  "cacheScope": "public"
}

客户端拿到这条模板,就知道 notes:///mcp/pagination 是一个合法的资源地址,不需要它出现在清单里。占位符还可以接上补全接口,让用户在界面上边打边提示。

URI 的 scheme 怎么选,规范给了几条现成的:file:// 用于表现得像文件系统的东西(不一定真是文件);git:// 是版本控制;https:// 有一条特别的限制——只在客户端能自己从网上抓到时才用它,如果内容其实得经过服务端,就别用 https,另定一个 scheme。自定义 scheme 只要符合 RFC 3986 就行,本课的实验用的是 notes:///

自定义 scheme 好写,但有个必须自己堵的坑:

分页是必修课:游标不透明,页大小由服务端说了算

回到那三千篇笔记。MCP 的分页用的是不透明游标(opaque cursor),不是页码。规则只有几条,但每一条都有人踩:

  • 游标是一个不透明的字符串,客户端 不得 解析它、修改它,也 不得 根据它的内容做任何判断。
  • 页大小由服务端决定,客户端 不得 假设它是固定值。
  • 只有 nextCursor 缺失才代表结束。 空字符串是一个完全合法的游标,把它当成结束是明确被禁止的。
  • 非法游标 应当-32602(Invalid params),而不是静默返回第一页。

支持分页的一共四个操作:resources/listresources/templates/listprompts/listtools/list

服务端这边爱怎么编游标都行——base64 包一个偏移量、一个数据库主键、一个时间戳都可以,反正对面不许看:

pagination.ts
const PAGE_SIZE = 50
 
export function encodeCursor(offset: number): string {
  // 包一层 base64 不是为了保密,是为了让客户端没法顺手去解析它
  return Buffer.from(`offset:${offset}`, 'utf8').toString('base64url')
}
 
export function decodeCursor(cursor: string | undefined): number {
  if (cursor === undefined) return 0 // 只有 undefined 才是「没有游标」
  const decoded = Buffer.from(cursor, 'base64url').toString('utf8')
  const match = /^offset:(\d+)$/.exec(decoded)
  if (!match) throw new InvalidCursorError(cursor) // 非法游标要报错,不能静默回第一页
  return Number(match[1])
}
 
export function paginate(all: string[], cursor: string | undefined) {
  const offset = decodeCursor(cursor)
  const items = all.slice(offset, offset + PAGE_SIZE)
  const nextOffset = offset + items.length
  // 只有确实还有下一页时才给 nextCursor;缺失本身就是结束的信号
  return nextOffset < all.length ? { items, nextCursor: encodeCursor(nextOffset) } : { items }
}

工程代价藏在一个不显眼的地方:偏移量式游标要求列表顺序稳定readdir 在不同文件系统上的返回顺序并不一致,中途插入一篇笔记也会让后面所有偏移量整体错位,客户端翻到第二页时会漏掉或重复。所以要么先排序,要么把游标编成"上一条的主键"而不是偏移量。今天的实验用的是排序加偏移量,够用且好读,但你要知道它的前提。

缓存提示 ttlMs 与 cacheScope

2026-07-28 新增了一个 CacheableResult 接口,把两个字段变成了必填:tools/listprompts/listresources/listresources/readresources/templates/list 这五个结果都要带上 ttlMscacheScope

ttlMs 是新鲜度提示,单位毫秒,告诉客户端"这份东西可以缓存多久",目的是减少轮询。cacheScope 只有两个取值:public 表示共享的中间层(网关、代理)也可以缓存这份响应,private 表示只能缓在这个客户端自己那儿。

用起来的直觉是:清单变得慢,正文变得快,两者的 ttl 应该差一个数量级跟调用者身份有关的东西一律 private——工具清单可能随授权范围变化,那就不能是 public

JSONJSON
{
  "resources": [{ "uri": "notes:///pagination", "name": "pagination", "mimeType": "text/markdown" }],
  "nextCursor": "b2Zmc2V0OjM",
  "ttlMs": 300000,
  "cacheScope": "public"
}

这套缓存提示和 listChanged 通知是互补关系,不是二选一:ttl 管的是"没有通知时你可以放心用多久",通知管的是"变了我告诉你"。两个都给,客户端才能既不频繁轮询、又不拿着过期数据。

顺带一提,规范还建议 tools/list 应当返回确定性顺序——同一批工具在多次请求间的排列不要变。理由不只是好看:顺序稳定,客户端才敢缓存工具清单,而工具清单通常是要塞进模型上下文的,顺序一抖动,提示缓存就整体失效,这是实打实的钱。

提示模板:把团队里那句问烂了的话变成一个斜杠命令

第三种原语是提示模板(prompt)。它是用户控制的:由用户显式选中,典型形态就是聊天框里的斜杠命令。

它解决的是一个很土但很real的问题:团队里总有那么几句话被反复打,"帮我 review 这段代码,重点看并发安全"、"把这个报错翻译成人话并给三个排查方向"。每个人自己存一份,版本还各不相同。做成提示模板之后,它跟着服务端走,改一次所有人都更新。

prompts/get 返回的是一组消息,role 只有 user 和 assistant 两种,content 可以是文本、图片、音频、资源链接(resource_link)或内嵌资源(resource)。这里有个值得琢磨的设计选择:

digest-prompt.ts
// 按标签汇总笔记的提示模板
async function getDigestPrompt(tag: string) {
  const hits = await notesByTag(tag)
  return {
    description: `标签 ${tag} 下共 ${hits.length} 篇笔记`,
    messages: [
      { role: 'user', content: { type: 'text', text: `请把下面这些笔记汇总成不超过 200 字的要点。` } },
      // 用 resource_link 而不是内嵌正文:让宿主自己决定要不要真去读,
      // 正文进不进上下文由应用说了算——这正是「资源是应用驱动」在报文层面的样子
      ...hits.map((n) => ({
        role: 'user' as const,
        content: { type: 'resource_link' as const, uri: `notes:///${n.slug}`, name: n.slug },
      })),
    ],
  }
}

返回链接而不是正文,是把"要不要花这份 token"的决定权交还给宿主。如果你直接内嵌,一个命中三十篇的标签会瞬间灌进几万 token,而用户可能只想看其中两篇。错误处理这边很简单:提示模板名无效、缺必填参数,都用 -32602;服务端内部出错用 -32603

两条通知通道:订阅流管长期变更,响应流管这一次的进度

这一节是本章和旧文档差别最大的地方,读到网上任何讲 resources/subscribe 的材料都可以直接跳过了。

2026-07-28 把 resources/subscriberesources/unsubscribe 两个方法、以及 HTTP 上那条独立的 GET 长连接,一起换成了一个 subscriptions/listen。它本身是一条普通请求,只不过它的响应是一条一直开着的通知流。客户端在 notifications 过滤器里显式勾选想收哪几类,服务端 不得 推送没被勾选的类型:

JSONJSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "subscriptions/listen",
  "params": {
    "notifications": {
      "toolsListChanged": true,
      "resourceSubscriptions": ["notes:///pagination"]
    }
  }
}

四个可选字段是 toolsListChangedpromptsListChangedresourcesListChangedresourceSubscriptions(一个 URI 数组)。流上的第一条消息必须notifications/subscriptions/acknowledged,里面回的是服务端实际答应的那个子集——它不支持的类型会被悄悄去掉,所以客户端应当拿它和自己请求的对一遍。此后每一条通知都会在 _meta 里带上 io.modelcontextprotocol/subscriptionId,值就是当初那条 subscriptions/listen 请求的 JSON-RPC id。stdio 上所有消息共用一条通道,客户端必须靠这个字段区分是哪个订阅来的。

另一条通道完全不同:请求内通知只走它所属那条请求的响应流notifications/progressnotifications/message 属于这一类,它们不会出现在订阅流上。进度要靠客户端在请求的 _meta 里放一个 progressToken 来开启,之后服务端可以发若干条带 progress(必须递增)、可选 totalmessage 的通知:

JSONJSON
{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": { "progressToken": "abc123", "progress": 50, "total": 100, "message": "正在建索引" }
}

一句话记住分工:订阅流回答"世界变了吗",响应流回答"我这一单做到哪了"。 前者跨请求、长期存在;后者随请求生随请求死。

日志这边有个必须知道的变化:日志功能(Logging)连同 Roots、Sampling 一起,在这一版被标记为弃用(deprecated),弃用窗口至少十二个月,新实现不该再采用。logging/setLevel 方法已经删除,日志级别改成每请求在 _meta.io.modelcontextprotocol/logLevel 里指定,而且服务端不得对没带这个字段的请求发 notifications/message。官方建议的迁移路径是:stdio 直接写 stderr,或者接 OpenTelemetry。

客户端能力:为什么服务端不能假设对面会弹表单

最后一块拼图。服务端想让用户填个东西、或者想借客户端的模型跑一段推理,前提是对面得有这个能力。规范对此的要求是硬的:服务端 不得 依赖客户端没有声明的能力;真的需要而对方没声明时,必须回 -32021MissingRequiredClientCapability),并在 data.requiredCapabilities 里列出缺了哪些。

这一版的客户端能力以 elicitation(征询)为主——就是"弹个表单问用户"这件事。声明长这样:

JSONJSON
{
  "_meta": {
    "io.modelcontextprotocol/clientCapabilities": {
      "elicitation": { "form": {}, "url": {} }
    }
  }
}

两种模式:form 是在客户端界面里收结构化数据,schema 被刻意限制成扁平对象加基础类型(字符串、数字、布尔、枚举),复杂嵌套一律不支持,为的是让任何客户端都能自动生成表单。url 是把用户导到服务端自己的安全页面上去。规范硬性规定:密码、API key、访问令牌、支付凭证这类东西不得用 form 模式收,必须走 url 模式——因为 form 收上来的数据会经过客户端,而 url 模式下敏感信息只在用户浏览器和服务端之间流动。为了向后兼容,一个空的 elicitation: {} 等价于"只支持 form"。

Roots 和 Sampling 已经弃用了,所以你今天设计服务端时,能指望的客户端能力基本就是 elicitation 这一个。默认假设对面什么都不会,需要额外输入时把它做成工具参数让模型来填,实在不行再退回到征询——这是 2026 年写 MCP 服务端最稳妥的姿势。

至于"工具和资源太多,上下文塞不下"的通用裁剪策略,那是另一门课的主场,需要时去看工具结果与检索的上下文管理,本课只负责把 MCP 这一层的接口讲干净。

源码导读

动手实验

🧪 D3 实验:一个把本地 Markdown 笔记库暴露成资源的 MCP 服务端

代码位置:labs/mcp-7days/day-03-notes-resource-server

验收标准:

  1. resources/list 能翻完全部笔记,第二页与第一页不重复,最后一页不带 nextCursor
  2. 非法游标被拒绝,而不是静默返回第一页
  3. resources/templates/list 返回一条带 uriTemplate 的资源模板,resources/read 能读到笔记正文
  4. prompts/get 按标签筛出笔记,并为每一篇附上一条 resource_link
  5. MOCK=1 SELFTEST=1 pnpm start 六项全绿,退出码为 0

实验完全离线,数据就是仓库里的八篇 Markdown,不需要任何 key。starter 原样跑是 1 / 6 通过,四个练习点各对应若干条红项——把红的一条条变绿,就是今天的全部工作。做完第一个练习之后会撞上一个很值得琢磨的现象:清单条数对了,翻页却在原地打转。原因是 starter 的游标编码返回的是空字符串、解码恒定返回 0,而空串是一个合法游标、并不代表结束——这正是本章那条规则的现场演示。卡住了先回到上面"分页是必修课"那一节。

  1. 打开 solution 的 server.ts,找到 resources/list 与 resources/templates/list 两个处理器,看清静态清单与 URI 模板分别长什么样。
  2. 在 starter 的 notes.ts 里补全 scanNotes,跑一次自测,看到第 1 项的条数从 1 变成真实笔记数。
  3. 补全 pagination.ts 的游标编解码,跑自测确认第 2 项变绿、并且第 6 项也跟着变绿。
  4. 补全 server.ts 里的资源模板与按标签检索的提示模板,让第 4、5 项变绿。
  5. 用 MOCK=1 pnpm start 常驻起来,接上你自己的客户端或 Inspector,亲眼确认 ttlMs 与 cacheScope 出现在响应里。

面试题

今天 3 道题在下方题库区,侧重资源与工具的边界、URI 设计、分页与缓存、通知通道的分工。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。

检查清单与明日预告

  • 能判断一份数据该做成资源还是做成工具,并说出判断依据
  • 能用 URI 模板暴露一组参数化资源,并为列表接口实现游标分页
  • 能说清订阅流与请求内通知的区别,并各举一个使用场景
  • 能说出 ttlMscacheScope 各管什么,以及它们和 listChanged 是什么关系
  • 知道 Roots、Sampling、Logging 三项已被弃用,以及各自建议的迁移路径
  • 实验的 5 条验收标准全部通过
  • 3 道面试题不看要点也能答出至少 2 道

明天(D4)把服务端搬到公网上去。顺序是有意的:前三天所有东西都跑在本机子进程里,传输的坑被 stdio 挡掉了大半;一旦换成 HTTP,鉴权、多副本、来源校验、超时会一次性全冒出来,而这一版恰好把会话、GET 长连接和断流续传全删了,客户端要补的东西比你想的多。今天学的分页与缓存到那时会立刻显出价值——无状态加上可缓存的清单,才是多副本部署能横着扩的前提。

面试题库

  • 同一份数据,做成 MCP 资源和做成工具有什么区别?你按什么标准选?For the same data, what is the difference between exposing it as an MCP resource versus a tool, and how do you choose?
    国内高频海外高频基础#primitives#server-design

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

    1. 这题在筛「有没有真的设计过服务端」。答成「资源是只读的、工具会改数据」只能算及格,因为只读的检索照样该做成工具,区分度全在这一步。
    2. 拆法:不要问「它是什么」,问「这一次由谁决定用不用它」。规范把资源定成应用驱动——由宿主应用或用户挑;工具是模型控制——模型看着描述自己调。控制方定了,出错时该找谁负责也就定了。
    3. 再补一条更实用的判据:能不能被枚举。资源要出现在一张可翻页的清单里让人挑,所以「搜索代码」这种输入空间无限的能力,哪怕完全只读也必须做成工具。
    4. 结论:只读、可枚举、希望用户在界面上挑的做成资源;有副作用、或需要模型自己判断时机、或无法枚举的做成工具。
    5. 生产视角要主动加一句成本:工具定义不管用不用,每轮都要塞进请求;资源不被选中就一个 token 都不占。三千篇文档做成三千个工具会直接撑爆上下文,做成资源则按需付费。
    6. 可预期的追问:那提示模板算第几种?答案是第三种,由用户显式选中,典型形态是斜杠命令——三种原语的差别只在控制方,不在能力。

    How to reason about it · think before answering

    1. This screens for real server design experience. Saying resources are read-only and tools mutate scores a pass at best, because read-only search still belongs in a tool.
    2. Reframe it: do not ask what the data is, ask who decides to use it this time. The spec makes resources application-driven, picked by the host or the user, while tools are model-controlled. Fixing the controller also fixes who is accountable when it goes wrong.
    3. Add the practical test: enumerability. A resource has to appear in a paginated list a human can pick from, so a code search with an unbounded input space must be a tool even though it never writes anything.
    4. Conclusion: read-only, enumerable, user-selectable becomes a resource; side-effecting, model-timed, or non-enumerable becomes a tool.
    5. Bring up cost unprompted: tool definitions ship on every turn whether used or not, while an unselected resource costs zero tokens. Three thousand documents as three thousand tools blows up the context window; as resources they are pay-per-use.
    6. Likely follow-up: where do prompts fit? They are the third primitive, user-selected and usually surfaced as slash commands — the three differ only by who controls them.

    答题要点

    • 资源是应用驱动的,由宿主或用户挑;工具是模型控制的,由模型看描述自己调
    • 能不能枚举是最实用的分界线:搜索这类输入空间无限的能力即使只读也做成工具
    • 成本上工具定义每轮都占上下文,资源不被选中就不花钱,大规模知识库必须走资源
    • 控制方决定了出错时找谁负责:模型选错是描述问题,用户选错是命名问题,应用塞错是产品问题

    Key points

    • Resources are application-driven and picked by host or user; tools are model-controlled and chosen from their descriptions
    • Enumerability is the practical dividing line: unbounded-input capabilities like search stay tools even when read-only
    • Cost-wise tool definitions occupy context every turn while unselected resources cost nothing, so large corpora must be resources
    • The controller determines accountability: bad tool choice means bad descriptions, bad prompt choice means bad naming, bad resource injection is a product problem
  • MCP 的分页游标为什么必须是不透明的?如果客户端去解析它,会出什么问题?Why must MCP pagination cursors be opaque, and what breaks if a client parses them?
    国内高频海外高频进阶#pagination#api-design

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

    1. 这题表面考规范条文,实际考「有没有做过带分页的对外接口」。只背出「规范说不透明」拿不到分,要能说出解析之后具体哪一步会崩。
    2. 拆法:先问游标里到底装的是什么。服务端可以装偏移量、主键、时间戳、甚至一段加密状态,而且**换实现时它随时会变**。客户端一旦按某种格式解析,服务端从偏移量换成主键那天,所有客户端一起挂——这是把服务端的内部实现变成了公开契约。
    3. 第二个坑是伪造。客户端自己造一个 offset:9999 递给服务端,等于绕过了服务端对翻页范围的控制;如果游标里编了权限或过滤条件,伪造它就是一次越权。
    4. 第三个坑最阴:把空字符串当成结束。规范写死了只有 nextCursor **缺失**才代表没有下一页,空串是完全合法的游标。判错的表现是最后一页数据被静默丢掉,而且不报错,测试也很难发现。
    5. 结论:客户端对游标只允许做一个判断——nextCursor 在不在。页大小同理不得假设固定值,服务端随时可以改。非法游标服务端应当回 -32602,而不是静默返回第一页,否则客户端会陷进死循环。
    6. 可预期的追问:那服务端这边有什么坑?偏移量式游标要求列表顺序稳定,中途插入一条会让后面全部错位,所以要么先排序、要么把游标编成上一条的主键。

    How to reason about it · think before answering

    1. It looks like a spec-recitation question but really tests whether you have shipped a paginated public API. Quoting the rule earns nothing; naming the concrete failure does.
    2. Start from what a cursor holds. A server may encode an offset, a primary key, a timestamp, or encrypted state, and it may change that at any time. A client that parses one format breaks everywhere the day the server switches, because parsing turned an internal detail into a public contract.
    3. Second failure is forgery. A client that fabricates offset:9999 bypasses the server's control over paging range, and if the cursor encodes filters or permissions, forging it is a privilege escalation.
    4. Third and nastiest: treating an empty string as the end. The spec is explicit that only a missing nextCursor ends the sequence; an empty string is a valid cursor. Getting this wrong silently drops the last page with no error, which tests rarely catch.
    5. Conclusion: a client may make exactly one judgment about a cursor — whether nextCursor is present. Page size likewise must not be assumed fixed. Servers should reject invalid cursors with -32602 rather than silently returning page one, which would loop the client forever.
    6. Likely follow-up: what bites the server side? Offset cursors require a stable ordering, since an insertion shifts everything after it, so either sort first or encode the last item's key instead.

    答题要点

    • 游标内容是服务端的内部实现,解析它等于把实现细节变成公开契约,服务端换实现时客户端全挂
    • 伪造游标可以绕过服务端对翻页范围的控制,游标里若编了过滤或权限条件就是越权
    • 只有 nextCursor 缺失才代表结束,空字符串是合法游标,判错会静默丢掉最后一页
    • 页大小由服务端决定不得假设固定,非法游标服务端应回 -32602 而不是静默回第一页

    Key points

    • Cursor contents are server internals; parsing them turns an implementation detail into a public contract that breaks on any change
    • Forged cursors bypass server-side paging control, and become privilege escalation if the cursor encodes filters or permissions
    • Only a missing nextCursor ends the sequence — an empty string is valid, and getting it wrong silently drops the last page
    • Page size is server-decided and must not be assumed fixed; invalid cursors should return -32602 rather than silently resetting
  • 订阅流和请求内的进度通知在 HTTP 上都走 SSE,为什么 2026-07-28 规范要把它们分成两个通道?On HTTP both subscription streams and in-request progress notifications ride SSE, so why does the 2026-07-28 spec split them into two channels?
    国内高频海外高频深入#subscriptions#notifications

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

    1. 这题的区分度在版本认知。还在讲 resources/subscribe 和一条独立 GET 长连接的人会当场暴露——这两样在这一版被合并替换成了 subscriptions/listen。
    2. 先把事实摆清:subscriptions/listen 本身是一条普通请求,只是它的响应是一条一直开着的通知流;客户端在 notifications 过滤器里显式勾选 toolsListChanged、promptsListChanged、resourcesListChanged、resourceSubscriptions,服务端不得推送没勾选的类型;第一条消息必须是 acknowledged,之后每条通知在 _meta 里带 subscriptionId。
    3. 拆法:问两类通知的生命周期一样吗。进度和日志属于某一次具体请求,请求结束它们就该停;列表变更、资源更新属于整个连接期,跟任何单次请求都无关。生命周期不同的东西混在一条流里,取消语义就说不清——HTTP 上关闭响应流就是取消该请求,你不会希望取消一次工具调用顺带把订阅也掐了。
    4. 第二个理由是无状态与可路由。请求内通知天然跟着那条请求的响应流走,任意副本都能处理;订阅是唯一一条长活连接,把它单独隔出来,剩下的请求才能真正做到无粘性路由。
    5. 结论:订阅流回答「世界变了吗」,跨请求、长期存在;响应流回答「我这一单做到哪了」,随请求生随请求死。规范明确写了进度与日志通知不在订阅流上出现。
    6. 可预期的追问:日志通知现在怎么开?logging/setLevel 已删除,改为每请求在 _meta 的 logLevel 里指定,且服务端不得对没带这个字段的请求发日志通知;而且 Logging 连同 Roots、Sampling 一起已被标记弃用,建议迁到 stderr 或 OpenTelemetry。

    How to reason about it · think before answering

    1. The discriminator is version awareness. Anyone still describing resources/subscribe and a standalone GET stream exposes themselves — both were replaced by subscriptions/listen in this revision.
    2. Get the facts straight first: subscriptions/listen is an ordinary request whose response is a stream that stays open. The client explicitly opts into toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions; the server must not push unselected types; the first message must be the acknowledgment, and every later notification carries subscriptionId in _meta.
    3. Then compare lifetimes. Progress and log notifications belong to one specific request and should stop when it ends. List changes and resource updates span the whole connection and relate to no single request. Mixing different lifetimes into one stream wrecks cancellation semantics, because closing a response stream on HTTP is the cancel signal — you do not want cancelling a tool call to kill your subscriptions.
    4. The second reason is statelessness and routability. In-request notifications naturally ride their own response stream so any replica can serve them; isolating the one genuinely long-lived connection is what lets every other request avoid sticky routing.
    5. Conclusion: the subscription stream answers has the world changed, spanning requests; the response stream answers how far along is my request, living and dying with it. The spec states outright that progress and message notifications never appear on the listen stream.
    6. Likely follow-up: how do you enable log notifications now? logging/setLevel was removed in favour of a per-request logLevel in _meta, and servers must not emit message notifications for requests that omit it. Logging is also deprecated alongside Roots and Sampling, with stderr or OpenTelemetry as the suggested migration.

    答题要点

    • 这一版用 subscriptions/listen 取代了 resources/subscribe 与独立的 GET 长连接,客户端显式勾选通知类型
    • 两类通知生命周期不同:进度日志随请求生灭,列表与资源变更跨请求长期存在
    • 混在一条流里会让取消语义失效,HTTP 上关闭响应流即取消该请求,不该顺带掐掉订阅
    • 隔离出唯一的长活连接,其余请求才能无粘性路由,这是无状态设计能横向扩容的前提

    Key points

    • This revision replaced resources/subscribe and the standalone GET stream with subscriptions/listen, where clients explicitly opt into notification types
    • The two kinds have different lifetimes: progress and logs live and die with a request, list and resource changes span the connection
    • Merging them breaks cancellation, since closing a response stream on HTTP cancels that request and must not kill subscriptions
    • Isolating the single long-lived stream is what lets every other request route without stickiness, enabling horizontal scaling

评论

登录后即可参与讨论

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