diff --git a/AGENTS.md b/AGENTS.md index 728fc0f..ef70deb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,22 @@ with the external ContextForge control plane in `https://github.com/IBM/mcp-context-forge`, but it must not become a control-plane, UI, IAM, or metrics-storage app. +## MCP Protocol Support + +- The downstream dataplane contract targets modern MCP clients using protocol + version `2026-07-28` over Streamable HTTP. +- Do not add new dataplane compatibility for older MCP protocol versions, + legacy `initialize`/session behavior, or the legacy SSE transport. Replace + remaining legacy paths with their `2026-07-28` equivalents as that migration + proceeds. +- The external ContextForge control plane owns and serves legacy clients. + Older MCP versions and SSE remain on control-plane routes and must not be + routed through this dataplane. +- Tests, examples, and new protocol behavior should use `server/discover`, + per-request client metadata, and the `2026-07-28` protocol version. +- Temporary compatibility shims in the current implementation are migration + details, not supported client contracts. Do not build new behavior on them. + ## Architecture Architecture documentation lives in The ContextForge Gateway Book under diff --git a/Cargo.lock b/Cargo.lock index 2944d7c..47cd6c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2819,8 +2819,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.0.0-beta.2" -source = "git+https://github.com/contextforge-gateway-rs/mcp-rust-sdk?rev=cd847d0a5b17138eec926872152a5f9f5d83a59b#cd847d0a5b17138eec926872152a5f9f5d83a59b" +version = "3.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13a8472324b3d6c74f092cc5d07b2707b128196b020ed7c3c2d52bede542680" dependencies = [ "async-trait", "base64", @@ -2852,8 +2853,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.0.0-beta.2" -source = "git+https://github.com/contextforge-gateway-rs/mcp-rust-sdk?rev=cd847d0a5b17138eec926872152a5f9f5d83a59b#cd847d0a5b17138eec926872152a5f9f5d83a59b" +version = "3.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa1de06ecdc489a8a03ca84f8491111c1e37acd8c69a815e253123039619d72" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index b0809db..5c76c50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ repository = "https://github.com/contextforge-gateway-rs/contextforge-gateway-rs # Keep dependencies here only when at least two workspace members inherit them. contextforge-gateway-rs-cpex = { path = "./crates/contextforge-gateway-rs-cpex" } contextforge-gateway-rs-apis = { path = "./crates/contextforge-gateway-rs-apis"} -rmcp = { version = "3.0.0-beta.2", features = [ +rmcp = { version = "3.0.0-beta.3", features = [ "server", "client", "auth", @@ -31,7 +31,7 @@ rmcp = { version = "3.0.0-beta.2", features = [ "reqwest", "transport-streamable-http-client-reqwest", "elicitation", -], git = "https://github.com/contextforge-gateway-rs/mcp-rust-sdk", rev = "cd847d0a5b17138eec926872152a5f9f5d83a59b" } +] } serde = {version= "1.0"} serde_json = "1.0" tracing = "0.1" diff --git a/crates/contextforge-gateway-rs-lib/src/const_values.rs b/crates/contextforge-gateway-rs-lib/src/const_values.rs index 892c3d5..78e2c7d 100644 --- a/crates/contextforge-gateway-rs-lib/src/const_values.rs +++ b/crates/contextforge-gateway-rs-lib/src/const_values.rs @@ -5,4 +5,5 @@ pub const LRU_CACHE_EXPIRY_DURATION: Duration = Duration::from_hours(1); pub const CONTEXT_FORGE_GATEWAY_AUDIENCE: &str = "mcpgateway-api"; pub const CONTEXT_FORGE_GATEWAY_ISSUER: &str = "mcpgateway"; pub const MCP_SESSION_ID: &str = "mcp-session-id"; +pub const MOCK_SESSION_ID: &str = "contextforge-mock-session"; pub const REDIS_RETRIES: usize = 1000; // keep re-trying forver diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs index 839a5ee..16b94ba 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_call_validator.rs @@ -1,9 +1,6 @@ use contextforge_gateway_rs_apis::user_store::{UserConfig, VirtualHost}; use http::request::Parts; -use rmcp::{ - ErrorData, RoleServer, model::ErrorCode, service::RequestContext, - transport::streamable_http_server::tower::DownstreamSessionId, -}; +use rmcp::{ErrorData, RoleServer, model::ErrorCode, service::RequestContext}; use tracing::debug; use crate::{ @@ -95,31 +92,23 @@ impl<'a> InitializeCallValidator<'a> { pub fn new(ctx: &'a RequestContext) -> Self { Self { ctx } } - pub fn validate(self) -> Result<(&'a VirtualHost, &'a DownstreamSessionId, &'a ContextForgeClaims), ErrorData> { + pub fn validate(self) -> Result<(&'a VirtualHost, SessionId, &'a ContextForgeClaims), ErrorData> { let maybe_parts = self.ctx.extensions.get::(); - let maybe_downstream_session = self.ctx.extensions.get::(); + let downstream_session_id = SessionId::mock(); let maybe_user_config = maybe_parts.and_then(|parts| parts.extensions.get::()); let maybe_virtual_host_id = maybe_parts.and_then(|parts| parts.extensions.get::()); let maybe_claims = maybe_parts.and_then(|parts| parts.extensions.get::()); let call_name = "initialize"; let has_user_config = maybe_user_config.is_some(); let virtual_hosts = maybe_user_config.map_or(0, |user_config| user_config.virtual_hosts.len()); - let has_session_id = maybe_downstream_session.is_some(); + let has_session_id = true; let has_claims = maybe_claims.is_some(); let virtual_host_id = maybe_virtual_host_id.map_or("", |id| id.value().as_str()); debug!( "InitializeCallValidator::validate - mcp call validation call_name = {call_name} has_user_config = {has_user_config} virtual_hosts = {virtual_hosts} has_session_id = {has_session_id} has_claims = {has_claims} virtual_host_id = {virtual_host_id}" ); - let Some(downstream_session_id) = maybe_downstream_session else { - return Err(ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: "Routing problem... downstream session id not created".into(), - data: None, - }); - }; - let Some(user_config) = maybe_user_config else { return Err(ErrorData { code: ErrorCode::INTERNAL_ERROR, diff --git a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_service/initialization.rs index 460237c..f83904c 100644 --- a/crates/contextforge-gateway-rs-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-gateway-rs-lib/src/gateway/mcp_service/initialization.rs @@ -28,7 +28,7 @@ where let (virtual_host, downstream_session_id, claims) = call_validator.validate()?; let session_mapping = if let Ok(maybe_session_mapping) = mcp_service .user_session_store - .get_session(&UserSession::new(claims.sub.clone(), Arc::clone(&downstream_session_id.session_id))) + .get_session(&UserSession::new(claims.sub.clone(), Arc::from(downstream_session_id.value().as_str()))) .await { maybe_session_mapping.unwrap_or_default() @@ -113,7 +113,7 @@ where if mcp_service .user_session_store .set_session( - &UserSession::new(claims.sub.clone(), Arc::clone(&downstream_session_id.session_id)), + &UserSession::new(claims.sub.clone(), Arc::from(downstream_session_id.value().as_str())), &session_mapping, ) .await @@ -129,7 +129,11 @@ where let mut transports = mcp_service.transports.inner().lock().await; for (name, service) in backend_services { transports - .entry(BackendTransportKey::from((name.as_str(), downstream_session_id.value(), claims.sub.as_str()))) + .entry(BackendTransportKey::from(( + name.as_str(), + downstream_session_id.value().as_str(), + claims.sub.as_str(), + ))) .insert_entry(service); } drop(transports); diff --git a/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs b/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs index 7ba42b0..62f8426 100644 --- a/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs +++ b/crates/contextforge-gateway-rs-lib/src/layers/session_id.rs @@ -6,7 +6,7 @@ use tracing::info; use crate::{ common::ContextForgeClaims, - const_values::MCP_SESSION_ID, + const_values::{MCP_SESSION_ID, MOCK_SESSION_ID}, gateway::{BackendTransports, UserSession, UserSessionStore}, }; @@ -16,6 +16,10 @@ pub struct SessionId { } impl SessionId { + pub(crate) fn mock() -> Self { + Self { value: MOCK_SESSION_ID.to_owned() } + } + pub fn value(&self) -> &String { &self.value } @@ -28,8 +32,11 @@ pub struct SessionIdState { } pub async fn session_id_layer(State(state): State, mut request: Request, next: Next) -> Response { - let session_id = - request.headers().get(MCP_SESSION_ID).and_then(|session_id| session_id.to_str().ok()).map(str::to_owned); + let session_id = request + .headers() + .get(MCP_SESSION_ID) + .and_then(|session_id| session_id.to_str().ok()) + .map(|_| MOCK_SESSION_ID.to_owned()); if let Some(session_id) = &session_id { info!("MCP Session ID {session_id}"); diff --git a/docs/book/src/architectural-choices.md b/docs/book/src/architectural-choices.md index 7b82bf6..55a0307 100644 --- a/docs/book/src/architectural-choices.md +++ b/docs/book/src/architectural-choices.md @@ -13,6 +13,7 @@ They describe the shape of the current Rust dataplane, not just preferences. | Choice | Current decision | Why it matters | | --- | --- | --- | | Dataplane, not control plane | This repo consumes runtime config and handles traffic. It does not own UI, IAM lifecycle, management APIs, or durable observability storage. | Keeps the hot path small and prevents product workflows from leaking into request routing. | +| Modern downstream MCP only | The target dataplane contract is MCP `2026-07-28` over Streamable HTTP. The control plane serves older versions and SSE without routing them through the dataplane. | Keeps legacy negotiation and transport compatibility out of the hot dataplane. | | Config access is abstracted | MCP routing depends on `UserConfig`, `VirtualHost`, and `UserConfigStore`, not Redis commands. | Keeps future xDS/gRPC or another config stream possible. | | Backend names are public | The backend map key is part of tool/resource/prompt names. | Backend renames are client-visible behavior changes. | | Sessions are local today | Backend RMCP services live in `BackendTransports` inside one process. | Load-balanced deployments need sticky routing or a new session ownership design. | @@ -39,6 +40,26 @@ If it is a workflow, it probably belongs outside this repo. The Rust gateway should enforce the result of management decisions, not become the place where those decisions are authored. +## Modern Downstream MCP Only + +The dataplane is moving to one downstream protocol contract: + +```text +MCP 2026-07-28 + -> Streamable HTTP + -> server/discover + -> required per-request client context +``` + +The Rust gateway should not grow adapters for older MCP versions, legacy +session initialization, or SSE. The external control plane serves those +clients on control-plane routes. Legacy traffic does not enter the dataplane; +only clients using the modern contract are routed here. + +Some current code and diagrams still describe session-oriented implementation +details. Treat them as migration inventory. Replace them with the modern +protocol path rather than preserving them as public compatibility behavior. + ## Config Access Is Abstracted Redis is the current storage and transport adapter. It is not the routing @@ -158,6 +179,7 @@ Changing one of these choices should update more than one file. | Change | Expected follow-through | | --- | --- | +| Downstream MCP version changes | Coordinate with the control plane, update modern protocol tests and examples, and keep legacy traffic on control-plane routes. | | Backend namespace changes | Update merge logic, split logic, tests, docs, and migration notes. | | Session state moves external | Update `SessionManager`, cleanup behavior, load-balancing docs, and failure-mode tests. | | Config transport changes | Keep `UserConfigStore` as the boundary and update adapter tests. | diff --git a/docs/book/src/capability-flow.md b/docs/book/src/capability-flow.md index 58d72c3..0551a81 100644 --- a/docs/book/src/capability-flow.md +++ b/docs/book/src/capability-flow.md @@ -1,5 +1,9 @@ # Capability Flow +> **Migration note:** this page documents the current `initialize` capability +> path. The target downstream contract is MCP `2026-07-28`, where +> `server/discover` replaces this client-facing lifecycle. + This page explains where MCP capabilities come from during `initialize`, how the gateway stores them, and what the downstream client sees today. @@ -32,7 +36,7 @@ runs this capability-related flow: The code path that reads the upstream capabilities is: -```rust +```rust,ignore let server_capabilities = running_service .as_ref() .and_then(|rs| rs.peer().peer_info().as_ref().map(|pi| pi.capabilities.clone())); @@ -40,7 +44,7 @@ let server_capabilities = running_service Those values are then stored here: -```rust +```rust,ignore BackendTransportService::from((server_capabilities, running_service.map(Arc::new))) ``` @@ -49,7 +53,7 @@ BackendTransportService::from((server_capabilities, running_service.map(Arc::new The source of truth is the upstream backend server. For example, a backend test server can return: -```rust +```rust,ignore InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) ``` diff --git a/docs/book/src/control-plane-integration.md b/docs/book/src/control-plane-integration.md index 154b3df..9a157d0 100644 --- a/docs/book/src/control-plane-integration.md +++ b/docs/book/src/control-plane-integration.md @@ -13,7 +13,8 @@ These are the values both sides currently rely on: | Agreement | Value today | | --- | --- | -| Client-facing route | `/servers/{virtual_host_id}/mcp` behaves like the legacy ContextForge MCP endpoint. The front door rewrites it to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the dataplane. | +| Client-facing route | The public route remains `/servers/{virtual_host_id}/mcp`. The front door rewrites modern MCP `2026-07-28` Streamable HTTP traffic to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the dataplane. | +| Protocol compatibility | The dataplane target is MCP `2026-07-28` only. The control plane serves older MCP versions, legacy session initialization, and SSE on its own routes; that traffic is not forwarded to the dataplane. | | Unknown virtual host | `404` with body `{"detail":"Server not found"}`, matching the control-plane response shape. | | Token issuer and audience | `iss = mcpgateway`, `aud = mcpgateway-api` — the values the control plane mints. | | Claims shape | `sub`, `jti`, `iss`, `aud`, `exp`, and `user` are required. `token_use`, `iat`, `teams`, and `scopes` are optional, as is `user.full_name`. The dataplane routes on `sub` only. | @@ -24,6 +25,10 @@ These are the values both sides currently rely on: Changing any of these is a cross-repo change: the dataplane, the control-plane publisher, and the integration harness all need updating together. +The protocol boundary is a target contract while the implementation migration +is in progress. Temporary session-oriented code inside the dataplane does not +move legacy compatibility ownership back into this repository. + ## Config Publishing The control plane owns durable config and publishes runtime snapshots to @@ -58,10 +63,11 @@ commit them whenever `UserConfig`, `VirtualHost`, `BackendMCPGateway`, or the ## Front-Door Split -Only MCP dataplane traffic comes to this process. The repository's reference -`docker/nginx.conf` proxies `location ^~ /contextforge-rs` to the gateway; -all UI, management API, and other ContextForge traffic stays on the existing -control-plane paths. +Only modern MCP `2026-07-28` Streamable HTTP traffic comes to this process. +The repository's reference `docker/nginx.conf` proxies +`location ^~ /contextforge-rs` to the gateway. Legacy MCP and SSE traffic, +plus all UI, management API, and other ContextForge traffic, stays on the +control-plane paths and does not enter the dataplane. ## Verifying The Integration diff --git a/docs/book/src/mcp-behavior.md b/docs/book/src/mcp-behavior.md index fd3d048..4b6cd18 100644 --- a/docs/book/src/mcp-behavior.md +++ b/docs/book/src/mcp-behavior.md @@ -3,9 +3,26 @@ This section describes what the gateway exposes as an MCP server and how it maps downstream MCP calls onto configured backend MCP servers. +## Protocol Support Direction + +The downstream dataplane contract is MCP `2026-07-28` over Streamable HTTP. +Modern clients use `server/discover` and provide the required client context on +each request. + +Older MCP versions, legacy session initialization, and the legacy SSE transport +are not target dataplane compatibility surfaces. The external ContextForge +control plane serves those clients on its own routes. Legacy traffic does not +enter the Rust dataplane. + +The implementation is still being migrated to this boundary. Pages that +describe `initialize`, `Mcp-Session-Id`, or local session state document +temporary current internals, not a client contract to preserve. New code, +tests, and examples should use MCP `2026-07-28`; remaining legacy paths should +be replaced or removed rather than extended. + > 📋 **Use this section for protocol behavior.** It covers the public MCP > surface, backend fanout, namespacing, targeted routing, pagination, and -> streaming gaps. +> the modern Streamable HTTP path. | Page | What it covers | | --- | --- | diff --git a/docs/book/src/mcp-method-reference.md b/docs/book/src/mcp-method-reference.md index 0f38ee5..061e36f 100644 --- a/docs/book/src/mcp-method-reference.md +++ b/docs/book/src/mcp-method-reference.md @@ -4,6 +4,11 @@ > gateway today, from the client's point of view. For how identifiers are > preserved, aliased, or namespaced, see [MCP Routing Semantics](mcp-routing-semantics.md). +> **Migration note:** the supported target uses MCP `2026-07-28`, +> `server/discover`, and per-request client context. The legacy `initialize`, +> session, and subscription paths below are implementation inventory to replace, +> not compatibility contracts. + Gateway methods fall into three groups: `initialize` creates backend sessions, routed methods use them, and `ping` remains local to the gateway process. diff --git a/docs/book/src/request-flow.md b/docs/book/src/request-flow.md index d28ba4b..09abf09 100644 --- a/docs/book/src/request-flow.md +++ b/docs/book/src/request-flow.md @@ -1,5 +1,10 @@ # Request Flow +> **Migration note:** the flow below documents the current session-oriented +> implementation. The downstream target is MCP `2026-07-28` with +> `server/discover` and per-request client context. Older clients and SSE remain +> on control-plane routes and do not enter this dataplane. + > 🎯 **Flow invariant:** Axum builds request context before RMCP handlers route MCP > methods. MCP handlers should read typed extensions, not parse headers, paths, > or Redis keys directly. diff --git a/docs/book/src/running-the-gateway.md b/docs/book/src/running-the-gateway.md index 9d17d1d..d2a1bd3 100644 --- a/docs/book/src/running-the-gateway.md +++ b/docs/book/src/running-the-gateway.md @@ -144,7 +144,14 @@ JWT subject admin@example.com -> backend MCP URLs ``` -## 5. Initialize an MCP Session +## 5. Temporary Legacy Smoke Flow + +> This section exercises the current migration-era implementation. It is not +> the supported downstream client contract. New clients must target MCP +> `2026-07-28` over Streamable HTTP with `server/discover` and per-request +> client context. Legacy clients and SSE remain on control-plane routes and do +> not use this dataplane endpoint; this smoke flow will be replaced as the +> dataplane migration lands. Open a streamable HTTP MCP session and save the returned `mcp-session-id` header: diff --git a/docs/book/src/session-ownership.md b/docs/book/src/session-ownership.md index e33d095..cff4b59 100644 --- a/docs/book/src/session-ownership.md +++ b/docs/book/src/session-ownership.md @@ -9,6 +9,13 @@ The current gateway keeps backend MCP client services in memory. That choice is simple and fast, but it defines how initialized MCP sessions can be routed in a deployment. +> **Temporary RMCP v3 compatibility:** the gateway currently maps initialize +> and every downstream `Mcp-Session-Id` header to one fixed internal session +> key. RMCP still manages the real transport session; the fixed key only keeps +> the existing gateway-owned backend lookup working until that state is +> removed. It is not part of the supported MCP `2026-07-28` client contract; +> legacy clients remain on control-plane routes and do not enter the dataplane. + ## Owned State Several pieces of state participate in one MCP session. They do not all have @@ -16,7 +23,7 @@ the same owner. | State | Key | Owner today | Lifetime | | --- | --- | --- | --- | -| RMCP downstream session | RMCP `DownstreamSessionId` and later `Mcp-session-id`. | RMCP `LocalSessionManager`. | Local process. | +| RMCP downstream session | RMCP `Mcp-Session-Id`; fixed internal gateway session key. | RMCP `LocalSessionManager`. | Local process. | | User session mapping | `UserSession { principal, downstream_session_id }`. | `LocalUserSessionStore`. | Local LRU cache, 50,000 entries, 1 hour. | | Backend running service | `principal + backend_name + session_id`. | `BackendTransports`. | Local process. | | Backend upstream MCP session | Managed inside RMCP running client service. | Backend service handle. | Local process and backend server. | @@ -34,7 +41,7 @@ unless that architecture changes too. InitializeCallValidator -> selected VirtualHost -> claims.sub - -> RMCP DownstreamSessionId + -> fixed internal gateway session key -> one upstream client service per backend -> BackendTransports entries ``` diff --git a/docs/book/src/system-shape.md b/docs/book/src/system-shape.md index 18273b6..ee0f756 100644 --- a/docs/book/src/system-shape.md +++ b/docs/book/src/system-shape.md @@ -1,5 +1,10 @@ # System Shape +> **Migration note:** session-oriented components shown on this page describe +> current internals. The downstream target is MCP `2026-07-28` over Streamable +> HTTP. Legacy MCP and SSE stay on control-plane routes and do not enter the +> dataplane. + > 🧭 **Architecture lens:** this page explains what the gateway is, what it is > not, and which code owns each boundary. diff --git a/docs/book/src/testing.md b/docs/book/src/testing.md index 218883f..0b071fe 100644 --- a/docs/book/src/testing.md +++ b/docs/book/src/testing.md @@ -19,6 +19,13 @@ cargo nextest run --locked --workspace Use `cargo test` when nextest is unavailable. For book changes, also run `mdbook build docs/book` and `mdbook test docs/book`. +Protocol tests and fixtures should target MCP `2026-07-28`, use +`server/discover`, and include the required per-request client metadata. Do not +add new dataplane coverage for older protocol versions, legacy session +initialization, or SSE; those paths belong in control-plane tests and must not +route legacy traffic through the dataplane. Existing legacy-shaped tests are +migration inventory and should be replaced as the modern implementation lands. + ## In-Repo Integration Tests `crates/contextforge-gateway-rs-lib/tests/` exercises the gateway against diff --git a/docs/book/src/what-is-contextforge-gateway.md b/docs/book/src/what-is-contextforge-gateway.md index e3bc8c5..1d692cb 100644 --- a/docs/book/src/what-is-contextforge-gateway.md +++ b/docs/book/src/what-is-contextforge-gateway.md @@ -15,6 +15,12 @@ presents those backends to the client as one merged MCP server. Throughout this book, *downstream* means the client side of the gateway and *upstream* means the backend side. +> **Protocol boundary:** the downstream dataplane target is MCP `2026-07-28` +> over Streamable HTTP. Older MCP versions, legacy session initialization, and +> SSE remain on external ContextForge control-plane routes and do not enter the +> dataplane. The current session-oriented internals are temporary migration +> state, not a compatibility promise for direct dataplane clients. + ![Gateway overview](assets/gateway-overview.svg) Blue arrows show request traffic. Green arrows show backend responses returning @@ -22,7 +28,7 @@ to the gateway and the merged MCP response going back to the client. | Layer | What happens | | --- | --- | -| Client edge | An MCP client calls `/contextforge-rs/servers/{virtual_host_id}/mcp` with a bearer token and MCP session headers. | +| Client edge | A modern MCP `2026-07-28` client calls `/contextforge-rs/servers/{virtual_host_id}/mcp` over Streamable HTTP with a bearer token and per-request client context. | | Gateway hot path | The gateway validates identity, loads runtime config, selects the virtual host, and routes MCP methods. | | Backend edge | The gateway opens or reuses MCP client sessions to configured backend MCP servers and merges what the client sees. | @@ -45,7 +51,7 @@ think about one client-facing MCP endpoint: Behind that endpoint, the gateway has three main boundaries: - the downstream boundary, where clients connect over streamable HTTP and - present their bearer token and MCP session id + present their bearer token and MCP `2026-07-28` per-request context - the configuration boundary, where the gateway turns the JWT subject into a runtime `UserConfig` - the upstream boundary, where the gateway opens or reuses MCP client sessions