feat(client): accept a cached era verdict on ConnectOptions.prior (#2511)

This commit is contained in:
Felix Weinberger 2026-07-17 17:25:02 +01:00 • committed by GitHub
commit f60dff0674
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 523 additions and 86 deletions

View file

@ -1,6 +1,6 @@
# gateway
`connect({ prior: DiscoverResult })` — zero-round-trip connect for gateways and distributed clients (protocol revision 2026-07-28).
`connect({ prior: { kind: 'modern', discover } })` — zero-round-trip connect for gateways and distributed clients (protocol revision 2026-07-28).
```bash
pnpm --filter @mcp-examples/gateway server -- --http --port 3000
@ -21,11 +21,11 @@ await bootstrap.close();
// 2. Every worker: zero-round-trip connect from the persisted blob.
const worker = new Client({ name: 'worker', version: '1.0.0' });
await worker.connect(new StreamableHTTPClientTransport(url), { prior: JSON.parse(persisted) });
await worker.connect(new StreamableHTTPClientTransport(url), { prior: { kind: 'modern', discover: JSON.parse(persisted) } });
await worker.callTool({ name: 'echo', arguments: { text: 'hi' } }); // first wire traffic
```
`getDiscoverResult()` is populated by the `'auto'`/pinned probe path, by `client.discover()`, and by `connect({ prior })` itself. The value round-trips through `JSON.stringify`/`JSON.parse`.
`getDiscoverResult()` is populated by the `'auto'`/pinned probe path, by `client.discover()`, and by a modern-verdict `connect({ prior })` (a legacy verdict leaves it `undefined`). The value round-trips through `JSON.stringify`/`JSON.parse`.
## What this story asserts
@ -47,4 +47,4 @@ The server exposes a `request_count` tool returning how many MCP requests reache
Only reuse a persisted `DiscoverResult` across workers that present the **same authorization context** as the bootstrap client (key the blob on a credential hash). Adopting a wider `prior` does not grant access — the server authorizes every request — but it can mislead
client-side capability gating.
`connect({ prior })` is **modern-only**: no mutual 2026-07-28+ revision → `SdkError(EraNegotiationFailed)`. Use `versionNegotiation: { mode: 'auto' }` for legacy-era fallback.
The modern verdict — `prior: { kind: 'modern', discover }` wrapping a persisted `DiscoverResult` — is **modern-only**: no mutual 2026-07-28+ revision → `SdkError(EraNegotiationFailed)`. For a server known to be legacy, pass the negative verdict instead — `prior: { kind: 'legacy' }` skips the probe and goes straight to `initialize`. Freshness is the host's responsibility: date cached legacy verdicts in your own storage and stop supplying them past your policy horizon (a stale one succeeds silently against an upgraded server; with no `prior`, a `mode: 'auto'` client re-probes).

View file

@ -7,8 +7,8 @@
* 2. The result is `JSON.stringify`-ed (the "persist" step — in a real gateway
* you would write this to Redis, a config map, or a process-local cache).
* 3. Three fresh worker clients connect with
* `connect(transport, { prior: JSON.parse(persisted) })`: each connect()
* sends nothing on the wire, and `callTool` works immediately.
* `connect(transport, { prior: { kind: 'modern', discover: JSON.parse(persisted) } })`:
* each connect() sends nothing on the wire, and `callTool` works immediately.
* 4. The server's `request_count` tool proves it: after three worker connects
* the count is unchanged (no extra discover/initialize from the workers).
*
@ -19,7 +19,7 @@
* `DiscoverResult` across principals.
*/
import { check, parseExampleArgs } from '@mcp-examples/shared';
import type { DiscoverResult } from '@modelcontextprotocol/client';
import type { DiscoverResult, PriorDiscovery } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
async function requestCount(client: Client): Promise<number> {
@ -56,7 +56,7 @@ await bootstrap.close();
// round trips each. Every worker presents the same authorization context
// as the bootstrap (unauthenticated here), so reuse is safe.
// ---------------------------------------------------------------------
const prior: DiscoverResult = JSON.parse(persisted) as DiscoverResult;
const prior: PriorDiscovery = { kind: 'modern', discover: JSON.parse(persisted) as DiscoverResult };
const workers = await Promise.all(
['worker-a', 'worker-b', 'worker-c'].map(async name => {
const worker = new Client({ name, version: '1.0.0' });