feat(client): accept a cached era verdict on ConnectOptions.prior (#2511)
This commit is contained in:
parent
1480241e2a
commit
f60dff0674
2 changed files with 523 additions and 86 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
Loading…
Reference in a new issue