Skip to content

feat(client): MCP SDK v2, stateless 2026-07-28 protocol with fallback, pinnable protocol version, conformance tests, proxy support - #22

Merged
github-actions[bot] merged 9 commits into
mainfrom
claude/wonderful-cerf-fv8ydk
Oct 21, 2026
Merged

github-actions[bot] merged 9 commits into
mainfrom
claude/wonderful-cerf-fv8ydk

Conversation

@ticoAg

@ticoAg ticoAg commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

What and why · 改了什么、为什么

实现路线图 0.4 的 e1、e2、e3、e8、f2(MCP 协议对齐),外加一处 SDK v2 带来的回归修复。

e1 迁移到 SDK v2(dcca6c3)

  • 客户端改用 @modelcontextprotocol/client 2.3.1,via serve 改用 @modelcontextprotocol/server 2.3.1,都精确锁定版本。1.x @modelcontextprotocol/sdk 保留为 devDependency,用来跑旧协议的夹具 server。
  • 结果仍然宽松处理:不用 SDK 的类型化方法,因为它遇到一个格式不对的工具就会拒绝整个列表。
  • OAuth 登录探测不再写死 2025-06-18。via 自己的版本列表放在 src/protocol-versions.ts,配置加载不需要载入 SDK;有单测在 SDK 升级时发现版本列表漂移。
  • SDK 仍是懒加载。

e2 2026-07-28 无状态协议(bf2f9d4,ADR 0017)

  • 先用 server/discover 探测,不支持时在同一连接上退回 initialize。
  • daemon 通过 subscriptions/listen 跟踪列表变更,断开后按 1–30 s 退避重订。
  • server 给的 ttlMs 缩短目录缓存的 24 h TTL,取两者较短的;0 或不给视为没有提示。
  • roots 经新协议的 input_required 应答。
  • 新协议下工具调用经 SDK 的 callTool,会把 x-mcp-header 参数镜像成请求头。
  • stdio server 记住上次协商出的协议,避免每次探测都把进程拉起两次。

e3 协议版本可见、可固定(04f6a4b)

  • via servers 新增协议列,从缓存读,不连接 server。按列解析输出的脚本需要更新,已写入 CHANGELOG。
  • via doctor 每个正常的 server 加一行,例如 github: MCP 2026-07-28 stateless。
  • 配置字段 servers.<name>.protocolVersion 固定版本;server 不支持时退出码 6 加 next:,不退回旧版本。

e8 一致性测试(4607109)

  • test/conformance/client.mjs 用构建产物跑官方 @modelcontextprotocol/conformance 0.1.16。initialize、tools_call、sse-retry 和 14 个 auth/* 场景通过。
  • 8 个失败场景列在 test/conformance/expected-failures.yml:其中 CIMD、client credentials 等 6 个归 e7,elicitation 归 e5,各自对应路线图条目;另有 2025-03-26 的元数据兼容场景,原因是 SDK 严格校验 issuer。
  • CI 新增 conformance 与 stateless 两个任务。夹具 server 用同一张处理表组装出新旧两代,VIA_TEST_PROTOCOL=modern 切到新协议;回放 server 支持 --protocol 2026-07-28。

f2 出站代理(83f10e4、b1ef141)

  • MCP HTTP、OAuth 和 embedding 请求遵守 HTTPS_PROXY/HTTP_PROXY/NO_PROXY。
  • undici 的 ProxyAgent 只在第一次走代理时加载(新依赖 undici ^7);Bun 用 fetch 自带的 proxy 选项。
  • 不可用的代理(如 socks5://)退出码 2,绝不静默直连。代理的 userinfo 纳入脱敏,via doctor 新增 proxy 一节。

回归修复(ffddc92)

  • SDK v2 的 callTool() 会因 outputSchema 抛错:缺少 structuredContent、内容不匹配、schema 无法编译都会抛。规范要求只警告,警告由 CLI 自己的校验给出。
  • 只有 SDK 自己的工具缓存被填充时才会触发,在 via 里就是 x-mcp-header 不匹配后的重试。现在 validator provider 不返回 validator,各条路径都跳过这些检查。新增两个忽略 outputSchema 的夹具工具,两代协议都按原样返回。

性能(npm run bench:perf,前后交替运行):

项 改动前 改动后
浏览命令冷启动 — 在噪声范围内,不加载 SDK
热 daemon 调用开销 5.33 ms 5.35 ms
无 daemon 的调用 — 约 +40 ms
daemon 会话后 RSS 约 80 MiB 约 85 MiB
单文件可执行 86 MiB 89 MiB

规范与文档:daemon.md(新增「协议版本与无状态连接」)、discovery.md、config.md、cli.md、security.md、auth.md、ADR 0017、ADR 0004 附注、glossary、implementation.md(「MCP SDK(v2)」)、testing.md(「两代协议」「官方一致性测试」)、CHANGELOG、路线图状态。

本地结果:

  • npm run typecheck、npm run build 通过。
  • npm test:旧协议夹具 1842 通过 / 8 跳过;VIA_TEST_PROTOCOL=modern 1841 通过 / 9 跳过。

还没做:

  • 新测试与 stateless 任务只在 Linux 上跑,没接进 Windows 和 macOS。
  • Bun 的代理与 https CONNECT 路径没有端到端测试。
  • daemon status 的文本输出不显示协议,只有 -o json 带。

Checklist · 检查清单

  • npm run typecheck, npm test and npm run build pass
  • User-visible behavior changes update docs/spec/ and the Unreleased section of CHANGELOG.md
  • Rendering changes come with snapshot tests · 渲染改动附带快照测试
  • Verb list, stopword and naming changes say in CHANGELOG which canonical names they affect — 不涉及
  • No resolved credential values in output, logs or snapshots

Generated by Claude Code

Client moves to @modelcontextprotocol/client 2.3.1 and via serve to
@modelcontextprotocol/server 2.3.1; the 1.x SDK stays as a dev dependency for the
fixture servers. Results are still handled leniently (generic requests with an
accept-anything schema), error mapping recognises the v2 error classes, the OAuth
login probe no longer hardcodes a protocol version, and a unit test guards via's
protocol-version constants against drift from the SDK.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
Connections probe with server/discover and use the stateless protocol when the server
offers it, otherwise fall back to the initialize handshake on the same connection.
Stateless servers keep no session: the daemon holds the connection and the caches,
list changes arrive on a subscriptions/listen stream (re-opened with backoff when it
drops), a server's ttlMs shortens the catalog TTL, and the catalog cache remembers the
negotiated protocol so a stdio server of the old protocol is not probed (started
twice) on every connection. Fixture servers now speak either generation
(VIA_TEST_PROTOCOL=modern runs the CLI, daemon and client suites against the new one).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
via servers shows the protocol and statefulness each server last negotiated (from the
catalog cache), via doctor shows it live. servers.<name>.protocolVersion pins a
version; a server that does not support it is an error, not a fallback.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
…s for both protocol generations

test/conformance/client.mjs drives the built via from @modelcontextprotocol/conformance
client scenarios (initialize, tools_call, sse-retry, 14 auth scenarios), known gaps are
in expected-failures.yml. The replay server gets a --protocol 2026-07-28 mode and CI runs
the server-dependent suites a second time against stateless fixtures
(npm run test:stateless). A configured OAuth client id no longer makes the SDK warn about
a missing issuer stamp.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
…XY and NO_PROXY

The MCP HTTP transport, OAuth (login, refresh, revoke) and the embedding endpoint now go
through the proxy the environment asks for (names case-insensitive, HTTPS_PROXY for https
URLs, NO_PROXY matching). Nothing is loaded without a proxy variable; undici's ProxyAgent
is imported on the first proxied request. An unusable proxy is an error, never a direct
connection; proxy credentials are redacted and via doctor shows the effective proxy with
userinfo hidden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
…s and measurements

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
…hema

SDK v2's callTool() throws when a tool with an outputSchema returns no
structuredContent, returns one that does not match, or has a schema that
does not compile. via only warns about a mismatch (arguments.md, 输出校验)
and does that in the CLI. The SDK applies these checks once its own tools
cache is filled, which via's raw listing does not do, but its x-mcp-header
mismatch retry does. The validator provider now returns no validator, so
callTool() skips the checks on every path. The mock server gains two tools
that ignore their outputSchema; both protocol generations return them as
sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
On the Windows runner the shutdown test timed out in the daemon handshake.
The test starts a 300 ms call on a stdio server that is not running yet,
waits a fixed 100 ms and then expects a new connection's handshake to
finish within 1 s. Since the protocol probe, the first connection to a
stdio server starts two processes (the probe copy, then the server), and
spawn blocks the event loop while CreateProcess runs, which is slow on
Windows runners. The test is about shutdown, not connecting, so it now
makes one call first and times only the shutdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bCAHTTNtGTFAPNcsrjeS2
@github-actions
github-actions Bot merged commit 12926b5 into main Oct 21, 2026
8 checks passed
@github-actions
github-actions Bot deleted the claude/wonderful-cerf-fv8ydk branch October 21, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants