You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`ConnectOptions.prior` accepts a cached era verdict — the new exported type `PriorDiscovery`. `{ kind: 'modern', discover }` adopts a previously obtained `DiscoverResult` with zero round trips; `{ kind: 'legacy' }` skips the `server/discover` probe and runs the plain `initialize` handshake directly, for servers known out-of-band to be legacy — without pinning the client to `mode: 'legacy'`: stop supplying the verdict and `connect()` falls back to the configured `versionNegotiation` mode (under `'auto'`, it re-probes and rediscovers an upgraded server). Freshness is the supplying host's responsibility — a stale legacy verdict succeeds silently against an upgraded server, so hosts must date cached legacy verdicts in their own storage and stop supplying them past their policy horizon. Persisted-blob plumbing is hardened: `prior: null` is treated as absent, the modern arm's `discover` payload is schema-validated before any connection state changes, and an unrecognized shape rejects with a typed `SdkError(EraNegotiationFailed)` instead of a `TypeError`.
@@ -7,7 +7,7 @@ A **gateway** — a proxy, a worker pool, any process that fronts one MCP server
7
7
8
8
## Connect with a prior discover result
9
9
10
-
`connect()` takes an optional `prior`: a persisted `DiscoverResult` from an earlier probe. With it,`connect()` adopts the server's advertisement directly and sends nothing on the wire.
10
+
`connect()` takes an optional `prior`: a cached era verdict (`PriorDiscovery`). Its modern arm, `{ kind: 'modern', discover }`, wraps a persisted `DiscoverResult` from an earlier probe —`connect()` adopts the server's advertisement directly and sends nothing on the wire.
`worker` is connected: `callTool` works immediately, and the server has not heard from it yet.
28
28
29
-
`connect({ prior })` is 2026-07-28+ only — see [Protocol versions](../protocol-versions.md).
29
+
The modern verdict is 2026-07-28+ only — see [Protocol versions](../protocol-versions.md). For a server known to be legacy, supply the negative verdict instead — see [Skip the probe for a known-legacy server](#skip-the-probe-for-a-known-legacy-server).
30
30
31
31
## Probe once at bootstrap
32
32
@@ -50,17 +50,18 @@ The recorded value is the server's whole advertisement — supported versions, c
50
50
```
51
51
52
52
::: tip
53
-
An already-connected client can re-probe at any time: `await client.discover()` sends `server/discover` and updates `getDiscoverResult()`. A default-mode connect never probes, so its `getDiscoverResult()` is `undefined` — [Protocol versions](../protocol-versions.md#pin-an-era) lists the negotiation modes.
53
+
A client on a modern-era connection can re-probe at any time: `await client.discover()` sends `server/discover` and updates `getDiscoverResult()`. On a legacy-era connection `discover()` throws (`server/discover` is not a 2025-era method) — to re-check a legacy verdict, reconnect without a `prior` under `mode: 'auto'`, as in [Caching discovery verdicts](#caching-discovery-verdicts). A default-mode connect never probes, so its `getDiscoverResult()` is `undefined` — [Protocol versions](../protocol-versions.md#pin-an-era) lists the negotiation modes.
54
54
:::
55
55
56
56
## Persist the advertisement
57
57
58
-
The value is plain JSON. Write the string to Redis, a config map, or a process-local cache; parse it back wherever a client needs it.
58
+
The value is plain JSON. Write the string to Redis, a config map, or a process-local cache; parse it back and wrap it as the modern verdict wherever a client needs it.
Nothing about `prior` is tied to the process that probed: any client that can reach the same URL can adopt it.
@@ -100,7 +101,7 @@ Never share a persisted `DiscoverResult` across principals — key the blob on t
100
101
101
102
## Open a listen stream when a worker needs notifications
102
103
103
-
`connect({ prior })` never auto-opens a `subscriptions/listen` stream — prior-connected clients are request-only until you open one yourself.
104
+
A modern-verdict `connect({ prior })` never auto-opens a `subscriptions/listen` stream — the client is request-only until you open one yourself. (A legacy-verdict connect is an ordinary 2025-era connection: unsolicited notifications, no `listen()`.)
@@ -116,21 +117,21 @@ The server acknowledges the filter it agreed to honor:
116
117
From here the stream behaves like any other subscription — [Subscriptions](../clients/subscriptions.md) covers the notification handlers and the close semantics.
117
118
118
119
::: info
119
-
A `listChanged` option configured on a prior-connected client registers its handlers but stays silent: no stream opens until you call `listen()`.
120
+
A `listChanged` option configured on a modern-verdict client registers its handlers but stays silent: no stream opens until you call `listen()`.
120
121
:::
121
122
122
123
## Handle a stale or incompatible advertisement
123
124
124
-
A `prior` that shares no 2026-07-28+ revision with the client rejects with `SdkError(EraNegotiationFailed)` before anything reaches the transport.
125
+
A modern verdict whose `discover` shares no 2026-07-28+ revision with the client rejects with `SdkError(EraNegotiationFailed)` before anything reaches the transport.
awaitlate.connect(newStreamableHTTPClientTransport(url), { prior: stale });
134
+
awaitlate.connect(newStreamableHTTPClientTransport(url), { prior: { kind: 'modern', discover: stale } });
134
135
} catch (error) {
135
136
if (!(errorinstanceofSdkError) ||error.code!==SdkErrorCode.EraNegotiationFailed) throwerror;
136
137
console.log(error.code);
@@ -150,11 +151,95 @@ re-probed: 2026-07-28
150
151
151
152
Replace the persisted blob with the fresh `getDiscoverResult()` and the rest of the fleet recovers on its next read.
152
153
154
+
## Skip the probe for a known-legacy server
155
+
156
+
When out-of-band metadata already says the server is pre-2026 — a registry entry, an earlier connection's outcome — an `'auto'`-mode probe is a round trip that fails on every single connect. Supply the negative verdict instead: `PriorDiscovery`'s `{ kind: 'legacy' }` arm skips the probe and goes straight to the `initialize` handshake.
Freshness is your responsibility: the SDK adopts whatever verdict you hand it. A stale modern verdict fails loudly at the first request, but a stale legacy verdict succeeds silently forever — an upgraded server still answers `initialize`, so nothing ever corrects it. Date cached legacy verdicts in your own storage and stop supplying them past your policy horizon; with no `prior`, the configured mode decides again (under `mode: 'auto'` the connect re-probes, so the upgrade is discovered).
171
+
172
+
## Caching discovery verdicts
173
+
174
+
The pieces above compose into the full host-side loop: probe once, cache the verdict under your own timestamp, and gate every later connect on a freshness check you control.
175
+
176
+
The first connect under `mode: 'auto'` pays one probe. Afterwards the outcome is readable on the client: `getDiscoverResult()` returns the `DiscoverResult` on a modern server, and on a connected client its absence means the era is legacy. Store that verdict together with when you stored it — the `Map` below keeps `storedAt` explicitly; in a real deployment the store does the dating for you (a Redis key TTL, where an expired read simply comes back empty, or a database row's `created_at` column).
177
+
178
+
Before each connect, supply the cached verdict only while your own policy says it is fresh — a fixed horizon here, any predicate in practice. Supplying `undefined` under `mode: 'auto'`*is* the re-probe, and the fresh outcome re-populates the cache. The timestamp matters most for the legacy branch: a stale legacy verdict succeeds silently forever (an upgraded server still answers `initialize`), so only the timestamp ever retires it; a stale modern verdict fails loudly at the first request.
const first =awaitconnectCached('gateway-target'); // no entry: probes
219
+
console.log('probes after first connect:', probes);
220
+
221
+
const second =awaitconnectCached('gateway-target'); // fresh entry: verdict supplied, no probe
222
+
console.log('probes after second connect:', probes);
223
+
224
+
verdicts.get('gateway-target')!.storedAt-=HORIZON_MS+1; // the horizon passes
225
+
const third =awaitconnectCached('gateway-target'); // stale: dropped, re-probed, re-cached
226
+
console.log('probes after the horizon passes:', probes);
227
+
```
228
+
229
+
```
230
+
probes after first connect: 1
231
+
probes after second connect: 1
232
+
probes after the horizon passes: 2
233
+
```
234
+
235
+
The wire trace shows the shape of the loop: one probe to fill the cache, none while the verdict is fresh, one more when the horizon forces rediscovery.
236
+
153
237
## Recap
154
238
155
-
-`connect(transport, { prior })` adopts a persisted `DiscoverResult` with zero round trips.
239
+
-`connect(transport, { prior: { kind: 'modern', discover } })` adopts a persisted `DiscoverResult` with zero round trips.
156
240
- The advertisement comes from one `'auto'`-mode or pinned probe — or an explicit `client.discover()` — and `getDiscoverResult()` reads it back.
157
-
- The value is plain JSON: stringify it into a shared cache, parse it in any process that fronts the same server.
241
+
- The value is plain JSON: stringify it into a shared cache, parse it back and wrap it as the modern verdict in any process that fronts the same server.
158
242
- Reuse a `DiscoverResult` only across clients that present the same authorization context.
159
-
-Prior-connected clients are request-only; call `listen()` on the one that needs notifications.
243
+
-Modern-verdict clients are request-only; call `listen()` on the one that needs notifications.
160
244
- An incompatible `prior` rejects with `SdkError(EraNegotiationFailed)`; fall back to a fresh probe and re-persist.
245
+
- A known-legacy server takes `prior: { kind: 'legacy' }` — no probe, straight to `initialize`. Stale legacy verdicts fail silently (an upgraded server still answers `initialize`), so date them in your own storage and stop supplying them past your policy horizon.
Copy file name to clipboardExpand all lines: docs/clients/caching.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -94,6 +94,10 @@ Fresh or not, the cache also evicts itself when the server signals a change: a `
94
94
Cache hints are a 2026-07-28 surface — see [Protocol versions](../protocol-versions.md). Against a 2025-era server, `defaultCacheTtlMs` is the only lever.
95
95
:::
96
96
97
+
## Two caches, two owners
98
+
99
+
The response cache on this page is the only cache the SDK manages: the server declares each entry's lifetime (`ttlMs`) and the SDK enforces it. A host that also caches **discovery verdicts** — the connect-time era outcome supplied as `ConnectOptions.prior` — owns that policy itself: the SDK never expires a supplied verdict. See [Protocol versions](../protocol-versions.md#skip-the-probe-with-a-cached-verdict) for the verdict shapes and [Caching discovery verdicts](../advanced/gateway.md#caching-discovery-verdicts) for the full host-side loop.
100
+
97
101
## Recap
98
102
99
103
- Caching is one feature with two halves: the server attaches `ttlMs` / `cacheScope`, the client honours them — by default neither half does anything alone.
Copy file name to clipboardExpand all lines: docs/clients/connect.md
+14-1Lines changed: 14 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,6 +85,19 @@ Call list-trips before book-trip. Dates are ISO 8601.
85
85
86
86
The capability object gates every verb on [the next page](./calling.md): only ask for what the server advertised. `getInstructions()` is the server's usage guide for the model — put it in the system prompt.
87
87
88
+
A fourth accessor, `getDiscoverResult()`, tells the eras apart at connect time. Present, it is the modern-era `DiscoverResult` — persistable with `JSON.stringify` and usable on a later connect as `prior: { kind: 'modern', discover }` to skip the probe. Absent on a connected client, the era is legacy. This page's client used the default legacy handshake:
// The default mode ran the legacy initialize handshake — no DiscoverResult.
92
+
console.log(client.getDiscoverResult());
93
+
```
94
+
95
+
```
96
+
undefined
97
+
```
98
+
99
+
Under `versionNegotiation: { mode: 'auto' }` against a 2026-era server it returns the advertisement — see [Protocol versions](../protocol-versions.md#skip-the-probe-with-a-cached-verdict) for the cached-verdict shapes and [Caching discovery verdicts](../advanced/gateway.md#caching-discovery-verdicts) for the full host-side loop.
100
+
88
101
## Disconnect cleanly
89
102
90
103
Over Streamable HTTP, terminate the server-side session, then close the client.
@@ -101,6 +114,6 @@ await client.close();
101
114
-`new Client({ name, version })`, a transport, and `connect()` are the whole setup; `connect()` runs the `initialize` handshake.
102
115
-`StreamableHTTPClientTransport` connects to remote servers; `StdioClientTransport`, from `@modelcontextprotocol/client/stdio`, spawns local ones; `SSEClientTransport` is the fallback for SSE-only servers.
103
116
-`InMemoryTransport.createLinkedPair()` links a client and a server in one process.
104
-
- After `connect()`, `getServerVersion()`, `getServerCapabilities()`, and `getInstructions()` return what the server declared.
117
+
- After `connect()`, `getServerVersion()`, `getServerCapabilities()`, and `getInstructions()` return what the server declared; `getDiscoverResult()` tells the eras apart (present = modern, absent = legacy).
105
118
-`close()` tears down the transport and rejects in-flight requests.
106
119
- Protocol-revision differences live on the protocol versions page, not here.
0 commit comments