Skip to content

Commit d57d832

Browse files
test(e2e): dedicated entry-side cells for HTTP-mechanics and bearer-auth on createMcpHandler
Entry-side siblings of the hosting:http / hosting:stateless / hosting:auth / hosting:context rows whose bodies hand-host their own server transport and so never reach createMcpHandler when given an entry arm. Ten new typescript:hosting:entry:* requirements + bodies, no edits to existing rows: HTTP mechanics through the harness-hosted entry (wired.fetch probes): - method-405 (both arms), parse-error-400 (both arms), no-session-id (both arms) - legacy-accept-406, legacy-content-type-415, legacy-protocol-version-header-400, legacy-protocol-version-default (entryStateless only — the legacy fallback delegates to the streamable HTTP server transport whose validation is unchanged; the modern per-request path does not apply Accept/Content-Type negotiation) Per-request HTTP context and bearer-auth composition (self-hosted createMcpHandler, matrix arm selects legacy posture + client pin): - ctx-http-req-headers (both arms): custom client header reaches ctx.http.req - auth:missing-401 (both arms): user-composed bearer gate answers 401 + WWW-Authenticate before the entry; factory never runs - auth:authinfo-propagates (both arms): authInfo handed to handler.fetch reaches ctx.http.authInfo and the factory's McpRequestContext.authInfo unchanged Test-only; no src/ changes; no existing scenario-body or knownFailure edits.
1 parent 9235f3b commit d57d832

3 files changed

Lines changed: 476 additions & 0 deletions

File tree

test/e2e/requirements.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,6 +2397,93 @@ export const REQUIREMENTS: Record<string, Requirement> = {
23972397
addedInSpecVersion: '2026-07-28',
23982398
note: "Runs on the entryModern arm; the body wires one harness-hosted endpoint per responseMode value via wire()'s entry.responseMode option and asserts the response shape on the arm-recorded HTTP exchanges."
23992399
},
2400+
2401+
// v2 features: dual-era HTTP entry — HTTP request mechanics on the harness-hosted entry
2402+
// (entry-side siblings of the hosting:http / hosting:stateless families, which hand-host the
2403+
// server transport themselves and so never reach createMcpHandler when given an entry arm).
2404+
2405+
'typescript:hosting:entry:method-405': {
2406+
source: 'sdk',
2407+
behavior:
2408+
'An unsupported HTTP method (PUT, PATCH) on a createMcpHandler endpoint is answered 405 with a JSON-RPC Method-not-allowed body on both legs: the stateless legacy fallback rejects every non-POST method, and the modern-only strict path rejects body-less non-POST traffic via the modern-only-method-not-allowed cell.',
2409+
transports: ['entryStateless', 'entryModern'],
2410+
note: 'Runs on the createMcpHandler entry arms; the unsupported methods are POSTed through wired.fetch so the HTTP status and body are observed directly. The entry does not emit an Allow header (the per-session server transport does), so only the status and JSON-RPC error shape are pinned.'
2411+
},
2412+
'typescript:hosting:entry:parse-error-400': {
2413+
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server',
2414+
behavior:
2415+
'A POST whose body is not valid JSON is answered 400 by a createMcpHandler endpoint on both legs, with a JSON-RPC Parse-error (-32700) body: the entry classifier reads no envelope claim from a non-JSON body, so the stateless legacy fallback delegates the parse error and the modern-only strict path emits it itself.',
2416+
transports: ['entryStateless', 'entryModern'],
2417+
note: 'Runs on the createMcpHandler entry arms; the malformed body is POSTed through wired.fetch so the HTTP status and JSON-RPC error code are observed directly.'
2418+
},
2419+
'typescript:hosting:entry:legacy-accept-406': {
2420+
source: 'sdk',
2421+
behavior:
2422+
"A 2025-era POST whose Accept header does not allow both application/json and text/event-stream is answered 406 by a createMcpHandler endpoint's stateless legacy slot (the legacy fallback delegates to the streamable HTTP server transport, whose Accept negotiation is unchanged).",
2423+
transports: ['entryStateless'],
2424+
removedInSpecVersion: '2026-07-28',
2425+
note: 'Runs on the entryStateless arm and is bounded to the 2025-11-25 axis: Accept negotiation is enforced by the legacy server transport the fallback delegates to, not by the modern per-request path. The probes are POSTed through wired.fetch so the 406 is observed directly.'
2426+
},
2427+
'typescript:hosting:entry:legacy-content-type-415': {
2428+
source: 'sdk',
2429+
behavior:
2430+
"A 2025-era POST whose Content-Type is not application/json is answered 415 by a createMcpHandler endpoint's stateless legacy slot (the legacy fallback delegates to the streamable HTTP server transport, whose Content-Type validation is unchanged).",
2431+
transports: ['entryStateless'],
2432+
removedInSpecVersion: '2026-07-28',
2433+
note: 'Runs on the entryStateless arm and is bounded to the 2025-11-25 axis: Content-Type validation is enforced by the legacy server transport the fallback delegates to. The entry classifier reads the body before that delegate runs, so a body that happens to be valid JSON is still rejected on Content-Type alone.'
2434+
},
2435+
'typescript:hosting:entry:legacy-protocol-version-header-400': {
2436+
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header',
2437+
behavior:
2438+
"A 2025-era POST carrying an MCP-Protocol-Version header naming an unknown revision is answered 400 by a createMcpHandler endpoint's stateless legacy slot, with the response body naming the supported version(s).",
2439+
transports: ['entryStateless'],
2440+
removedInSpecVersion: '2026-07-28',
2441+
note: 'Runs on the entryStateless arm and is bounded to the 2025-11-25 axis: the protocol-version header check is enforced by the legacy server transport the fallback delegates to. Header/body cross-checks on the modern path are pinned by the entry std-header rows; this row pins only that a non-modern unsupported header still surfaces as 400 through the fallback.'
2442+
},
2443+
'typescript:hosting:entry:legacy-protocol-version-default': {
2444+
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header',
2445+
behavior:
2446+
"A 2025-era POST without an MCP-Protocol-Version header is served by a createMcpHandler endpoint's stateless legacy slot under the assumed default protocol version (2025-03-26): a tools/list round-trips without the header.",
2447+
transports: ['entryStateless'],
2448+
removedInSpecVersion: '2026-07-28',
2449+
note: 'Runs on the entryStateless arm and is bounded to the 2025-11-25 axis. The probe is POSTed through wired.fetch with only Accept and Content-Type headers so the default-version path is the one exercised.'
2450+
},
2451+
'typescript:hosting:entry:no-session-id': {
2452+
source: 'sdk',
2453+
behavior:
2454+
'A createMcpHandler endpoint emits no Mcp-Session-Id response header on either leg: the stateless legacy fallback hosts a sessionless server transport per request, and the modern per-request path has no session at all — every recorded exchange of a connect-then-tools/call round trip carries no session header.',
2455+
transports: ['entryStateless', 'entryModern'],
2456+
note: "Runs on the createMcpHandler entry arms; asserted on the arm-recorded httpLog response clones. The entry's BYO sessionful composition is the only way to issue a session id and is pinned by typescript:hosting:entry:byo-sessionful-legacy."
2457+
},
2458+
'typescript:hosting:entry:ctx-http-req-headers': {
2459+
source: 'sdk',
2460+
behavior:
2461+
"A custom HTTP header set on the StreamableHTTP client transport reaches a tool handler's ctx.http.req as Fetch Headers when the server is hosted by createMcpHandler, on both legs: the stateless legacy fallback and the modern per-request path each thread the original Request through to handler context.",
2462+
transports: ['entryStateless', 'entryModern'],
2463+
note: "The body hosts createMcpHandler itself (the wire() entry arm builds the client transport without a custom-header hook) and the matrix arm selects the legacy posture and client pin: entryStateless drives a plain client through legacy: 'stateless', entryModern drives a 2026-07-28-pinned client through legacy: 'reject'."
2464+
},
2465+
2466+
// v2 features: dual-era HTTP entry — bearer auth composed in front of createMcpHandler
2467+
// (entry-side siblings of the hosting:auth family, which hand-hosts an Express stack and so
2468+
// never reaches createMcpHandler when given an entry arm). The SDK does not enforce endpoint
2469+
// authentication on either era — bearer/OAuth auth is deployer-composed middleware in front of
2470+
// whichever handler is mounted, and the entry passes a verified AuthInfo through unchanged.
2471+
2472+
'typescript:hosting:entry:auth:missing-401': {
2473+
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#error-handling',
2474+
behavior:
2475+
'A bearer-protected createMcpHandler deployment — a user-composed verification gate in front of handler.fetch — answers a request without an Authorization header with 401 and a WWW-Authenticate challenge on both legs, and the entry is never reached for that request (no factory call).',
2476+
transports: ['entryStateless', 'entryModern'],
2477+
note: "The body hosts createMcpHandler itself behind the documented bearer-gate composition (verify the Authorization header, then call handler.fetch(request, { authInfo })); the matrix arm selects the legacy posture and client pin. The 401/WWW-Authenticate is the gate's own response — the entry performs no token verification — and the body asserts the gate composes correctly with both serving paths."
2478+
},
2479+
'typescript:hosting:entry:auth:authinfo-propagates': {
2480+
source: 'sdk',
2481+
behavior:
2482+
"A verified AuthInfo handed to createMcpHandler.fetch(request, { authInfo }) reaches per-request handlers as ctx.http.authInfo unchanged on both legs, and the same AuthInfo is exposed on the factory's per-request context (McpRequestContext.authInfo) before the instance is built.",
2483+
transports: ['entryStateless', 'entryModern'],
2484+
note: 'The body hosts createMcpHandler itself behind the documented bearer-gate composition; the matrix arm selects the legacy posture and client pin. authInfo is strictly pass-through — the entry never derives it from request headers — so the cell pins delivery, not verification. The OAuth client flow that obtains the token is hosting-agnostic and is covered by the client-auth family; the dedicated client-completes-OAuth-then-negotiates-2026 journey rides the auth-package redo (M13.1) so it is targeted at the surviving auth surface.'
2485+
},
2486+
24002487
'typescript:transport:stdio:dual-era-serving': {
24012488
source: 'sdk',
24022489
behavior:

0 commit comments

Comments
 (0)