Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69131 additions and 9001 deletions

View file

@ -0,0 +1,50 @@
# gateway
`connect({ prior: DiscoverResult })` — zero-round-trip connect for gateways and distributed clients (protocol revision 2026-07-28).
```bash
pnpm --filter @mcp-examples/gateway server -- --http --port 3000
pnpm --filter @mcp-examples/gateway client -- --http http://127.0.0.1:3000/
```
The 2026 protocol is **stateless on HTTP**: every request carries the per-request `_meta` envelope (protocol version, client info, client capabilities), so once you know the server's `DiscoverResult` there is nothing left to negotiate. A gateway, proxy, or worker fleet that
fronts the same server should not re-probe per worker — it probes once and every subsequent connect is free.
## The pattern
```ts
// 1. Bootstrap: probe once.
const bootstrap = new Client({ name: 'bootstrap', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await bootstrap.connect(new StreamableHTTPClientTransport(url));
const persisted = JSON.stringify(bootstrap.getDiscoverResult()); // → write to Redis / config / process-local cache
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.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`.
## What this story asserts
The server exposes a `request_count` tool returning how many MCP requests reached the process (`createMcpHandler` builds one server instance per request). The client asserts:
- after the bootstrap probe + one `request_count` call, the count is **2**;
- after three worker `connect({ prior })` calls + one `request_count` call, the count is **3** — proving the three connects sent **zero** requests;
- each worker can `callTool` immediately;
- after three `echo` calls + one `request_count` call, the count is **7**.
## When to use `prior`
- A gateway/proxy that holds a long-lived connection pool to one server and constructs a fresh `Client` per downstream request.
- A horizontally-scaled host where one worker's probe should seed the fleet (persist the blob to a shared cache).
- Reconnecting after a transient transport drop without re-probing.
## Security: same-credential reuse only
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.

View file

@ -0,0 +1,87 @@
/**
* Gateway / distributed-client pattern: probe once, persist the
* `DiscoverResult`, feed it to every worker for a zero-round-trip connect.
*
* 1. A "bootstrap" client connects with `versionNegotiation: { mode: 'auto' }`
* — one `server/discover` round trip — and reads `getDiscoverResult()`.
* 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.
* 4. The server's `request_count` tool proves it: after three worker connects
* the count is unchanged (no extra discover/initialize from the workers).
*
* **Security:** the persisted advertisement is what the server returned for the
* bootstrap client's credential. Only reuse it across workers that present the
* SAME authorization context — here every client speaks to the same
* unauthenticated endpoint, so the constraint holds trivially. Do not share a
* `DiscoverResult` across principals.
*/
import { check, parseExampleArgs } from '@mcp-examples/shared';
import type { DiscoverResult } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
async function requestCount(client: Client): Promise<number> {
const r = await client.callTool({ name: 'request_count' });
return Number((r.content?.[0] as { text: string }).text);
}
const { url } = parseExampleArgs();
// ---------------------------------------------------------------------
// Step 1: bootstrap — one server/discover probe.
// ---------------------------------------------------------------------
const bootstrap = new Client({ name: 'gateway-bootstrap', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await bootstrap.connect(new StreamableHTTPClientTransport(new URL(url)));
check.equal(bootstrap.getNegotiatedProtocolVersion(), '2026-07-28');
const discovered = bootstrap.getDiscoverResult();
check.ok(discovered, 'bootstrap connect populated getDiscoverResult()');
check.deepEqual(discovered?.serverInfo, { name: 'gateway-target', version: '1.0.0' });
// The probe was the only request so far; the request_count call is the
// second. (createMcpHandler builds one server instance per request.)
check.equal(await requestCount(bootstrap), 2);
// ---------------------------------------------------------------------
// Step 2: persist. In a real gateway you'd write this to Redis / a config
// map / a process-local cache here. JSON round-trips by design.
// ---------------------------------------------------------------------
const persisted: string = JSON.stringify(discovered);
await bootstrap.close();
// ---------------------------------------------------------------------
// Step 3: three fresh workers connect from the persisted blob — zero
// 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 workers = await Promise.all(
['worker-a', 'worker-b', 'worker-c'].map(async name => {
const worker = new Client({ name, version: '1.0.0' });
await worker.connect(new StreamableHTTPClientTransport(new URL(url)), { prior });
// Adopted directly from prior — no probe, no initialize.
check.equal(worker.getNegotiatedProtocolVersion(), '2026-07-28');
check.deepEqual(worker.getServerVersion(), { name: 'gateway-target', version: '1.0.0' });
return worker;
})
);
// ---------------------------------------------------------------------
// Step 4: prove it. Three connect() calls and the count is unchanged
// (still 2 from the bootstrap leg + this request_count call = 3). Had
// each worker probed/initialized, this would read 6.
// ---------------------------------------------------------------------
check.equal(await requestCount(workers[0]!), 3);
// Each worker can callTool immediately.
for (const [i, worker] of workers.entries()) {
const echoed = await worker.callTool({ name: 'echo', arguments: { text: `hello from ${i}` } });
check.equal((echoed.content?.[0] as { text: string }).text, `hello from ${i}`);
}
// 3 (above) + 3 echo calls + this request_count call = 7.
check.equal(await requestCount(workers[0]!), 7);
for (const worker of workers) await worker.close();

View file

@ -0,0 +1,26 @@
{
"name": "@mcp-examples/gateway",
"private": true,
"type": "module",
"scripts": {
"server": "tsx server.ts",
"client": "tsx client.ts"
},
"dependencies": {
"@mcp-examples/shared": "workspace:*",
"@modelcontextprotocol/client": "workspace:*",
"@modelcontextprotocol/node": "workspace:*",
"@modelcontextprotocol/server": "workspace:*",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
"tsx": "catalog:devTools"
},
"example": {
"transports": [
"http"
],
"era": "modern",
"//": "connect({ prior }) is zero-round-trip on 2026-07-28 only — the legacy era still needs initialize. HTTP-only because the proof counts per-request factory calls (createMcpHandler builds one server instance per request); on stdio the factory is per-connection."
}
}

View file

@ -0,0 +1,49 @@
/**
* Gateway / distributed-client target server. A plain 2026-era MCP server with
* a couple of tools and a `request_count` instrumentation tool that returns how
* many requests have reached this process — `createMcpHandler` builds one
* server instance per inbound request, so the module-level counter equals the
* number of MCP requests served (server/discover, tools/call, …). The client
* asserts against it to PROVE that `connect({ prior })` sent nothing.
*/
import { createServer } from 'node:http';
import { parseExampleArgs } from '@mcp-examples/shared';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
let requestCount = 0;
function buildServer(): McpServer {
requestCount++;
const server = new McpServer({ name: 'gateway-target', version: '1.0.0' });
server.registerTool('echo', { description: 'Echo the input back', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text }]
}));
server.registerTool('uppercase', { inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: text.toUpperCase() }]
}));
// Exposes the process-wide request count so the client can assert exactly
// which round trips happened. The factory increment for THIS call has
// already run by the time the handler executes, so the returned value
// includes the request_count call itself.
server.registerTool('request_count', { description: 'Number of MCP requests this server process has received' }, async () => ({
content: [{ type: 'text', text: String(requestCount) }]
}));
return server;
}
// HTTP-only — the request_count proof depends on `createMcpHandler`'s
// per-request factory; on stdio the factory is per-connection and the 2/3/7
// assertions would not hold.
const { port } = parseExampleArgs();
const handler = createMcpHandler(buildServer);
createServer(toNodeHandler(handler)).listen(port, () => {
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
});