Uh oh!
There was an error while loading. Please reload this page.
feat(client): per-request envelope auto-emission and probe completion - #2320
Conversation
…a connections On a connection that negotiated a 2026-07-28+ protocol revision (auto-negotiated or pinned), the client now automatically attaches the per-request `_meta` envelope — the reserved protocol-version / client-info / client-capabilities keys — to every outgoing request and notification, not just the connect-time `server/discover` probe. User-supplied `_meta` keys take precedence over the auto-attached ones. The auto-attached client-capabilities reflect what the client actually registered (sampling/elicitation/roots). Legacy-era connections never gain these keys: the seam returns `undefined` and outbound traffic is byte-identical to a 2025 client, so the `'auto'`-mode fallback and the plain legacy connect stay byte-untouched. Adds a small protected `Protocol._outboundMetaEnvelope()` seam (base: no-op) applied at the request, notification, and cancellation send sites. Adds the `ProtocolEra` type (`'legacy' | 'modern'`), `Client.getProtocolEra()`, and `Client.setVersionNegotiation()` for configuring negotiation pre-connect on an already-constructed instance.
Pins the final `probe: { timeoutMs?, maxRetries? }` member names. `maxRetries`
(default `0`) governs timeout re-sends only: the probe is re-sent after a
timeout up to `maxRetries` times before the transport-aware timeout verdict
applies. The spec-mandated `-32004` corrective continuation
(select-and-continue with a mutual modern version) is a separate negotiation
step and is never counted against `maxRetries`.The client now attaches the per-request `_meta` envelope itself once a modern era is negotiated, and exposes `setVersionNegotiation()` for pre-connect configuration — so the entryModern arm no longer needs the `attachModernEnvelope` transport wrap or the `pinModernNegotiation` private-field write. The arm pins the scenario's client to 2026-07-28 via the public setter and connects. The 130 entryModern matrix cells stay green with both shims deleted. One scenario assertion relaxed (`protocol:error:invalid-params`): the recorded outbound `tools/call` params now carry the auto-attached `_meta` envelope on the entryModern arm, which is additive and not part of the assertion's intent (the body still proves the malformed request reached the wire and `name` is absent).
…est envelope Document explicitly that the v2 default of `versionNegotiation` is `'legacy'` (absent ⇒ the plain 2025 connect sequence, byte-identical to v1.x) and show how to opt into `'auto'` and pin, including what happens against a 2025-only server. Record the per-request `_meta` envelope auto-emission, the new `getProtocolEra()` / `setVersionNegotiation()` accessors, and the `probe.maxRetries` knob with its `-32004` disambiguation sentence. Adds a short version-negotiation section to the client guide and a changeset for the new public surface.
🦋 Changeset detectedLatest commit: 2451b1a The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client@modelcontextprotocol/codemod@modelcontextprotocol/server@modelcontextprotocol/server-legacy@modelcontextprotocol/express@modelcontextprotocol/fastify@modelcontextprotocol/hono@modelcontextprotocol/nodecommit: |
…w the client auto-attaches it The client now attaches the per-request `_meta` envelope itself on modern-era connections, so the example clients and the conformance fixture no longer need to build and pass it by hand. - examples/client/src/multiRoundTripClient.ts: remove the `envelope()` helper and the META_KEY imports; the auto-fulfilment leg is a plain `client.callTool()`; the manual leg keeps `client.request()` (its `allowInputRequired: true` return is the union, which `callTool()`s typed signature does not surface) but drops the manual `_meta`. Stop-gap header comment removed. - examples/client/src/dualEraStdioClient.ts: modern leg is a plain `client.callTool()`; envelope literal, META_KEY imports, and the stop-gap header comment removed. - test/conformance/src/everythingClient.ts: remove `modernEnvelope()` and the META_KEY imports; `runToolsCallModernClient` uses `listTools()` / `callTool()`; the four `sep-2322` callTool sites drop the explicit `_meta`. The local `server/discover` response shim stays (separate upstream gap).
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…low-up" wording and anchor the negotiation snippet - examples/server/src/dualEraStreamableHttp.ts: header comment now states the client attaches the per-request `_meta` envelope itself once a modern era is negotiated. - test/integration/test/server/createMcpHandler.test.ts: the two typed tools/call round trips through an auto-negotiating Client use plain `client.callTool()` (no manual `_meta`); the `modernEnvelope()` helper is gone and the raw-fetch unsupported-revision negative test builds its envelope inline. - docs/client.md + examples/client/src/clientGuide.examples.ts: the "Protocol version negotiation" snippet is now sourced from a type-checked `Client_versionNegotiation` region.
…ly scenarios
The entryModern arm unconditionally pins the scenario client to 2026-07-28
via `setVersionNegotiation({mode:{pin:...}})` before connect(), so any
`versionNegotiation: {mode: 'auto'}` passed at construction time is
overridden and never observed. Drop it from the scenarios that only run
on entry arms (mrtr ×4, hosting-entry-streaming, hosting-entry-stamping,
hosting-entry dual-era-one-factory) and update the now-stale
"auto-negotiating" wording in comments. The dual-era-one-factory
discover assertion is satisfied by pin mode (pin discovers too), so its
ternary collapses to a single plain client.
Also document the unconditional pin in test/e2e/CLAUDE.md so scenarios
that need to assert non-pin negotiation behavior know to restrict off
entryModern.Uh oh!
There was an error while loading. Please reload this page.
… calls in dual-era stdio tests The auto-negotiating Client now attaches the per-request envelope itself on modern-era connections, so the hand-built `_meta` on `client.request`/ `client.callTool` was dead code that bypassed the auto-emission path the test exercises. Raw `rawRequest`/`transport.send` bodies (which never go through Client) keep building the envelope by hand. Also drops the stale "stop-gap until automatic envelope emission lands client-side" comment and the now-unused meta-key imports.
Uh oh!
There was an error while loading. Please reload this page.
| // Both cells host the same handler shape — one ctx-taking factory, the | ||
| // 'stateless' legacy posture — and differ only in the client driving it. | ||
| const client = | ||
| transport === 'entryModern' | ||
| ? new Client({ name: 'auto-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } }) | ||
| : new Client({ name: 'plain-2025-client', version: '1.0.0' }); | ||
| // 'stateless' legacy posture — driven by a plain client; the entry arm | ||
| // decides which era serves it (entryModern pins the client to 2026-07-28). | ||
| const client = new Client({ name: 'dual-era-client', version: '1.0.0' }); |
There was a problem hiding this comment.
🟡 The 'typescript:hosting:entry:dual-era-one-factory' requirement in test/e2e/requirements.ts:2280-2285 still says the cell proves "an auto-negotiating client reaches 2026-07-28 via server/discover", but this PR rewrote the cell to use a plain Client that the entryModern arm pins via setVersionNegotiation({ mode: { pin: '2026-07-28' } }), so the cell now exercises pin-mode negotiation and semantically duplicates the separate 'typescript:hosting:entry:pin-negotiation' requirement. Update the manifest's behavior/note wording to match the arm-pinned client (or restore auto-mode coverage on a non-entry transport so the described behavior is still proven somewhere e2e).
Extended reasoning...
What drifted. This PR changes the modern leg of the typescript:hosting:entry:dual-era-one-factory cell (test/e2e/scenarios/hosting-entry.test.ts:35-38): the scenario no longer constructs an auto-negotiating client (versionNegotiation: { mode: 'auto' }); it builds a plain Client and relies on the entryModern arm's unconditional client.setVersionNegotiation({ mode: { pin: '2026-07-28' } }) in test/e2e/helpers/index.ts. test/e2e/CLAUDE.md was updated to document the unconditional pin ("a scenario that needs to assert non-pin negotiation behavior (e.g. mode: 'auto' probing) must restrict off entryModern"), but the requirement manifest entry was not: test/e2e/requirements.ts:2283 still reads "...an auto-negotiating client reaches 2026-07-28 via server/discover (never initialize)...".\n\nWhy it matters. Per the suite's own conventions, requirements.ts is the pure-data source of truth for what each cell proves; reviewers and the coverage gates rely on its behavior wording. The coverage gate only checks that the requirement id is cited by a verifies() call, not that the prose matches the test body, so this drift is silent. After this PR the cell exercises pin-mode negotiation on the entryModern arm — the same path already covered by typescript:hosting:entry:pin-negotiation (requirements.ts:2287-2292) — so the manifest both misdescribes the cell and masks the fact that no e2e cell now exercises 'auto'-mode negotiation against the createMcpHandler entry (only the integration suite covers auto over real HTTP, e.g. test/integration/test/server/createMcpHandler.test.ts).\n\nStep-by-step. (1) On the entryModern arm, the cell builds new Client({ name: 'dual-era-client', ... }) with no versionNegotiation. (2) wire('entryModern', ...) calls client.setVersionNegotiation({ mode: { pin: '2026-07-28' } }) before connect(). (3) connect() resolves the pin plan in resolveVersionNegotiation() — the auto probe-and-fallback state machine never runs. (4) The cell's wire assertions ("server/discover sent, never initialize") still pass, because pin mode also probes via server/discover, so nothing in CI flags that the behavior described at requirements.ts:2283 ("an auto-negotiating client...") is no longer what the cell does.\n\nWhy nothing else catches it. The PR's own follow-up commits (3a3cc3e, 3ac89ea) deliberately dropped the now-dead mode: 'auto' configs from the entryModern-only scenarios, and the existing review comment on helpers/index.ts:173 covers the unconditional pin overriding scenario clients — but neither touches the stale requirements.ts behavior text, which is a distinct, still-unaddressed item.\n\nHow to fix. Either (a) update the requirement's behavior/note wording at requirements.ts:2280-2285 to say the entryModern leg is driven by an arm-pinned client reaching 2026-07-28 via server/discover, or (b) if auto-mode coverage against the entry is meant to be preserved, restore it explicitly (e.g. a cell on a non-entry transport, or have the scenario pass its negotiation mode through wire()). Option (a) is a one-line wording change and matches what the test now actually proves.
On a connection that negotiated a 2026-07-28+ protocol revision (auto-negotiated or pinned), the client now automatically attaches the per-request
_metaenvelope — the reserved protocol-version / client-info / client-capabilities keys — to every outgoing request and notification, not just the connect-timeserver/discoverprobe. User-supplied_metakeys take precedence; the auto-attached client-capabilities reflect what the client actually registered. Legacy-era connections (the default, and the'auto'-mode fallback) never gain these keys, so 2025-era outbound traffic is byte-identical to before.Also adds
Client.getProtocolEra()('legacy' | 'modern' | undefined), theProtocolEratype,Client.setVersionNegotiation()for configuring negotiation pre-connect on an already-constructed instance, and theprobe.maxRetriesknob (default0, governs probe-timeout re-sends only — the spec-mandated-32004corrective continuation is never counted against it). TheversionNegotiationdefault remains'legacy'.