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:
- 2026-07-28 release candidate
- Draft protocol changelog
- SEP-2567: Sessionless MCP via Explicit State Handles
- SEP-2575: Make MCP Stateless
- SEP-2243: HTTP Header Standardization
- Draft base protocol
- Draft Streamable HTTP transport
- Draft discovery contract
- Draft tools contract
- Draft authorization contract
- SEP-1303: Input Validation Errors as Tool Execution Errors
- SEP-1319: Decouple Request Payload from RPC Method Definitions
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:
- The final revision's TypeScript schema, which the specification identifies as the protocol message source of truth.
- The final normative specification pages and generated JSON Schema.
- The final revision changelog and error-code registry.
- Final SEP text for rationale and requirements not changed during integration.
- 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:
- A client calls
initialize. light-gatewaycreates a gateway-owned frontend session and returns anMcp-Session-Id.- Every later request is validated against that session.
- For an
apiType: mcptool, the gateway lazily initializes a backend MCP session and maps it to the frontend session and backend target. - 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:
initializeandnotifications/initializedare removed.Mcp-Session-Idis removed.- Protocol version and client capabilities travel with every request.
server/discoverreports supported versions, server capabilities, and server identity.- Streamable HTTP requests expose routing fields through
Mcp-Methodand, 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/listenrather 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-28clients 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/listenuntil the Pingora response path can keep an SSE response open and cancel it safely.
Terminology
| Term | Meaning |
|---|---|
| Legacy profile | MCP 2025-11-25 and supported earlier revisions using initialization and protocol-level sessions |
| Stateless profile | MCP 2026-07-28, with per-request version/capabilities and no protocol-level session |
| Frontend | The MCP client-to-light-gateway side |
| Backend | An HTTP API or MCP server invoked by a configured gateway tool |
| Frontend session | A gateway-owned legacy client session identified by Mcp-Session-Id |
| Backend session | A legacy upstream MCP session owned by the gateway |
| Explicit state handle | An opaque application identifier returned by a tool and supplied to later tool calls; it is not an MCP protocol primitive |
| Principal fingerprint | A stable, non-secret digest of the authenticated subject, issuer, tenant, and other identity fields required to bind state or caches |
Protocol Profiles
| Concern | Legacy stateful profile | 2026-07-28 stateless profile |
|---|---|---|
| Lifecycle | initialize, then notifications/initialized | No initialization handshake |
| Version | Negotiated and retained in the session | Sent in the HTTP header and request params._meta |
| Client capabilities | Retained from initialization | Supplied for every request |
| Client identity | Supplied during initialization | clientInfo SHOULD be supplied in request params._meta |
| Gateway session | Mcp-Session-Id | Prohibited |
| Discovery | Initialization result | server/discover |
| Tool lists | May be session-sensitive | Must not vary by connection; may vary by authenticated principal or deployment state |
| Application state | May exist behind a legacy session | Explicit tool arguments and server-minted handles |
| Server notifications | Legacy Streamable HTTP behavior | subscriptions/listen POST response stream |
| Horizontal routing | Requires affinity or shared session routing | Any replica can handle any ordinary request |
| Stream resumption | Legacy-version behavior | No 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 method | Legacy stateful profile | 2026-07-28 stateless profile |
|---|---|---|
POST | Initialize, notifications, requests, and responses permitted by the negotiated legacy revision | Single self-contained JSON-RPC message; the only request entry point |
DELETE | Terminates the identified frontend session and its backend sessions | Not a protocol operation; reject without touching state |
GET | Keep the currently implemented behavior; this gateway returns 405 unless a separately supported legacy transport requires it | No server-notification endpoint; use subscriptions/listen through POST |
| Other methods | 405 Method Not Allowed | 405 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:
| Condition | Result |
|---|---|
Method is initialize, no session id, and requested version is an enabled legacy version | Legacy initialization path |
Mcp-Session-Id is present and the request does not claim 2026-07-28 | Legacy session path |
Version header and request params._meta both select enabled 2026-07-28, agree exactly, and required routing headers are valid | Stateless path |
Version header and body select enabled 2026-07-28, with a stale Mcp-Session-Id also present | Select stateless, ignore the legacy header, and never read, mint, echo, or delete session state |
| Legacy non-initialize request has no session id | Reject as missing legacy session id |
Stateless version is present only in the header or only in params._meta | Reject as a header mismatch |
| Version is missing or unsupported and no valid legacy initialization can negotiate it | Reject; 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-Typemust 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.Acceptmust list bothapplication/jsonandtext/event-stream.MCP-Protocol-Versionis required and must equalparams._meta["io.modelcontextprotocol/protocolVersion"].Mcp-Methodis required for every JSON-RPC request and must equal the JSON-RPCmethod. This revision does not define routing-header requirements for notification POSTs; the gateway must not invent them.Mcp-Nameis required where SEP-2243 defines a named operation, includingtools/call, and must equal the correspondingparams.nameorparams.urivalue.- Header names are compared case-insensitively; method and name values are case-sensitive after decoding. Ambiguous duplicate semantic headers are rejected.
Mcp-NameandMcp-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
400withHeaderMismatchcode-32020. - An unsupported version returns HTTP
400withUnsupportedProtocolVersioncode-32022and includes the requested and supported versions. - A required capability that the client did not declare returns
MissingRequiredClientCapabilitycode-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:
- Method allowlisting.
- Delegated-authority validation.
- Tool-list visibility and deterministic ordering.
- Request access control.
- Input-schema validation and request masking.
- Backend target resolution and SSRF protection.
- Tool execution and bounded retry policy.
- Response filtering and output-schema handling.
- JSON-RPC application error mapping.
- 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; ttlMsandcacheScopebecause 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, andcacheScope. cacheScopedefaults toprivatebecause 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:
inputSchemamust 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, ornull.- A schema without
$schemauses 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
$refand$defsresolution is supported within the schema document. - Network dereferencing of external
$refURIs 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:
- Apply transport byte/depth limits and authenticate or accept the explicitly anonymous route.
- Parse, classify, validate routing headers, resolve the configured tool, and apply principal/tool-level visibility and coarse authorization.
- Validate the original arguments against
inputSchemawithin the validation budget. - Evaluate delegation and argument-dependent request access policy against the validated, unmasked arguments.
- Apply request masking and invoke the backend.
- Parse the backend result, apply response policy and filtering, and then
validate the final
structuredContentagainstoutputSchemawhen present. - Construct the version-specific result envelope.
The error boundary is intentionally split:
- an unknown tool or malformed
tools/callenvelope that does not satisfy the protocol'sCallToolRequestschema returns a JSON-RPC protocol error; - arguments that fail the selected tool's
inputSchemareturn a completed tool result withresultType: "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/discoveromits 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
forbiddenunless 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 profile | HTTP backend | Legacy MCP backend | Stateless MCP backend |
|---|---|---|---|
| Legacy stateful | Existing direct translation | Existing mapped backend session | Translate stored legacy client metadata into each stateless backend request |
| Stateless | Direct translation | Reject by default; optional per-request bridge | Direct 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:
- Re-authorizes the configured gateway tool.
- Resolves and validates the backend target.
- constructs a new backend request using the backend's supported stateless version;
- regenerates
MCP-Protocol-Version,Mcp-Method,Mcp-Name, and applicableMcp-Param-*headers; - propagates the effective client capabilities and safe trace context;
- 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:
- Probe
server/discoverusing the preferred enabled stateless version. - Cache the result by normalized target, principal fingerprint, configuration revision, and a bounded TTL.
- Select stateless only after a valid discovery response.
- 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: Beareron every protected HTTP request, never a query-string access token;- HTTP
401with an appropriateWWW-Authenticatechallenge for a missing, invalid, or expired token; - HTTP
403and aninsufficient_scopechallenge with the required scope set when the authenticated principal lacks permission; - a
resource_metadatalink 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
issvalidation against previously validated issuer metadata; - binding persisted client credentials to the issuer that created them;
- correct OpenID Connect
application_typewhen deprecated Dynamic Client Registration is used for compatibility; - Client ID Metadata Documents as the preferred dynamic registration model;
.well-knownprotected-resource and authorization-server discovery rules;- confidential refresh-token storage, rotation requirements for public clients,
and correct optional
offline_accessbehavior; - 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:
- Authorize the listen request independently.
- Enforce global and per-principal subscription limits.
- Open a bounded event channel.
- Send
notifications/subscriptions/acknowledgedas the first JSON-RPC message. - Identify the subscription with the original request id.
- Tag each emitted notification with
io.modelcontextprotocol/subscriptionId. - Emit only notification types requested by the client and supported by the gateway.
- 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-28listed 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
cacheScopeor bridge values; allowExternalRefs: truewithout 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-headerannotations; - zero or internally inconsistent resource limits;
sessionIndependenton a normal HTTP tool when treated as an MCP bridge control;- a stateless MCP target without explicit
backendCredentialMode, or acaller/exchangetarget withoutbackendResource; - the deprecated
caller-compatcredential mode on a new stateless target; - conflicting backend profiles for one normalized backend target;
autowhen 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/discoverreturns 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 area | Primary sources | Gateway decision and owner | Status and gate |
|---|---|---|---|
Remove protocol sessions and Mcp-Session-Id | SEP-2567 | Stateless frontend adapter never touches the session store; legacy adapter remains isolated | Required, Phases 1-2 |
| Remove initialize handshake; per-request version and capabilities | SEP-2575 | Classifier and versioned frontend adapters normalize into EffectiveMcpRequestContext | Required, Phases 1-2 |
server/discover | SEP-2575, discovery spec | Implement auth-aware cacheable discovery with truthful capabilities, server identity, TTL, and cache scope | Required, Phase 2 |
| Standard HTTP routing and parameter headers | SEP-2243 | Validate and regenerate Mcp-Method, applicable Mcp-Name, and valid Mcp-Param-* headers | Required, Phases 2-3 |
| Streamable HTTP request rules and Origin protection | Transport spec | Enforce POST body/content negotiation, exact Origin allowlist, profile-specific GET/DELETE rules, initial notification rejection, and 202 only for a future supported notification | Required, Phase 2 |
| Session-independent list results | SEP-2567 | Catalog may vary by principal/config/policy, never by connection or prior call | Required, Phase 2 |
| Cache TTL and scope | SEP-2549 | server/discover and tools/list return bounded ttlMs and normally private cacheScope | Required, Phase 2 |
| Deterministic tool ordering | Tools spec | Preserve BTreeMap ordering after authorization filtering and test byte-stable results | Required, Phase 2 |
| Tool name format and collision handling | SEP-986, tools spec | Validate stateless-exposed names; require explicit aliases for aggregate collisions | Required, Phases 1-2 |
| JSON Schema 2020-12 and schema dialects | SEP-1613, SEP-2106, base spec | Add bounded compile/validation; 2020-12 mandatory; external network references disabled | Required, Phases 1-2 |
| Tool input validation error semantics | SEP-1303, tools spec | Return schema failures as bounded isError: true tool results; reserve protocol errors for malformed envelopes and unknown tools | Required, Phases 1-2 |
Arbitrary structuredContent and output schemas | SEP-2106, tools spec | Support all JSON roots and validate final filtered output against configured outputSchema | Required, Phase 2 |
| Standalone request/result payload definitions | SEP-1319 | No wire-format change; pin generated schemas and keep protocol-neutral payload models separate from JSON-RPC adapters | Required architecture boundary, Phases 0-1 |
Required resultType | SEP-2322, base spec | Emit complete; accept absent only from earlier negotiated backends; reject unknown values | Required, Phases 2-3 |
| Multi Round-Trip Requests | SEP-2322, SEP-2260 | Do not advertise initially; reject input_required from a backend without down-conversion | Deferred, separate design |
| Elicitation and MRTR migration changes | SEP-1034, SEP-1036, SEP-1330, changelog | Not reachable while MRTR and elicitation are unadvertised; a later design must cover defaults, URL/enum schemas, and removal of notifications/elicitation/complete and elicitationId | Deferred with MRTR |
subscriptions/listen | SEP-2575 | Replace buffered streaming abstraction, then add bounded tools-list-change streams | Deferred to Phase 5 |
Remove SSE replay and Last-Event-ID | SEP-2575 | No replay buffer; clients retry with a new request id and rehydrate state | Required, Phase 2/5 |
Remove ping, logging/setLevel, roots-list-changed, old subscribe methods | SEP-2575 | Return method not found and keep capabilities absent | Required boundary, Phase 2 |
| Per-request log level and message notification rule | SEP-2575, changelog | Do not emit or forward notifications/message unless that request supplied io.modelcontextprotocol/logLevel; retain no logging state | Required boundary, Phase 2 |
Trace context in _meta | SEP-414 | Validate and bridge W3C trace context without overriding trusted gateway correlation state | Required, Phase 2 |
| Core extensions map and independent extension versions | SEP-2133 | Empty/absent initially; only validated adapters may advertise or forward an extension | Required boundary, Phase 2 |
| Tasks extension | SEP-2663 | Do not advertise; task support is forbidden/absent until a separate extension design | Deferred |
| MCP Apps extension | SEP-1865 | Do not advertise or forward UI metadata without a separate sandbox/consent design | Deferred |
Deprecate roots, sampling, logging, and sampling includeContext values | SEP-2577, SEP-2596 | Do not add new deprecated features or the deprecated thisServer/allServers values to this tool-only gateway profile | No new implementation |
| Feature lifecycle and HTTP+SSE deprecation | SEP-2596 | Keep legacy Streamable HTTP only; no implicit two-endpoint HTTP+SSE fallback | Required boundary, Phase 0 |
| MCP error-code allocation | Base spec/changelog | Preserve legacy implementation codes by version; reserve -32020 through -32022 for their assigned stateless meanings; lock implementation-defined -32000 to the catalog resource-limit error | Required, Phases 0-2 |
Resource-not-found error becomes -32602 | SEP-2164 | No direct tool-only behavior; any future resource adapter must be version-aware | Deferred resource design |
| Authorization issuer validation and mix-up defense | SEP-2468 | Security/client runtime validates recorded issuer and returned iss | Dependency, Phase 0 gate |
| Client registration type and issuer-bound credentials | SEP-837, SEP-2352 | Client runtime owns registration metadata and keys credentials by issuer | Dependency, Phase 0 gate |
| Protected-resource discovery, refresh tokens, and scope step-up | Authorization spec, SEP-2207, SEP-2350, SEP-2351 | Security/client runtime owns metadata, token, challenge, and bounded step-up behavior | Dependency, Phase 0 gate |
| Dynamic Client Registration deprecation | Changelog | Do not add DCR to the MCP router; prefer Client ID Metadata Documents in owning client code | Dependency/boundary |
| Authorization extensions | SEP-2133, authorization extensions | Disabled and unadvertised unless separately configured, negotiated, and tested | Deferred |
| Conformance scenarios required for standards | SEP-2484 | Pin official schema/scenarios and map every supported feature to CI evidence | Required, Phase 6 |
| Schema generator numeric correction | Changelog | Pin final generated schema; do not maintain hand-copied numeric field types | Required, Phase 0 |
| Governance and SEP process changes | SEP-1850 and governance entries | No runtime behavior; retain source links and final-spec refresh procedure | No 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-25revision to the legacy compatibility suite before introducing2026-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
-32000catalog resource-limit error and over-limit no-truncation fixtures before implementing statelesstools/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-headerannotations at load time.
Phase 2: Stateless Frontend Vertical Slice
- Implement
server/discover,tools/list, andtools/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
toolsListChangedsubscriptions and truthful discovery capability.
Phase 6: Conformance and Canary
- Run the final official conformance suite where available.
- Verify every
RequiredandDependencycoverage-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:
| Area | Required cases |
|---|---|
| Classification | Legacy initialize, legacy session request, stateless request, missing session, stale session header ignored by a fully identified stateless request, header/meta mismatch, unsupported version |
| HTTP transport | JSON 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 regression | Existing JSON and SSE responses, DELETE, expiry, reload preservation, backend session reuse and teardown |
| Stateless discovery | Deterministic versions/capabilities, no false capabilities, server identity in _meta, TTL/scope, principal-aware cache, unsupported version details |
| Stateless list | Per-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 schemas | Default and explicit dialects, invalid schema, local and external $ref, composition/depth/time bounds, arbitrary output roots, post-filter output validation |
| Tool error semantics | Malformed envelope and unknown tool produce protocol errors; input-schema failures produce bounded resultType: complete, isError: true results before backend traffic |
| Tool headers and names | Valid/invalid names, collision handling, all x-mcp-header constraints, encoding, missing/null values, mismatch, sensitive/header conflict |
| Stateless call | HTTP backend, stateless MCP backend, authorization denial, masking, filtering, output conformance, safe retries, no session-store mutation |
| Results and MRTR | Required 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 notifications | No notifications/message without per-request log level; no retained log-level state; initiating response stream used instead of subscription stream |
| Extensions | Empty capability map, unknown optional extension, required unsupported extension, no backend extension smuggling, Tasks and Apps absent |
| Authorization | Protected-resource metadata, token on every request, 401/403 challenges, issuer/audience binding, frontend token not forwarded to wrong backend, bounded scope step-up |
| Backend compatibility | All six frontend/backend matrix cells with explicit success or incompatibility outcome |
| Downgrade resistance | No fallback after 401, 403, TLS, timeout, malformed response, mismatch, SSRF rejection, or 5xx |
| Resource safety | Body/depth/concurrency/cache/subscription bounds, cancellation, permit release, slow consumer |
| Streaming | First acknowledgment, subscription id tagging, disconnect cleanup, reload, no replay or resumption |
| Scaling | Stateless calls across alternating replicas; legacy behavior requires documented affinity |
| Secrets | No 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
| Risk | Mitigation |
|---|---|
| Missing session id is misclassified as stateless | Require the complete 2026-07-28 version and metadata contract before selecting stateless |
| Legacy session id crosses principals | Bind the session to a verified principal fingerprint and revalidate current authentication on every request |
| Wire behavior drifts between profiles | Share application logic and isolate only versioned adapters and envelopes |
| Gateway silently downgrades after a security failure | Permit fallback only for explicit method/version compatibility responses |
| Stateless client is proxied through shared hidden legacy state | Reject by default; allow only bounded per-request bridging for declared session-independent tools |
| Tool catalog leaks across users | Use private cache scope and auth/policy/config-aware cache keys |
| Capability advertisement exceeds implementation | Build capabilities from enabled handlers and tested features, not schema availability |
| Buffered SSE is mistaken for subscription support | Replace the response type and add disconnect/cancellation tests before advertising subscriptions |
| Config reload leaves stale list results | Include revisions in cache keys, invalidate on reload, and publish list change only after a successful swap |
| A future discovery-dependent catalog is stale until TTL | Do 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 SSRF | Compile with depth/subschema/time limits and disable external network $ref resolution |
| Filtering produces output that violates the advertised schema | Validate final structuredContent after filtering and return a tool error on mismatch |
| Unknown extensions or result types pass through the facade | Advertise and forward only explicitly adapted, versioned extensions; reject unknown result types |
| Frontend bearer token is replayed to another resource | Require an audience-correct backend credential strategy and never transit arbitrary tokens |
| Raw frontend or unrecognized headers reach a stateless backend | Use a typed outbound allowlist and regenerate admitted context from trusted gateway state |
| Browser Origin is treated as ordinary CORS metadata | Enforce exact request Origin validation before JSON parsing; empty allowlist rejects browser origins |
| Release-candidate schema changes | Keep stateless disabled until the final schema and error registry are pinned and conformance tests pass |
Acceptance Criteria
This design is implemented when:
- One
/mcpendpoint concurrently accepts enabled legacy and2026-07-28clients without heuristic profile selection. - Existing legacy contract tests remain unchanged and pass.
- A stateless list or call touches no frontend session state and can be routed to alternating gateway replicas.
- Both profiles use the same access-control, masking, execution, filtering, audit, and metrics core.
- Backend protocol compatibility follows the explicit matrix and never silently pools hidden state.
- Discovery and capabilities describe only implemented behavior.
- Required stateless headers, metadata, result fields, cache fields, and error
codes conform to the final
2026-07-28specification. - Tool names,
inputSchema, optionaloutputSchema, arbitrary structured content, andx-mcp-headerbehavior conform to the final tools and schema contracts under bounded validation. - Origin validation, frontend OAuth challenges, issuer/audience binding, and backend credential selection pass their release gates.
- Unknown or deferred core features and extensions remain unadvertised and cannot pass transparently through the gateway.
- Every
RequiredandDependencyrelease-coverage row links to test or operational evidence. - All request, response, schema, concurrency, cache, and optional streaming resources are bounded and cancellation-safe.
- 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
clientInfoandserverInforequirements; the SEP text and consolidated RC use different normative strength. - Decide whether MCP Origin policy is stored directly in
mcp-router.ymlor 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.