MCP 2026-07-28 Dual-Profile Gateway Design

Status

Implemented through the release-candidate profile and disabled by default. Phase 7 promotion readiness is implemented; final-spec reconciliation and the production canary remain blocked until the final revision is published and operational evidence is recorded.

This design was written against the 2026-07-28 release candidate available on July 16, 2026. The two principal SEPs are final, but the consolidated protocol revision is not final until July 28, 2026. Before enabling the new profile, the implementation must be checked against the final schema, conformance suite, and error-code registry.

The executable readiness, provenance, coverage, canary, and rollback contracts live under implementation/light-gateway/mcp-2026-07-28/phase7. Its promotion gate fails closed while the final provenance or operational evidence has pending status.

The current stable MCP protocol revision is 2025-11-25. The existing mcp-router design remains the source of truth for the legacy stateful profile. This document defines how light-gateway adds the new stateless profile without removing or weakening that behavior.

Primary specification sources:

Specification Interpretation

The consolidated specification defines the wire contract. Individual SEPs explain why a change was made, its security implications, rejected alternatives, and migration guidance, but their proposal text can predate later integration edits.

When sources differ, use this order:

  1. The final revision's TypeScript schema, which the specification identifies as the protocol message source of truth.
  2. The final normative specification pages and generated JSON Schema.
  3. The final revision changelog and error-code registry.
  4. Final SEP text for rationale and requirements not changed during integration.
  5. Release-candidate blog examples and non-normative guidance.

Until July 28, the locked release-candidate schema occupies the first position, but every difference from the final schema must be reviewed before enablement. For example, SEP-2575 proposal text and the consolidated draft differ in the normative strength of clientInfo and in error-code allocations. The gateway must implement the consolidated schema rather than preserve superseded SEP examples.

The release coverage matrix later in this document records every consolidated changelog area that affects light-gateway. A row marked deferred still needs an explicit capability or compatibility boundary; deferred does not mean that arbitrary messages may pass through unchecked.

Executive Decision

light-gateway will support the legacy stateful and 2026-07-28 stateless MCP profiles at the same configured endpoint, normally /mcp.

The gateway selects a profile from the request's protocol contract. It must not select the stateless profile merely because Mcp-Session-Id is absent. An absent session id can also mean that a legacy request is malformed, and treating it as stateless would bypass the legacy session boundary.

Both profiles terminate at one transport-neutral application core for tool visibility, authorization, request masking, execution, response filtering, auditing, and metrics. Protocol adapters own only the lifecycle, wire fields, response envelope, and transport behavior specific to their revision.

The first production milestone will support server/discover, tools/list, and tools/call for stateless clients. Long-lived subscriptions/listen, multi-round-trip requests, and an optional stateless-to-legacy backend bridge are separate gates and must not be advertised before they are implemented.

Context

The existing Rust MCP router implements a stateful Streamable HTTP facade:

  1. A client calls initialize.
  2. light-gateway creates a gateway-owned frontend session and returns an Mcp-Session-Id.
  3. Every later request is validated against that session.
  4. For an apiType: mcp tool, the gateway lazily initializes a backend MCP session and maps it to the frontend session and backend target.
  5. Deleting or expiring the frontend session terminates its backend sessions.

The current implementation keeps frontend and backend sessions in the McpRouterRuntime. It preserves them across configuration reloads within the same process. It also maintains an authorization-aware tools-list cache.

The code currently negotiates 2025-06-18, 2025-03-26, and 2024-11-05; it does not yet negotiate the stable 2025-11-25 revision. Phase 0 must add a real 2025-11-25 tool-only compatibility contract and fixtures before adding the stateless adapter. Merely accepting the version string is insufficient. The existing 2024-11-05 value remains a frozen gateway compatibility behavior and does not mean this design adds or claims compliance with the deprecated two-endpoint HTTP+SSE transport.

The 2026-07-28 profile changes that lifecycle:

  • initialize and notifications/initialized are removed.
  • Mcp-Session-Id is removed.
  • Protocol version and client capabilities travel with every request.
  • server/discover reports supported versions, server capabilities, and server identity.
  • Streamable HTTP requests expose routing fields through Mcp-Method and, when applicable, Mcp-Name.
  • List results include freshness and cache-scope information.
  • Ordinary results carry resultType: "complete".
  • Long-lived server notifications use a POST response stream created with subscriptions/listen rather than the HTTP GET endpoint.

Supporting both generations therefore requires two versioned protocol adapters, not an optional-session branch inside one wire contract.

Goals

  • Serve legacy and 2026-07-28 clients concurrently on one MCP path.
  • Preserve existing legacy initialization, session validation, backend session mapping, teardown, authorization, and filtering behavior.
  • Make every stateless request independently understandable, authenticated, authorized, bounded, and routable to any gateway replica.
  • Reuse one application core for tool listing and tool execution so the two profiles do not drift in policy behavior.
  • Support HTTP tools, legacy MCP backends, and stateless MCP backends through explicit compatibility rules.
  • Return truthful capabilities and explicit incompatibility errors rather than silently downgrading or emulating unsupported semantics.
  • Keep new behavior disabled by default until the final specification and conformance gates pass.

Non-Goals

  • Do not add an application setting that changes the meaning of one protocol version between stateful and stateless operation.
  • Do not infer protocol profile from user agent, HTTP version, connection reuse, missing headers, or backend topology.
  • Do not remove the legacy profile while supported clients still depend on it.
  • Do not pool hidden legacy backend sessions by user identity for stateless clients.
  • Do not interpret or authorize arbitrary application state handles in the gateway. Handles are normal tool arguments and results.
  • Do not implement prompts, resources, sampling, roots, logging, MCP Apps, Tasks, or multi-round-trip requests merely because the protocol schema can represent them.
  • Do not advertise subscriptions/listen until the Pingora response path can keep an SSE response open and cancel it safely.

Terminology

TermMeaning
Legacy profileMCP 2025-11-25 and supported earlier revisions using initialization and protocol-level sessions
Stateless profileMCP 2026-07-28, with per-request version/capabilities and no protocol-level session
FrontendThe MCP client-to-light-gateway side
BackendAn HTTP API or MCP server invoked by a configured gateway tool
Frontend sessionA gateway-owned legacy client session identified by Mcp-Session-Id
Backend sessionA legacy upstream MCP session owned by the gateway
Explicit state handleAn opaque application identifier returned by a tool and supplied to later tool calls; it is not an MCP protocol primitive
Principal fingerprintA stable, non-secret digest of the authenticated subject, issuer, tenant, and other identity fields required to bind state or caches

Protocol Profiles

ConcernLegacy stateful profile2026-07-28 stateless profile
Lifecycleinitialize, then notifications/initializedNo initialization handshake
VersionNegotiated and retained in the sessionSent in the HTTP header and request params._meta
Client capabilitiesRetained from initializationSupplied for every request
Client identitySupplied during initializationclientInfo SHOULD be supplied in request params._meta
Gateway sessionMcp-Session-IdProhibited
DiscoveryInitialization resultserver/discover
Tool listsMay be session-sensitiveMust not vary by connection; may vary by authenticated principal or deployment state
Application stateMay exist behind a legacy sessionExplicit tool arguments and server-minted handles
Server notificationsLegacy Streamable HTTP behaviorsubscriptions/listen POST response stream
Horizontal routingRequires affinity or shared session routingAny replica can handle any ordinary request
Stream resumptionLegacy-version behaviorNo SSE event replay or Last-Event-ID resumption

The version adapter must apply the complete contract for its selected profile. It must not mix a legacy lifecycle with stateless result shapes or accept a stateless lifecycle under a legacy version.

HTTP Method Matrix

HTTP methodLegacy stateful profile2026-07-28 stateless profile
POSTInitialize, notifications, requests, and responses permitted by the negotiated legacy revisionSingle self-contained JSON-RPC message; the only request entry point
DELETETerminates the identified frontend session and its backend sessionsNot a protocol operation; reject without touching state
GETKeep the currently implemented behavior; this gateway returns 405 unless a separately supported legacy transport requires itNo server-notification endpoint; use subscriptions/listen through POST
Other methods405 Method Not Allowed405 Method Not Allowed

The deprecated HTTP+SSE transport is not added as part of dual-profile support. Compatibility with that older two-endpoint transport requires a separate, explicit design and route so it cannot be confused with Streamable HTTP.

Request Classification

Classification Rules

The gateway classifies a POST only after enforcing request-body limits and parsing a single JSON-RPC message. Batch requests remain unsupported unless a future design explicitly adds them.

Use this ordered decision table:

ConditionResult
Method is initialize, no session id, and requested version is an enabled legacy versionLegacy initialization path
Mcp-Session-Id is present and the request does not claim 2026-07-28Legacy session path
Version header and request params._meta both select enabled 2026-07-28, agree exactly, and required routing headers are validStateless path
Version header and body select enabled 2026-07-28, with a stale Mcp-Session-Id also presentSelect stateless, ignore the legacy header, and never read, mint, echo, or delete session state
Legacy non-initialize request has no session idReject as missing legacy session id
Stateless version is present only in the header or only in params._metaReject as a header mismatch
Version is missing or unsupported and no valid legacy initialization can negotiate itReject; do not guess

The classifier must be a pure, unit-tested component. Tool execution must not start and no frontend or backend state may be mutated until classification, version validation, authentication, header/body validation, and authorization have succeeded.

Stateless HTTP Header Validation

For a 2026-07-28 POST:

  • Content-Type must identify JSON and the body must be one UTF-8 JSON-RPC request or notification permitted by the protocol. Client-sent JSON-RPC responses are prohibited because this revision has no server-initiated requests.
  • Accept must list both application/json and text/event-stream.
  • MCP-Protocol-Version is required and must equal params._meta["io.modelcontextprotocol/protocolVersion"].
  • Mcp-Method is required for every JSON-RPC request and must equal the JSON-RPC method. This revision does not define routing-header requirements for notification POSTs; the gateway must not invent them.
  • Mcp-Name is required where SEP-2243 defines a named operation, including tools/call, and must equal the corresponding params.name or params.uri value.
  • Header names are compared case-insensitively; method and name values are case-sensitive after decoding. Ambiguous duplicate semantic headers are rejected.
  • Mcp-Name and Mcp-Param-* use the specified visible-ASCII representation. A non-ASCII, control-containing, leading/trailing-whitespace, or literal sentinel-shaped value uses the exact =?base64?{base64-utf8}?= encoding. Decode before comparing with the body; compare integer parameters numerically rather than requiring one decimal spelling.
  • This sentinel is MCP-specific and is not an RFC 2047 MIME encoded-word. Implementations must not substitute =?utf-8?B?...?= or apply MIME header decoding. The lowercase =?base64? prefix and ?= suffix are literal, case-sensitive protocol markers.
  • A missing, malformed, or mismatched required header returns HTTP 400 with HeaderMismatch code -32020.
  • An unsupported version returns HTTP 400 with UnsupportedProtocolVersion code -32022 and includes the requested and supported versions.
  • A required capability that the client did not declare returns MissingRequiredClientCapability code -32021.

The core 2026-07-28 protocol defines no client-to-server notification over Streamable HTTP. Because the first milestone also advertises no extension that defines one, it rejects notification POSTs as unsupported. If a supported extension notification is added later, acceptance returns HTTP 202 with no body; rejection uses an HTTP error and may include an id-less JSON-RPC error. A JSON-RPC request returns either one application/json object or an SSE response stream. The adapter never returns a JSON-RPC response to a notification and always rejects a client-sent JSON-RPC response.

Origin validation happens before JSON parsing or state mutation. When Origin is present, compare the complete normalized origin against an exact allowlist; suffix, substring, wildcard-host, and reflected-origin matching are prohibited. An invalid origin returns HTTP 403. An empty or absent allowlist rejects requests that carry Origin while still allowing non-browser clients that do not send the header. This is an MCP transport security boundary, not merely a response CORS-header concern. The same rule applies to both enabled Streamable HTTP profiles; legacy compatibility does not weaken it.

Trusted reverse proxies, WAFs, ingresses, and load balancers must preserve the browser's Origin header exactly. Stripping or rewriting it is deployment nonconformance because the gateway cannot reliably distinguish that browser from a real non-browser client. User-Agent and other spoofable headers are not acceptable recovery signals. Deployment conformance tests must exercise Origin preservation through the complete external path.

If a tool schema uses x-mcp-header, the gateway terminates and parses the request, so it must validate each applicable Mcp-Param-* header against the tool argument before policy evaluation or execution. A gateway must never authorize or route on a header value and then execute a different body value.

The final July 28 schema and error registry override release-candidate details if they change before publication.

Legacy Validation Hardening

Legacy behavior remains version-specific, but simultaneous support must not leave the legacy session id as an authorization bearer token.

When creating a frontend session on a protected route, store a principal fingerprint derived from the independently verified request identity. Every session-bound POST or DELETE must recompute and compare the fingerprint before touching the session or a backend. Missing identity, a different identity, or a changed tenant binding fails closed.

An anonymous MCP route must be an explicit product decision rather than the result of missing or failed authentication. An anonymous legacy session uses a separate gateway-derived anonymous client binding for capacity and abuse controls; its cryptographically random session id necessarily remains a bearer capability. A protected and anonymous session must never share the same binding namespace.

The stored fingerprint must contain no bearer token, cookie, CSRF value, or other reusable credential. The gateway may retain the existing client key for capacity accounting, but capacity identity and security identity must be separate concepts.

Common Application Core

Both adapters normalize accepted messages into an effective request context:

#![allow(unused)]
fn main() {
enum FrontendProtocol {
    Legacy {
        session_id: String,
        negotiated_version: String,
    },
    Stateless,
}

struct EffectiveMcpRequestContext {
    protocol: FrontendProtocol,
    protocol_version: String,
    client_info: Option<ClientInfo>,
    client_capabilities: ClientCapabilities,
    requested_log_level: Option<LoggingLevel>,
    auth: Option<AuthPrincipal>,
    correlation_id: Option<String>,
    delegation: Option<DelegationClaims>,
}
}

The common core owns:

  1. Method allowlisting.
  2. Delegated-authority validation.
  3. Tool-list visibility and deterministic ordering.
  4. Request access control.
  5. Input-schema validation and request masking.
  6. Backend target resolution and SSRF protection.
  7. Tool execution and bounded retry policy.
  8. Response filtering and output-schema handling.
  9. JSON-RPC application error mapping.
  10. Audit events, metrics, and safe diagnostics.

The common core returns a protocol-neutral result. The selected adapter adds the correct result envelope, _meta, cache fields, protocol headers, or legacy session headers.

Stateless Methods

server/discover

The stateless adapter must implement server/discover and return:

  • enabled protocol versions in deterministic preference order;
  • only capabilities implemented and enabled by this gateway instance;
  • optional instructions that describe the configured tool facade;
  • resultType: "complete";
  • gateway server identity in _meta["io.modelcontextprotocol/serverInfo"] with the normative strength from the final schema;
  • ttlMs and cacheScope because discovery is cacheable.

Discovery is independently authenticated and authorization-aware. Its cache key uses the same principal, protocol, policy, and configuration revisions as the capability result. The first milestone reports gateway capabilities derived from enabled handlers and configured tools; it does not depend on call-time portal-registry discovery. cacheScope defaults to private; public is valid only when the complete discovery result is identical for every caller. The advertised TTL must not exceed the internal entry lifetime.

A configuration or policy swap advances its revision and makes old entries unreachable before a response is served from the new runtime. TTL is an expiry backstop, not the primary reload-coherence mechanism. If a future dynamically discovered catalog makes discovery/list results depend on portal-registry state, that work must first add a monotonic registry generation or canonical snapshot revision plus a push/watch invalidation signal. The current request/response DiscoverySnapshot has neither. Until that contract exists, the gateway must not claim immediate backend-discovery invalidation or advertise listChanged; a documented short TTL is the only available staleness bound.

The initial stateless release advertises tools only. It must not advertise prompts, resources, notifications, multi-round-trip requests, Tasks, Apps, or extensions that are not wired through the application core.

Legacy clients continue to use initialize. A dual-version client may probe server/discover; failure may cause legacy fallback only under the downgrade rules defined later in this document.

tools/list

Stateless tools/list uses the same authorization-aware visibility logic as legacy tools/list, with these additional rules:

  • The returned order is deterministic.
  • The list must not vary because of a connection or prior tool call.
  • It may vary by current authenticated principal, scopes, tenant, active policy, or gateway configuration. The first milestone does not hide or add configured tools based on call-time backend discovery; backend availability is checked by tools/call.
  • The result contains resultType: "complete", ttlMs, and cacheScope.
  • cacheScope defaults to private because the visible catalog can vary by authenticated principal and access-control policy.

The first milestone returns the complete bounded visible catalog, omits nextCursor, and rejects a non-empty cursor that it did not issue. It does not pretend to paginate. If the computed visible catalog exceeds maxToolsListItems, or its encoded response would exceed maxResponseBodyBytes, the gateway fails the whole request with the bounded implementation-defined -32000 resource-limit error locked in Phase 0. The message states that the visible catalog exceeds a gateway limit and that pagination is not supported. It must not truncate the catalog, emit a cursor, or cache a partial result. Cursor generation, integrity, principal binding, expiry, and reload invalidation require a separate pagination design before larger visible catalogs are accepted.

The internal cache key must include at least:

  • protocol profile and version;
  • normalized query or intent parameters;
  • authenticated-principal fingerprint;
  • relevant forwarded-header fingerprint;
  • MCP router configuration revision;
  • access-control policy revision;

The initial configured catalog has no backend-discovery component in its cache key. A future discovery-dependent catalog must add the registry generation or snapshot revision described above; a TTL alone is insufficient to claim immediate invalidation.

Cache entries must expire no later than the advertised ttlMs. A policy or router reload invalidates affected entries before new responses are served.

tools/call

Stateless tools/call is authorized and executed independently. It must not read, create, touch, or delete a frontend session. A successful ordinary result includes resultType: "complete" and the server identity fields required or recommended by the final protocol schema.

Mutating calls are not automatically replayed after ambiguous transport failure. Existing retry metadata remains authoritative, but retries must be limited to operations explicitly declared safe or idempotent.

Stateless calls use a typed outbound-header allowlist. The gateway regenerates profile routing, correlation, trace, tenant, locale, and backend credential headers from trusted request context and target configuration. It does not copy raw frontend X-Forwarded-*, cookies, authorization, backend-specific credentials, unknown Mcp-*, or arbitrary extension headers to a backend. Legacy header forwarding remains a separately versioned compatibility contract and must not be reused as the modern default.

Unsupported Methods

Methods not implemented by the gateway return a normal JSON-RPC method-not-found response. For the stateless Streamable HTTP profile this is HTTP 404 Not Found with JSON-RPC code -32601; the body distinguishes a modern unknown method from a missing legacy HTTP+SSE endpoint. Capabilities must not imply that those methods are available. This rule is especially important for deprecated roots, sampling, and logging features and for extensions that are not part of the first milestone.

Tool and JSON Schema Contract

The current router stores and advertises configured inputSchema values and uses schema annotations for request masking. That is not equivalent to full JSON Schema validation. Supporting the 2026-07-28 tools contract requires a dedicated, bounded schema compilation and validation path.

Schema Loading

At configuration load, before a runtime swap:

  • inputSchema must be a valid JSON Schema object and must describe an object at the root. A no-argument tool should use { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": {}, "additionalProperties": false }.
  • outputSchema, when present, may describe any JSON value, including arrays, primitives, or null.
  • A schema without $schema uses JSON Schema 2020-12.
  • JSON Schema 2020-12 is mandatory. Any additional dialect is an explicit, documented configuration choice; an unsupported dialect is rejected rather than interpreted as 2020-12 or treated permissively.
  • Local $ref and $defs resolution is supported within the schema document.
  • Network dereferencing of external $ref URIs is disabled. An unresolved external reference rejects the tool. A future opt-in resolver needs a separate SSRF review, exact host allowlist, byte/depth/time limits, and cache policy.
  • Schema compilation is bounded by document bytes, nesting depth, subschema count, reference expansions, regular-expression complexity where supported, and a time budget.

Compilation produces immutable validators stored with the tool configuration. Invalid schemas fail the candidate configuration before it becomes active. One invalid configured tool must not silently turn into an unconstrained tool.

Composition does not replace the MCP object-root declaration. allOf, anyOf, oneOf, conditionals, and local references are supported as siblings of root type: object. If an older Portal-generated tool contains only allOf or another composition keyword, add type: object at the same level as an immediate repair, then regenerate the tool. Remove or reset any stale selected-tool schema override so it cannot replace the regenerated schema. An empty string is not an empty schema; no-argument tools use the explicit closed object above.

Tool Names and Aggregation

Names exposed through the stateless profile are case-sensitive, unique within the gateway catalog, 1 to 128 characters, and limited to ASCII letters, digits, underscore, hyphen, and dot. Enabling the stateless profile fails validation if an exposed configured name violates this contract. Legacy-only deployments may retain their existing names until migrated.

If future dynamic backend discovery introduces collisions, configuration must provide a stable explicit alias or prefix. Backend serverInfo.name is not a unique identifier and must not be used as the automatic disambiguation key.

x-mcp-header Validation

At schema load, every x-mcp-header annotation must:

  • be non-empty and match the HTTP field-name token syntax;
  • contain no control, carriage-return, or line-feed characters;
  • be case-insensitively unique within the tool schema;
  • annotate only a statically reachable primitive string, integer, or boolean;
  • keep integers within the protocol's safe IEEE-754 integer range;
  • not name a pseudo-header, hop-by-hop or proxy-authentication header, HTTP framing header, credential header, or gateway-owned MCP routing/session header;
  • not annotate the same property as sensitive or schema-masked data.

Invalid annotations fail a configured tool. If a future backend-discovery client receives an invalid backend tool definition, it excludes that tool and emits a bounded warning without exposing schema values. Sensitive parameters, tokens, secrets, and PII must never be mirrored into Mcp-Param-* headers.

At call time, extract the annotated value according to the final transport encoding rules and require the regenerated or received header to match the validated argument exactly. Missing argument and JSON null mean that the header is absent.

Input and Output Validation

For tools/call, the common core applies this order:

  1. Apply transport byte/depth limits and authenticate or accept the explicitly anonymous route.
  2. Parse, classify, validate routing headers, resolve the configured tool, and apply principal/tool-level visibility and coarse authorization.
  3. Validate the original arguments against inputSchema within the validation budget.
  4. Evaluate delegation and argument-dependent request access policy against the validated, unmasked arguments.
  5. Apply request masking and invoke the backend.
  6. Parse the backend result, apply response policy and filtering, and then validate the final structuredContent against outputSchema when present.
  7. Construct the version-specific result envelope.

The error boundary is intentionally split:

  • an unknown tool or malformed tools/call envelope that does not satisfy the protocol's CallToolRequest schema returns a JSON-RPC protocol error;
  • arguments that fail the selected tool's inputSchema return a completed tool result with resultType: "complete", isError: true, and bounded, model-actionable content, without backend traffic.

The validation result must identify the failing field and constraint when safe, but must not echo the complete input, secrets, schema-masked values, or unbounded validator diagnostics. Coarse authorization runs first so validation details cannot be used to probe a tool the principal cannot invoke. This SEP-1303 distinction lets a model correct tool arguments while preserving protocol errors for malformed envelopes and unknown tool names.

A configured output schema makes final output conformance a gateway responsibility because the gateway terminates and may filter the backend result. A backend or response filter that produces non-conforming structured content results in a tool error; the gateway must not emit data that contradicts its advertised schema.

structuredContent may be any JSON value. Masking and response filtering must therefore handle objects, arrays, strings, numbers, booleans, and null without assuming an object root. When structured content is returned, the gateway should also provide its serialized JSON in a text content block for backward compatibility unless the backend result already supplies an equivalent block.

Result Types, MRTR, and Extensions

Result Types

Every successful 2026-07-28 result contains a recognized resultType. Ordinary results use "complete". A stateless backend response that omits the field is invalid for that backend version. When consuming a response from an earlier negotiated backend version, the gateway treats an absent field as "complete" for backward compatibility.

An unknown result type is invalid unless it belongs to an extension explicitly supported and negotiated by both sides. The gateway must not relabel an unknown or input_required result as complete merely to fit a legacy frontend.

Multi Round-Trip Requests

The first stateless milestone does not implement MRTR and must not advertise the associated client or server capabilities. If a backend returns resultType: "input_required" before the gateway implements MRTR, the gateway replaces that backend result with a terminal gateway-generated tool error. For a modern frontend it has resultType: "complete", isError: true, and the bounded message light-gateway does not support MCP multi round-trip bridging for this backend. This is not a schema-validation failure and does not relabel the backend's input request as a successful complete result. The gateway does not expose backend requestState, inputRequests, or other opaque retry state to the frontend.

A later MRTR design must specify:

  • the exact supported input-request methods;
  • required client-capability validation;
  • authorization and integrity protection for opaque requestState;
  • bounds on state bytes, input-request count, nesting, and retry count;
  • a new JSON-RPC id for every retry while preserving correlation and audit lineage;
  • translation behavior for every frontend/backend profile combination;
  • how policy is reevaluated on every retry.

Request-scoped notifications or server input requests belong to the response stream of the initiating request, not to subscriptions/listen.

The per-request io.modelcontextprotocol/logLevel value is never retained as gateway session state. The gateway must not emit or forward notifications/message for a stateless request that omitted it. If request- scoped logging is later implemented, the requested threshold applies only to that response stream and is independently bounded and filtered; the deprecated Logging capability and logging/setLevel remain absent.

Extensions

The core ClientCapabilities and ServerCapabilities contain an extensions map. Extension support is optional, independently versioned, and disabled by default.

For the first milestone:

  • server/discover omits or returns an empty extension map;
  • unknown client extensions do not change core behavior;
  • an operation that requires an unsupported extension fails explicitly;
  • extension metadata and result types are not forwarded through the gateway unless a versioned gateway adapter validates and translates that extension;
  • MCP Apps and the Tasks extension remain separate designs and are not advertised;
  • tool task support is omitted or normalized to forbidden unless the Tasks extension is implemented and negotiated.

The gateway is not a transparent byte proxy, so backend extension support does not automatically make the same extension available to frontend clients. Each supported extension needs an owner, version allowlist, capability intersection, resource limits, authorization review, and conformance tests.

Deprecated and Removed Core Features

The stateless profile does not implement removed initialize, notifications/initialized, ping, or logging/setLevel methods. It does not add the removed HTTP GET notification endpoint or SSE resumption.

Roots, sampling, and logging are deprecated in this release. Because the gateway does not currently implement them, it leaves their capabilities absent rather than introducing new deprecated functionality. The deprecated HTTP+SSE transport, deprecated includeContext values, and deprecated Dynamic Client Registration are likewise not added by the MCP router.

Frontend and Backend Compatibility

The gateway is both an MCP server to the frontend and, for apiType: mcp, an MCP client to the backend. These protocol profiles are independent.

Frontend profileHTTP backendLegacy MCP backendStateless MCP backend
Legacy statefulExisting direct translationExisting mapped backend sessionTranslate stored legacy client metadata into each stateless backend request
StatelessDirect translationReject by default; optional per-request bridgeDirect stateless proxy

Controller WebSocket Control Plane Is Separate

The browser control-plane route /ctrl/mcp is not the Streamable HTTP /mcp endpoint described here. It is routed by websocket-router, remains payload-opaque at light-gateway, and continues to use the separately frozen controller JSON/WebSocket contract. This design does not route it through mcp-router, translate it to the stateless profile, or require statelessToLegacyBridge.

Only a future tool explicitly configured with apiType: mcp and a controller Streamable HTTP target would enter the compatibility matrix above. Such a tool must not be marked sessionIndependent: true merely because controller operations appear request/response-shaped. The preferred choices are to keep that frontend/backend path legacy or upgrade the target to stateless; a bridge still requires the proof and opt-in defined below.

Backend Profile Configuration

Each MCP tool target has an explicit backend profile:

backendMcpProtocol: legacy # legacy, stateless, or auto
sessionIndependent: false

legacy is the compatibility default for existing apiType: mcp tools. The fields are ignored for apiType: http.

All tools resolving to the same normalized MCP backend target must declare a compatible backend profile. Configuration loading fails if one target is simultaneously declared legacy and stateless or has conflicting bridge properties.

Legacy Frontend to Stateless Backend

This direction is supported. The gateway retains the legacy frontend's negotiated client information and capabilities, converts them into stateless per-request metadata, generates the required backend headers, and does not create a backend session.

The gateway returns a legacy-shaped result to the frontend. Fields introduced only in 2026-07-28 are consumed or translated deliberately; they must not be copied blindly into an older result schema.

Stateless Frontend to Stateless Backend

This is the preferred proxy path. The gateway:

  1. Re-authorizes the configured gateway tool.
  2. Resolves and validates the backend target.
  3. constructs a new backend request using the backend's supported stateless version;
  4. regenerates MCP-Protocol-Version, Mcp-Method, Mcp-Name, and applicable Mcp-Param-* headers;
  5. propagates the effective client capabilities and safe trace context;
  6. filters the backend response before returning it.

Ingress routing headers are never forwarded without regeneration and body/header consistency validation.

Stateless Frontend to Legacy Backend

This direction is rejected by default. A stateless frontend has no lifecycle scope that can safely own, route, or terminate a legacy backend session. Pooling backend sessions by authenticated principal would mix independent agents, conversations, browser tabs, or subagents and recreate hidden application state.

An optional perRequest bridge may be implemented later only when all of these conditions hold:

  • the administrator explicitly enables the bridge;
  • the tool is declared sessionIndependent: true;
  • one request can be completed without state from a prior backend call;
  • initialize, call, and delete are bounded by independent deadlines;
  • backend session creation has separate global and per-principal limits;
  • every success and failure path attempts backend teardown;
  • metrics expose incomplete teardown without logging session ids.

The bridge performs initialize, one operation, and delete. It must never be selected silently by auto discovery.

Backend auto Discovery

auto is optional and must be conservative:

  1. Probe server/discover using the preferred enabled stateless version.
  2. Cache the result by normalized target, principal fingerprint, configuration revision, and a bounded TTL.
  3. Select stateless only after a valid discovery response.
  4. Fall back to legacy only for an explicit unsupported-version or method-not-found response indicating an older server.

Do not fall back after authentication or authorization rejection, TLS failure, timeout, malformed response, header mismatch, DNS/SSRF rejection, or HTTP 5xx. Those failures are terminal for that attempt because fallback could become a downgrade path.

Explicit Application State Handles

The stateless protocol does not prohibit stateful applications. A backend may return an opaque handle such as basket_id or browser_id, and later tools may accept that handle as an ordinary argument.

The gateway does not introduce a generic handle type or handle registry. It continues to authorize the tool call, validate the input schema, apply masking, and filter the result. The backend that owns the handle must:

  • validate (handle, auth_context) on every call;
  • avoid treating possession as authorization when authentication exists;
  • document lifetime and recovery behavior in the tool description;
  • return a useful expired-handle error;
  • provide bounded expiry and optional cleanup tools;
  • use at least 128 bits of cryptographic entropy and a bounded lifetime when an unauthenticated handle necessarily acts as a bearer capability.

Handles, session ids, request ids, and bearer credentials must not be metric labels or appear in normal logs.

Authentication and Authorization

The MCP handler remains inside the normal light-gateway handler chain. The security or unified-security handler must establish McpRequestContext.auth before the MCP handler runs on a protected route.

For both profiles:

  • complete authentication, or an explicit anonymous-route decision, before protocol state mutation or backend traffic;
  • apply delegation binding before the requested operation;
  • evaluate tools-list visibility against the current request identity;
  • evaluate request access control before tool execution;
  • apply response filtering after backend execution;
  • fail closed when policy is unavailable under a default-deny deployment;
  • forward only explicitly allowed identity/delegation material to a backend;
  • strip and regenerate hop-by-hop, MCP routing, protocol, and session headers.

For the stateless profile, every protected request carries fresh authorization input and must be independently authenticated and authorized. An explicitly anonymous request is still independently rate-limited and evaluated against the route's anonymous policy. No previous request, connection, discovery response, or subscription grants authority to a later request.

For the legacy profile on a protected route, the current request must both authenticate successfully and match the session's stored principal fingerprint. Session validation does not replace current-token expiry, revocation, audience, issuer, or scope validation.

Frontend Resource-Server Boundary

For a protected MCP route, light-gateway is the OAuth resource server even though the MCP router delegates token parsing to security or unified-security. The product deployment must provide:

  • OAuth Protected Resource Metadata for the canonical MCP resource URI;
  • exact audience/resource validation for every bearer token;
  • Authorization: Bearer on every protected HTTP request, never a query-string access token;
  • HTTP 401 with an appropriate WWW-Authenticate challenge for a missing, invalid, or expired token;
  • HTTP 403 and an insufficient_scope challenge with the required scope set when the authenticated principal lacks permission;
  • a resource_metadata link and scope guidance consistent with the canonical MCP resource;
  • bounded step-up behavior on clients, without repeatedly replaying an ambiguous mutation.

JSON-RPC authorization errors may accompany the HTTP response where permitted, but they do not replace the required HTTP status and challenge headers.

Backend Client and Token Audience Boundary

For an apiType: mcp target, light-gateway is also an MCP client. A bearer token accepted for the frontend gateway resource must not be copied to a different backend MCP resource merely because it arrived in an agent header. That would violate audience binding and create a confused-deputy path.

Each backend target must select one credential strategy:

  • forward a caller token only when independent validation proves that the backend is an intended audience/resource for that exact token;
  • exchange or mint a bounded delegated token for the backend resource;
  • use a configured service credential when the call is intentionally performed as the gateway rather than the end user;
  • use no credential only for an explicitly anonymous backend.

The strategy is part of the normalized backend identity and cache key. Tokens, refresh tokens, client secrets, PKCE verifiers, and registered client metadata are owned by the security/client runtime, not stored in McpGatewaySession, backend discovery caches, or tool configuration.

For caller forwarding, the gateway security boundary verifies aud against the normalized configured backendResource before opening backend traffic; backend validation is defense in depth, not the gateway's authorization decision. String and array claims use exact audience membership. Missing or mismatched audience evidence fails closed. An opaque token cannot use caller mode unless trusted introspection returns the required audience evidence.

Release Authorization Dependencies

The MCP router consumes an authenticated principal, but the complete release also changes OAuth behavior. Before claiming 2026-07-28 compliance, the relevant light-fabric security and client modules must verify or explicitly defer:

  • authorization-response iss validation against previously validated issuer metadata;
  • binding persisted client credentials to the issuer that created them;
  • correct OpenID Connect application_type when deprecated Dynamic Client Registration is used for compatibility;
  • Client ID Metadata Documents as the preferred dynamic registration model;
  • .well-known protected-resource and authorization-server discovery rules;
  • confidential refresh-token storage, rotation requirements for public clients, and correct optional offline_access behavior;
  • bounded scope accumulation and step-up retries.

These are cross-cutting security dependencies rather than duplicate MCP-router implementations. Their release-gate evidence must nevertheless be linked from the coverage matrix.

Response Model and Streaming

The existing response model stores a complete Vec<u8> and a streamed boolean. The Pingora writer sends that body once with end = true. That is sufficient for a single JSON response or one buffered SSE frame, but it cannot implement subscriptions/listen.

Before adding subscriptions, replace it with an explicit response body:

#![allow(unused)]
fn main() {
enum McpResponseBody {
    Empty,
    Buffered(Bytes),
    Stream(McpResponseStream),
}

struct McpResponseStream {
    receiver: BoundedReceiver<Bytes>,
    cancellation: CancellationToken,
}
}

The gateway writer must keep a streaming response open, apply backpressure, detect disconnect, cancel producers, and finish exactly once. Buffered SSE and long-lived SSE must not share a misleading boolean flag.

For a stateless SSE response, closing the HTTP stream cancels that request; notifications/cancelled is not expected on Streamable HTTP. The writer stops work as soon as practical and emits nothing after cancellation. It also sends X-Accel-Buffering: no. A long-lived subscription may emit bounded SSE comment keep-alives to survive intermediary idle timeouts; comments carry no JSON-RPC meaning and consume the subscription byte/rate budget.

subscriptions/listen

When implemented, a client sends subscriptions/listen through POST and explicitly requests supported notification types. The response is a long-lived SSE stream.

The gateway must:

  1. Authorize the listen request independently.
  2. Enforce global and per-principal subscription limits.
  3. Open a bounded event channel.
  4. Send notifications/subscriptions/acknowledged as the first JSON-RPC message.
  5. Identify the subscription with the original request id.
  6. Tag each emitted notification with io.modelcontextprotocol/subscriptionId.
  7. Emit only notification types requested by the client and supported by the gateway.
  8. Cancel the producer when the HTTP stream closes, expires, reloads incompatibly, or encounters a slow consumer.

On deliberate server teardown, the gateway sends the empty subscriptions/listen result (with the original request id and the modern complete discriminator) before closing the SSE response. A saturated slow consumer may instead observe a remote close when the bounded channel cannot admit that terminal result; the gateway never grows or replays the queue to make graceful close succeed.

The first supported notification should be toolsListChanged, produced after a successful MCP router or relevant policy reload. Prompts and resources remain unadvertised until the gateway owns equivalent event sources.

A disconnected stream is not resumable. The client must create a new request with a new JSON-RPC id, re-fetch authoritative state when necessary, and re-subscribe. The gateway ignores Last-Event-ID and keeps no replay buffer for this profile.

An access token does not gain an indefinite lifetime because its response stream remains open. A protected subscription closes at token expiry, policy revocation convergence deadline, configured maximum duration, or gateway shutdown, whichever occurs first. Reconnection performs fresh authentication, authorization, discovery/list rehydration when needed, and subscription creation.

Configuration

The following is the locked configuration contract. All new fields have Serde defaults so existing configuration continues to load unchanged.

enabled: ${mcp-router.enabled:true}
path: ${mcp-router.path:/mcp}
maxSessions: ${mcp-router.maxSessions:10000}
maxSessionsPerClient: ${mcp-router.maxSessionsPerClient:100}
maxRequestBodyBytes: ${mcp-router.maxRequestBodyBytes:1048576}
maxResponseBodyBytes: ${mcp-router.maxResponseBodyBytes:4194304}
maxJsonDepth: ${mcp-router.maxJsonDepth:128}
originAllowlist: ${mcp-router.originAllowlist:[]}

schema:
  defaultDialect: ${mcp-router.schema.defaultDialect:https://json-schema.org/draft/2020-12/schema}
  allowExternalRefs: ${mcp-router.schema.allowExternalRefs:false}
  maxSchemaBytes: ${mcp-router.schema.maxSchemaBytes:1048576}
  maxDepth: ${mcp-router.schema.maxDepth:64}
  maxSubschemas: ${mcp-router.schema.maxSubschemas:4096}
  maxConcurrentValidations: ${mcp-router.schema.maxConcurrentValidations:32}
  validationWatchdogMs: ${mcp-router.schema.validationWatchdogMs:50}

protocols:
  legacy:
    enabled: ${mcp-router.protocols.legacy.enabled:true}
    versions: ${mcp-router.protocols.legacy.versions:["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]}
  stateless:
    enabled: ${mcp-router.protocols.stateless.enabled:false}
    versions: ${mcp-router.protocols.stateless.versions:["2026-07-28"]}
    discoverTtlMs: ${mcp-router.protocols.stateless.discoverTtlMs:30000}
    discoverCacheScope: ${mcp-router.protocols.stateless.discoverCacheScope:private}
    maxDiscoverCacheEntries: ${mcp-router.protocols.stateless.maxDiscoverCacheEntries:1024}
    toolsListTtlMs: ${mcp-router.protocols.stateless.toolsListTtlMs:30000}
    toolsListCacheScope: ${mcp-router.protocols.stateless.toolsListCacheScope:private}
    maxToolsListCacheEntries: ${mcp-router.protocols.stateless.maxToolsListCacheEntries:4096}
    maxToolsListItems: ${mcp-router.protocols.stateless.maxToolsListItems:1024}
    maxConcurrentRequests: ${mcp-router.protocols.stateless.maxConcurrentRequests:1024}
    maxConcurrentRequestsPerPrincipal: ${mcp-router.protocols.stateless.maxConcurrentRequestsPerPrincipal:32}
    maxConcurrentBackendCallsPerTarget: ${mcp-router.protocols.stateless.maxConcurrentBackendCallsPerTarget:32}
    maxSubscriptions: ${mcp-router.protocols.stateless.maxSubscriptions:10000}
    maxSubscriptionsPerPrincipal: ${mcp-router.protocols.stateless.maxSubscriptionsPerPrincipal:4}
    maxSubscriptionDurationMs: ${mcp-router.protocols.stateless.maxSubscriptionDurationMs:900000}
    statelessToLegacyBridge: ${mcp-router.protocols.stateless.statelessToLegacyBridge:reject}

tools: ${mcp-router.tools:[]}

The legacy version list above is the active compatibility baseline for this release.

Example backend declaration:

tools:
  - name: weather
    description: Get weather information
    apiType: mcp
    targetHost: https://weather.internal
    path: /mcp
    method: call
    backendMcpProtocol: stateless
    sessionIndependent: true
    backendCredentialMode: service
    backendResource: https://weather.internal/mcp
    inputSchema:
      type: object
      properties:
        city:
          type: string

Configuration validation must reject:

  • no enabled protocol versions;
  • 2026-07-28 listed under the legacy adapter;
  • a legacy version listed under the stateless adapter;
  • an invalid or non-normalized origin or a wildcard/suffix origin rule;
  • unsupported cacheScope or bridge values;
  • allowExternalRefs: true without a separately approved resolver policy;
  • invalid, unsupported-dialect, or over-limit tool schemas;
  • invalid tool names when the stateless profile exposes those tools;
  • invalid, duplicate, sensitive, or unreachable x-mcp-header annotations;
  • zero or internally inconsistent resource limits;
  • sessionIndependent on a normal HTTP tool when treated as an MCP bridge control;
  • a stateless MCP target without explicit backendCredentialMode, or a caller/exchange target without backendResource;
  • the deprecated caller-compat credential mode on a new stateless target;
  • conflicting backend profiles for one normalized backend target;
  • auto when backend discovery is disabled by product policy.

discoverCacheScope: public and toolsListCacheScope: public are invalid whenever authentication, delegation, or access-control policy can change the corresponding result. An empty origin allowlist means browser-originated MCP requests are rejected; it does not reject non-browser requests without an Origin header.

These field names are the locked configuration contract. The secure defaults are normative: legacy enabled, stateless disabled, and stateless-to-legacy bridging rejected. The runtime models the fixed-value cache-scope and bridge fields explicitly. Only private and reject, respectively, are accepted in this release; unknown stateless protocol fields fail configuration loading instead of being silently ignored.

For an explicit targetHost that does not set toolMetadata.runtime.allowPrivateTargetHost: true, SSRF protection is split across two checks. Literal IP addresses are rejected during static URL validation when they are loopback, private, link-local, or metadata addresses. Hostnames are also checked by the HTTP client's connection-time DNS resolver; the resolver rejects the entire lookup when any returned address is non-public, closing the validation-to-connect DNS rebinding window.

Targets resolved from the privileged service registry control plane are already approved internal targets. This includes both direct-registry.directUrls and portal-discovered nodes. The resolved target carries that trust decision to the separate private-target client without requiring duplicate per-tool metadata. The public client is never silently downgraded for an explicit target. The public-target resolver cannot be combined with an HTTP proxy because the proxy would resolve the origin outside the gateway's connection-time policy; such a client configuration fails closed.

Resource Limits

Legacy session capacity and stateless request capacity are separate budgets. One must not consume or release permits from the other. maxResponseBodyBytes must be at least 2048 so the gateway can always return a bounded protocol error; error messages are truncated safely and backend error bodies are never copied into client-visible errors.

The stateless profile requires bounded:

  • request body bytes and JSON nesting depth;
  • tools returned per list result, in addition to the buffered response-byte limit;
  • schema document bytes, schema depth, subschema count, reference expansion, and validation time;
  • concurrent requests globally and per principal;
  • concurrent backend calls per target;
  • response bytes for buffered responses;
  • tools-list cache entries and TTL;
  • discovery cache entries and TTL;
  • extension metadata and MRTR state, even when the initial limit is zero because those features are unsupported;
  • open subscriptions globally and per principal;
  • events and bytes queued per subscription;
  • subscription lifetime, additionally capped by current credential expiry on a protected route;
  • initialization and teardown work for an enabled compatibility bridge.

Schema validation runs on a dedicated fixed-size worker pool, not Tokio's core workers or shared blocking pool. The pool is bounded by available parallelism and maxConcurrentValidations; admission and queue capacity are bounded by the same configured ceiling, and overload fails before enqueue. The duration watchdog is observational because a running validator cannot be cancelled. Structural bounds, linear-time regex, the adversarial corpus, and isolation bound the gateway-wide impact without claiming a hard per-validation timeout.

Overload responses must be explicit and observable. A slow subscription is closed rather than allowed to grow an unbounded queue. Limits should use RAII permits so completion, timeout, cancellation, panic unwinding, and disconnect all release capacity.

Reload, Scaling, and Failure Semantics

Configuration Reload

Legacy frontend sessions and their mapped backend sessions continue to survive a compatible in-process reload. If a reload disables a version still used by an established legacy session, the rollout policy must decide whether to retain that version until session expiry or terminate affected sessions explicitly; it must not reinterpret the session under another version.

Stateless ordinary requests retain no protocol state across calls. New requests immediately use the new router and policy revisions.

An active subscription owns a live response stream, not a protocol session. The subscription hub should be shared across compatible runtime swaps so a successful catalog reload can publish toolsListChanged. An incompatible reload closes affected streams; clients reconnect and re-subscribe.

Multiple Replicas

Ordinary stateless requests must work through round-robin routing without affinity or a shared protocol session store. Caches may remain per replica if their keys and invalidation rules are safe.

Legacy requests still require affinity to the gateway process that owns the frontend session, or a separately designed shared session/routing layer. Adding the stateless profile does not make legacy sessions horizontally portable.

Subscriptions remain attached to the replica holding their HTTP stream. They do not require subsequent ordinary requests to return to that replica.

Retry and Downgrade

The gateway and clients must distinguish compatibility failures from security and availability failures.

Permitted compatibility fallback:

  • server/discover returns method not found from a server believed to predate the stateless profile;
  • the server returns a well-formed unsupported-version error listing a mutually supported legacy version.

Terminal failures with no automatic downgrade:

  • authentication or authorization rejection;
  • missing or mismatched headers;
  • TLS or certificate failure;
  • DNS or SSRF rejection;
  • malformed JSON-RPC or discovery response;
  • timeout, connection failure, or HTTP 5xx;
  • missing required client capability;
  • policy or internal gateway failure.

Mutations with ambiguous outcomes are not replayed unless their tool metadata explicitly allows a safe retry. Read-only or declared-idempotent calls may use the existing bounded retry policy.

Observability

Record bounded labels for:

  • frontend profile and protocol version;
  • JSON-RPC method;
  • configured tool name;
  • backend type and backend profile;
  • status and normalized error class;
  • compatibility fallback decision;
  • cache hit or miss;
  • active request and subscription counts;
  • subscription termination reason;
  • legacy session and bridge capacity utilization.

Structured diagnostics may include correlation id, method, configured tool name, status, elapsed time, and byte counts. They must omit request arguments, result bodies, cookies, authorization headers, CSRF values, session ids, explicit state handles, and raw principal identifiers.

Valid W3C trace context received in the protocol-defined _meta fields may be propagated through the gateway's tracing model. Conflicting or malformed trace context must not replace independently generated correlation identifiers.

2026-07-28 Release Coverage Matrix

This matrix is the traceability contract between the consolidated release, this design, the responsible light-fabric boundary, and the delivery gate. Required means the feature is needed before the stateless profile is enabled. Deferred means the gateway must omit the capability and reject or translate the feature explicitly. Dependency means another light-fabric module owns the behavior but must provide release evidence.

Release areaPrimary sourcesGateway decision and ownerStatus and gate
Remove protocol sessions and Mcp-Session-IdSEP-2567Stateless frontend adapter never touches the session store; legacy adapter remains isolatedRequired, Phases 1-2
Remove initialize handshake; per-request version and capabilitiesSEP-2575Classifier and versioned frontend adapters normalize into EffectiveMcpRequestContextRequired, Phases 1-2
server/discoverSEP-2575, discovery specImplement auth-aware cacheable discovery with truthful capabilities, server identity, TTL, and cache scopeRequired, Phase 2
Standard HTTP routing and parameter headersSEP-2243Validate and regenerate Mcp-Method, applicable Mcp-Name, and valid Mcp-Param-* headersRequired, Phases 2-3
Streamable HTTP request rules and Origin protectionTransport specEnforce POST body/content negotiation, exact Origin allowlist, profile-specific GET/DELETE rules, initial notification rejection, and 202 only for a future supported notificationRequired, Phase 2
Session-independent list resultsSEP-2567Catalog may vary by principal/config/policy, never by connection or prior callRequired, Phase 2
Cache TTL and scopeSEP-2549server/discover and tools/list return bounded ttlMs and normally private cacheScopeRequired, Phase 2
Deterministic tool orderingTools specPreserve BTreeMap ordering after authorization filtering and test byte-stable resultsRequired, Phase 2
Tool name format and collision handlingSEP-986, tools specValidate stateless-exposed names; require explicit aliases for aggregate collisionsRequired, Phases 1-2
JSON Schema 2020-12 and schema dialectsSEP-1613, SEP-2106, base specAdd bounded compile/validation; 2020-12 mandatory; external network references disabledRequired, Phases 1-2
Tool input validation error semanticsSEP-1303, tools specReturn schema failures as bounded isError: true tool results; reserve protocol errors for malformed envelopes and unknown toolsRequired, Phases 1-2
Arbitrary structuredContent and output schemasSEP-2106, tools specSupport all JSON roots and validate final filtered output against configured outputSchemaRequired, Phase 2
Standalone request/result payload definitionsSEP-1319No wire-format change; pin generated schemas and keep protocol-neutral payload models separate from JSON-RPC adaptersRequired architecture boundary, Phases 0-1
Required resultTypeSEP-2322, base specEmit complete; accept absent only from earlier negotiated backends; reject unknown valuesRequired, Phases 2-3
Multi Round-Trip RequestsSEP-2322, SEP-2260Do not advertise initially; reject input_required from a backend without down-conversionDeferred, separate design
Elicitation and MRTR migration changesSEP-1034, SEP-1036, SEP-1330, changelogNot reachable while MRTR and elicitation are unadvertised; a later design must cover defaults, URL/enum schemas, and removal of notifications/elicitation/complete and elicitationIdDeferred with MRTR
subscriptions/listenSEP-2575Replace buffered streaming abstraction, then add bounded tools-list-change streamsDeferred to Phase 5
Remove SSE replay and Last-Event-IDSEP-2575No replay buffer; clients retry with a new request id and rehydrate stateRequired, Phase 2/5
Remove ping, logging/setLevel, roots-list-changed, old subscribe methodsSEP-2575Return method not found and keep capabilities absentRequired boundary, Phase 2
Per-request log level and message notification ruleSEP-2575, changelogDo not emit or forward notifications/message unless that request supplied io.modelcontextprotocol/logLevel; retain no logging stateRequired boundary, Phase 2
Trace context in _metaSEP-414Validate and bridge W3C trace context without overriding trusted gateway correlation stateRequired, Phase 2
Core extensions map and independent extension versionsSEP-2133Empty/absent initially; only validated adapters may advertise or forward an extensionRequired boundary, Phase 2
Tasks extensionSEP-2663Do not advertise; task support is forbidden/absent until a separate extension designDeferred
MCP Apps extensionSEP-1865Do not advertise or forward UI metadata without a separate sandbox/consent designDeferred
Deprecate roots, sampling, logging, and sampling includeContext valuesSEP-2577, SEP-2596Do not add new deprecated features or the deprecated thisServer/allServers values to this tool-only gateway profileNo new implementation
Feature lifecycle and HTTP+SSE deprecationSEP-2596Keep legacy Streamable HTTP only; no implicit two-endpoint HTTP+SSE fallbackRequired boundary, Phase 0
MCP error-code allocationBase spec/changelogPreserve legacy implementation codes by version; reserve -32020 through -32022 for their assigned stateless meanings; lock implementation-defined -32000 to the catalog resource-limit errorRequired, Phases 0-2
Resource-not-found error becomes -32602SEP-2164No direct tool-only behavior; any future resource adapter must be version-awareDeferred resource design
Authorization issuer validation and mix-up defenseSEP-2468Security/client runtime validates recorded issuer and returned issDependency, Phase 0 gate
Client registration type and issuer-bound credentialsSEP-837, SEP-2352Client runtime owns registration metadata and keys credentials by issuerDependency, Phase 0 gate
Protected-resource discovery, refresh tokens, and scope step-upAuthorization spec, SEP-2207, SEP-2350, SEP-2351Security/client runtime owns metadata, token, challenge, and bounded step-up behaviorDependency, Phase 0 gate
Dynamic Client Registration deprecationChangelogDo not add DCR to the MCP router; prefer Client ID Metadata Documents in owning client codeDependency/boundary
Authorization extensionsSEP-2133, authorization extensionsDisabled and unadvertised unless separately configured, negotiated, and testedDeferred
Conformance scenarios required for standardsSEP-2484Pin official schema/scenarios and map every supported feature to CI evidenceRequired, Phase 6
Schema generator numeric correctionChangelogPin final generated schema; do not maintain hand-copied numeric field typesRequired, Phase 0
Governance and SEP process changesSEP-1850 and governance entriesNo runtime behavior; retain source links and final-spec refresh procedureNo runtime impact

The matrix must be updated in the same change whenever the implementation advertises another core capability or extension. A capability without an owner, resource bounds, authorization behavior, and conformance evidence is invalid.

Delivery Sequence

Phase 0: Contract and Legacy Baseline

  • Add the current stable 2025-11-25 revision to the legacy compatibility suite before introducing 2026-07-28.
  • Freeze legacy initialize, notification, list, call, SSE, DELETE, access-control, reload, and backend-session fixtures.
  • Vendor or pin the RC TypeScript and generated JSON schemas plus conformance scenario revision, then replace them with the final July 28 artifacts.
  • Record schema/checksum provenance so generated field types are not copied by hand.
  • Lock the -32000 catalog resource-limit error and over-limit no-truncation fixtures before implementing stateless tools/list.
  • Add legacy principal-to-session binding.
  • Close or assign every authorization dependency in the release coverage matrix, including backend token-audience strategy.

Phase 1: Protocol-Neutral Core

  • Extract the classifier and EffectiveMcpRequestContext.
  • Separate protocol validation/envelopes from common authorization and tool execution.
  • Replace version constants with configured profile registries.
  • Add configuration fields with backward-compatible defaults.
  • Add bounded JSON Schema 2020-12 compilation and input/output validators.
  • Validate stateless tool names and x-mcp-header annotations at load time.

Phase 2: Stateless Frontend Vertical Slice

  • Implement server/discover, tools/list, and tools/call.
  • Enforce Origin, content negotiation, per-request metadata, method rules, and SEP-2243 headers.
  • Add stateless result, cache, and error envelopes.
  • Add the empty extension boundary and reject unsupported result types/MRTR.
  • Keep subscriptions and backend bridging disabled and unadvertised.

Phase 3: Stateless Backend Adapter

  • Add explicit backend profiles.
  • Implement legacy-to-stateless and stateless-to-stateless translation.
  • Validate backend schemas, result types, extension capabilities, and output conformance according to the negotiated backend version.
  • Add an audience-correct credential strategy per backend target.
  • Add conservative backend discovery and downgrade tests.

Phase 4: Optional Compatibility Bridge

  • Keep rejection as the default.
  • If a real compatibility requirement exists, implement the bounded per-request bridge only for explicitly session-independent tools.

Phase 5: Streaming and Subscriptions

  • Replace the buffered response abstraction.
  • Implement cancellation-safe long-lived POST response streams.
  • Add toolsListChanged subscriptions and truthful discovery capability.

Phase 6: Conformance and Canary

  • Run the final official conformance suite where available.
  • Verify every Required and Dependency coverage-matrix row has linked test or operational evidence.
  • Exercise the full frontend/backend compatibility matrix.
  • Test reload, multi-replica routing, downgrade resistance, limits, disconnect, and credential leakage.
  • Enable stateless support for a canary client and target before changing the product default.

Verification Matrix

At minimum, automated tests must cover:

AreaRequired cases
ClassificationLegacy initialize, legacy session request, stateless request, missing session, stale session header ignored by a fully identified stateless request, header/meta mismatch, unsupported version
HTTP transportJSON content type, Accept requires JSON and SSE, exact Origin allowlist, empty allowlist with browser/non-browser clients, POST/GET/DELETE matrix, client-response rejection, unsupported notification rejection, future accepted-extension notification 202, unknown-method HTTP 404 plus JSON-RPC -32601
Legacy regressionExisting JSON and SSE responses, DELETE, expiry, reload preservation, backend session reuse and teardown
Stateless discoveryDeterministic versions/capabilities, no false capabilities, server identity in _meta, TTL/scope, principal-aware cache, unsupported version details
Stateless listPer-principal visibility, deterministic order, bounded complete catalog without nextCursor, rejection of unissued cursors, whole-request -32000 failure without truncation/caching when item or response limits are exceeded, private cache, TTL, policy/config invalidation
Tool schemasDefault and explicit dialects, invalid schema, local and external $ref, composition/depth/time bounds, arbitrary output roots, post-filter output validation
Tool error semanticsMalformed envelope and unknown tool produce protocol errors; input-schema failures produce bounded resultType: complete, isError: true results before backend traffic
Tool headers and namesValid/invalid names, collision handling, all x-mcp-header constraints, encoding, missing/null values, mismatch, sensitive/header conflict
Stateless callHTTP backend, stateless MCP backend, authorization denial, masking, filtering, output conformance, safe retries, no session-store mutation
Results and MRTRRequired complete, missing field by backend version, unknown extension result, unsupported input_required mapped to the exact gateway-generated tool error without opaque state, new request id on a future retry
Request-scoped notificationsNo notifications/message without per-request log level; no retained log-level state; initiating response stream used instead of subscription stream
ExtensionsEmpty capability map, unknown optional extension, required unsupported extension, no backend extension smuggling, Tasks and Apps absent
AuthorizationProtected-resource metadata, token on every request, 401/403 challenges, issuer/audience binding, frontend token not forwarded to wrong backend, bounded scope step-up
Backend compatibilityAll six frontend/backend matrix cells with explicit success or incompatibility outcome
Downgrade resistanceNo fallback after 401, 403, TLS, timeout, malformed response, mismatch, SSRF rejection, or 5xx
Resource safetyBody/depth/concurrency/cache/subscription bounds, cancellation, permit release, slow consumer
StreamingFirst acknowledgment, subscription id tagging, disconnect cleanup, reload, no replay or resumption
ScalingStateless calls across alternating replicas; legacy behavior requires documented affinity
SecretsNo token, cookie, CSRF, session id, handle, key, or private payload in logs and gate output

The release gate must prove that enabling the stateless adapter does not alter legacy fixtures when the same legacy configuration is loaded.

Risks and Mitigations

RiskMitigation
Missing session id is misclassified as statelessRequire the complete 2026-07-28 version and metadata contract before selecting stateless
Legacy session id crosses principalsBind the session to a verified principal fingerprint and revalidate current authentication on every request
Wire behavior drifts between profilesShare application logic and isolate only versioned adapters and envelopes
Gateway silently downgrades after a security failurePermit fallback only for explicit method/version compatibility responses
Stateless client is proxied through shared hidden legacy stateReject by default; allow only bounded per-request bridging for declared session-independent tools
Tool catalog leaks across usersUse private cache scope and auth/policy/config-aware cache keys
Capability advertisement exceeds implementationBuild capabilities from enabled handlers and tested features, not schema availability
Buffered SSE is mistaken for subscription supportReplace the response type and add disconnect/cancellation tests before advertising subscriptions
Config reload leaves stale list resultsInclude revisions in cache keys, invalidate on reload, and publish list change only after a successful swap
A future discovery-dependent catalog is stale until TTLDo not enable it until the registry provides a monotonic revision plus push/watch invalidation; treat TTL only as a documented backstop
Complex schemas exhaust CPU or trigger SSRFCompile with depth/subschema/time limits and disable external network $ref resolution
Filtering produces output that violates the advertised schemaValidate final structuredContent after filtering and return a tool error on mismatch
Unknown extensions or result types pass through the facadeAdvertise and forward only explicitly adapted, versioned extensions; reject unknown result types
Frontend bearer token is replayed to another resourceRequire an audience-correct backend credential strategy and never transit arbitrary tokens
Raw frontend or unrecognized headers reach a stateless backendUse a typed outbound allowlist and regenerate admitted context from trusted gateway state
Browser Origin is treated as ordinary CORS metadataEnforce exact request Origin validation before JSON parsing; empty allowlist rejects browser origins
Release-candidate schema changesKeep stateless disabled until the final schema and error registry are pinned and conformance tests pass

Acceptance Criteria

This design is implemented when:

  1. One /mcp endpoint concurrently accepts enabled legacy and 2026-07-28 clients without heuristic profile selection.
  2. Existing legacy contract tests remain unchanged and pass.
  3. A stateless list or call touches no frontend session state and can be routed to alternating gateway replicas.
  4. Both profiles use the same access-control, masking, execution, filtering, audit, and metrics core.
  5. Backend protocol compatibility follows the explicit matrix and never silently pools hidden state.
  6. Discovery and capabilities describe only implemented behavior.
  7. Required stateless headers, metadata, result fields, cache fields, and error codes conform to the final 2026-07-28 specification.
  8. Tool names, inputSchema, optional outputSchema, arbitrary structured content, and x-mcp-header behavior conform to the final tools and schema contracts under bounded validation.
  9. Origin validation, frontend OAuth challenges, issuer/audience binding, and backend credential selection pass their release gates.
  10. Unknown or deferred core features and extensions remain unadvertised and cannot pass transparently through the gateway.
  11. Every Required and Dependency release-coverage row links to test or operational evidence.
  12. All request, response, schema, concurrency, cache, and optional streaming resources are bounded and cancellation-safe.
  13. Stateless support remains opt-in until the final conformance and canary gates pass.

Resolved Conditional Profile Decision

The Phase 9 dependency assessment found no configured backend that requires auto discovery and no named legacy backend with an approved session-independence proof. The release therefore keeps backend profiles explicit, keeps stateless-to-legacy behavior at reject, and adds no dormant discovery cache or hidden backend-session machinery. This decision may be reopened only for a named dependency with the compatibility fixtures, security review, administrator opt-in, lifecycle bounds, and teardown evidence required by the applicable design section.

The browser controller route /ctrl/mcp does not qualify: it remains a separate JSON/WebSocket control plane routed by websocket-router, not an mcp-router Streamable HTTP backend.

Open Decisions Before Implementation Lock

  • Confirm the final July 28 schema's exact clientInfo and serverInfo requirements; the SEP text and consolidated RC use different normative strength.
  • Decide whether MCP Origin policy is stored directly in mcp-router.yml or supplied by a shared exact-origin security module. One component must be the authoritative validator; duplicated allowlists are not acceptable.
  • Select additional JSON Schema dialects, if any. Supporting only mandatory 2020-12 is the safest initial profile.
  • Select each backend target's audience-correct credential strategy and define which component performs token exchange or service-token acquisition.
  • Select final default request, cache, and subscription limits from baseline measurements rather than treating the illustrative values as tuned limits.
  • Decide whether subscription state should survive an in-process MCP router configuration swap or close and force re-subscription. The implementation must make either outcome deterministic and tested.