Add modern-era (2026-07-28) test-server preset (#1700) (#1703)

* Add modern-era (2026-07-28) test-server preset (#1700)

The era-aware connection model (#1626, PR #1697) added per-server
`protocolEra` and the era-aware Connection Info UI, but no bundled test
server negotiated the modern era — the modern happy path had only
unit/Storybook fixtures, no live coverage.

Serve the modern (2026-07-28) protocol era from the HTTP test server via
the SDK's `createMcpHandler`. A `modern` flag on `ServerConfig` (and a
`transport.modern` field in the JSON config) mounts the handler with a
dual-era `legacy: "stateless"` fallback by default, or `legacy: "reject"`
for modern-only strict serving. The web-standard `fetch` handler reuses
the existing `toWebRequest`/`writeWebResponse` conversion, so no new
`@modelcontextprotocol/node` dependency is needed.

An SDK v2 client negotiating `protocolEra: "auto" | "modern"` now reaches
the modern leg end-to-end: `getProtocolEra() === "modern"`,
`getNegotiatedProtocolVersion() === "2026-07-28"`, and a populated
`getDiscoverResult()`. Verified live in the Inspector — Connection Info
shows Era Modern / Session Sessionless / the Discovery section.

- Modern serving path + `listen`/`startModernHttp` helpers in
  test-server-http.ts; modern-leg teardown in stop().
- `modern` config field threaded through load-config/resolve-config with
  a streamable-http-only validation guard; modern-http.json demo config.
- Integration test covering auto/modern negotiation, the dual-era legacy
  fallback, a modern tools/call, and the strict `legacy: "reject"`
  rejection.

Closes #1700

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Review nit: gate transport.modern guard on truthiness

A falsy `transport.modern` (e.g. `false`) is a no-op that `resolveConfig`
normalizes to `undefined`, so it shouldn't trip the streamable-http-only
guard. Gate on truthiness instead of `!== undefined` so only *enabling*
modern on a non-HTTP transport is rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-17 11:12:40 -04:00 • committed by GitHub
commit e06c0a45f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -522,6 +522,27 @@ export interface ServerConfig {
*/
supportRefreshTokens?: boolean;
};
/**
* Serve the modern (2026-07-28) protocol era via the SDK's
* `createMcpHandler` instead of the 2025 session-based streamable transport.
* Only meaningful for `serverType: "streamable-http"` (the modern model is
* HTTP-centric: per-request POST + `server/discover`).
*
* When present, the HTTP server mounts `createMcpHandler(factory, { legacy })`
* so an SDK v2 client negotiating `protocolEra: "auto" | "modern"` reaches the
* modern leg end-to-end (populated `server/discover` result, sessionless).
*
* - `legacy: "stateless"` (default) — dual-era: modern envelope requests use
* the modern leg; legacy-classified requests (e.g. a plain `initialize`
* handshake) fall back to per-request stateless 2025 serving, so a
* `protocolEra: "legacy"` client still connects.
* - `legacy: "reject"` — modern-only strict: legacy-classified requests get
* the unsupported-protocol-version error, exercising the pin-failure and
* `server/discover` surfaces.
*/
modern?: {
legacy?: "stateless" | "reject";
};
/**
* Optional server control for orderly shutdown (test HTTP server).
* When present, progress-sending tools check isClosing() before sending and skip/break if closing.