Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Request Flow

🎯 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.

Request flow

Graph Legend

The graph uses color only to separate paths:

ColorMeaning
BlueRequest direction: listener, middleware, RMCP dispatch, and backend calls.
GreenResponse direction: backend result, response unwind, and client response.
Amberinitialize, where backend MCP client sessions are created.
PurpleAuthorized MCP calls after Mcp-session-id exists.
RedLayer-local HTTP rejection before MCP method handling.

This page follows a normal streamable HTTP MCP request through the code. The shape below is based on the current main.rs, runtime.rs, Gateway::run_gateway, the request layers, and McpService. To watch the same flow with real requests, follow Run the Gateway Locally alongside this page.

Startup Path

Startup begins in crates/contextforge-gateway-rs/src/main.rs:

install rustls crypto provider
  -> Config::parse()
  -> logging::init_tracing_logging(&config)
  -> Runtime::from(&config)
  -> optional CpexRuntimeRegistry
  -> Gateway::builder()
       .with_config(config)
       .with_user_config_store_type(UserConfigStoreType::Redis)
       .with_session_manager(LocalSessionManager::default())
       .with_plugin_runtime(...)
       .build()
  -> runtime.execute(gateway, plugin_registry)

runtime.execute either runs one multi-thread Tokio runtime or starts multiple current-thread runtimes. In both modes it initializes the optional CPEX runtime and then calls gateway.run_gateway().

HTTP Stack Order

Gateway::run_gateway builds the service stack in crates/contextforge-gateway-rs-lib/src/lib.rs. Tower layers execute from the outside in, so a normal MCP request reaches the handler in this order:

TCP/TLS listener
  -> HttpMetricsLayer
  -> TraceLayer
  -> /contextforge-rs nested router
  -> CORS layer
  -> virtual_host_id_layer
  -> claims_layer
  -> session_id_layer
  -> user_config_store_layer
  -> virtual_host_config_layer
  -> /servers/{virtual_host_name}/mcp RMCP service

The inner Axum route is:

/servers/{virtual_host_name}/mcp

The public route is nested under:

/contextforge-rs/servers/{virtual_host_name}/mcp

The route segment is named virtual_host_name, but the layer stores the value as VirtualHostId.

Flow Checkpoints

CheckpointEstablished factNext dependency
ListenerThe request reached the Rust dataplane over TCP or TLS.Metrics, tracing, and nested routing can observe it.
Path extractionThe inner path matched /servers/{virtual_host_id}/mcp.MCP handlers can resolve a VirtualHost.
Claims validationThe bearer token was accepted and ContextForgeClaims exists.Config lookup can use claims.sub.
User config lookupA UserConfig exists for the authenticated subject.The virtual host check can run against that config.
Virtual host checkThe path’s virtual host id exists in the caller’s config.MCP validators can resolve the selected VirtualHost.
RMCP dispatchThe streamable HTTP request is mapped to an MCP method.The handler chooses initialize, routed backend calls, or local behavior.

Middleware Context

The request layers insert the context used later by RMCP handlers:

LayerRequest behaviorFailure behavior
virtual_host_id_layerExtracts /servers/{virtual_host_id}/mcp and inserts VirtualHostId.Returns 400 when the inner path does not match.
claims_layerValidates Authorization: Bearer ... with configured RS/HMAC decoder, issuer, audience, and expiration. Inserts ContextForgeClaims.Returns 401 for missing or invalid bearer auth.
session_id_layerReads Mcp-session-id and inserts SessionId when present.Missing session id is allowed here; authorized MCP handlers reject it later when required.
user_config_store_layerUses claims.sub as User::new(subject), loads UserConfig, and inserts it.Returns 400 for missing config, 500 for other store failures, and 400 if claims are absent.
virtual_host_config_layerChecks that the path’s virtual host id exists in the loaded UserConfig.Returns 404 with body {"detail":"Server not found"} when the virtual host is not in the caller’s config.

For DELETE, session_id_layer also has response-side behavior. It lets RMCP handle the request first. If the RMCP response succeeds and a session id exists, it removes the local user session mapping and removes backend transports for principal + session_id.

Initialize Flow

initialize does not require the downstream Mcp-session-id header. RMCP creates a DownstreamSessionId and places it in the request context.

McpService::initialize runs this sequence:

  1. InitializeCallValidator reads DownstreamSessionId, UserConfig, VirtualHostId, and ContextForgeClaims.
  2. It resolves user_config.virtual_hosts[virtual_host_id].
  3. It reads the local user session mapping for claims.sub + downstream_session_id.
  4. For every backend in the selected virtual host, it concurrently builds a StreamableHttpClientTransport with the configured backend URL and serves a GatewayBackendClient over that transport.
  5. It collects backend capabilities and running RMCP client services. The capabilities are stored with backend transport state for future routing, but they do not shape the downstream initialize response yet.
  6. It writes the local user session mapping.
  7. It stores each backend service in BackendTransports keyed by principal, backend name, and downstream session id.
  8. It returns InitializeResult with the gateway’s current fixed capability set: completions, prompts, resources, and tools enabled.

Backend initialization is concurrent through futures::future::join_all.

Authorized MCP Calls

Routed MCP calls after initialization use AuthorizedCallValidator. It requires:

SessionId
UserConfig
VirtualHostId
ContextForgeClaims

The validator resolves the same virtual host from the authenticated user’s config, then SessionManager locates backend services for:

principal + backend_name + session_id

Current routed method families:

Method familyFlow
list_tools, list_resources, list_prompts, list_resource_templatesBorrow all configured backend services, call every available backend concurrently with fan_out_list, preserve identifiers for one backend or namespace them for multiple backends, sort merged output, and return one list. Explicit tool aliases are preserved exactly.
call_toolResolve an exact tool alias first; otherwise preserve the name for one backend or split {backend_name}-{tool_name} for multiple backends. Resolve one backend, run the optional tool hooks, track progress, call the backend, and return the result.
read_resource, subscribe, unsubscribe, get_promptSelect the only backend and forward the identifier unchanged, or split and strip the prefix for a multi-backend host, then call the resolved backend.
completeApply the same conditional routing to the prompt name or resource URI in ref, then return the selected backend’s completion result.

GatewayBackendClient handles backend progress notifications for call_tool. RMCP assigns a new progress token to each backend request, so the gateway maps that backend token to the corresponding downstream token. Request enqueue and mapping publication are serialized against progress lookup so an immediate backend notification cannot overtake registration. When a notification matches an in-flight backend token, the gateway restores the downstream token, optionally runs the stream-event post hook, and forwards the notification to the downstream client.

call_backend_tool also watches the downstream cancellation token. If the downstream call is cancelled before the backend responds, the gateway sends a cancel request to the backend handle.

Local MCP Methods

Some MCP methods are currently local to the gateway implementation rather than backend-routed:

MethodCurrent behavior
pingReturns success.

This path still passes through the same HTTP middleware, but it does not use backend fanout or identifier routing.

Response Path

Backend responses return to McpService first. call_tool may run response plugin hooks before returning. List calls merge backend output, preserving single-backend identifiers and namespacing multi-backend identifiers as needed.

The HTTP response then unwinds through virtual_host_config_layer, user_config_store_layer, session_id_layer, claims_layer, virtual_host_id_layer, CORS, trace, and metrics. On successful DELETE, session_id_layer performs local session and backend transport cleanup during this unwind.