ContextForge External Dataplane — Wiki
This wiki captures durable project context and working preferences. Check this index at the start of a task to decide whether deeper context is needed, then follow only the links that are relevant.
Pages
| File | What it covers |
|---|---|
| getting-started.md | Full docker stack, local cargo dev, cf-integration — commands and URIs |
| project.md | What the project is, goals, stakeholders, key modules, crate ownership, active work |
| preferences.md | Working standards, code style, logging rules, branch naming, AI interaction preferences |
| architecture.md | Current middleware stack order, pipeline shape, module boundaries, state ownership, executor shapes |
| routing.md | Stateless routing model: VirtualHost routing tables, per-request backend lifecycle, method quick reference, header forwarding, plugin hooks |
| mcp-capability-allocation.md | Tentative ContextForge 2.0 target topology, ownership, state model, Phase 1-4 roadmap, and Phase 3 flows |
| failure-modes.md | HTTP/MCP/routing/backend/plugin failure table — exact HTTP codes and JSON-RPC errors |
| config.md | Key CLI flags, JWT claims, UserConfig shape, plugin config, telemetry debugging, startup validation, local observability stack |
| deployment.md | External-dataplane deployment checklist, health endpoint caveat, nginx routing, TLS choices, session affinity, Redis availability, image pinning |
| security.md | Trust boundaries among the control plane, built-in dataplane, and external dataplane; Origin/Host validation; transport security; secrets handling |
| performance.md | Control-plane Locust load runs, benchmark settings, and built-in-dataplane baseline |
| testing.md | Workspace checks, in-repo integration tests, full-stack harness lanes, settings, and control-plane baseline |
Quick orientation
- Repo:
contextforge-data-plane— the Rust ContextForge external dataplane. - Core invariant: the ContextForge external dataplane is pure routing logic. No IAM, UI, or metrics storage.
- Protocol target: new external-dataplane behavior targets MCP
2026-07-28over Streamable HTTP withserver/discoverand per-request client metadata. The remaining2025-11-25/initializepaths are temporary compatibility coverage, not an expansion surface. Legacy SSE is outside the external dataplane. - Status convention: project, architecture, routing, and operations pages describe the current implementation. The page under Upcoming describes the tentative ContextForge 2.0 target and migration roadmap.
- Architecture context: architecture.md — read before touching the hot path. Full wiki index above.
- Validation gate:
cargo fmt+cargo clippy+cargo nextest+cargo denymust be clean; CI also runscargo shear. See preferences.md for by-change-type requirements. - System topology:
client → nginx → [ContextForge built-in dataplane | ContextForge external dataplane | ContextForge control plane]; external-dataplane config flows from the control plane viadataplane_publisher.py→ Redis → external dataplane. See project.md § System topology.
Project Overview
This page describes the current implementation. The tentative product end state and Phase 1-4 migration are documented in ContextForge 2.0 Target Architecture and Roadmap.
What this project is
contextforge-data-plane is the Rust-based ContextForge external dataplane.
It is a scalable, separately deployable MCP (Model Context Protocol) gateway
that routes AI tool calls from MCP clients to backend MCP servers.
The IBM/mcp-context-forge
Python repository contains two different product components: the ContextForge
control plane and the ContextForge built-in dataplane. This Rust repository is
the third component:
| Layer | Owns today |
|---|---|
| ContextForge control plane (Python) | IAM, UI, management APIs, durable administrative state, policy/catalog compilation, metrics storage, and external-dataplane configuration publishing. |
| ContextForge built-in dataplane (Python) | MCP request handling shipped in the same repository as the control plane. Supports 2026-07-28 and 2025-11-25, including stateful and stateless behavior. |
| ContextForge external dataplane (Rust, this repo) | Separately deployed MCP request routing and authorization enforcement. The target supports both protocol versions without session state; cross-version adaptation is best effort. |
The ContextForge external dataplane must never take on control-plane concerns.
Terminology
Use the full component names in product-wide architecture and deployment documentation:
- ContextForge control plane means the Python management plane in
IBM/mcp-context-forge. It owns administrative workflows and publishes effective runtime configuration; it is not the name for every process or MCP route in that repository. - ContextForge built-in dataplane means the Python MCP request path in the
same
IBM/mcp-context-forgerepository. “Built-in” describes where it ships, not a legacy-only or slow-path role. It handles the old and new protocol versions and can serve stateful or stateless clients. - ContextForge external dataplane means this independently deployable Rust repository. “External” means external to the Python repository/deployment, not untrusted or third-party. Its target request path is stateless for both supported protocol versions.
- Stateful means later MCP requests can depend on session context established
by
initializeor a session identifier. Stateless means every request is independently authenticated, authorized, resolved, and completed without reusable MCP session state.
Always use one of the three canonical names. Do not use unqualified “dataplane,” “local dataplane,” “slow dataplane,” or “fast dataplane” as a product component name.
flowchart LR
C(["MCP Client\nold/new · stateful/stateless"])
subgraph Infra["Infrastructure"]
N["nginx\nTLS termination\nrouting fan-out"]
end
subgraph EDP["ContextForge External Dataplane (Rust, this repo)"]
direction TB
MW["Middleware stack\nvirtual host · JWT · session · user config"]
RT["MCP Routing\nfan-out · prefix namespace\nlist merge · capability merge"]
PL["Plugin hooks\ncmf.tool_pre_invoke\ncmf.tool_post_invoke\ncmf.prompt_pre_fetch\ncmf.prompt_post_fetch"]
MW --> RT --> PL
end
subgraph PythonRepo["IBM/mcp-context-forge (Python repo)"]
direction TB
CP["ContextForge control plane\nIAM · UI · management"]
BDP["ContextForge built-in dataplane\nold/new · stateful/stateless"]
PUB["dataplane_publisher.py\nwrites UserConfig to Redis"]
CP --> PUB
end
R[("Redis\nUserConfig store\nMessagePack")]
BE["Backend MCP Servers"]
C --> N
N -->|"external route - currently 2026-07-28"| EDP
N -->|"UI / IAM / management"| CP
N -->|"built-in MCP routes"| BDP
PUB --> R
EDP -->|"read-only UserConfig"| R
EDP -->|"MCP calls"| BE
BDP -->|"MCP calls"| BE
Goals and objectives
- Provide a production-grade, low-latency routing layer between MCP clients and backend MCP servers.
- Support MCP
2026-07-28and2025-11-25over Streamable HTTP as stateless downstream contracts. - Enforce a clean ContextForge external dataplane/control plane boundary — no IAM, UI, or metrics storage logic in this repo.
- Keep config access behind the
UserConfigStoreabstraction (backed by Redis/MessagePack). - Remain in the right architectural shape during early development, prioritising correctness over backward compatibility.
Key stakeholders and users
- Platform teams — deploy and operate the gateway as infrastructure.
- AI application developers — use the gateway as the MCP proxy layer for their applications.
- Internal contributors — engineers evolving the ContextForge external dataplane toward stateless
2026-07-28and2025-11-25protocol support.
Key modules and architecture
Architecture context lives in the wiki. Key pages:
| Wiki page | Covers |
|---|---|
| architecture.md | Crate layout, pipeline shape, state ownership, module boundaries |
| routing.md | Backend prefix namespace, routing contract, session state, method reference |
| mcp-capability-allocation.md | Tentative ContextForge 2.0 end state, responsibility allocation, and Phase 1-4 roadmap |
| config.md | JWT validation, config keying, UserConfig shape, cache behavior |
| security.md | Trust boundaries, invariants, and tradeoffs |
Crate ownership
| Crate | Purpose |
|---|---|
contextforge-data-plane-lib | All ContextForge external-dataplane behavior: routing, middleware, sessions, transports. Almost everything goes here. |
contextforge-data-plane (binary) | Process shell only: CLI flags, logging, runtime shape. No ContextForge external-dataplane logic. |
contextforge-data-plane-apis | Shared config shapes (UserConfig, User, plugin config). Regenerate JSON schemas after any change: cargo run -p contextforge-data-plane-apis. |
contextforge-data-plane-cpex | Plugin integration (CPEX hook factories). |
Key invariants:
- Redis/config access goes through
UserConfigStoreonly — never leak Redis details into routing code. - The backend prefix naming contract must not change without updating merge logic, split logic, and tests.
- When behavior on the hot path changes, the matching wiki page must be updated in the same change.
Active work (near-term)
- Protocol migration: support same-version
2026-07-28and2025-11-25paths over Streamable HTTP, provide best-effort translation in either cross-version direction, and replace stateful session paths with request-scoped handling. - Legacy SSE transport and session affinity are being removed from the ContextForge external dataplane.
initializeis retained as a stateless compatibility request and must not create persistent external-dataplane or backend session state. - Protocol-sensitive tests must cover the two direct and two best-effort cross-version combinations. Modern examples should continue to use
server/discoverand per-request client metadata; compatibility examples may useinitializewithout relying on later session reuse.
ContextForge Integration Contract
Provisional. No formal contract has been stipulated yet. This section documents the current de-facto integration surface with IBM/mcp-context-forge. Any row may change while the project is early; when a proper contract is agreed, update this section to track it.
| Agreement | Value today |
|---|---|
| Client-facing route | /servers/{virtual_host_id}/mcp. Front door rewrites modern MCP 2026-07-28 Streamable HTTP traffic to /contextforge-rs/servers/{virtual_host_id}/mcp on the ContextForge external dataplane. |
| Protocol compatibility | Today the external-dataplane route accepts MCP 2026-07-28; the built-in dataplane handles 2026-07-28 and 2025-11-25, including stateful and stateless behavior and legacy SSE compatibility. The external-dataplane target handles both supported Streamable HTTP versions statelessly, with cross-version adaptation on a best-effort basis. |
| 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. |
| Claims shape | sub, jti, iss, aud, exp, and user required. token_use, iat, teams, scopes, and user.full_name optional. The ContextForge external dataplane routes on sub only. |
| User config Redis key | MessagePack(User::new(jwt_subject)) — key type plus subject, not the raw subject string. |
| User config Redis value | MessagePack(UserConfig). JSON schema at schemas/user_config.json. |
| User key Redis schema | schemas/user.json. |
| Plugin config key | ContextForgeGatewayRuntimePluginConfig, JSON or MessagePack, version: 1 with a cpex section. |
Coordination rule: changing any row above is a cross-repo change. The external dataplane, the control-plane publisher (dataplane_publisher.py), and the cf-integration harness all need updating together.
Regenerate both schemas after any struct change to UserConfig, VirtualHost, BackendMCPGateway, or the User key type:
cargo run -p contextforge-data-plane-apis
System topology (current)
All external traffic enters through nginx, which routes management traffic to the control plane and MCP traffic to either the built-in or external dataplane:
flowchart LR
client(["client"]) --> nginx["nginx"]
nginx --> external["external dataplane\nRust · this repo"]
nginx --> builtin["built-in dataplane\nPython repo"]
nginx --> control["control plane\nPython repo"]
external --> redis["redis"]
control --> redis
control --> postgres["postgres\n(via pgbouncer)"]
external --> fastts["fast_time_server"]
How the control plane publishes config to the external dataplane
The control plane and external dataplane do not communicate over HTTP. Config is exchanged exclusively through Redis:
- The control plane runs
dataplane_publisher.py— a publisher script that writes external-dataplane configuration (user config, backend definitions, etc.) into Redis. - The external dataplane reads that config from Redis via the
UserConfigStoreabstraction (MessagePack-encodedUserConfig).
This means:
- The external dataplane is a pure reader of Redis config. It never writes back to the control plane’s Redis keys.
- The control plane is the sole writer of external-dataplane config; the external dataplane has no direct dependency on the control-plane process at runtime.
- Config changes from the control plane are picked up by the external dataplane through normal cache refresh / Redis reads — no restart or direct RPC required.
Per-component responsibilities
| Component | Role | Persistence |
|---|---|---|
| nginx | TLS termination, routing fan-out | — |
ContextForge external dataplane (contextforge-data-plane) | MCP routing, auth enforcement, and backend calls; current session-backed paths are migration state, while the target is stateless | Redis (read-only for config) |
ContextForge built-in dataplane (IBM/mcp-context-forge) | Python MCP request handling for old/new protocols and stateful/stateless clients | Python repository runtime state and stores |
ContextForge control plane (IBM/mcp-context-forge) | IAM, UI, management APIs, metrics, and external-dataplane config publishing | Redis (write) + PostgreSQL (via pgbouncer) |
| redis | Runtime config store, inter-component pub/sub channel | In-memory + persistence |
| postgres (via pgbouncer) | Control-plane relational store | Durable |
| fast_time_server | High-resolution time source used by the ContextForge external dataplane | — |
External dependencies and integration points
- Redis — runtime config store (MessagePack-encoded
UserConfig). Populated bydataplane_publisher.pyon the control plane; read by the external dataplane viaUserConfigStore. - ContextForge control plane (
IBM/mcp-context-forge) — owns management workflows and publishes external-dataplane config viadataplane_publisher.py. - ContextForge built-in dataplane (
IBM/mcp-context-forge) — owns the Python repository’s MCP request paths, including old/new and stateful/stateless handling. Requests sent there do not route through the external dataplane. - fast_time_server — high-resolution time source consumed by the ContextForge external dataplane.
- Tokio + Axum — fixed async runtime and web framework.
Getting Started
Full Docker Stack
make docker-prod # build contextforge-data-plane:latest from docker/Dockerfile
make compose-up # start nginx, Python control/built-in components, Redis, Postgres, external dataplane, fast_time_server
Wait for register_fast_time to finish, then allow ~60s config propagation:
docker compose -f docker/docker-compose.yml logs -f register_fast_time
# Look for: Fast Time Server registration complete!
| Resource | URL |
|---|---|
| MCP endpoint | http://localhost:8080/contextforge-rs/servers/{virtual_host_id}/mcp |
| Bearer token | GET http://localhost:8080/contextforge-rs/admin/tokens/admin@example.com |
| fast_time_server virtual host id | b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8 |
Critical:
/contextforge-rsprefix → ContextForge external dataplane. Without it, MCP routes reach the ContextForge built-in dataplane (you’ll get{"detail":"..."}from mcpgateway, not an external-dataplane response).
Teardown: make compose-down (stops containers; volumes kept).
cf-integration Conformance
cargo binstall cf-integration@0.1.0 --no-confirm
make conformance
This runs the modern client and modern server eras through the committed
external-dataplane HEAD, including fixture-direct server comparison and the
scoped client suite. Use make conformance-bless to replace all selected
baselines transactionally after a fully successful run. Generated checkouts,
results, reports, and logs stay under .integration/.
Local Cargo Dev Workflow
For debugger/profiler/rapid iteration, start Redis and the counter/conformance fixtures:
docker compose -f docker/docker-compose-local.yaml up -d
docker compose -f docker/docker-compose-local.yaml ps redis gateway-one gateway-two
| Service | Endpoint | Role |
|---|---|---|
redis | 127.0.0.1:6379 | Runtime configuration store. |
gateway-one | http://127.0.0.1:5555/mcp | MCP Rust SDK counter fixture. |
gateway-two | http://127.0.0.1:5556/mcp | MCP Rust SDK conformance fixture. |
Run the binary with bootstrap helpers:
cargo run -p contextforge-data-plane \
--features contextforge-data-plane-lib/with_tools \
--bin contextforge-data-plane -- \
--address 127.0.0.1:8001 \
--redis-address 127.0.0.1 \
--redis-port 6379 \
--redis-mode plain-text \
--token-verification-public-key assets/jwt.key.pub \
--token-verification-private-key assets/jwt.key \
--upstream-connection-mode plain-text-or-tls \
--number-of-cpus 4
The client-facing route is http://127.0.0.1:8001/contextforge-rs/servers/{virtual_host_id}/mcp.
Mint a local test token
USER_ID=11111111-1111-1111-1111-111111111111
TOKEN=$(curl --silent --show-error \
--url "http://127.0.0.1:8001/contextforge-rs/admin/tokens/${USER_ID}?email=admin@example.com")
Seed runtime configuration
VIRTUAL_HOST_ID=c0ffee00f001f00df00ddeadbeefdead
curl --silent --show-error --request POST \
--url "http://127.0.0.1:8001/contextforge-rs/admin/userconfigs/${USER_ID}" \
--header 'content-type: application/json' \
--data '{
"virtual_hosts": {
"c0ffee00f001f00df00ddeadbeefdead": {
"backends": {
"gateway-one": {
"name": "gateway-one",
"url": "http://127.0.0.1:5555/mcp",
"passthrough_headers": [], "allowed_tool_names": [],
"allowed_resource_names": [], "allowed_prompt_names": []
},
"gateway-two": {
"name": "gateway-two",
"url": "http://127.0.0.1:5556/mcp",
"passthrough_headers": [], "allowed_tool_names": [],
"allowed_resource_names": [], "allowed_prompt_names": []
}
}
}
}
}'
Verify with mcp-inspector
npx @modelcontextprotocol/inspector
| Field | Value |
|---|---|
| URL | http://127.0.0.1:8001/contextforge-rs/servers/c0ffee00f001f00df00ddeadbeefdead/mcp |
| Transport | Streamable HTTP |
| Auth token | $TOKEN |
Modern protocol probe (server/discover)
curl --silent --show-error \
--url "http://127.0.0.1:8001/contextforge-rs/servers/${VIRTUAL_HOST_ID}/mcp" \
--header "authorization: Bearer ${TOKEN}" \
--header 'content-type: application/json' \
--header 'accept: application/json, text/event-stream' \
--header 'mcp-protocol-version: 2026-07-28' \
--header 'mcp-method: server/discover' \
--data '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0.1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 Unauthorized | Missing/invalid bearer token, wrong issuer/audience, or expired token. |
400 Problem occurred retrieving the configuration | Redis has no UserConfig for the token subject. Re-run the config POST. |
404 {"detail":"Server not found"} | The URL virtual-host id does not exist in the user’s config. |
400 mentioning request metadata | MCP protocol header and _meta version differ, or client metadata missing. |
| Backend calls fail | Backend URL wrong, fixture down, or --upstream-connection-mode rejects plain HTTP. |
Architecture
This page describes the current Rust implementation, including temporary backend fan-out and session behavior. The tentative configuration-driven end state is in ContextForge 2.0 Target Architecture and Roadmap.
Middleware Stack Order
Tower layers execute outside-in. A request reaches MCP handlers with these extensions already set:
TCP/TLS listener
-> HttpMetricsLayer
-> TraceLayer
-> /contextforge-rs nested router
-> mcp_origin_layer → validates Origin (403 when invalid/disallowed)
-> CORS layer
-> mcp_header_limits_layer → MCP standard header budgets (431 when exceeded)
-> virtual_host_id_layer → inserts VirtualHostId (400 on path mismatch)
-> claims_layer → inserts ContextForgeClaims (401 on bad/missing JWT)
-> session_id_layer → inserts SessionId if present
-> user_config_store_layer → inserts UserConfig (400 no config, 500 store error)
-> virtual_host_config_layer → rejects unknown vhost (404 "Server not found")
-> /servers/{virtual_host_name}/mcp RMCP service → validates Host, then dispatches MCP
DNS-rebinding validation is split by behavior. mcp_origin_layer rejects any
present Origin that is malformed or not allowlisted; requests without Origin
continue. RMCP validates the optional Host allowlist at the MCP service
boundary. See Security.
mcp_header_limits_layer rejects excessive MCP standard headers before JWT
validation, config lookup, session creation, backend fanout, or RMCP body
parsing.
MCP handlers read typed extensions and never parse paths or Redis keys directly.
tools/call reads the downstream header map from RMCP’s request-context
Parts extension for parameter-header validation.
Pipeline Shape
downstream request
-> Origin validation → MCP header limits → virtual host extraction → JWT validation → session extraction
-> user config lookup → RMCP request validation → MCP handler validation
-> request plugin hooks
-> backend MCP call (concurrent via join_all for initialize/list)
upstream response
-> response plugin hooks → merge/namespace/passthrough
-> metrics, tracing, logging → downstream response
flowchart TD
bin["binary\nCLI · logging · runtime"]
lib["lib\nrouting · middleware\nsessions · transports"]
apis["apis\nUserConfig · VirtualHost\nBackendMCPGateway"]
cpex["cpex\nCPEX hook factories"]
bin --> lib
lib --> apis
lib --> cpex
Hot-path pipeline (each stage must complete before the next):
flowchart TD
D(["downstream request"])
A["virtual host · JWT\nsession extract"]
C["user config lookup\nRMCP · MCP validate"]
P1["request plugins\ntool_pre_invoke"]
B["backend MCP call\njoin_all for init/list"]
P2["response plugins\ntool_post_invoke"]
M["merge · namespace\npassthrough"]
T["metrics · tracing · logging"]
U(["downstream response"])
D --> A --> C --> P1 --> B --> P2 --> M --> T --> U
RMCP enforces its configured request-body cap and validates modern standard
headers before dispatch. The tools/call handler then resolves the request’s
backend and original tool name. When UserConfig contains that tool’s input
schema, it validates recognized Mcp-Param-* headers against the request body;
it does not call backend tools/list. Without a published schema, parameter
headers are unrecognized and forwarded without local validation.
Published annotations are validated for MCP token, uniqueness, primitive type,
and properties-only reachability constraints. Nested annotations read the exact
argument path. Present non-null values require a matching header; absent or
null values require no header.
Parameter headers are forwarded unchanged; request plugins run afterward, so a
plugin that changes an annotated argument also owns any resulting upstream
mismatch.
Order is invariant: auth/config before backend selection; request plugins before upstream; response plugins before returning.
Module Boundaries (contextforge-data-plane-lib)
| Module | Owns |
|---|---|
common.rs | CLI config shape, JWT claims, Redis config validation, reqwest::Client construction |
layers/ | HTTP request extension extraction, request-bound validation |
gateway/ | MCP server behavior, initialize fanout, list merging, prefixed routing, backend service state |
gateway/session_store/ | Local and Redis user session storage |
user_config_store/ | UserConfigStore trait, Redis-backed store |
transports/ | Downstream TCP and TLS listener setup |
tools.rs | Local bootstrap helpers (with_tools feature only) |
State Ownership
| State | Owner | Lifetime |
|---|---|---|
CLI Config | Binary startup + Gateway | Process |
| JWT decoders | ContextForgeDataPlaneAppState | Process |
| User config | RedisUserConfigStore (LRU + Redis) | Request-path consumed; control-plane authored |
| Request identity / VirtualHostId | Request extensions | One HTTP request |
| Downstream session id | RMCP + SessionId extension | MCP session |
| Backend RMCP services (initialize, list ops) | BackendTransports map | Local process, per principal/backend/session |
| Backend RMCP services (call_tool) | Per-request connection | Single HTTP request |
| Local user session mapping | LocalUserSessionStore | Local LRU, 50k entries, 1 hour |
| Plugin manager | CpexRuntimeRegistry | Process, reloadable |
Session rule: backend MCP services are local process state. Sticky routing required for load-balanced deployments.
Executor Shapes
--single-runtime | Shape |
|---|---|
true (default) | One multi-thread Tokio runtime, --number-of-cpus workers. All connections share one BackendTransports. |
false | One OS thread per CPU, each with its own current-thread Tokio runtime and own BackendTransports. SO_REUSEPORT spreads connections — no session affinity. Stateful MCP sessions need --single-runtime true. |
In multi-runtime mode, the first thread initializes the optional CPEX plugin runtime before the others start; the current-thread builders are tuned with a global queue interval of 1024 and 4 I/O events per tick.
Multi-runtime consequence: each runtime thread builds its own
BackendTransportsmap and user-session store. Backend session state is per-runtime-thread, andSO_REUSEPORTgives no connection affinity — later requests in a streamable HTTP session can land on a thread that does not own the session. Treat single-runtime as the only mode supporting stateful MCP sessions today.
Lock Design
| State | Lock | Contention profile |
|---|---|---|
BackendTransports map | Arc<tokio::sync::Mutex<HashMap<...>>> | Locked briefly on initialize insert, list-op borrow, and cleanup. Borrowing clones Arc<RunningService> handles so the lock is not held across backend calls. call_tool bypasses this map entirely. |
| Subscription set | Arc<tokio::sync::Mutex<HashSet<String>>> | Local subscribe/unsubscribe only. |
| User config LRU cache | Arc<tokio::sync::Mutex<LruCache>> inside RedisUserConfigStore | One lock per config lookup on the hot path; misses add a Redis round trip. |
| User session LRU cache | Same pattern in LocalUserSessionStore | Initialize and delete paths. |
JWT decoders, upstream reqwest::Client, process Config | No lock — immutable after startup, shared by Arc/clone. | None. |
Design rule: locks guard maps of handles, not I/O. Backend calls, Redis reads, and plugin hooks all run outside any gateway lock.
Listener Behavior
The TCP listener binds with reuseaddr, reuseport, and keepalive, listens with a backlog of 1024, and serves Axum with graceful shutdown on ctrl_c. The TLS listener accepts by hand through Rustls and serves the same router via Hyper.
Allocator
The binary sets tikv_jemallocator as the global allocator. jemalloc holds up better than the system allocator under the many small, short-lived allocations of per-request JSON and header processing.
Fanout And Cancellation
initializeopens one backend transport per configured backend concurrently (futures::future::join_all); a failed backend degrades that backend only.- List methods fan out to all connected backends concurrently and merge.
- Targeted calls (except
call_tool) resolve exactly one backend service handle fromBackendTransports. - Targeted tool, prompt, and resource calls run configured pre/post plugin hooks after backend routing.
call_toolcreates a fresh per-request backend connection viaconnect_backend_for_request, then explicitly closes it before returning. call_toolwatches the downstream cancellation token and forwards a cancel to the backend if the client gives up first; backend progress notifications are forwarded downstream while the call is in flight.
Resource reads carry a concrete, request-owned hook state across backend I/O. It pins the runtime selected before the read, or records that no post hook was configured. Post processing consumes that state without type erasure, downcasts, or a second registry lookup.
Startup And Response Flow
Startup sequence (main.rs → Gateway::run_gateway):
install rustls crypto provider
-> Config::parse()
-> logging::init_tracing_logging(&config)
-> Runtime::from(&config) ← sets executor shape
-> 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)
Response unwind order (Tower layers execute outside-in, so unwind is inside-out):
backend response
-> response plugin hooks (tool, prompt, and resource calls)
-> merge / namespace / pass through
-> virtual_host_config_layer response side
-> user_config_store_layer response side
-> session_id_layer response side ← on DELETE success: remove session + backend transports
-> claims_layer response side
-> virtual_host_id_layer response side
-> CORS, mcp_origin_layer, TraceLayer, HttpMetricsLayer
-> downstream response
Flow checkpoints — each must exist before the next dependency runs:
| Checkpoint | Fact established | Next dependency |
|---|---|---|
| Listener | Request reached the ContextForge external dataplane over TCP/TLS. | Metrics, tracing, nested routing. |
| Path extraction | Inner path matched /servers/{virtual_host_id}/mcp. | MCP handlers can resolve a VirtualHost. |
| Claims validation | Bearer token accepted; ContextForgeClaims exists. | Config lookup can use claims.sub. |
| User config lookup | UserConfig exists for the authenticated subject. | Virtual host check can run. |
| Virtual host check | Path’s virtual host id exists in the caller’s config. | MCP validators can resolve the selected VirtualHost. |
| RMCP dispatch | Streamable HTTP request mapped to an MCP method. | Handler chooses initialize, routed call, or local behavior. |
MCP-First, Not MCP-Only
The current code implements MCP behavior, but the gateway shell is broader:
auth → config lookup → transport setup → plugin runtime → telemetry → session strategy
Keep protocol-neutral concerns (auth, config ingestion, TLS handling, plugin execution, telemetry, runtime shape, session strategy) reusable. Future A2A or model-provider routing should reuse the gateway shell without copying the MCP routing stack. MCP-specific behavior must remain isolated to the current MCP modules.
Transport Security Split
Transport security is split across two owners; keep this visible:
| Concern | Stable owner | Expected evolution |
|---|---|---|
| Gateway listener certificate | Process config. | Stays process config — it belongs to the listener. |
| JWT verification keys | Process config. | Stays process config. |
| Backend URL, auth headers, pass-through policy, allowed objects | Runtime user config (BackendMCPGateway). | Grows as per-backend policy detail increases. |
| Backend-specific TLS trust and client identity | Process config today. | Should move to runtime config or referenced secret material per backend. |
Do not bury transport security decisions inside MCP method handlers. They belong in startup assembly or explicit backend transport construction.
Plugin Hook Expansion Requirements
Current supported hooks cover tool, prompt, and resource pre/post lifecycles. Before adding any new hook point, define all of the following:
| Requirement | Why |
|---|---|
| Failure behavior | Does a plugin error abort the call, degrade gracefully, or log and continue? |
| Timeout behavior | What happens when a plugin takes too long on the hot path? |
| Cancellation behavior | Can the downstream cancel propagate through the plugin? |
| Streaming/SSE behavior | Does the hook fire once or per-chunk? What is the backpressure model? |
| Telemetry attribution | Which span/metric owns plugin latency and errors? |
Avoid ad hoc plugin calls in routing code. New hook points belong at explicit, documented pipeline positions.
Architecture-Change Follow-Through Matrix
Changing a load-bearing choice requires updating more than one file:
| Change | Required follow-through |
|---|---|
| Downstream MCP version | Coordinate with the ContextForge control plane and built-in dataplane; update the 2026-07-28/2025-11-25 compatibility matrix, protocol tests, examples, and front-door routing. The ContextForge built-in dataplane handles both stateful and stateless traffic; the ContextForge external dataplane handles both supported Streamable HTTP versions statelessly. |
| Backend namespace / prefix contract | Update merge logic, split logic, tests, docs, and control-plane integration if client-facing surface moves. |
| Session state moves external | Update SessionManager, cleanup behavior, load-balancing docs, and failure-mode tests. |
| Config transport changes | Keep UserConfigStore as the boundary; update adapter tests. |
| Plugin hook surface expands | Document ordering, failure, timeout, cancellation, streaming, and telemetry before landing. |
| New protocol joins the gateway | Keep shared shell protocol-neutral; isolate new protocol-specific routing. |
MCP Routing Semantics
The external dataplane is a pure stateless router. No session state, no BackendTransports, no sticky-routing requirement.
How a request is routed
validate_statelessextractsVirtualHostfrom request extensions (set byvirtual_host_configlayer from the JWT virtual-host ID).- Downstream name is looked up in
VirtualHost::tools,::resources, or::prompts— an O(1) table lookup. connect_backend_for_requestopens a freshStreamableHttpClientTransport, runs the call, closes the connection.
The control plane builds and publishes the routing tables to Redis; the dataplane never derives names at call time.
Routing table shape
VirtualHost { backends: HashMap<String, BackendMCPGateway>,
tools: HashMap<String, ServiceRoute>,
resources: HashMap<String, ServiceRoute>,
resource_templates: HashMap<String, ServiceRoute>,
prompts: HashMap<String, ServiceRoute> }
ServiceRoute { backend_name: String, // key into VirtualHost::backends
upstream_name: String } // name/URI forwarded to the backend
Source: user_store.rs
Method quick reference
| Method | Behavior |
|---|---|
initialize (2026-07-28) | INVALID_REQUEST — not supported by this dataplane. |
initialize (legacy) | Stub InitializeResult; no backend fanout. Supports older clients during migration. |
list_tools, list_resources, list_resource_templates, list_prompts | INVALID_REQUEST — delegated to control plane. |
call_tool | Lookup in tools map → pre-hook → fresh connection → call → post-hook → close. Forwards cancellation; tracks progress tokens. |
read_resource | Lookup in resources map → fresh connection → call with upstream URI → close. |
get_prompt | Lookup in prompts map → pre-hook → fresh connection → call → post-hook → close. |
subscribe, unsubscribe, complete | INVALID_REQUEST — delegated to control plane. |
ping | Local success; no backend fanout. |
DELETE | RMCP handles; session_id_layer removes the LocalUserSessionStore entry. No backend state to clean up. |
Header forwarding
Applied in order per upstream call: Host (from backend URL, HTTPS only) → passthrough (BackendMCPGateway::passthrough_headers) → Mcp-Param-* auto-forward → trace context → add (add_headers, overrides passthrough) → remove (remove_headers, applied last).
Protected headers that config can never touch: Host, Content-Length, Content-Type, all RFC 7230 hop-by-hop headers, Mcp-Session-Id, Accept, Last-Event-Id, and all computed MCP standard headers (Mcp-Method, Mcp-Name, Mcp-Protocol-Version, Mcp-Param-*).
For clients on ≥ 2026-07-28, call_tool validates Mcp-Param-* headers against BackendMCPGateway::tool_schemas before contacting the backend.
Plugin hooks
call_tool, get_prompt, and read_resource run pre/post hooks when a
GatewayPluginRuntimeHandle is configured. Pre-hooks can deny a request or edit
tool/prompt arguments or the resource URI. Resource URI edits must resolve through
the caller’s published routes. Post-hooks can rewrite or reject the response.
The handle selects a runtime before backend I/O. It returns typed request state
whose after_* method runs the post-hook on that same runtime, even after a reload
or a reload failure. A request that started without post-hooks never gains one
mid-flight. Tool state is shared under a mutex so progress notifications and the
final response use the same correlation ID and serialize plugin-context updates.
Prompt and resource state is owned by a single request and needs no mutex.
The internal CPEX crate separates these responsibilities:
| Module | Owns |
|---|---|
registry/ | Runtime selection, factory registration, config reloads, and the watcher |
runtime.rs | Manager lifecycle, shared pre/post execution, and request context |
tools/, prompts/, resources/ | Typed request state, operation-specific CMF conversion, and conversion tests |
cmf.rs | Hook inventory, message envelopes, and the CmfResponse conversion trait |
hooks.rs | Shared argument edits and pre-hook results |
config.rs, factory.rs | Config decoding/storage and compiled-in plugin factories |
Each response adapter implements CmfResponse; the runner handles invocation,
unchanged payloads, context propagation, and denial. Conversion and rejection
rules remain operation-specific: tools, rendered prompts, and resource reads
have different MCP representations. See Config
for those contracts.
Security Model
Trust Boundaries
| Boundary | Trust level | Enforced by |
|---|---|---|
| Downstream client | Untrusted. Every request must present a valid bearer JWT; session id alone grants nothing without matching principal state. | claims_layer, validators, and principal-scoped backend session keys. |
| JWT verification material | Trust anchor. The RSA public key or HMAC secret in process config decides which tokens are accepted. | Process config; loaded at startup. |
| Redis | Control-plane trust boundary. Whoever can write Redis controls routing (UserConfig) and, when runtime plugins are enabled, which registered hooks execute (ContextForgeGatewayRuntimePluginConfig). | Redis TLS/mTLS connection modes; the external dataplane never writes user config in production builds. |
| Backend MCP servers | Trusted per configured URL. The gateway forwards caller traffic to them and merges their responses. | UserConfig backend URLs plus the upstream connection mode. |
| Plugins | Fully trusted code. Hooks run in-process and can read and mutate tool payloads. | Compiled-in factories only; Redis config activates registered factories, it cannot load new code. |
Authentication And Authorization
| Plane | Current responsibility |
|---|---|
| ContextForge control plane | Owns login/SSO, users, teams, IAM, API-token issuance and revocation, and external-dataplane configuration publication. dataplane_publisher.py writes visibility-filtered UserConfig snapshots to Redis by user email. |
| ContextForge built-in dataplane | Owns the Python repository’s MCP request routes, including old/new protocol and stateful/stateless behavior. |
| ContextForge external dataplane | Has no IAM or user database. It currently verifies modern MCP bearer JWTs locally, loads UserConfig by sub, and requires the requested virtual host to exist. No runtime control-plane call occurs. |
External-dataplane request path: control-plane API token (sub = email) → Origin check →
claims_layer → Redis config lookup → virtual-host check → RMCP Host check →
MCP routing.
Browser/login session tokens are management-plane credentials, not the
external-dataplane contract.
- JWT validation accepts
RS256/384/512orHS256/384/512and requires a valid signature,iss=mcpgateway,aud=mcpgateway-api, andexp.jtianduserare required fields;token_use,iat,teams, andscopesare optional. - Failures: bad/missing JWT →
401; no user config →400; unavailable virtual host →404. - Authorization is currently coarse: valid JWT plus published virtual host. JWT scopes/teams and object allowlists are not enforced; publishing a backend exposes all objects returned by it.
- This coarse current behavior does not meet the tentative Phase 3 target. The target requires principal- and isolation-bound snapshots, per-request scope and compiled-RBAC enforcement, and default denial for missing or unauthorized entries. See Target Authorization Invariants.
- External-dataplane requests do not consult the control-plane token blocklist.
Revoked tokens pass JWT validation until
expor signing-key rotation/restart. Removing a subject’s config eventually blocks all its tokens after publisher and cache expiry.
What Compromise Means
| If this is compromised | Impact |
|---|---|
| JWT signing key or HMAC secret | Attacker mints tokens for any subject and reaches that subject’s backends. Rotate the key and restart; no revocation exists. |
| Redis write access | Attacker rewrites routing (arbitrary backend URLs receive caller traffic) and, if runtime plugins are enabled, chooses which registered hooks run on payloads. Protect Redis with TLS/mTLS and control-plane-only write access. |
| A backend MCP server | Attacker sees requests routed to that backend and controls its responses; the namespace prefix limits blast radius to that backend’s objects. |
| The gateway process | Full compromise: it holds the decoding keys in memory and live backend sessions. |
Transport Security
| Leg | Current posture |
|---|---|
| Downstream | TLS optional (--tls-address, no client auth — identity is the bearer token). Plain HTTP is acceptable only behind a trusted front door on a private network. |
| Upstream | HTTPS-only by default; plain HTTP must be opted into with --upstream-connection-mode. mTLS client identity is supported per process. |
| Redis | Plain, TLS, or mTLS via --redis-mode. Use TLS or mTLS anywhere Redis crosses a trust zone — Redis is the config trust boundary. |
MCP Origin and Host Validation
mcp_origin_layer validates Origin before authentication. RMCP validates Host
at the MCP service boundary. Together they enforce MCP 2026-07-28
DNS-rebinding protection.
| Environment variable | Default | Contract |
|---|---|---|
CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS | Host check disabled | When set, RMCP requires request authority from Host (URI fallback) to match. A portless entry matches any port; an explicit port matches exactly. |
CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_ORIGINS | Only requests without Origin pass | A present Origin must be a strict serialized origin in the allowlist. |
Missing Origin is accepted. null, malformed, unlisted, or
path/query/fragment/userinfo-bearing origins are rejected with HTTP 403.
Default ports are normalized (https://a equals https://a:443). When the Host
allowlist is configured, RMCP returns 400 for a missing or malformed authority
and 403 for an unlisted authority. There is no same-origin fallback; configure
both allowlists for public deployments.
Host validation runs only after the request reaches the RMCP service. Origin,
CORS, authentication, user-config, and virtual-host middleware can return a
response first, so the Host-specific 400 and 403 statuses apply only after
those earlier stages succeed.
mcp_header_limits_layer enforces configurable count, per-value byte, and
approximate request-level aggregate byte budgets for MCP standard request
headers before JWT validation or RMCP body parsing. The aggregate budget covers
all matched header names and values on one request, while the per-value budget
still caps each individual header value. That budget covers Mcp-Method,
Mcp-Name, Mcp-Protocol-Version, and Mcp-Param-*; the same guardrail also
covers the legacy/RMCP transport header Mcp-Session-Id. It is an
application-level guard for MCP-related headers only; non-MCP headers remain
bounded by the HTTP transport.
Backend header policy cannot add, remove, or replace MCP standard or parameter
headers. For modern tools/call, the dataplane resolves the authenticated
user, virtual host, backend, and original tool name before validating
recognized Mcp-Param-* against the control-plane-published input schema. A
recognized missing, malformed, unexpected, conflicting repeated, or mismatched header fails closed
with JSON-RPC -32020. Schema annotations also fail closed unless their names
are non-empty, case-insensitively unique HTTP tokens, their properties have an
allowed primitive type, and their paths are statically reachable through
properties only. Nested values are checked at their exact path, and integers
must remain in the IEEE 754 safe range. When no schema is published, parameter
headers are unrecognized and forwarded without local validation; their absence
does not block the tool call.
Validation does not call backend tools/list. Parameter values are forwarded
unchanged, while RMCP regenerates method, routed-name, and protocol-version
headers. If a plugin later changes an annotated argument, the original header
remains and the upstream server may reject the mismatch.
Local Bootstrap Helpers (with_tools)
The contextforge-data-plane-lib/with_tools feature compiles in:
/contextforge-rs/admin/tokens/{user}/contextforge-rs/admin/userconfigs/{user}/contextforge-rs/health
These routes are registered outside the authentication middleware — unauthenticated by design. They exist only for local bootstrap. Production builds must not enable this feature. In a real deployment the control plane mints tokens and writes config.
Secrets Handling
- JWT validation keys are fetched from a remote JWKS endpoint; TLS certificate material is read from disk paths at startup.
- Never log: tokens, authorization headers, secrets, Redis key/value bytes, full
UserConfigdocuments, or backend credentials.
Failure Modes
Rule: failures come from the layer that owns the missing fact. Identity/config failures are HTTP responses before MCP handling; routing/backend failures are JSON-RPC errors.
HTTP Layer (middleware, before MCP)
| Failure | Response | Layer |
|---|---|---|
Path doesn’t match /servers/{id}/mcp | 400 | virtual_host_id_layer |
Missing Authorization / non-Bearer scheme | 401 | claims_layer |
| JWT undecoded, unsupported algorithm, no key | 401 | claims_layer |
| Expired token, wrong issuer/audience | 401 | claims_layer |
No user config for claims.sub, or claims absent | 400 | user_config_store_layer |
| Config store error (not missing) | 500 | user_config_store_layer |
| Virtual host id absent from caller’s config | 404 {"detail":"Server not found"} | virtual_host_config_layer |
MCP Validation (defense-in-depth, normally unreachable)
| Failure | JSON-RPC error |
|---|---|
| Missing session id / config / vhost / claims extension | Internal error (Routing problem...) |
| Virtual host absent from user config | RESOURCE_NOT_FOUND No configuration |
Routing
| Failure | Behavior |
|---|---|
Prefixed name doesn’t start with backend name + - | Internal error |
| No backend entry matches split name | Internal error (got no responses from backends) |
| Backend entry exists but no running service | Internal error (backend failed during initialize) |
| More than one backend entry matches | INVALID_REQUEST; session backend entries cleaned up |
| Undecodable pagination cursor | -32602 Invalid params |
Backend Session
| Situation | Behavior |
|---|---|
Backend unreachable during initialize | Stored with no running service; initialize still succeeds |
| Backend unreachable during routed call | Call returns internal error; other backends unaffected |
| Gateway process restart | All session state lost; clients must re-run initialize |
| Request lands on wrong gateway node | List returns empty; routed calls fail — need sticky routing |
Plugins
| Failure | Behavior |
|---|---|
| Plugin denies call/response | Becomes MCP error to caller |
| Soft plugin error | Logged; call proceeds |
| Invalid plugin config on reload | Runtime marked failed; plugin calls return internal MCP error until valid config applied |
Config Store (Redis)
| Failure | Behavior |
|---|---|
| Redis connection loss | Connection manager retries (1,000 configured) |
| User config missing | 400 from user_config_store_layer |
Redis GET error | Reported as missing → 400 |
| Undecodable config / key encoding failure | 500 |
ContextForge 2.0 Target Architecture and Roadmap
Tentative target: this page records the proposed ContextForge 2.0 end state and delivery phases. It is not a description of the current Rust implementation. See Architecture and MCP Routing Semantics for current behavior.
This is a product-wide view because the ContextForge external dataplane
boundary depends on work owned by the ContextForge control plane and built-in
dataplane in the Python IBM/mcp-context-forge repository. It does not move
control-plane or built-in-dataplane responsibilities into this repository. See
Project terminology for the canonical component
names.
Vision and Constraints
- ContextForge supports MCP
2026-07-28and2025-11-25over Streamable HTTP on both the client-facing and backend-facing sides of the ContextForge external dataplane. - Same-version client/backend paths are supported directly. Cross-version
2026-07-28→2025-11-25and2025-11-25→2026-07-28adaptation is best effort. - All external-dataplane request/response handling is stateless for both versions. The target request path does not depend on an MCP session, session affinity, or a retained backend transport.
initializeremains supported for compatibility, but it is a stateless request: the external dataplane generates its response from effective configuration and does not use it to establish state required by later requests.- Legacy SSE transport is not part of the external-dataplane target.
- Fan-out and other one-to-many MCP work is limited to the control plane. The built-in and external dataplanes generate discovery, capability, and list responses from control-plane-authored effective configuration.
- Effective configuration flows one way from the control plane to the built-in and external dataplanes through externally shared state. A process-local cache may speed reads but is never the source of truth.
- MCP subscriptions and notifications remain Phase 4 work.
Stateless Protocol Compatibility
IBM/mcp-context-forge issue #6327
tracks the first targeted-operation slice for tools/call. The issue calls the
incoming/client-facing side “upstream” and the selected backend-facing side
“downstream”; this wiki uses the explicit names below.
| Incoming client | Selected backend | Target behavior |
|---|---|---|
2026-07-28 | 2026-07-28 | Supported directly as one stateless request. |
2026-07-28 | 2025-11-25 | Best-effort protocol adaptation within one stateless request. |
2025-11-25 | 2026-07-28 | Best-effort protocol adaptation within one stateless request. |
2025-11-25 | 2025-11-25 | Supported directly as one stateless request. |
For every row, the external dataplane authenticates and authorizes the request, reads
the principal-bound effective configuration, validates that the requested
object is visible and permitted, resolves exactly one backend, adapts the
protocol when necessary, and closes the request-scoped backend connection after
the response. A client may call initialize, but later operations neither
require nor reuse state created by it.
“Best effort” never permits hidden session state. If a semantic difference or backend requirement cannot be handled within the current request, the external dataplane returns an explicit error instead of creating affinity or retaining a backend transport for a later request.
For the initial tools/call slice, issue #6327 assumes that the selected
backend needs neither application authentication nor mTLS and that its server
certificate chains to the system CA. Those are issue-scope assumptions, not a
change to the external dataplane’s broader transport-security model.
Target End State
The front door separates management traffic from MCP traffic and chooses the built-in or external dataplane by deployment route and session model, not only by protocol version. The built-in dataplane can handle either supported version in stateful or stateless mode. The external dataplane can handle either supported version only in stateless mode. PostgreSQL remains the durable management store; the shared runtime store carries compiled configuration to both dataplanes.
flowchart TB
subgraph Clients[Traffic]
direction LR
AdminClient([Admin or User])
CompatClient([MCP 2025-11-25 Client])
ModernClient([MCP 2026-07-28 Client])
end
FrontDoor[Load Balancer and Router]
subgraph ContextForge[ContextForge 2.0]
direction LR
subgraph PythonRepo[IBM mcp-context-forge Python Repository]
direction TB
Control[ContextForge Control Plane]
Builtin[ContextForge Built-In Dataplane]
end
External[ContextForge External Dataplane - Rust]
end
Postgres[(PostgreSQL Management State)]
RuntimeStore[(Shared Effective Configuration)]
Upstreams[MCP 2026-07-28 and 2025-11-25 Servers]
AdminClient -->|Management API| FrontDoor
CompatClient -->|Streamable HTTP MCP 2025-11-25| FrontDoor
ModernClient -->|Streamable HTTP MCP 2026-07-28| FrontDoor
FrontDoor -->|Management routes| Control
FrontDoor -->|Stateful or built-in MCP routes| Builtin
FrontDoor -->|Stateless external MCP routes| External
Control -->|Persist administrative state| Postgres
Control -->|Publish effective configuration| RuntimeStore
RuntimeStore -->|Read shared configuration| Builtin
RuntimeStore -->|Read-only configuration| External
Control -->|Discover catalogs and poll liveness| Upstreams
Builtin -->|Stateful or stateless MCP calls| Upstreams
External -->|Stateless targeted MCP calls| Upstreams
Redis is the current external-dataplane configuration store and the preferred shared implementation. The built-in dataplane may consume the same compiled configuration from Redis or PostgreSQL. When multiple built-in-dataplane instances are deployed, stateful MCP behavior requires an explicit shared-state or affinity design; stateless behavior must not rely on process memory.
Component Responsibilities
| Component | Target responsibility |
|---|---|
| Front door | Route management APIs to the ContextForge control plane. Route MCP to the built-in dataplane when the built-in route or stateful behavior is required, and to the external dataplane when the configured stateless external route is selected. Protocol version alone does not identify the component. |
| ContextForge control plane | Manage the virtual-server lifecycle and upstream assignments; connect to heterogeneous upstreams; retrieve and page through capabilities, tools, resources, prompts, completions, and other catalogs; normalize and persist them; let administrators select exposed objects and rules; compile effective runtime configuration; poll upstream liveness and changes. |
| PostgreSQL | Persist administrative source data such as virtual servers, upstream definitions, normalized catalogs, selections, and policies. It is not on the external-dataplane request path. |
| Configuration synchronization | Publish effective configuration one way from the control plane to externally shared state. The built-in and external dataplanes should consume the same shape where practical. |
| ContextForge built-in dataplane | Handle 2026-07-28 and 2025-11-25 MCP requests in Python, including stateful and stateless behavior. It is the MCP request path shipped in the same repository as the control plane, not the control plane itself. |
| ContextForge external dataplane | Handle 2026-07-28 and 2025-11-25 Streamable HTTP requests statelessly in Rust. Read effective configuration, serve aggregate and initialize responses locally, and route a targeted method to exactly one selected backend. Cross-version adaptation is best effort. It does not own IAM, UI, management APIs, or durable metrics storage. |
| Backend MCP servers | May use 2026-07-28 or 2025-11-25, independently of the incoming client version. Connections and any required negotiation are request-scoped and leave no reusable session; the architecture does not require backend session affinity. |
Administrative State and Effective Configuration
The control plane owns two distinct forms of state:
| State | Contents | Owner and consumers |
|---|---|---|
| Administrative source state | Virtual servers, upstream registrations, raw and normalized catalogs, exposure selections, policies, and liveness. | Written by the control plane to PostgreSQL; used by management workflows and reconciliation. |
| Effective runtime configuration | Effective server identity and capabilities, visible tools/resources/prompts/completions, downstream paging material, backend resolution, required scopes/roles, and applicable runtime policy for a tenant or isolation domain, user, team, or other principal. | Compiled and published by the control plane; read by the built-in and external dataplanes. |
The control plane must exhaust upstream pagination while reconciling catalogs. The compiled snapshot must contain enough information for either the built-in or external dataplane to produce downstream paging without contacting every upstream. Publication must be atomic or revisioned so neither the built-in nor external dataplane combines partial catalog and policy state.
Target Authorization Invariants
The effective-configuration model requires identity isolation as well as catalog precomputation. A cached snapshot is data, not an authorization grant. Every downstream request must independently establish and enforce its trusted authorization context.
- The external dataplane derives the authorization key only from verified JWT claims and the validated server route. MCP params and client metadata must not supply or override a principal, team, tenant, virtual server, backend, or cache key.
- Snapshot and cache partitions include the applicable trust or tenant
boundary, authenticated
sub, effective team or other principal, virtual server, and configuration revision. Entries must never be reused across authorization contexts. - The control plane maps verified identity attributes to an effective principal and compiles its visible objects and RBAC policy. The built-in and external dataplanes enforce required token scopes or roles and the compiled policy on every discovery, list, and targeted operation.
- Missing, unmapped, ambiguous, expired, or unauthorized snapshots and objects are denied by default. A targeted denial makes no upstream call, and errors must not disclose another principal’s catalog or backend mapping.
- The exact tenant/team claim mapping and token-scope-to-RBAC rules are a
cross-repository contract that the control plane, publisher, schemas,
external dataplane, and integration tests must define together. The current
coarse
sub-only implementation is not the Phase 3 target.
MCP Work Allocation
| Work | Target owner and behavior |
|---|---|
| Virtual-server creation and upstream assignment | Control plane persists management state and connects to assigned upstreams. |
| Upstream discovery, initialization where required, catalog pagination, capability aggregation, filtering, and liveness polling | Control plane only; this is the intentional fan-out boundary. |
server/discover, initialize, and effective capabilities | After per-request authorization, the built-in or external dataplane generates the response from principal-bound effective configuration. The built-in dataplane may support a stateful flow; the external dataplane treats initialize as stateless compatibility and creates no state required by later requests. |
tools/list, resources/list, prompts/list, resource-template listing, and similar aggregate methods | After method-scope and compiled-RBAC enforcement, the built-in or external dataplane generates the visible response from principal-bound effective configuration with no live upstream fan-out. |
tools/call, resources/read, prompts/get, completion, and similar targeted methods | The built-in or external dataplane resolves the effective entry under the trusted authorization key, applies default-deny scope and object policy, and calls exactly one selected backend only when authorized. The external dataplane adapts protocol versions when necessary and leaves no reusable session; the built-in dataplane may use its stateful or stateless execution model. |
| Plugins for trusted aggregate responses | Prefer policy compiled by the control plane; avoid mandatory per-request plugin calls for a response already produced from trusted effective configuration. |
| Plugins for targeted calls | May run on the external-dataplane request path when request or response inspection is required. Exact hook allocation remains an implementation decision. |
| Subscriptions, server notifications, and downstream list-change notifications | Deferred to Phase 4 because their state and delivery model do not fit the request/response simplification. |
Delivery Roadmap
| Phase | Scope |
|---|---|
| 1. Separate control-plane and built-in-dataplane responsibilities | Establish a clear boundary between the ContextForge control plane and built-in dataplane inside the Python repository. The control plane writes effective configuration per user, team, or other principal to shared state; the built-in dataplane reads it and handles MCP requests. |
| 2. Route targeted calls through the external dataplane | Make the built-in and external dataplanes follow the same configuration-driven contract. Send selected targeted operations such as tools/call, resources/read, prompts/get, and completion to the external dataplane. For each operation, support both same-version 2026-07-28/2025-11-25 paths and attempt both cross-version paths on a best-effort basis, always without reusable session state. The tools/call slice is tracked by #6327. |
| 3. Route all stateless request/response MCP methods through the external dataplane | Serve discovery, stateless initialize, capabilities, aggregate lists, and targeted calls for both supported protocol versions from the external dataplane. Aggregate responses come from effective configuration; targeted calls reach exactly one backend. The built-in dataplane continues to support both stateful and stateless behavior. |
| 4. Implement subscriptions and notifications | Add the state, routing, and delivery model for upstream subscriptions, resource notifications, and list-change notifications after the request/response architecture is complete. |
Phase 3 Reference Flows
The examples below use tools, but the same ownership applies to resources,
prompts, completions, and other aggregate or targeted request/response methods.
“Supported MCP client” and “supported MCP server” mean either 2026-07-28 or
2025-11-25; when the two sides differ, adaptation is best effort.
1. Create a Virtual Server and Select Capabilities
sequenceDiagram
autonumber
actor User
participant UI as Admin UI or API
participant CP as ContextForge Control Plane
participant DB as Control Plane DB
participant MCP1 as MCP 2026-07-28 Server
participant MCP2 as MCP 2025-11-25 Server
participant Store as Shared Config Store (Redis)
participant DP as ContextForge External Dataplane
User->>UI: Create virtual server
UI->>CP: Submit virtual server
CP->>DB: Store virtual server
User->>UI: Assign MCP Server 1 and MCP Server 2
UI->>CP: Update backend associations
CP->>DB: Store backend associations
par Inspect 2026-07-28 backend
CP->>MCP1: Discover capabilities and retrieve catalogs
MCP1-->>CP: Capabilities and catalog
and Inspect 2025-11-25 backend
CP->>MCP2: Initialize or discover and retrieve catalogs
MCP2-->>CP: Capabilities and catalog
end
CP->>DB: Reconcile normalized catalog
User->>UI: View available catalog entries
UI->>CP: Request reconciled catalog
CP->>DB: Read catalog
DB-->>CP: inc, sum, dec, diff
CP-->>UI: Display available catalog entries
User->>UI: Allow inc and sum
UI->>CP: Update virtual server policy
CP->>DB: Store selected tools and policy
CP->>CP: Compile snapshot by tenant, principal and vhost
CP->>Store: Atomically publish revision N
Store-->>DP: Configuration revision available
DP->>Store: Load revision N
DP->>DP: Replace local cache atomically
Note over CP,MCP2: Control Plane handles upstream protocol and pagination
Note over CP,DP: Effective configuration flows one way from CP to DP
Note over CP,DP: Snapshot carries compiled scopes, RBAC and visible objects
2. Initialize or Discover the Server and List Tools
sequenceDiagram
autonumber
participant Client as Supported MCP Client
participant Ingress
participant DP as ContextForge External Dataplane
participant Cache as Local Cache
participant Store as Shared Config Store (Redis)
Client->>Ingress: initialize or server/discover
Ingress->>DP: Forward supported MCP request
DP->>DP: Verify JWT, metadata and server route
DP->>DP: Derive authorization key from trusted context
DP->>Cache: Get snapshot by authorization key
alt Snapshot available
Cache-->>DP: Snapshot revision N
else Snapshot missing or expired
Cache-->>DP: Cache miss
DP->>Store: Read by authorization key
Store-->>DP: Snapshot revision N or not found
opt Authorized snapshot returned
DP->>Cache: Store under authorization key
end
end
DP->>DP: Enforce discovery scope and compiled RBAC
alt Snapshot mapped and authorized
DP-->>Client: Version-appropriate identity and visible capabilities
else Missing, unmapped or denied
DP-->>Client: Authorization error without catalog details
end
Client->>Ingress: tools/list as independent request
Ingress->>DP: Forward supported MCP request
DP->>DP: Reverify and derive authorization key
DP->>DP: Enforce tools/list scope and compiled RBAC
alt Snapshot mapped and authorized
DP->>Cache: Read visible tools by authorization key
Cache-->>DP: inc and sum
DP-->>Client: tools/list result
else Missing, unmapped or denied
DP-->>Client: Authorization error without catalog details
end
Note over DP,Store: The shared store distributes compiled state
Note over DP: No live upstream call for discovery or aggregate lists
Note over Client,DP: initialize does not create required session state
Note over Client,DP: Client-supplied identity or routing metadata is untrusted
3. Call a Tool
sequenceDiagram
autonumber
participant Client as Supported MCP Client
participant Ingress
participant DP as ContextForge External Dataplane
participant Cache as Local Cache
participant CPEX as Policy and CPEX
participant MCP as Selected Supported MCP Server
Client->>Ingress: tools/call name inc
Ingress->>DP: Forward supported MCP request
DP->>DP: Verify JWT, metadata and server route
DP->>DP: Derive authorization key from trusted context
DP->>Cache: Resolve inc under authorization key
Cache-->>DP: Backend mapping, protocol version and policy or missing
DP->>DP: Enforce tools/call scope and compiled RBAC
alt Tool mapped and authorized
DP->>CPEX: Run pre-call policy
CPEX-->>DP: Allow or modify request
DP->>DP: Adapt client protocol to backend protocol
opt Backend negotiation is required
DP->>MCP: Request-scoped initialize
MCP-->>DP: Initialize result
end
DP->>MCP: tools/call name inc
MCP-->>DP: Tool result
DP->>MCP: Close request-scoped connection
DP->>CPEX: Run post-call policy
CPEX-->>DP: Allow or modify result
DP-->>Client: Return version-appropriate tool result
else Missing, unmapped or denied
DP-->>Client: Authorization error with no upstream call
end
Note over DP,MCP: Exactly one backend is called
Note over DP,MCP: Client and backend versions are independently 2026-07-28 or 2025-11-25
Note over DP,MCP: No durable backend MCP session is required
Note over DP: Control Plane, DB and Redis are not on this result path
Note over Client,DP: Client-supplied identity or backend selection is untrusted
4. Reconcile an Upstream Catalog Change
sequenceDiagram
autonumber
participant MCP as Supported MCP Server
participant CP as Control Plane Reconciler
participant DB as Control Plane DB
participant Store as Shared Config Store (Redis)
participant DP as ContextForge External Dataplane
participant Client as Supported MCP Client
CP->>MCP: Poll liveness and refresh discovery and lists
MCP-->>CP: Updated catalog
CP->>DB: Reconcile catalog changes
CP->>CP: Recompile affected snapshots
CP->>Store: Atomically publish revision N plus 1
Store-->>DP: Configuration revision available
DP->>Store: Load revision N plus 1
DP->>DP: Replace local cache atomically
Client->>DP: tools/list
DP-->>Client: Updated list from local snapshot
Note over DP,Client: Phase 4 owns MCP list-change notifications
Configuration Reference
Minimum Required Flags
--redis-address --redis-port --redis-mode
Plus at least: --address or --tls-address, --token-verification-public-key or --token-verification-secret.
Complete CLI and Environment Reference
The binary parses both CLI flags and environment variables with clap; a CLI
flag wins when both forms are supplied. Use the binary for the always-current
generated reference:
cargo run -p contextforge-data-plane --bin contextforge-data-plane -- --help
Most environment variables use the CONTEXTFORGE_DATA_PLANE_ prefix. The MCP
Origin and Host settings retain the explicitly configured
CONTEXTFORGE_GATEWAY_RS_ names shown below.
Listeners and JWT
| Flag | Environment variable | Default / requirement | Purpose |
|---|---|---|---|
--address <host:port> | CONTEXTFORGE_DATA_PLANE_ADDRESS | Optional | Plain HTTP listener. |
--tls-address <host:port> | CONTEXTFORGE_DATA_PLANE_TLS_ADDRESS | Optional | TLS listener; requires server certificate and key. |
--server-certificate <path> | CONTEXTFORGE_DATA_PLANE_TLS_SERVER_CERTIFICATE | With --tls-address | PEM certificate chain for downstream TLS. |
--server-private-key <path> | CONTEXTFORGE_DATA_PLANE_TLS_SERVER_PRIVATE_KEY | With --tls-address | PEM private key for downstream TLS. |
--token-verification-public-key <path> | CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PUBLIC_KEY | For RSA tokens | Verifies RS256, RS384, and RS512 tokens. |
--token-verification-secret <secret> | CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET | For HMAC tokens | Verifies HS256, HS384, and HS512 tokens. |
--token-verification-private-key <path> | CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY | Required when built with with_tools | Signs tokens for the optional local bootstrap helper. |
MCP request validation
| Flag | Environment variable | Default | Purpose |
|---|---|---|---|
--mcp-allowed-origins <origin,...> | CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_ORIGINS | None | Browser Origin allowlist. Without it, requests lacking Origin pass and every request carrying Origin receives HTTP 403. |
--mcp-allowed-hosts <authority,...> | CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS | None | Optional RMCP request-authority allowlist. For requests that reach the RMCP service, missing or malformed authorities receive HTTP 400; unlisted authorities receive HTTP 403. Earlier middleware may return first. |
--mcp-standard-header-max-count <n> | CONTEXTFORGE_DATA_PLANE_MCP_STANDARD_HEADER_MAX_COUNT | 32 | Maximum MCP standard headers accepted on one request. |
--mcp-standard-header-max-value-bytes <n> | CONTEXTFORGE_DATA_PLANE_MCP_STANDARD_HEADER_MAX_VALUE_BYTES | 8192 | Maximum byte length accepted for one MCP standard header value. |
--mcp-standard-header-max-total-bytes <n> | CONTEXTFORGE_DATA_PLANE_MCP_STANDARD_HEADER_MAX_TOTAL_BYTES | 65536 | Approximate request-level aggregate bytes across all matched MCP standard header names and values. |
Values are comma-separated. Origin entries must be fully qualified serialized
origins such as https://app.example.com; Host entries are authorities such as
gateway.example.com or gateway.example.com:8443. See Security.
The MCP standard header limits apply to Mcp-Method, Mcp-Name,
Mcp-Protocol-Version, and Mcp-Param-*. The same guardrail also covers the
legacy/RMCP transport header Mcp-Session-Id. A configured value of 0 is
treated as the documented default. The byte totals are application-level
aggregate budgets based on all matched header name and value lengths on one
request; they do not allow a single oversized value, which is still capped by
--mcp-standard-header-max-value-bytes. They are not exact wire-size accounting
and do not model HTTP/2 header compression. Non-MCP headers remain bounded by
the HTTP transport.
Redis
| Flag | Environment variable | Default / requirement | Purpose |
|---|---|---|---|
--redis-address <host> | CONTEXTFORGE_DATA_PLANE_REDIS_HOSTNAME | Required | Redis host name or IP. |
--redis-port <port> | CONTEXTFORGE_DATA_PLANE_REDIS_PORT | Required | Redis port. |
--redis-mode <mode> | CONTEXTFORGE_DATA_PLANE_REDIS_CONNECTION_MODE | Required | plain-text, tls, or mtls. |
--redis-tls-trust-bundle <path> | CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_TRUST_BUNDLE | TLS and mTLS | PEM trust bundle. |
--redis-tls-client-certificate <path> | CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_CLIENT_CERTIFICATE | mTLS | PEM client certificate. |
--redis-tls-client-private-key <path> | CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_CLIENT_PRIVATE_KEY | mTLS | PEM client private key. |
--user-config-cache-expiry-seconds <n> | CONTEXTFORGE_DATA_PLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS | 60 | In-process cache expiry; 0 reads Redis on every request. |
Upstream connections
| Flag | Environment variable | Default / requirement | Purpose |
|---|---|---|---|
--upstream-connection-mode <mode> | CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE | tls-only | Permits HTTPS only, HTTP and HTTPS, or an mTLS mode. |
--upstream-trust-bundle <path> | CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_TRUST_BUNDLE | Optional | Additional PEM trust bundle for HTTPS backends. |
--upstream-certificate <path> | CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_CERTIFICATE | mTLS modes | PEM client certificate. |
--upstream-private-key <path> | CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_PRIVATE_KEY | mTLS modes | PEM client private key. |
Runtime and plugins
| Flag | Environment variable | Default | Purpose |
|---|---|---|---|
--number-of-cpus <n> | CONTEXTFORGE_DATA_PLANE_NUMBER_OF_CPUS | Host CPU count | Tokio worker/runtime thread count. |
--single-runtime <bool> | CONTEXTFORGE_DATA_PLANE_SINGLE_RUNTIME | true | false creates per-CPU runtimes without session affinity. |
--runtime-plugins-enabled <bool> | CONTEXTFORGE_DATA_PLANE_RUNTIME_PLUGINS_ENABLED | false | Enables compiled-in CPEX hooks and Redis plugin config loading. |
Telemetry and logging
| Flag | Environment variable | Default | Purpose |
|---|---|---|---|
--enable-open-telemetry <bool> | CONTEXTFORGE_DATA_PLANE_ENABLE_OPEN_TELEMETRY | false | Enables OTLP trace export. |
--enable-otel-metrics <bool> | CONTEXTFORGE_DATA_PLANE_ENABLE_OTEL_METRICS | false | Enables OTLP HTTP-server metric export. |
--otlp-protocol <protocol> | CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_PROTOCOL | grpc | grpc or http-protobuf. |
--otlp-endpoint <uri> | CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_ENDPOINT | Protocol-specific | Trace endpoint; defaults to http://127.0.0.1:4317 for gRPC or http://127.0.0.1:4318/v1/traces for HTTP. |
--otlp-metrics-endpoint <uri> | CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | Protocol-specific | Metrics endpoint; defaults to http://127.0.0.1:4317 for gRPC or http://127.0.0.1:4318/v1/metrics for HTTP. |
--otlp-headers <headers> | CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_HEADERS | None | Comma-separated key=value exporter headers. |
--otlp-service-name <name> | CONTEXTFORGE_DATA_PLANE_OTEL_SERVICE_NAME | CONTEXTFORGE-DATA-PLANE | OpenTelemetry service.name. |
--log-name <name> | CONTEXTFORGE_DATA_PLANE_LOG_NAME | contextforge-data-plane.log | File log name in the current directory. |
--log-rotation <mode> | CONTEXTFORGE_DATA_PLANE_LOG_ROTATION | hourly | minutely, hourly, daily, or never. |
JWT Claims (validated by claims_layer)
| Claim | Required value |
|---|---|
iss | mcpgateway |
aud | mcpgateway-api |
exp | present, not expired |
sub | → selects Redis user config key |
Optional: token_use, iat, teams, scopes, user.full_name.
No revocation: a leaked token is valid until
exp. Rotate the signing key and restart to invalidate all outstanding tokens.
UserConfig Shape (from contextforge-data-plane-apis)
UserConfig
virtual_hosts: HashMap<String, VirtualHost>
VirtualHost
backends: HashMap<String, BackendMCPGateway> ← map key = routing prefix
BackendMCPGateway
name: String
url: Url
passthrough_headers: Vec<String> ← snapshotted at initialize; session-scoped
add_headers: HashMap<String, String> ← injected after passthrough
remove_headers: Vec<String> ← stripped after add
allowed_tool_names: Vec<String> ← model exists, NOT currently enforced
tool_schemas: HashMap<String, JsonObject> ← optional, defaults to {}; upstream name → input schema
tool_name_aliases: HashMap<String, String> ← downstream_alias → upstream_original
allowed_resource_names: Vec<String> ← model exists, NOT currently enforced
allowed_prompt_names: Vec<String> ← model exists, NOT currently enforced
tool_schemas lets the dataplane recognize and validate x-mcp-header
annotations without calling backend tools/list. The control plane may omit the
field or individual unannotated tools. Without a published schema, parameter
headers are forwarded as unrecognized intermediary headers and are not locally
validated. A published annotation must name a non-empty, case-insensitively
unique HTTP token on a string, integer, or boolean property reachable from
the schema root through properties keys only. Nested properties use their
exact property path. For a recognized annotation, a non-null argument requires
an equal header; an absent or null argument requires the header to be absent.
Integer values are limited to the IEEE 754 safe range.
Header apply order: passthrough_headers → add_headers (override passthrough) → remove_headers (applied last).
passthrough_headers is session-scoped. Values are snapshotted from the initialize request and baked into the backend transport for the session lifetime. Post-initialize calls (tool calls, list calls) reuse those headers. Request-scoped propagation requires per-request transport reconstruction (future work).
Protected headers — silently skipped in all three phases (passthrough/add/remove):
| Category | Headers |
|---|---|
| Body-framing | Content-Length, Content-Type |
| Hop-by-hop | Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, TE, Trailer, Trailers, Transfer-Encoding, Upgrade |
| RMCP-reserved | Mcp-Session-Id, Accept, Last-Event-Id |
| Gateway-managed | Host (set from backend URL host + port; never overridden by config) |
| MCP standard | Mcp-Method, Mcp-Name, Mcp-Protocol-Version, Mcp-Param-* |
Authorization and Cookie are not protected here because backend
authentication through passthrough_headers or add_headers is intentional
runtime configuration.
Redis storage: MessagePack(User::new(sub)) → MessagePack(UserConfig).
Two schemas are generated — both must be regenerated and committed when UserConfig, VirtualHost, BackendMCPGateway, or the User key type changes:
| Schema file | Covers |
|---|---|
schemas/user_config.json | UserConfig routing document written to Redis. |
schemas/user.json | User key type used as the Redis key. |
cargo run -p contextforge-data-plane-apis
Plugin Config (Redis key: ContextForgeGatewayRuntimePluginConfig)
RuntimePluginConfigDocument
version: 1
cpex: CpexConfig
Supported: tool, prompt, and resource pre/post CMF hooks.
Rejected: routing-based selection, plugin dirs, global policies, LLM hooks, plugin conditions.
Config validation and CmfPluginFactory registration must agree on that list: a hook accepted by validation but not registered leaves the plugin loaded and silently inert.
Reload watcher: 10-minute interval. Invalid reload → runtime marked failed.
Tool Call Hook Behavior
For call_tool, the pre hook runs after backend routing has selected the backend and stripped the public prefix. The hook sees the backend name, routed tool name, and arguments. It can leave arguments unchanged, replace arguments, or deny the call.
After the upstream backend returns, the post hook can leave the result unchanged, rewrite the result payload, or deny the response. Hook state is carried across the upstream call so pre and post hooks can share CPEX context for the same logical tool call.
Plugin execution must not poison shared gateway state. A plugin denial becomes an MCP error. Soft plugin errors are logged. Unsupported plugin configuration fails validation before the runtime is accepted.
Prompt Fetch Hook Behavior
For get_prompt, the pre hook runs after backend routing, so the plugin sees the backend-local prompt name and the owning backend separately rather than the gateway-prefixed identifier. It can leave the arguments unchanged, replace them, or deny the fetch before the backend renders anything.
The post hook receives the rendered prompt as one CMF message per rendered MCP message, each carrying its role and its content block: text, image, audio, embedded resource, or resource link. A plugin can inspect or rewrite any of them, so a policy can act on a file interpolated into a prompt rather than only on the surrounding text.
Writing plugin edits back follows three rules:
- A message the plugin left unchanged is returned exactly as the backend sent it, so annotations,
_meta, and binary resource blobs survive untouched. - A message the plugin changed is rebuilt from CMF. CMF does not model MCP annotations or
_meta, so an edited message loses them. - Edits that cannot be applied faithfully fail the call rather than falling back to the backend’s original. A changed message count, anything other than exactly one prompt result in the payload, a role MCP prompts cannot express, or a resource whose text the plugin removed all return an error. Silently restoring the backend’s content would undo a redaction.
MCP prompt results carry no error flag, so a plugin setting is_error on the CMF prompt result is rejecting the prompt rather than describing it. The gateway turns that into an MCP error carrying the plugin’s error_message, and the rendered content never reaches the client. This differs from tools, where is_error is a field on CallToolResult and is forwarded as a successful response.
Binary resources embedded in prompts reach plugins by URI and MIME type but not by content. A plugin can deny such a message; editing one fails the write-back. Resource-read hooks below have their own binary conversion.
Resource Read Hook Behavior
For resources/read, the pre hook receives the canonical backend-local URI and may allow, deny or rewrite it. A rewritten URI must resolve unambiguously through the caller’s published virtual-host resources before a backend connection is opened. Aliases for the same backend target do not create ambiguity.
The post hook may replace each returned resource’s text or binary content, URI and MIME type, including converting text to a blob or a blob to text. Existing MCP _meta is preserved. CMF-only envelope and descriptive fields do not restrict these changes. Each resource still needs a valid MCP content representation; binary resource reads are decoded for CPEX and re-encoded after edits, while unchanged blob bytes retain their original wire value. This resource path does not add prompt-wide payload validation.
The pre call returns an opaque, concrete ResourceHookState consumed by the post call. It captures both the runtime and the decision to run or skip post hooks before backend I/O. A reload only affects subsequent requests, including when it enables or disables resource hooks. Callers cannot construct missing or mismatched active state, and requests without a post hook allocate no correlation state.
Demo Plugin Workflow
The optional test-plugins feature compiles demo factories from the cpex-plugins-rs repository. Redis configuration activates factories already present in the binary; it never loads new Rust code into a running process.
Start lightweight dependencies:
docker compose -f docker/docker-compose-local.yaml up -d redis gateway-one gateway-two
Register payload-marker configuration before starting the ContextForge external dataplane:
docker compose -f docker/docker-compose-local.yaml exec -T redis \
redis-cli SET ContextForgeGatewayRuntimePluginConfig '{
"version": 1,
"cpex": {
"plugins": [
{
"name": "payload-marker",
"kind": "contextforge/payload-marker",
"hooks": ["cmf.tool_post_invoke"]
}
]
}
}'
Build and run with demo factories and runtime execution enabled:
cargo run -p contextforge-data-plane \
--features 'contextforge-data-plane-lib/with_tools,test-plugins' \
--bin contextforge-data-plane -- \
--address 127.0.0.1:8001 \
--redis-address 127.0.0.1 \
--redis-port 6379 \
--redis-mode plain-text \
--token-verification-public-key assets/jwt.key.pub \
--token-verification-private-key assets/jwt.key \
--upstream-connection-mode plain-text-or-tls \
--runtime-plugins-enabled true
Startup should log successful CPEX initialization. The payload marker appends [cpex:payload-marker] to successful tool results. The hook path is also covered by:
cargo nextest run --locked -p contextforge-data-plane-lib --test gateway_plugins
Startup Validation (fails fast)
| Invalid combo | Reason |
|---|---|
--tls-address without cert or key | Rustls needs both |
Same address for --address and --tls-address | Cannot bind same socket twice |
--redis-mode tls without trust bundle | Required |
--redis-mode mtls without trust bundle + client cert + key | All three required |
| mTLS upstream without cert and key | reqwest identity cannot be built |
| HTTP backend URL with default upstream mode (HTTPS-only) | Calls fail before reaching backend |
Upstream Connection Modes
| Mode | Behavior |
|---|---|
omitted / tls-only | HTTPS backends only (safe default) |
plain-text-or-tls | HTTP or HTTPS (use for local Compose backends) |
plain-text-or-m-tls | HTTP or HTTPS + client identity |
mtls-only | HTTPS + client cert/key required |
Logging Env Vars
| Var | Default | Controls |
|---|---|---|
RUST_LOG | debug | Console filter |
RUST_FILE_LOG | debug | File filter |
RUST_TRACE_LOG | info | OTLP span filter (debug for local trace verification) |
Telemetry Debugging Notes
RUST_TRACE_LOG=debugis required for trace export. The default (info) drops HTTP spans before they reach the OTLP exporter — nothing arrives at the trace backend.
Metrics are pushed by a PeriodicReader every 30 seconds. Allow ~35s after the first request before data appears downstream.
Stable log prefixes for grepping (use these to scope log searches by boundary):
| Prefix | Boundary |
|---|---|
claims_layer | JWT validation failures |
user_config_store_layer | Config lookup / Redis errors |
virtual_host_config_layer | Unknown virtual host |
AuthorizedCallValidator::validate | Post-session MCP validation |
initialize: | Backend session creation |
call_tool | Tool routing and backend invocation |
Debugging by symptom:
| Symptom | Where to look |
|---|---|
401 | claims_layer logs: missing/invalid token, unsupported algorithm, no decoder key |
400 config error | user_config_store_layer logs + Redis content for the JWT subject |
404 Server not found | virtual_host_config_layer debug: requested vhost id vs caller’s config |
| MCP routing errors | AuthorizedCallValidator::validate debug, then call_tool/read_resource/get_prompt warns |
| Backend failures | initialize: warns for failed backends; routed-call warns name the failing backend |
| Plugin problems | CPEX pipeline error logs; invalid reload marks runtime failed |
Local Telemetry Verification Stack
A complete local observability pipeline ships under docker/ as overlays:
| Component | Role | Endpoint |
|---|---|---|
| Langfuse | Trace backend and span viewer. | http://localhost:3100, login admin@example.com / changeme, project ContextForge Data Plane. |
| OTel Collector | Receives OTLP from the gateway; fans traces and metrics out. | OTLP/HTTP on :4318, Prometheus exposition on :8889. |
| Prometheus | Scrapes the collector for browsable PromQL. | http://localhost:9090. |
flowchart LR
GW["Gateway\n(contextforge-data-plane)"]
subgraph Local["Local Observability Stack (docker/)"]
COL["OTel Collector\nOTLP/HTTP :4318\nPrometheus :8889"]
LF["Langfuse\n:3100\nspan viewer + trace backend"]
PR["Prometheus\n:9090\nPromQL browser"]
end
GW -->|"OTLP/HTTP traces\n(RUST_TRACE_LOG=debug required)"| COL
GW -->|"OTLP/HTTP metrics\n(PeriodicReader every 30s)"| COL
COL -->|"fan-out traces"| LF
COL -->|"scrape target :8889"| PR
OP(["operator"]) -->|"PromQL queries"| PR
OP -->|"span viewer\nlogin: admin@example.com"| LF
Debugging by symptom:
flowchart TD
SYM["Symptom"] --> S401["401 Unauthorized"]
SYM --> S400["400 config error"]
SYM --> S404["404 Server not found"]
SYM --> SMCP["MCP routing error"]
SYM --> SBACK["Backend failure"]
SYM --> SPLUG["Plugin problem"]
S401 --> L401["grep: claims_layer\nmissing/invalid token\nbad algorithm / no decoder key"]
S400 --> L400["grep: user_config_store_layer\n+ Redis content for JWT subject"]
S404 --> L404["grep: virtual_host_config_layer\nrequested vhost vs caller config"]
SMCP --> LMCP["grep: AuthorizedCallValidator::validate\nthen call_tool / read_resource / get_prompt warns"]
SBACK --> LBACK["grep: initialize: warns\nrouted-call warns name failing backend"]
SPLUG --> LPLUG["CPEX pipeline error logs\ninvalid reload marks runtime failed"]
Start:
docker compose \
-f docker/docker-compose-local.yaml \
-f docker/docker-compose-langfuse.yaml \
-f docker/docker-compose-otel-collector.yaml \
up -d
Run the gateway with export enabled (RUST_TRACE_LOG=debug required for trace export):
RUST_TRACE_LOG=debug \
cargo run --release --bin contextforge-data-plane -- \
--address 0.0.0.0:8001 \
--redis-port 6379 --redis-address 127.0.0.1 --redis-mode=plain-text \
--token-verification-public-key assets/jwt.key.pub \
--number-of-cpus 4 \
--upstream-connection-mode=plain-text-or-tls \
--enable-open-telemetry true \
--enable-otel-metrics true \
--otlp-protocol http-protobuf \
--otlp-endpoint http://127.0.0.1:3100/api/public/otel/v1/traces \
--otlp-metrics-endpoint http://127.0.0.1:4318/v1/metrics \
--otlp-service-name contextforge-data-plane
Prometheus Starter Queries
| Question | Query |
|---|---|
| Request count by method, status, service | http_server_request_duration_seconds_count |
| p95 latency | histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket[1m]))) |
| In-flight requests | http_server_active_requests |
| Payload throughput | http_server_request_body_size_bytes_sum / http_server_response_body_size_bytes_sum |
Known Telemetry Gaps
Tracked upstream, not yet implemented in the ContextForge external dataplane:
| Gap | Issue |
|---|---|
| W3C trace-context propagation across gateway hops | mcp-context-forge#4723 |
| MCP-semantic spans with tool names and JSON-RPC method attributes | mcp-context-forge#4722 |
Deployment
This page describes current deployment requirements, including session affinity. The tentative target removes live aggregate fan-out and durable upstream-session dependence; see ContextForge 2.0 Target Architecture and Roadmap.
Checklist
- Front door routes only
/contextforge-rsto the ContextForge external dataplane. - JWT verification key/secret matches the control plane’s signing material; clients use control-plane API tokens whose
submatches the published user-config key. - Redis reachable; TLS/mTLS across trust zones; write access restricted to the control plane;
DATAPLANE_PUBLISHER=trueon the control plane. - Upstream connection mode matches backend URL schemes.
- One replica per
Mcp-session-id(single replica or sticky routing). with_toolsfeature disabled in the production build.- Telemetry export pointed at the collector.
- System limits raised:
nofile 65535, TCP tuning (tcp_fin_timeout=15, widened local port range).
Health Endpoint
/contextforge-rs/health is a with_tools bootstrap helper only. Production builds compile it out. Use TCP-level liveness checks or the exported metrics until a real health endpoint exists.
nginx Front-Door Routing
Reference docker/nginx.conf split:
location ^~ /contextforge-rs→ proxies to the ContextForge external dataplane.- UI and management traffic → ContextForge control plane.
- Other MCP routes, including stateful and legacy/SSE compatibility routes → ContextForge built-in dataplane.
- Upstream retries on
error timeout http_502/503/504: 2 tries, 10-second window. Non-idempotent MCPPOSTbodies are not re-sent after they reached an upstream — only connection-stage failures retry.
Session Affinity And Failover
Backend MCP sessions are local process state — see routing.md.
-
1 replica requires sticky routing by
Mcp-session-id. The reference nginx config does not provide this; safe shapes today are a single replica or a front door with stickiness. - On restart or failover, all sessions are lost. Design clients to treat session-not-found as “reinitialize”, not “retry”.
Redis Availability
- Redis is required at startup and on every uncached config lookup.
- Connection manager retries 1,000 times (rather than failing fast).
- In-process cache (default 60s) rides out short Redis blips for warm subjects.
- A cold subject during a Redis outage fails at
user_config_store_layer→400until Redis returns.
Images
- CI builds
docker/Dockerfileon every push tomainand publishes bothghcr.io/<owner>/contextforge-data-plane:v<version>andghcr.io/<owner>/contextforge-data-plane:latest, where<version>is the Cargo package version. - Pin the
v-prefixed tag for reproducible deployments.latesttracksmain. - Builder:
rust:1.96.1indocker/Dockerfile. - The reference Compose stack runs the gateway with raised limits worth copying to real deployments:
nofile 65535and TCP tuning (tcp_fin_timeout=15, widened local port range).
TLS Choices
| Leg | Options |
|---|---|
| Front door to gateway | Plain HTTP on a trusted private network (common shape behind nginx), or terminate TLS at the gateway with --tls-address plus certificate and key. Both listeners can run at once on different sockets. |
| Gateway to Redis | --redis-mode plain, TLS, or mTLS. Use TLS/mTLS across trust zones — Redis is the config trust boundary. |
| Gateway to backends | HTTPS-only by default; opt into plain HTTP or mTLS with --upstream-connection-mode. |
Config Propagation Delay
worst-case staleness = publisher interval + user-config cache expiry
Both default to ~60s. For functional tests, shorten the publisher interval and disable the cache. For throughput benchmarks, keep both at 60s.
Security Posture
| Concern | Current state |
|---|---|
| JWT revocation | None. A leaked token is valid until exp. Rotate the key and restart to invalidate. |
| CORS / Origin | CORS response headers are permissive. mcp_origin_layer validates Origin before authentication, and RMCP validates Host at the MCP service boundary. Configure both --mcp-allowed-hosts and --mcp-allowed-origins for production. |
| Local bootstrap routes | /contextforge-rs/admin/tokens/{user}, /admin/userconfigs/{user}, /health are outside auth middleware — unauthenticated by design. Only exist with with_tools. Production builds must not enable with_tools. |
| Redis trust | Whoever can write Redis controls routing (arbitrary backend URLs receive caller traffic) AND which registered plugin hooks execute on payloads. Protect with TLS/mTLS and restrict write access to the control plane. |
| Downstream TLS | Optional. Plain HTTP is acceptable only behind a trusted front door on a private network. Identity is always the bearer JWT, not mTLS. |
| Plugin code | Fully trusted, in-process. Redis config activates compiled-in factories only — it cannot inject new Rust code. |
Performance And Load Testing
Full-Stack Load (Locust via cf-integration)
Performance testing uses the control-plane Locust suite through
cf-integration.
It measures the nginx → external dataplane → backend request path while the
ContextForge control plane publishes configuration.
| Command | What it runs |
|---|---|
scripts/cf-integration.sh smoke | 1 user for 10 s — quick sanity pass. |
scripts/cf-integration.sh locust | Full load run, default 100 users for 5 minutes. |
Tune with environment variables:
LOCUST_USERS=20 LOCUST_SPAWN_RATE=5 LOCUST_RUN_TIME=2m \
scripts/cf-integration.sh locust
MCP_VIRTUAL_SERVER_ID— target a UI-created virtual server instead of the auto-registered Fast Time one.MCP_TOOL_NAMES— pick the tools to call.- Output:
.integration/mcp-context-forge/reports/(HTML and CSV).
Headless vs Web UI
The harness runs Locust headless by default (LOCUST_MODE=headless). Set LOCUST_MODE=web to switch to interactive mode (master + web UI on port 8089). The one-off locust command does not publish container ports; for the web UI, start via the stack’s testing Compose profile which maps 8089:8089.
Benchmark Settings
Restore both to 60 before measuring throughput — fast publish + per-request Redis reads distort numbers:
| Variable | Functional default | Benchmark value |
|---|---|---|
CF_DATAPLANE_PUBLISHER_INTERVAL_SECONDS | 2 (fast config publish) | 60 (upstream default) |
CF_DATAPLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS | 0 (cache disabled) | 60 (upstream default) |
Built-In-Dataplane Baseline
Compare against the stock Python repository, where MCP traffic uses the ContextForge built-in dataplane and the ContextForge external dataplane is absent:
scripts/cf-integration.sh down # free shared ports
scripts/cf-integration.sh controlplane-locust
CONTROLPLANE_LOCUST_CLASSES=all adds admin/UI/mutating surfaces. LOCUST_USERS, LOCUST_SPAWN_RATE, and LOCUST_RUN_TIME apply here too.
Testing
Verification Rings
- Workspace checks — code compiles and unit behavior holds.
- In-repo integration tests — MCP routing against mock backends.
cf-integrationharness — full control-plane publication and external-dataplane request path end to end.- Load and benchmark — see Performance.
Workspace Validation
CI runs these on every change; run them locally before pushing:
cargo fmt --all --check
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
cargo nextest run --locked --workspace --all-features
cargo shear --check-test-targets --deny-warnings --locked
Use cargo test when nextest is unavailable. For wiki changes, also run mdbook build _context/wiki and mdbook test _context/wiki.
New protocol-sensitive tests target MCP 2026-07-28, connect through
server/discover, and send the required per-request client metadata. A small
compatibility module retains the active 2025-11-25/initialize cases until
that production compatibility surface is removed in a dedicated change; do not
add new behavior to that lane. Every case must remain request-independent, with
no required Mcp-Session-Id, session affinity, or retained backend transport.
SSE remains outside the external-dataplane contract.
In-Repo Integration Tests
crates/contextforge-data-plane-lib/tests/gateway.rs is the single library
integration target. It exercises the public gateway API against in-process MCP
backends without recompiling a shared support tree for every feature file.
| Area | Covers |
|---|---|
gateway/{tools,prompts,resources,subscriptions}.rs | Active routed operations and exact routing failures. |
gateway/plugins.rs | Gateway-owned CPEX ordering, mutation, denial, progress, and prompt seams using deterministic recording plugins. Resource coverage includes direct and aliased URIs, text/blob conversion, canonical pre-hook URIs, published-target rewrites, rejection of unpublished targets, metadata preservation, and pre/post denial. Concrete plugin behavior stays in each plugin crate. |
gateway/harness/ | Authentication, modern and compatibility clients, in-memory configuration, concrete mock backends, and owned server fixtures. |
gateway/future_contracts/ | Deferred fanout, pagination, TLS, completions, subscriptions, and cancellation contracts. |
TestServer binds 127.0.0.1:0 before spawning, uses cooperative
cancellation, and has a Drop fallback. GatewayFixture owns the gateway and
all backend servers. Tests should request the minimum topology: one virtual host
and one backend by default, with extra backends declared explicitly by the case.
The workspace currently keeps 13 ignored tests: 11 library future contracts and two real-process Redis/binary E2E tests. Ignored tests are not dead tests: keep them compiling, keep their intended assertions, give each a concrete blocker reason, and list them with:
cargo nextest list --locked --workspace --all-features --run-ignored only
The two binary E2E tests and tests/conformance/ remain separate infrastructure
boundaries. Active in-process tests run with no Docker or Redis dependency. Resource policy coverage belongs in this active harness, not in ignored binary tests or new legacy-client cases. Runtime unit tests verify that enabling or disabling hooks during a resource read preserves its original policy decision.
Parameter-header integration tests verify that calls without a published tool
schema skip local Mcp-Param-* validation and still reach the backend. Unit and
integration coverage also includes missing, malformed, unexpected, repeated,
and mismatched recognized headers; Base64 encoding; nested paths; numerically
equivalent integers; and invalid annotation names, types, duplicates, and
non-properties paths.
MCP Conformance
cf-integration
owns the official fixture, control-plane registration, Compose topology, server
and client runners, result rendering, and transactional baseline handling. This
repository keeps only the CI invocation, Make targets, and expected findings.
Comment exactly /conformance on a pull request to run the Conformance
Actions workflow. Only repository owners, members, and collaborators can start
it. The workflow acknowledges the command, tests the pull request head commit,
and reports the final result back to the pull request. CI builds and names the
conformance binary artifact using that same head SHA and retains it for 90 days,
so changes to main do not invalidate the artifact. It runs the modern client
and modern server eras through the external dataplane in standalone mode. This
starts Redis, the dataplane, nginx, and the official fixture without the control
plane. The harness discovers the fixture’s tools, resources, templates, and
prompts and publishes their routes and actual tool schemas through the
dataplane serializer. Selecting that lane also runs the fixture-direct server
leg and the scoped external-dataplane client leg:
cargo binstall cf-integration@0.3.1 --no-confirm
make conformance
The Make target tests the committed data-plane HEAD. It rejects tracked
uncommitted changes because the CLI clones the selected repository and commit
into .integration/. To use another local CLI binary:
CF_INTEGRATION=/path/to/cf-integration \
make conformance
Update every selected baseline atomically only after all operational work and baseline evaluation succeeds:
make conformance-bless
Baselines are partitioned beneath
tests/conformance/baselines/<client-version>/<server-era>/. Server findings
use fixture-direct.yml and external-data-plane.yml; scoped client findings
use client/external-data-plane.yml. Operational failures are always failures
and cannot be blessed. Runtime checkouts, logs, results, and reports remain
beneath .integration/.
A strict baseline match means the observed findings have not changed; it does
not mean complete protocol coverage. Inspect the report’s individual checks
before blessing changes. In particular, the external dataplane deliberately
rejects catalog list methods owned by the control plane. Scenarios that need
tools/list for setup cannot exercise the subsequent schema or parameter-header
checks through this lane; NotObserved findings record missing coverage, not
successful validation. The pinned fixture also lacks an x-mcp-header-annotated
tool for the custom-header server scenario. Parameter-header validation remains
covered by the active in-repo tests described above. Keep these setup gaps
distinct from observed protocol failures when interpreting conformance results.
Working Preferences and Standards
Validation gate — definition of “done”
A change is not done until:
cargo fmt --all --checkpasses.cargo clippy --locked --workspace --all-targets -- -D warningsis clean.cargo nextest run --locked --workspacepasses (fallback:cargo test).cargo deny check advisories licensespasses (pre-commit + CI).cargo build --locked --workspacesucceeds.- If the change touches the hot path, update the matching wiki page in
_context/wiki/in the same change.
CI additionally runs cargo shear --check-test-targets --deny-warnings --locked.
By change type:
| Change type | Minimum extra validation |
|---|---|
| Docs only | Run mdbook build _context/wiki and mdbook test _context/wiki; inspect affected headings, tables, and code blocks in the rendered output |
| Routing or session behavior | New/updated integration tests in crates/contextforge-data-plane-lib/tests/ against mock backends |
| Config shape | Schema regeneration (cargo run -p contextforge-data-plane-apis) + control-plane compatibility check |
| Plugin behavior | gateway_plugins.rs coverage for the new hook path |
| Performance-sensitive paths | Load-test run before and after |
Code style
- Idiomatic Rust — no unnecessary clones, heap allocations,
Arc, orMutexunless justified by the design. - Most ContextForge external-dataplane behavior lives in
contextforge-data-plane-lib. Do not let external-dataplane logic accumulate in the binary crate. - Typed errors — propagate errors rather than swallowing them silently.
- Keep change size minimal. Every changed line must trace directly to the task at hand.
Logging (tracing)
- Use
tracingfor all log output. - Prefer message-embedded fields:
level!("method_name - event field = {val} other_field = {other}"). Do not use structured field syntax (, field = val) for ContextForge external-dataplane logs. - Keep method/event prefixes stable and reuse the same field names and order for related events.
warn!is for unexpected conditions that need operator attention. Expected user/config misses →debug!orinfo!.- Never log: tokens, authorization headers, secrets, Redis key/value bytes, full
UserConfig, or backend credentials.
Change discipline
- Make the minimal change that solves the problem. No speculative refactors, no added abstractions beyond the task scope.
- Do not clean up surrounding code that is unrelated to the task.
- Do not add error handling for scenarios that cannot happen.
- Always read relevant code before suggesting or making changes. Never speculate about code that hasn’t been opened.
Architectural rules (non-negotiable)
- The ContextForge external dataplane is pure routing logic. No IAM, UI, or metrics-storage concerns.
- Config access goes through
UserConfigStoreonly — never push Redis details into routing code. - The backend prefix naming contract must not change without updating merge logic, split logic, and tests.
- Legacy SSE transport and stateful session behavior are being removed.
initializeremains supported as a stateless compatibility method; do not use it to create affinity, persist client state, or retain backend transports between requests. - Prefer the right architecture over backward compatibility; this project has no external users yet.
Protocol target
- The ContextForge external-dataplane target supports MCP
2026-07-28and2025-11-25over Streamable HTTP. - Every request is independent for both versions. Do not require
Mcp-Session-Id, session affinity, or a previously retained backend transport. - Retain
initializefor clients that use it, but generate its response from effective configuration and do not treat it as session establishment.2026-07-28tests and examples should continue to exerciseserver/discoverand per-request client metadata. - Protocol-sensitive tests cover both same-version paths and the best-effort cross-version paths (
2026-07-28→2025-11-25and the reverse). Do not add SSE or versions earlier than2025-11-25without a separate architecture decision.
AI interaction preferences
- Read before acting: always investigate relevant files before making suggestions or edits.
- Minimal scope: stay tightly scoped to the task — no unsolicited refactors or cleanups.
- Plan first for complex tasks: for changes with multiple moving parts, propose the approach before implementing.
- Run validation: run
cargo testandcargo clippyafter changes and report results before declaring done. - Update the wiki: when hot-path behavior changes, include the wiki page update in the same task.
- No hallucination: if something is unclear, ask rather than guess.
Branch naming
Format: user/<github-username>/<kebab-case-summary> — e.g. user/alice/fix-session-cleanup.
Open PRs as draft; mark ready only when implementation, tests, and wiki updates are complete.