diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a37b5d37..52057582 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,16 +1,16 @@ -name: Publish Book +name: Publish Docs on: workflow_dispatch: pull_request: paths: - ".github/workflows/pages.yml" - - "docs/book/**" + - "_context/wiki/**" push: branches: [main] paths: - ".github/workflows/pages.yml" - - "docs/book/**" + - "_context/wiki/**" permissions: contents: read @@ -27,12 +27,12 @@ jobs: uses: taiki-e/install-action@v2.75.27 with: tool: mdbook@0.5.3 - - name: Build book - run: mdbook build docs/book + - name: Build docs + run: mdbook build _context/wiki - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 with: - path: docs/book/book + path: _context/wiki/book deploy: if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.gitignore b/.gitignore index 9e301cdc..bea63b88 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,8 @@ target # Generated by gateway local runs contextforge-data-plane.log.* - -# Generated by mdBook -docs/book/book/ +# Generated by mdBook (wiki) +_context/wiki/book/ # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can diff --git a/AGENTS.md b/AGENTS.md index a238e3a2..a1f6a6d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,22 @@ # AGENTS.md +## Start with the wiki + +At the start of each task, check `_context/wiki/index.md` to decide +whether wiki context is needed before acting. Don't read the wiki in +full. Use the index and follow links only when they are relevant to +the task. + +## Update the wiki + +After completing a task, offer to update the wiki if the task yielded durable knowledge that could benefit future work, then wait for user approval. This includes new processes, architecture decisions, or insights that go beyond the immediate task. + +When adding a new wiki page, also update: +- `_context/wiki/index.md` — add a row to the pages table +- `_context/wiki/SUMMARY.md` — add the page under the appropriate section so it appears in the published book + +--- + Guidance for agents working on `contextforge-data-plane`. This repo is the Rust dataplane part of ContextForge. It must stay compatible @@ -23,30 +40,6 @@ control-plane, UI, IAM, or metrics-storage app. - Temporary compatibility shims in the current implementation are migration details, not supported client contracts. Do not build new behavior on them. -## Architecture - -Architecture documentation lives in The ContextForge Data Plane Book under -[docs/book](docs/book/README.md). Read the relevant page before changing the -hot path: - -| Page | Read it for | -| --- | --- | -| [What is ContextForge Data Plane?](docs/book/src/what-is-contextforge-data-plane.md) | Scope, boundaries, key terms, and the mental model. | -| [System Shape](docs/book/src/system-shape.md) | Crate layout, control-plane boundary, pipeline shape, state ownership, and module boundaries. | -| [Request Flow](docs/book/src/request-flow.md) | Startup, middleware order, initialize fanout, authorized calls, and the response path. | -| [Concurrency And Runtime Model](docs/book/src/concurrency-and-runtime.md) | Executor shapes, shared state and locks, fanout, and cancellation. | -| [Authentication And User Config Lookup](docs/book/src/authentication-and-user-config.md) | JWT validation, config keying, cache behavior, and failure responses. | -| [Security Model And Trust Boundaries](docs/book/src/security-model.md) | Trust boundaries, compromise impact, and transport security posture. | -| [Runtime Configuration](docs/book/src/runtime-configuration.md) | The `UserConfig` model, Redis/MessagePack persistence, and plugin runtime config. | -| [Control-Plane Integration](docs/book/src/control-plane-integration.md) | Redis keys, schemas, token shape, and route parity with the control plane. | -| [Backend Connections And Transports](docs/book/src/backend-connections-and-transports.md) | Downstream, upstream, and config-store transports plus TLS direction. | -| [Session Ownership](docs/book/src/session-ownership.md) | Backend session state, cleanup, and load-balancing constraints. | -| [MCP Routing Semantics](docs/book/src/mcp-routing-semantics.md) | The backend prefix namespace and routing contract. | -| [Architectural Choices](docs/book/src/architectural-choices.md) | Invariants and tradeoffs that must not change accidentally. | - -The book is rendered from `docs/book/src/` and published through GitHub Pages; -see [docs/book/README.md](docs/book/README.md) for build and validation steps. - ## Working Rules - Most product behavior belongs in `contextforge-data-plane-lib`; avoid adding @@ -57,7 +50,7 @@ see [docs/book/README.md](docs/book/README.md) for build and validation steps. logic, split logic, and tests. - This project is still early development with no external users; prefer the right architecture over preserving unstable APIs or compatibility surfaces. -- When behavior on the hot path changes, update the matching book page in the +- When behavior on the hot path changes, update the matching wiki page in the same change. ## Logging diff --git a/Makefile b/Makefile index 655d37c6..89645fb5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help docker-prod testing-up testing-down +.PHONY: help docker-prod compose-up compose-down docs-serve help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' @@ -6,12 +6,15 @@ help: ## Show this help docker-prod: ## Build production Docker image (contextforge-data-plane:latest) from docker/Dockerfile docker build -t contextforge-data-plane:latest -f docker/Dockerfile . -testing-up: ## Launch testing stack: nginx, control plane, redis, postgres, pgbouncer, dataplane, fast_time_server +compose-up: ## Launch stack: nginx, control plane, redis, postgres, pgbouncer, dataplane, fast_time_server @docker image inspect contextforge-data-plane:latest >/dev/null 2>&1 || { \ echo "Image contextforge-data-plane:latest not found. Run 'make docker-prod' first."; \ exit 1; \ } docker compose -f docker/docker-compose.yml up -d nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time -testing-down: ## Tear down the testing stack +compose-down: ## Tear down the stack docker compose -f docker/docker-compose.yml stop nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time + +docs-serve: ## Serve the wiki book locally at http://127.0.0.1:3000 + mdbook serve _context/wiki --hostname 127.0.0.1 --port 3000 --open diff --git a/README.md b/README.md index d29e29ae..b5a64674 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,8 @@ The Rust data plane for traffic, loads control-plane-published configuration from Redis, and routes authorized requests to configured MCP backends. -Architecture, configuration, operations, and development documentation lives -in [The ContextForge Data Plane Book](docs/book/src/SUMMARY.md). Build it locally -with `mdbook serve docs/book`; see [docs/book/README.md](docs/book/README.md). +Architecture, configuration, operations, and development context lives +in the wiki under [`_context/wiki/`](_context/wiki/index.md). ## Quick Start @@ -16,16 +15,16 @@ test stack: ```bash make docker-prod -make testing-up +make compose-up ``` The stack uses the current `fast_time_server` backend and exercises config -publication through the external ContextForge control plane. Follow -[Local Docker Stack](docs/book/src/local-docker-stack.md) for the complete smoke +publication through the external ContextForge control plane. See +[getting-started.md](_context/wiki/getting-started.md) for the complete smoke test, then stop it with: ```bash -make testing-down +make compose-down ``` ## Run the Binary from Cargo @@ -38,13 +37,13 @@ docker compose -f docker/docker-compose-local.yaml up -d docker compose -f docker/docker-compose-local.yaml ps redis gateway-one gateway-two ``` -Then follow [Run the Gateway Locally](docs/book/src/running-the-gateway.md). +Then follow [getting-started.md](_context/wiki/getting-started.md) for the local cargo dev workflow. ## Runtime CPEX Plugins Runtime CPEX plugins are disabled by default. When enabled, the data plane loads validated plugin configuration from Redis and supports the narrow hook surface -documented in [Plugins And Policy](docs/book/src/plugins-and-policy.md). +documented in [config.md](_context/wiki/config.md). The optional demo plugin crates still come from their independently hosted `cpex-plugins-rs` repository; they are unrelated to the retired MCP SDK fork. @@ -83,9 +82,7 @@ cargo run --release \ ## Tracing and Metrics -The data plane exports OTLP traces and metrics. The local Langfuse, -OpenTelemetry Collector, and Prometheus overlays are documented in -[Telemetry And Diagnostics](docs/book/src/telemetry-and-diagnostics.md). +The data plane exports OTLP traces and metrics. Local Langfuse, OTel Collector, and Prometheus overlays are documented in [config.md](_context/wiki/config.md) under "Local Telemetry Verification Stack". ## Performance Tests diff --git a/_context/wiki/SUMMARY.md b/_context/wiki/SUMMARY.md new file mode 100644 index 00000000..03f6e704 --- /dev/null +++ b/_context/wiki/SUMMARY.md @@ -0,0 +1,25 @@ +# Summary + +[Introduction](index.md) + +# The Project + +- [What is ContextForge Data Plane?](project.md) +- [Getting Started](getting-started.md) + +# Architecture + +- [Architecture](architecture.md) +- [MCP Routing Semantics](routing.md) +- [Security Model](security.md) +- [Failure Modes](failure-modes.md) + +# Operations + +- [Configuration Reference](config.md) +- [Deployment](deployment.md) +- [Performance](performance.md) + +# Contributing + +- [Working Preferences](preferences.md) diff --git a/_context/wiki/architecture.md b/_context/wiki/architecture.md new file mode 100644 index 00000000..9f279ebb --- /dev/null +++ b/_context/wiki/architecture.md @@ -0,0 +1,198 @@ +# Architecture + +## Middleware Stack Order + +Tower layers execute outside-in. A request reaches MCP handlers with these extensions already set: + +```text +TCP/TLS listener + -> HttpMetricsLayer + -> TraceLayer + -> /contextforge-rs nested router + -> CORS layer + -> 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 +``` + +MCP handlers read typed extensions — they never parse headers, paths, or Redis keys directly. + +## Pipeline Shape + +```text +downstream request + -> virtual host extraction → JWT validation → session extraction + -> user config lookup → 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 +``` + +```mermaid +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): + +```mermaid +flowchart TD + D(["downstream request"]) + A["virtual host · JWT\nsession extract"] + C["user config lookup\nMCP 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 +``` + + +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 | `BackendTransports` map | Local process, per principal/backend/session | +| 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`**. | + +## Lock Design + +Locks guard maps of handles, not I/O. Backend calls, Redis reads, and plugin hooks run outside any gateway lock. `borrow_transports()` clones `Arc` so the lock is not held across calls. + +## Startup And Response Flow + +Startup sequence (`main.rs` → `Gateway::run_gateway`): + +```text +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): + +```text +backend response + -> response plugin hooks (call_tool only) + -> 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, TraceLayer, HttpMetricsLayer + -> downstream response +``` + +Flow checkpoints — each must exist before the next dependency runs: + +| Checkpoint | Fact established | Next dependency | +| --- | --- | --- | +| Listener | Request reached Rust 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: + +```text +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 are intentionally narrow (`cmf.tool_pre_invoke`, `cmf.tool_post_invoke`). 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 control plane; update protocol tests and examples; keep legacy traffic on control-plane routes. | +| 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. | diff --git a/_context/wiki/book.toml b/_context/wiki/book.toml new file mode 100644 index 00000000..ba2487ae --- /dev/null +++ b/_context/wiki/book.toml @@ -0,0 +1,8 @@ +[book] +title = "ContextForge Data Plane" +description = "Architecture, configuration, operations, and development context for the Rust dataplane." +src = "." + +[output.html] +git-repository-url = "https://github.com/contextforge-org/contextforge-data-plane" +edit-url-template = "https://github.com/contextforge-org/contextforge-data-plane/edit/main/_context/wiki/{path}" diff --git a/_context/wiki/config.md b/_context/wiki/config.md new file mode 100644 index 00000000..cf9dba2b --- /dev/null +++ b/_context/wiki/config.md @@ -0,0 +1,253 @@ +# Configuration Reference + +## Minimum Required Flags + +```text +--redis-address --redis-port --redis-mode +``` + +Plus at least: `--address` or `--tls-address`, `--token-verification-public-key` or `--token-verification-secret`. + +## Key CLI Flags (env var: `CONTEXTFORGE_DATA_PLANE_*`) + +| Flag | Env suffix | Default | Note | +| --- | --- | --- | --- | +| `--address` | `ADDRESS` | — | Plain HTTP listener | +| `--tls-address` | `TLS_ADDRESS` | — | Requires cert + key | +| `--server-certificate` | `TLS_SERVER_CERTIFICATE` | — | With `--tls-address` | +| `--server-private-key` | `TLS_SERVER_PRIVATE_KEY` | — | With `--tls-address` | +| `--token-verification-public-key` | `TOKEN_VERIFICATION_PUBLIC_KEY` | — | RSA (RS256/384/512) | +| `--token-verification-secret` | `TOKEN_SECRET` | — | HMAC (HS256/384/512) | +| `--redis-address` | `REDIS_HOSTNAME` | **required** | | +| `--redis-port` | `REDIS_PORT` | **required** | | +| `--redis-mode` | `REDIS_CONNECTION_MODE` | **required** | `plain-text` \| `tls` \| `mtls` | +| `--user-config-cache-expiry-seconds` | `USER_CONFIG_CACHE_EXPIRY_SECONDS` | `60` | `0` = no cache | +| `--upstream-connection-mode` | `UPSTREAM_CONNECTION_MODE` | HTTPS-only | `plain-text-or-tls` for local HTTP backends | +| `--number-of-cpus` | `NUMBER_OF_CPUS` | host CPU count | Tokio worker threads | +| `--single-runtime` | `SINGLE_RUNTIME` | `true` | `false` = multi-runtime (no session affinity) | +| `--runtime-plugins-enabled` | `RUNTIME_PLUGINS_ENABLED` | `false` | Enables CPEX hooks | +| `--enable-open-telemetry` | `ENABLE_OPEN_TELEMETRY` | `false` | OTLP traces | +| `--enable-otel-metrics` | `ENABLE_OTEL_METRICS` | `false` | OTLP metrics | + +## 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`) + +```text +UserConfig + virtual_hosts: HashMap + +VirtualHost + backends: HashMap ← map key = routing prefix + +BackendMCPGateway + name: String + url: Url + transport: STREAMABLEHTTP | SSE | STDIO ← only STREAMABLEHTTP used today + passthrough_headers: Vec ← snapshotted at initialize; session-scoped + add_headers: HashMap ← injected after passthrough + remove_headers: Vec ← stripped after add + tool_name_aliases: HashMap ← downstream_alias → upstream_original + allowed_tool_names: Vec ← model exists, NOT currently enforced + allowed_resource_names: Vec ← model exists, NOT currently enforced + allowed_prompt_names: Vec ← model exists, NOT currently enforced +``` + +**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) | + +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. | + +```bash +cargo run -p contextforge-data-plane-apis +```text + +## Plugin Config (Redis key: `ContextForgeGatewayRuntimePluginConfig`) + +``` +RuntimePluginConfigDocument + version: 1 + cpex: CpexConfig +```text + +Supported: `cmf.tool_pre_invoke`, `cmf.tool_post_invoke` only. +Rejected: routing-based selection, plugin dirs, global policies, other hook types. +Reload watcher: 10-minute interval. Invalid reload → runtime marked failed. + +## 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=debug` is 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`. | + +```mermaid +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:** + +```mermaid +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: +```bash +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): +```bash +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 +```text + +## 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 dataplane: + +| Gap | Issue | +| --- | --- | +| W3C trace-context propagation across gateway hops | [mcp-context-forge#4723](https://github.com/IBM/mcp-context-forge/issues/4723) | +| MCP-semantic spans with tool names and JSON-RPC method attributes | [mcp-context-forge#4722](https://github.com/IBM/mcp-context-forge/issues/4722) | diff --git a/_context/wiki/deployment.md b/_context/wiki/deployment.md new file mode 100644 index 00000000..8709c6e1 --- /dev/null +++ b/_context/wiki/deployment.md @@ -0,0 +1,72 @@ +# Deployment + +## Checklist + +1. Front door routes only `/contextforge-rs` to the dataplane. +2. JWT verification key/secret in place and rotated with the control plane's signing key. +3. Redis reachable; TLS/mTLS across trust zones; write access restricted to the control plane; `DATAPLANE_PUBLISHER=true` on the control plane. +4. Upstream connection mode matches backend URL schemes. +5. One replica per `Mcp-session-id` (single replica or sticky routing). +6. `with_tools` feature **disabled** in the production build. +7. Telemetry export pointed at the collector. +8. 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 gateway. +- All other traffic (UI, management, SSE, legacy MCP) → control-plane. +- Upstream retries on `error timeout http_502/503/504`: 2 tries, 10-second window. Non-idempotent MCP `POST` bodies 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](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` → `400` until Redis returns. + +## Images + +- CI builds `docker/Dockerfile` on every push to `main` and publishes both `ghcr.io//contextforge-data-plane:v` and `ghcr.io//contextforge-data-plane:latest`, where `` is the Cargo package version. +- **Pin the `v`-prefixed tag for reproducible deployments.** `latest` tracks `main`. +- Builder: `rust:1.96.1` in `docker/Dockerfile`. +- The reference Compose stack runs the gateway with raised limits worth copying to real deployments: `nofile 65535` and 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 + +```text +worst-case staleness = publisher interval + user-config cache expiry +```text + +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 | Wide open (any origin, method, header). Bearer-token based + cookie-free → no CSRF risk, but expect tightening as policy work lands. | +| 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. | diff --git a/_context/wiki/failure-modes.md b/_context/wiki/failure-modes.md new file mode 100644 index 00000000..94b25416 --- /dev/null +++ b/_context/wiki/failure-modes.md @@ -0,0 +1,58 @@ +# 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` | diff --git a/_context/wiki/getting-started.md b/_context/wiki/getting-started.md new file mode 100644 index 00000000..ade291e1 --- /dev/null +++ b/_context/wiki/getting-started.md @@ -0,0 +1,145 @@ +# Getting Started + +## Full Docker Stack + +```bash +make docker-prod # build dataplane:latest from docker/Dockerfile +make compose-up # start nginx, control-plane, redis, postgres, dataplane, fast_time_server +```text + +Wait for `register_fast_time` to finish, then allow ~60s config propagation: + +```bash +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-rs` prefix → dataplane. Without it → control-plane (you'll get `{"detail":"..."}` from mcpgateway, not a dataplane response). + +Teardown: `make compose-down` (stops containers; volumes kept). + +## cf-integration Harness (full end-to-end) + +```bash +scripts/cf-integration.sh up # checkout control-plane, pull dataplane image, start full stack +scripts/cf-integration.sh probe # smoke: 401 check → initialize → tools/list → tools/call +scripts/cf-integration.sh test-all # all lanes: live-mcp, live-rbac, live-protocol +scripts/cf-integration.sh down +```text + +Admin UI (control-plane): `http://localhost:8080/admin` — `admin@example.com` / `changeme` + +Key env overrides: `CF_DATAPLANE_IMAGE`, `CF_DATAPLANE_VERSION`, `NGINX_PORT` (default `8080`). + +## Local Cargo Dev Workflow + +For debugger/profiler/rapid iteration, start Redis and the counter/conformance fixtures: + +```bash +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: + +```bash +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 +```text + +The client-facing route is `http://127.0.0.1:8001/contextforge-rs/servers/{virtual_host_id}/mcp`. + +### Mint a local test token + +```bash +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 + +```bash +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", + "transport": "STREAMABLEHTTP", + "passthrough_headers": [], "allowed_tool_names": [], + "allowed_resource_names": [], "allowed_prompt_names": [] + }, + "gateway-two": { + "name": "gateway-two", + "url": "http://127.0.0.1:5556/mcp", + "transport": "STREAMABLEHTTP", + "passthrough_headers": [], "allowed_tool_names": [], + "allowed_resource_names": [], "allowed_prompt_names": [] + } + } + } + } + }' +```text + +### Verify with mcp-inspector + +```bash +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) + +```bash +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":{}}}}' +```text + +### 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. | diff --git a/_context/wiki/index.md b/_context/wiki/index.md new file mode 100644 index 00000000..a5e063b8 --- /dev/null +++ b/_context/wiki/index.md @@ -0,0 +1,29 @@ +# ContextForge Data Plane — 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](getting-started.md) | Full docker stack, local cargo dev, cf-integration — commands and URIs | +| [project.md](project.md) | What the project is, goals, stakeholders, key modules, crate ownership, active work | +| [preferences.md](preferences.md) | Working standards, code style, logging rules, branch naming, AI interaction preferences | +| [architecture.md](architecture.md) | Middleware stack order, pipeline shape, module boundaries, state ownership, executor shapes | +| [routing.md](routing.md) | Backend prefix contract, list/routed ops, federated pagination, session state, capability merge | +| [failure-modes.md](failure-modes.md) | HTTP/MCP/routing/backend/plugin failure table — exact HTTP codes and JSON-RPC errors | +| [config.md](config.md) | Key CLI flags, JWT claims, UserConfig shape, plugin config, telemetry debugging, startup validation, local observability stack | +| [deployment.md](deployment.md) | Deployment checklist, health endpoint caveat, nginx routing, TLS choices, session affinity, Redis availability, image pinning | +| [security.md](security.md) | Trust boundaries, identity/authorization model, compromise impact, transport security, secrets handling | +| [performance.md](performance.md) | Dataplane-only load testing (Goose), full-stack Locust runs, benchmark settings, control-plane baseline | + +## Quick orientation + +- **Repo**: `contextforge-data-plane` — the Rust dataplane for ContextForge. +- **Core invariant**: this crate is pure routing logic. No IAM, UI, or metrics storage. +- **Protocol target**: MCP `2026-07-28` over Streamable HTTP. Legacy SSE paths are being removed. +- **Architecture context**: [architecture.md](architecture.md) — read before touching the hot path. Full wiki index above. +- **Validation gate**: `cargo fmt` + `cargo clippy` + `cargo nextest` + `cargo deny` must be clean; CI also runs `cargo shear`. See [preferences.md](preferences.md) for by-change-type requirements. +- **System topology**: `client → nginx → [dataplane | control-plane]`; config flows from control-plane via `dataplane_publisher.py` → Redis → dataplane. See [project.md § System topology](project.md#system-topology). diff --git a/_context/wiki/performance.md b/_context/wiki/performance.md new file mode 100644 index 00000000..8b55f6e1 --- /dev/null +++ b/_context/wiki/performance.md @@ -0,0 +1,63 @@ +# Performance And Load Testing + +## Two Load Paths + +- **Dataplane-only:** `contextforge-load-test` measures the Rust dataplane in isolation. +- **Full-stack:** `cf-integration` harness measures the full nginx → control-plane → dataplane path with Locust. + +Use the first to profile gateway changes; use the second to measure what users see. + +## Dataplane-Only (Goose) + +`crates/contextforge-load-test` is a [Goose](https://book.goose.rs/)-based driver that speaks full streamable HTTP MCP. Start the local stack and seed user config first (see [getting-started.md](getting-started.md)), then: + +```bash +cargo run --release --bin contextforge-load-test -- \ + --host 'http://127.0.0.1:8001' \ + -u 120 -r 40 --run-time 120s \ + --report-file report.html +```text + +`-u` = concurrent users, `-r` = spawn rate/s, `--report-file` = HTML report. Curated run reports live in `reports/`. + +## Full-Stack Load (Locust via cf-integration) + +| 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: + +```bash +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) | + +## Control-Plane Baseline + +Compare against the stack without the dataplane: + +```bash +scripts/cf-integration.sh down # free shared ports +scripts/cf-integration.sh controlplane-locust +```text + +`CONTROLPLANE_LOCUST_CLASSES=all` adds admin/UI/mutating surfaces. `LOCUST_USERS`, `LOCUST_SPAWN_RATE`, and `LOCUST_RUN_TIME` apply here too. diff --git a/_context/wiki/preferences.md b/_context/wiki/preferences.md new file mode 100644 index 00000000..f46341c8 --- /dev/null +++ b/_context/wiki/preferences.md @@ -0,0 +1,75 @@ +# Working Preferences and Standards + +## Validation gate — definition of "done" + +A change is not done until: +1. `cargo fmt --all --check` passes. +2. `cargo clippy --locked --workspace --all-targets -- -D warnings` is clean. +3. `cargo nextest run --locked --workspace` passes (fallback: `cargo test`). +4. `cargo deny check advisories licenses` passes (pre-commit + CI). +5. `cargo build --locked --workspace` succeeds. +6. 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 | Update the relevant wiki page in `_context/wiki/` | +| 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`, or `Mutex` unless justified by the design. +- Most product behavior lives in `contextforge-data-plane-lib`. Do not let 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 `tracing` for 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 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!` or `info!`. +- **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 dataplane is pure routing logic. **No IAM, UI, or metrics-storage concerns.** +- Config access goes through `UserConfigStore` only — 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 old `initialize`/session behavior are being **removed** — do not build on temporary shims. +- Prefer the right architecture over backward compatibility; this project has no external users yet. + +## Protocol target + +- All new behavior targets MCP protocol version **`2026-07-28`** over **Streamable HTTP**. +- New tests and examples use `server/discover`, per-request client metadata, and the `2026-07-28` version. +- Do not add new compatibility for older MCP protocol versions. + +## 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 test` and `cargo clippy` after 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//` — e.g. `user/alice/fix-session-cleanup`. +Open PRs as draft; mark ready only when implementation, tests, and wiki updates are complete. \ No newline at end of file diff --git a/_context/wiki/project.md b/_context/wiki/project.md new file mode 100644 index 00000000..ca9f53ff --- /dev/null +++ b/_context/wiki/project.md @@ -0,0 +1,162 @@ +# Project Overview + +## What this project is + +`contextforge-data-plane` is a Rust-based MCP (Model Context Protocol) gateway — the **dataplane** component of ContextForge. It acts as a scalable, secure proxy layer that routes AI tool calls from MCP clients to one or more backend MCP servers. + +It is paired with the external ContextForge control plane at [`IBM/mcp-context-forge`](https://github.com/IBM/mcp-context-forge). The two components have a strict division of responsibility: + +| Layer | Owns | +| --- | --- | +| **This repo (dataplane)** | Request routing, auth enforcement, backend fan-out, session ownership | +| **Control plane** | IAM, UI, metrics storage, legacy MCP client compatibility | + +The dataplane must never take on control-plane concerns. + +```mermaid +flowchart LR + C(["MCP Client\nprotocol 2026-07-28\nStreamable HTTP"]) + + subgraph Infra["Infrastructure"] + N["nginx\nTLS termination\nrouting fan-out"] + end + + subgraph DP["ContextForge Data Plane (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"] + MW --> RT --> PL + end + + subgraph CP["Control Plane (IBM/mcp-context-forge)"] + direction TB + IAM["IAM · UI\nmetrics storage"] + PUB["dataplane_publisher.py\nwrites UserConfig to Redis"] + end + + R[("Redis\nUserConfig store\nMessagePack")] + BE["Backend MCP Servers"] + + C --> N + N -->|"/contextforge-rs/*"| DP + N -->|"UI / IAM / legacy MCP / SSE"| CP + CP --> R + DP -->|"read-only UserConfig"| R + DP -->|"MCP calls"| BE +``` + + +## Goals and objectives + +- Provide a **production-grade, low-latency routing layer** between MCP clients and backend MCP servers. +- Target **MCP protocol version `2026-07-28`** over Streamable HTTP as the sole downstream contract. +- Enforce a clean **dataplane/control-plane boundary** — no IAM, UI, or metrics storage logic in this repo. +- Keep config access behind the **`UserConfigStore` abstraction** (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 dataplane toward the `2026-07-28` protocol target. + +## Key modules and architecture + +Architecture context lives in the wiki. Key pages: + +| Wiki page | Covers | +| --- | --- | +| [architecture.md](architecture.md) | Crate layout, pipeline shape, state ownership, module boundaries | +| [routing.md](routing.md) | Backend prefix namespace, routing contract, session state, method reference | +| [config.md](config.md) | JWT validation, config keying, UserConfig shape, cache behavior | +| [security.md](security.md) | Trust boundaries, invariants, and tradeoffs | + +## Crate ownership + +| Crate | Purpose | +| --- | --- | +| `contextforge-data-plane-lib` | All dataplane behavior: routing, middleware, sessions, transports. Almost everything goes here. | +| `contextforge-data-plane` (binary) | Process shell only: CLI flags, logging, runtime shape. No 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). | +| `contextforge-load-test` | Performance harness: end-to-end MCP traffic driver. | + +**Key invariants:** +- Redis/config access goes through `UserConfigStore` only — 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**: replacing all remaining legacy MCP paths (SSE transport, `initialize`/session shims) with `2026-07-28` equivalents over Streamable HTTP. +- Legacy SSE transport and old session behavior are **being removed**, not maintained. Do not build new behavior on temporary shims. +- New tests and examples should use `server/discover`, per-request client metadata, and protocol version `2026-07-28`. + +## Control-Plane Integration Contract + +> **Provisional.** No formal contract has been stipulated yet. This section documents the current de-facto integration surface with [IBM/mcp-context-forge](https://github.com/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 dataplane. | +| Protocol compatibility | Dataplane target is MCP `2026-07-28` only. Control plane serves older versions, legacy session init, and SSE on its own routes. | +| 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. 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 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: +```bash +cargo run -p contextforge-data-plane-apis +```text + +## System topology + +All external traffic enters through **nginx**, which fans out to either the dataplane or the control plane: + +```mermaid +flowchart LR + client(["client"]) --> nginx["nginx"] + nginx --> dataplane["data-plane"] + nginx --> controlplane["control-plane"] + dataplane --> redis["redis"] + controlplane --> redis + controlplane --> postgres["postgres\n(via pgbouncer)"] + dataplane --> fastts["fast_time_server"] +``` + +### How the control plane publishes config to the dataplane + +The control plane and dataplane do **not** communicate over HTTP. Config is exchanged exclusively through Redis: + +1. The control plane runs **`dataplane_publisher.py`** — a publisher script that writes dataplane configuration (user config, backend definitions, etc.) into Redis. +2. The dataplane reads that config from Redis via the **`UserConfigStore`** abstraction (MessagePack-encoded `UserConfig`). + +This means: +- The 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 dataplane config; the dataplane has no direct dependency on the control-plane process at runtime. +- Config changes from the control plane are picked up by the 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 | — | +| **dataplane** (`contextforge-data-plane`) | MCP routing, auth enforcement, fan-out to backends | Redis (read-only for config) | +| **control-plane** (`IBM/mcp-context-forge`) | IAM, UI, metrics, legacy MCP clients, 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 dataplane | — | + +## External dependencies and integration points + +- **Redis** — runtime config store (MessagePack-encoded `UserConfig`). Populated by `dataplane_publisher.py` on the control plane; read by the dataplane via `UserConfigStore`. +- **Control plane** (`IBM/mcp-context-forge`) — owns legacy MCP client routes and publishes dataplane config via `dataplane_publisher.py`. Does not route through this dataplane at runtime. +- **fast_time_server** — high-resolution time source consumed by the dataplane. +- **Tokio + Axum** — fixed async runtime and web framework. diff --git a/_context/wiki/routing.md b/_context/wiki/routing.md new file mode 100644 index 00000000..4a681a85 --- /dev/null +++ b/_context/wiki/routing.md @@ -0,0 +1,134 @@ +# MCP Routing Semantics + +## Backend Prefix Contract + +Backend map keys become public identifiers only for **multi-backend virtual hosts without an explicit tool alias**: + +```text +backend tool "increment" on backend "gateway-one" → "gateway-one-increment" +backend resource "counter" on backend "gateway-one" → "gateway-one-counter" +``` + +Single-backend virtual hosts: identifiers pass through **unchanged**. + +> **Breaking change rule:** changing a backend map key changes downstream identifiers for multi-backend virtual hosts. Do not rename without updating merge logic, split logic, and tests. + +## Tool Aliases + +`BackendMCPGateway.tool_name_aliases` maps `{downstream_alias: upstream_original}`. Aliases take precedence over prefix fallback. They are advertised and routed exactly as published (case, dots, underscores preserved). + +## List Operations (fan-out) + +All four list methods fan out to all connected backends concurrently and merge results: + +```text +list_tools / list_resources / list_prompts / list_resource_templates + → all connected backends → merged sorted output +``` + +Failed/unavailable backends are logged and skipped. Single-backend: identifiers unchanged. Multi-backend: prefixed with backend map key. + +## Routed Operations (single backend) + +Calls targeting one object use the inverse rule. The name splitter walks configured backend names and requires a `-` immediately after the backend name: + +```text +gateway-one-increment → backend: gateway-one, tool: increment +gateway-oneincrement → rejected (no - separator) +``` + +`call_tool` resolves explicit alias first, then falls back to single/multi-backend logic. + +Methods using the same conditional routing: `read_resource`, `subscribe`, `unsubscribe`, `get_prompt`, `complete`. + +## Federated Pagination + +The gateway wraps per-backend cursors inside its own opaque token (JSON, treated as opaque by MCP clients). First request: all backends queried. Resume: cursor decoded, exhausted backends skipped. New cursor emitted when any backend has more pages. + +**Known limitation:** if backend set changes between pages, removed backend's cursor is silently dropped. + +## Session State (local process) + +Backend RMCP services are stored in `BackendTransports` keyed by: +```text +principal (claims.sub) + backend_name (map key) + downstream_session_id +``` + +This is **local process state only**. Implications: +- After `initialize`, later requests must reach the same process. +- Sticky routing required for load-balanced deployments. +- Gateway restart → all sessions lost → clients must re-run `initialize`. +- Multi-runtime mode (`--single-runtime false`): each runtime thread has its own `BackendTransports` with no cross-thread affinity. + + +```mermaid +sequenceDiagram + participant C as MCP Client + participant GW as Gateway (RMCP) + participant BT as BackendTransports
(local process state) + participant LU as LocalUserSessionStore
(LRU 50k / 1h) + participant BA as Backend A + participant BB as Backend B + + C->>GW: POST initialize (Mcp-Session-Id: S) + GW->>BA: initialize (concurrent) + GW->>BB: initialize (concurrent) + BA-->>GW: InitializeResult + BB-->>GW: InitializeResult + GW->>BT: store RunningService keyed by sub+backend+S + GW->>LU: store session entry for sub+S + GW-->>C: merged InitializeResult + + C->>GW: POST call_tool (Mcp-Session-Id: S) + GW->>BT: lookup sub+backend+S → Arc + BT-->>GW: RunningService handle + GW->>BA: call_tool (routed by name prefix) + BA-->>GW: ToolResult + GW-->>C: ToolResult + + C->>GW: DELETE (Mcp-Session-Id: S) + GW->>GW: RMCP handles DELETE + GW->>LU: remove sub+S entry + GW->>BT: remove all sub+*+S entries + GW-->>C: 200 OK +``` + +## Capability Merge + +On `initialize`, the gateway builds one downstream `InitializeResult` — not a passthrough of any one backend. The source of truth is each backend's `InitializeResult`; the gateway reads `peer_info().capabilities` from each running service and stores them with the backend transport state. + +The merge rule (gateway-aware, not a raw union): +- Enable a top-level capability when ≥1 backend supports it **and** the gateway has a routing story for it. +- `resources.subscribe` preserved if any backend advertises it (the gateway routes subscribe/unsubscribe and forwards resource-update notifications). +- `listChanged` not yet advertised (gateway doesn't emit downstream list-changed notifications when upstream lists change). +- Single-backend passthrough is not a stable contract (`HashMap` iteration order). +- If no backend reports supported capabilities, returns `ServerCapabilities::default()`. + +**Do not** initialize the downstream capability from just one backend entry — the gateway fronts multiple backends, `HashMap` iteration is non-deterministic, and list methods already merge across all backends. + +## Cleanup + +`DELETE` with `Mcp-session-id`: +```text +→ RMCP handles request +→ on success: remove LocalUserSessionStore entry + BackendTransports entries for principal+session +``` +If RMCP rejects the delete, local state is untouched. + + +## MCP Method Quick Reference + +| Method | Group | Behavior | +| --- | --- | --- | +| `initialize` | Session | Concurrent fanout to all backends; failure of one backend is non-fatal (stored with no service). Returns merged capability set. Requires `DownstreamSessionId`, `UserConfig`, `VirtualHostId`, `ContextForgeClaims`. | +| `list_tools` | List | Fan-out all connected backends → merged sorted result. Cursor-based pagination across backends. | +| `list_resources` | List | Same as list_tools. | +| `list_prompts` | List | Same as list_tools. | +| `list_resource_templates` | List | Same — both name and URI template get prefixed for multi-backend. | +| `call_tool` | Targeted | Resolves alias → single/multi-backend fallback. Runs pre/post plugin hooks. Forwards downstream cancellation to backend. Tracks backend progress tokens: RMCP assigns a new token per backend request; the gateway maps each backend token to the downstream token. Request enqueue and mapping publication are serialized against progress lookup so an immediate backend notification cannot overtake registration. When the notification matches an in-flight token, the gateway restores the downstream token and forwards it to the client. | +| `read_resource` | Targeted | Single-backend: URI unchanged. Multi-backend: strips prefix. | +| `subscribe` / `unsubscribe` | Targeted | Same resource-URI routing; forwards/stops resource-update notifications. | +| `get_prompt` | Targeted | Single-backend: name unchanged. Multi-backend: strips prefix. | +| `complete` | Targeted | Routes on prompt name or resource URI inside `ref`. | +| `ping` | Local | Returns success; no backend fanout. | +| `DELETE` | Session | RMCP handles first; on success `session_id_layer` removes local session + backend transports. | diff --git a/docs/book/src/security-model.md b/_context/wiki/security.md similarity index 54% rename from docs/book/src/security-model.md rename to _context/wiki/security.md index e6dfb034..9d0906da 100644 --- a/docs/book/src/security-model.md +++ b/_context/wiki/security.md @@ -1,15 +1,10 @@ -# Security Model And Trust Boundaries - -> 🔒 **Trust rule:** the gateway trusts its process config and the -> control-plane-authored data in Redis. It does not trust downstream callers -> beyond a validated JWT, and it reaches backends only through configured -> URLs. +# Security Model ## Trust Boundaries | Boundary | Trust level | Enforced by | | --- | --- | --- | -| Downstream client | Untrusted. Every request must present a valid bearer JWT; the session id alone grants nothing without matching principal state. | `claims_layer`, validators, and the principal-scoped backend session keys. | +| 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 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. | @@ -19,26 +14,19 @@ Authentication is bearer-JWT only: -- Accepted algorithms are `RS256/RS384/RS512` (public key configured) or - `HS256/HS384/HS512` (shared secret configured); anything else is rejected. -- `iss` must be `mcpgateway`, `aud` must be `mcpgateway-api`, and `exp` is - validated. There is no revocation list: a leaked token is valid until it - expires. -- Authorization is config existence. The `sub` claim selects the caller's - `UserConfig`; the path selects one virtual host inside it. A caller can - never reach a backend that is not in their own config, and unknown virtual - hosts return `404` before MCP handling. -- `jti`, `token_use`, `iat`, `teams`, `user`, and `scopes` are carried but not - yet enforced; `token_use`, `iat`, `teams`, and `scopes` are optional, as is - `user.full_name`. Fine-grained permissions are future policy work. +- Accepted algorithms: `RS256/RS384/RS512` (public key configured) or `HS256/HS384/HS512` (shared secret configured). Anything else is rejected. +- `iss` must be `mcpgateway`, `aud` must be `mcpgateway-api`, and `exp` is validated. +- **No revocation list.** A leaked token is valid until it expires. Rotate the key and restart to invalidate all outstanding tokens. +- Authorization is config existence. The `sub` claim selects the caller's `UserConfig`; the path selects one virtual host inside it. A caller can never reach a backend not in their own config. Unknown virtual hosts return `404` before MCP handling. +- `jti`, `token_use`, `iat`, `teams`, `user`, and `scopes` are carried but not yet enforced. Fine-grained permissions are future policy work. ## 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; there is no revocation. | +| 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 the requests routed to that backend and controls its responses; the namespace prefix limits blast radius to that backend's objects. | +| 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 @@ -47,26 +35,20 @@ Authentication is bearer-JWT only: | --- | --- | | 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, because Redis is the config trust boundary. | +| Redis | Plain, TLS, or mTLS via `--redis-mode`. Use TLS or mTLS anywhere Redis crosses a trust zone — Redis is the config trust boundary. | + +CORS is currently wide open (any origin, method, and header). The API is bearer-token based and cookie-free, so CSRF does not apply, but expect this to tighten as policy work lands. -CORS is currently wide open (any origin, method, and header). The API is -bearer-token based and cookie-free, so cross-site request forgery does not -apply, but expect this to tighten as policy work lands. +## Local Bootstrap Helpers (`with_tools`) -## Local Bootstrap Helpers +The `contextforge-data-plane-lib/with_tools` feature compiles in: +- `/contextforge-rs/admin/tokens/{user}` +- `/contextforge-rs/admin/userconfigs/{user}` +- `/contextforge-rs/health` -The `contextforge-data-plane-lib/with_tools` feature compiles in -`/contextforge-rs/admin/tokens/{user}`, -`/contextforge-rs/admin/userconfigs/{user}`, and `/contextforge-rs/health`. -These routes are registered outside the authentication middleware, so token -minting and config writes are 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. +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 -- The HMAC secret is held as a `SecretString`; key and certificate material is - read from disk paths at startup. -- Log hygiene is a standing rule: never log tokens, authorization headers, - secrets, Redis key/value bytes, full `UserConfig` documents, or backend - credentials. +- The HMAC secret is held as a `SecretString`; key and certificate material is read from disk paths at startup. +- Never log: tokens, authorization headers, secrets, Redis key/value bytes, full `UserConfig` documents, or backend credentials. diff --git a/docs/book/README.md b/docs/book/README.md deleted file mode 100644 index 558e6073..00000000 --- a/docs/book/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# Developing The ContextForge Data Plane Book - -This directory contains the mdBook source for The ContextForge Data Plane Book. -The rendered book also documents its own publishing path in -[Publishing This Book](src/publishing-this-book.md); keep the two in sync when -the workflow or mdBook version changes. - -## Layout - -```text -docs/book/ - book.toml mdBook configuration - README.md contributor notes for this book - src/ - SUMMARY.md chapter order and sidebar structure - *.md rendered book chapters - book/ generated HTML output, ignored by git -``` - -Keep book source in `src/`. Do not edit generated files under `docs/book/book/`. - -## Install mdBook - -The GitHub Pages workflow installs `mdbook v0.5.3`, so local development should -use the same version: - -```bash -cargo install mdbook --version 0.5.3 --locked -``` - -Check the installed version: - -```bash -mdbook --version -``` - -## Render Locally - -Build the static HTML: - -```bash -mdbook build docs/book -``` - -The output is written to: - -```text -docs/book/book/ -``` - -Serve the book with live rebuilds: - -```bash -mdbook serve docs/book --hostname 127.0.0.1 --port 3000 -``` - -Then open: - -```text -http://127.0.0.1:3000 -``` - -Use `--open` if you want mdBook to open the browser: - -```bash -mdbook serve docs/book --hostname 127.0.0.1 --port 3000 --open -``` - -## Validate Changes - -Run these before pushing book changes: - -```bash -mdbook build docs/book -mdbook test docs/book -git diff --check -``` - -`mdbook test` runs Rust code blocks as tests. For prose-only pages, it still -checks that mdBook can parse and walk every chapter in `SUMMARY.md`. - -## Add Or Rename A Chapter - -1. Add the Markdown file under `docs/book/src/`. -2. Add it to `docs/book/src/SUMMARY.md` in the intended reading order. -3. Run `mdbook build docs/book`. -4. Run `mdbook test docs/book`. - -The chapter order in `SUMMARY.md` is the reader's numbered path through the -book. Keep that order intentional. - -## Draft Chapters - -Use this marker for pages that are intentionally present but not implemented: - -```markdown -> Status: draft. To be implemented. -``` - -Follow it with a `## To implement` section and concrete bullets. That keeps the -book navigable while making unfinished work obvious. - -## Publishing - -The workflow at `.github/workflows/pages.yml` builds the book on pull requests -that touch book files and deploys on pushes to `main`. - -Publishing expects GitHub Pages to use `GitHub Actions` as the repository's -Pages source. The workflow uploads `docs/book/book` as the Pages artifact. diff --git a/docs/book/book.toml b/docs/book/book.toml deleted file mode 100644 index 554b6372..00000000 --- a/docs/book/book.toml +++ /dev/null @@ -1,10 +0,0 @@ -[book] -authors = ["ContextForge Data Plane maintainers"] -language = "en" -src = "src" -title = "The ContextForge Data Plane Book" -description = "Architecture, configuration, and operations guide for contextforge-data-plane, the Rust MCP dataplane that presents many backend MCP servers as one gateway endpoint." - -[output.html] -git-repository-url = "https://github.com/contextforge-org/contextforge-data-plane" -edit-url-template = "https://github.com/contextforge-org/contextforge-data-plane/edit/main/docs/book/{path}" diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md deleted file mode 100644 index 05c1583a..00000000 --- a/docs/book/src/SUMMARY.md +++ /dev/null @@ -1,32 +0,0 @@ -# The ContextForge Data Plane Book - -- [🌉 What is ContextForge Data Plane?](what-is-contextforge-data-plane.md) -- [🚀 Getting Started](usage.md) - - [Run the Gateway Locally](running-the-gateway.md) - - [Configuration Reference](gateway-options.md) -- [🏗️ Architecture](architecture.md) - - [System Shape](system-shape.md) - - [Request Flow](request-flow.md) - - [Concurrency And Runtime Model](concurrency-and-runtime.md) - - [Authentication And User Config Lookup](authentication-and-user-config.md) - - [Security Model And Trust Boundaries](security-model.md) - - [Runtime Configuration](runtime-configuration.md) - - [Control-Plane Integration](control-plane-integration.md) - - [Backend Connections And Transports](backend-connections-and-transports.md) - - [Session Ownership](session-ownership.md) - - [Architectural Choices](architectural-choices.md) -- [🔌 MCP Behavior](mcp-behavior.md) - - [MCP Method Reference](mcp-method-reference.md) - - [Capability Flow](capability-flow.md) - - [MCP Routing Semantics](mcp-routing-semantics.md) -- [🧭 Operations](operations.md) - - [Plugins And Policy](plugins-and-policy.md) - - [Telemetry And Diagnostics](telemetry-and-diagnostics.md) - - [Failure Modes](failure-modes.md) - - [Testing](testing.md) - - [Local Docker Stack](local-docker-stack.md) - - [Performance](performance.md) - - [Deployment Notes](deployment-notes.md) -- [🛠️ Project](project.md) - - [Contributing To The Gateway](contributing.md) - - [Publishing This Book](publishing-this-book.md) diff --git a/docs/book/src/architectural-choices.md b/docs/book/src/architectural-choices.md deleted file mode 100644 index ae804b3a..00000000 --- a/docs/book/src/architectural-choices.md +++ /dev/null @@ -1,190 +0,0 @@ -# Architectural Choices - -> 🧱 **Design rule:** these choices are not permanent, but changing one should -> be a deliberate architecture decision with code, tests, and migration notes. - -![Architectural choices](assets/architectural-choices.svg) - -This page records the choices that should stay visible as the gateway evolves. -They describe the shape of the current Rust dataplane, not just preferences. - -## Choice Matrix - -| Choice | Current decision | Why it matters | -| --- | --- | --- | -| Dataplane, not control plane | This repo consumes runtime config and handles traffic. It does not own UI, IAM lifecycle, management APIs, or durable observability storage. | Keeps the hot path small and prevents product workflows from leaking into request routing. | -| Modern downstream MCP only | The target dataplane contract is MCP `2026-07-28` over Streamable HTTP. The control plane serves older versions and SSE without routing them through the dataplane. | Keeps legacy negotiation and transport compatibility out of the hot dataplane. | -| Config access is abstracted | MCP routing depends on `UserConfig`, `VirtualHost`, and `UserConfigStore`, not Redis commands. | Keeps future xDS/gRPC or another config stream possible. | -| Backend names are public | The backend map key is part of tool/resource/prompt names. | Backend renames are client-visible behavior changes. | -| Sessions are local today | Backend RMCP services live in `BackendTransports` inside one process. | Load-balanced deployments need sticky routing or a new session ownership design. | -| Merged MCP semantics define the contract | The client sees one gateway MCP server with namespaced backend objects. | Backend topology should not become a hard client dependency beyond the namespace contract. | -| Plugin boundaries stay explicit | Tool pre/post hooks are integrated at known points around backend invocation. | Payload mutation needs clear failure, timeout, cancellation, and telemetry behavior. | - -## Dataplane, Not Control Plane - -The gateway should enforce decisions already made elsewhere: - -```text -control plane authors config and policy - -> Redis/config transport exposes runtime data - -> Rust dataplane enforces it on MCP traffic -``` - -New features should start with one question: - -```text -Is this hot-path enforcement, or is this a management workflow? -``` - -If it is a workflow, it probably belongs outside this repo. The Rust gateway -should enforce the result of management decisions, not become the place where -those decisions are authored. - -## Modern Downstream MCP Only - -The dataplane is moving to one downstream protocol contract: - -```text -MCP 2026-07-28 - -> Streamable HTTP - -> server/discover - -> required per-request client context -``` - -The Rust gateway should not grow adapters for older MCP versions, legacy -session initialization, or SSE. The external control plane serves those -clients on control-plane routes. Legacy traffic does not enter the dataplane; -only clients using the modern contract are routed here. - -Some current code and diagrams still describe session-oriented implementation -details. Treat them as migration inventory. Replace them with the modern -protocol path rather than preserving them as public compatibility behavior. - -## Config Access Is Abstracted - -Redis is the current storage and transport adapter. It is not the routing -model. The routing model is: - -```text -UserConfig - -> VirtualHost - -> BackendMCPGateway -``` - -That is why request code should stay behind `UserConfigStore`. Redis key -encoding, MessagePack, cache expiry, and retry settings belong in the adapter, -not in MCP method handling. - -## Backend Names Are Conditionally Public - -Backend map keys are visible in the MCP namespace when multiple backends need -disambiguation: - -```text -backend map key: gateway-one -backend tool: increment -gateway tool: gateway-one-increment -``` - -For a single backend, upstream identifiers pass through unchanged. Explicit -`tool_name_aliases` published by the control plane take precedence in either -case. Otherwise the map key, not `BackendMCPGateway.name`, is the namespace -used by multi-backend routing. - -## Session State Is Local Today - -Backend services are not just data. They are live RMCP running services stored -under: - -```text -principal + backend_name + downstream_session_id -``` - -That makes the current state model fast and direct, but not horizontally -portable. Do not design request handling as if every node can serve every -stateful MCP session until backend service ownership has an external owner or -can be rebuilt safely. - -## Merged MCP Semantics Define The Contract - -The downstream client should reason about one MCP server: - -```text -client - -> ContextForge Data Plane - -> merged tools/resources/prompts -``` - -Backend identity appears only when namespacing is needed, and clients should -not need to know transport details, Redis storage, fanout mechanics, or plugin -runtime internals. - -This choice leaves room for filtering, policy, and route changes without -turning every backend topology change into a client integration change. - -## Plugin Boundaries Stay Explicit - -Plugins can inspect or mutate payloads. That is more powerful than a header -filter, so the hook points need to stay obvious. - -Current CPEX support is intentionally narrow: - -| Hook | Current boundary | -| --- | --- | -| `TOOL_PRE_INVOKE` | Runs before `call_tool` forwards to the selected backend. | -| `TOOL_POST_INVOKE` | Runs after `call_tool` receives a backend result and on backend progress events. | - -Avoid adding ad hoc plugin calls in the middle of routing code. If a new hook -is needed, define its ownership, failure behavior, timeout behavior, -cancellation behavior, streaming behavior, and telemetry attribution. - -## Transport Security Is Split - -Downstream TLS is listener-level process config. Upstream backend security is -also process config today, but some of it is naturally backend-specific. - -Keep this split visible: - -| Concern | Stable owner | -| --- | --- | -| Gateway listener certificate | Process config. | -| JWT verification keys | Process config. | -| Backend URL, auth headers, pass-through policy, allowed objects | Runtime user config. | -| Backend-specific trust and client identity | Likely runtime config or referenced secret material over time. | - -The gateway should not bury transport security decisions inside MCP method -handlers. They should remain either startup assembly or explicit backend -transport construction. - -## MCP-First, Not MCP-Only - -The current code implements MCP behavior, but the shell is broader: - -```text -auth -config lookup -transport setup -plugin runtime -telemetry -session strategy -``` - -Keep protocol-neutral concerns reusable. Future A2A or model-provider routing -should be able to reuse the gateway shell without copying the MCP routing -stack. - -## When A Choice Changes - -Changing one of these choices should update more than one file. - -| Change | Expected follow-through | -| --- | --- | -| Downstream MCP version changes | Coordinate with the control plane, update modern protocol tests and examples, and keep legacy traffic on control-plane routes. | -| Backend namespace changes | Update merge logic, split logic, tests, docs, and migration notes. | -| Session state moves external | Update `SessionManager`, cleanup behavior, load-balancing docs, and failure-mode tests. | -| Config transport changes | Keep `UserConfigStore` as the boundary and update adapter tests. | -| Plugin hook surface expands | Document ordering, failure behavior, cancellation, streaming, and telemetry. | -| New protocol joins the gateway | Keep shared shell code protocol-neutral and isolate protocol-specific routing. | - -These pages are part of that safety net: they make architecture drift visible -before it becomes accidental API behavior. diff --git a/docs/book/src/architecture.md b/docs/book/src/architecture.md deleted file mode 100644 index 7511f8a0..00000000 --- a/docs/book/src/architecture.md +++ /dev/null @@ -1,25 +0,0 @@ -# Architecture - -This section explains how the gateway is put together and why the main -boundaries exist. - -> 🧭 **Read this when changing the hot path.** The architecture pages keep -> request handling, config lookup, backend sessions, transports, and -> control-plane boundaries explicit. - -The pages are ordered for a first read: start at the top for the big picture, -then work down into each boundary. If you are changing one area, jump straight -to its page. - -| Page | What it covers | -| --- | --- | -| 🧭 [System Shape](system-shape.md) | The gateway's role in ContextForge, its crate layout, and the line between dataplane and control plane. | -| 🔀 [Request Flow](request-flow.md) | The ordered path from downstream HTTP request to backend MCP call and merged response. | -| 🧵 [Concurrency And Runtime Model](concurrency-and-runtime.md) | Executor shapes, shared state and locks, fanout, cancellation, and the allocator. | -| 🔐 [Authentication And User Config Lookup](authentication-and-user-config.md) | How JWT claims, Redis-backed user config, and virtual host selection combine before routing. | -| 🔒 [Security Model And Trust Boundaries](security-model.md) | What the gateway trusts, what compromise of each boundary means, and transport security posture. | -| 🗂️ [Runtime Configuration](runtime-configuration.md) | The current `UserConfig` model, MessagePack Redis persistence, cache behavior, and expected growth. | -| 🤝 [Control-Plane Integration](control-plane-integration.md) | The current, still-provisional integration surface: Redis keys, schemas, token shape, and route parity. | -| 🔌 [Backend Connections And Transports](backend-connections-and-transports.md) | Downstream listeners, upstream RMCP transports, config-store transport, and TLS direction. | -| 🧵 [Session Ownership](session-ownership.md) | How backend services are keyed, shared, cleaned up, and constrained by local process ownership. | -| 🧱 [Architectural Choices](architectural-choices.md) | The main tradeoffs behind dataplane scope, namespacing, config boundaries, and future protocols. | diff --git a/docs/book/src/assets/architectural-choices.svg b/docs/book/src/assets/architectural-choices.svg deleted file mode 100644 index ff098acf..00000000 --- a/docs/book/src/assets/architectural-choices.svg +++ /dev/null @@ -1,83 +0,0 @@ - - Architectural choices - The gateway keeps control plane concerns out, abstracts config access, treats backend names as public namespace, keeps sessions local today, exposes merged MCP semantics, and keeps plugin boundaries explicit. - - - - - - - - - Rust gateway dataplane - hot-path MCP enforcement - auth, config lookup, routing, hooks, telemetry - - - - - Not control plane - consume config, do not author it - - - Config behind traits - Redis is adapter, not model - - - Backend names public - map key becomes MCP namespace - - - - - Merged MCP contract - one gateway server view - - - Sessions local today - sticky routing or redesign needed - - - Explicit plugin hooks - known ordering and failure behavior - - - - - - - - - - - - - - - - - - - - - - - - - Changing a choice requires code, tests, docs, and migration notes. - - diff --git a/docs/book/src/assets/auth-user-config.svg b/docs/book/src/assets/auth-user-config.svg deleted file mode 100644 index ce67cd20..00000000 --- a/docs/book/src/assets/auth-user-config.svg +++ /dev/null @@ -1,122 +0,0 @@ - - Authentication and user config lookup - The gateway extracts the virtual host id, validates a bearer JWT into ContextForgeClaims, reads the MCP session id if present, loads UserConfig by JWT subject through the cache and Redis, rejects unknown virtual hosts with 404, and exposes typed extensions to MCP validators. - - - - - - - - - HTTP request - path + bearer JWT - optional Mcp-session-id - - - - - virtual_host_id_layer - insert VirtualHostId - - - claims_layer - validate issuer, audience, exp - insert ContextForgeClaims - - - session_id_layer - insert SessionId if present - - - - - user_config_store_layer - claims.sub -> User::new - load and insert UserConfig - - - - - LRU cache - 50k entries, 60s default - keyed by claims.sub string - - - Redis - MessagePack key/value - User -> UserConfig - - - - - MCP validators - VirtualHostId + UserConfig - SessionId when present - - - - - - - - - - - - - - claims - - - - - - - - - - - cache hit - - - - - - cache miss - - - - - - - - 401 auth - - - - 400/500 config - - - diff --git a/docs/book/src/assets/backend-transports.svg b/docs/book/src/assets/backend-transports.svg deleted file mode 100644 index 6964793d..00000000 --- a/docs/book/src/assets/backend-transports.svg +++ /dev/null @@ -1,99 +0,0 @@ - - Backend connections and transports - The gateway separates downstream TCP or TLS listener transport, upstream reqwest and RMCP streamable HTTP backend transport, and Redis config-store transport. - - - - - - - - - MCP client - streamable HTTP - front door or direct caller - - - - - Gateway dataplane - - - downstream listeners - TCP and optional Rustls TLS - - - Axum + RMCP service - middleware, validators, routing - - - upstream client boundary - reqwest + streamable HTTP - - - - - Redis config store - plain, TLS, or mTLS - UserConfig and plugin config - - - Backend MCP servers - streamable HTTP today - HTTPS-only by default - mTLS supported by process config - - - - - - TCP/TLS - - - - config lookup - - - - UserConfig - - - - reqwest client - - - - MCP response - - - - - Boundary rule: - listener setup, backend transport creation, and Redis config access should stay in separate modules. - - diff --git a/docs/book/src/assets/gateway-overview.svg b/docs/book/src/assets/gateway-overview.svg deleted file mode 100644 index 3bb1ec1f..00000000 --- a/docs/book/src/assets/gateway-overview.svg +++ /dev/null @@ -1,88 +0,0 @@ - - ContextForge Data Plane request and response path - An MCP client sends streamable HTTP traffic to the ContextForge Data Plane. The data plane validates the caller, loads runtime configuration, calls backend MCP servers, receives backend responses, merges them, and returns one MCP response or stream to the client. - - - - - - - - - MCP client - streamable HTTP - JWT + session id - - - - - - MCP request - - - - merged response - - - - - ContextForge Data Plane - - - validate caller - - - load runtime config - - - route and merge MCP methods - - - - - Redis runtime config by JWT subject - - - - - - - - backend calls - - - - backend responses - - - - - Backend MCP servers - - - gateway-one - - - gateway-two - - - more backends - - - - - one logical MCP server downstream - - diff --git a/docs/book/src/assets/request-flow.svg b/docs/book/src/assets/request-flow.svg deleted file mode 100644 index 573ed4d9..00000000 --- a/docs/book/src/assets/request-flow.svg +++ /dev/null @@ -1,230 +0,0 @@ - - ContextForge Data Plane request flow - A normal MCP HTTP request enters the TCP or TLS listener, passes through metrics, tracing, the contextforge nested router, CORS, virtual host extraction, claims validation, session extraction, user config lookup, a virtual host config check, and RMCP. RMCP then follows either initialize handling or authorized MCP method handling, calls backend MCP servers as needed, and returns the response through the same stack. - - - - - - - - - MCP client - streamable HTTP - - - TCP/TLS - listener transport - - - Metrics - HttpMetricsLayer - - - Tracing - TraceLayer - - - Nested router - /contextforge-rs - - - - - - - - - - - - - - Inner Axum request order - normal MCP request after /contextforge-rs nesting - - - CORS layer - may answer preflight before MCP handling - - - virtual_host_id_layer - extracts /servers/{virtual_host_id}/mcp - inserts VirtualHostId - - - claims_layer - validates Authorization: Bearer token - inserts ContextForgeClaims - - - session_id_layer - reads Mcp-session-id when present - inserts SessionId for authorized calls - DELETE cleanup happens on successful response - - - user_config_store_layer - loads UserConfig for claims.sub - inserts UserConfig; unknown virtual host gets 404 - - - - - - - - - - - - 400 - - 401 - - 400/404/500 - - - - - - request enters inner router - - - - - RMCP service - StreamableHttpService - creates or reuses McpService - handler reads typed extensions - - - - - - - - initialize path - - - InitializeCallValidator - DownstreamSessionId - UserConfig + VirtualHostId + claims - - - resolve selected VirtualHost - - - read local user session mapping - - - join_all over configured backends - StreamableHttpClientTransport - GatewayBackendClient::serve - - - set session mapping, then store BackendTransports - - - - - authorized MCP calls - - - AuthorizedCallValidator - SessionId - UserConfig + VirtualHostId + claims - - - list_tools / list_resources / list_prompts - borrow transports -> fan_out_list -> namespace + sort - - - call_tool - split prefix -> resolve backend -> hooks - backend call -> response - - - read_resource / get_prompt / complete - split prefix -> resolve backend -> strip prefix -> call - - ping, subscribe, unsubscribe are local today - - - - - - initialize - - - - post-init calls - - - - - Backend - MCP - servers - initialize - list/call/read/get - progress events - - - - backend init - - - - capabilities - - - - routed call - - - - result/progress - - - - - HTTP response unwinds stack - successful DELETE removes local session and backend transports - - - - - - - - - response to client - - diff --git a/docs/book/src/assets/runtime-config.svg b/docs/book/src/assets/runtime-config.svg deleted file mode 100644 index c22d4a23..00000000 --- a/docs/book/src/assets/runtime-config.svg +++ /dev/null @@ -1,115 +0,0 @@ - - Runtime configuration surfaces - Process configuration is loaded at startup. UserConfig is loaded per request by JWT subject and virtual host id. RuntimePluginConfigDocument is loaded from Redis when CPEX plugins are enabled and can be reloaded by the watcher. - - - - - - - - - Process Config - CLI + env at startup - listeners, JWT keys, Redis, TLS - telemetry, runtime shape, plugins - - - - - UserConfig - Redis MessagePack - key: User::new(claims.sub) - value: UserConfig - - - - - Plugin Config - RuntimePluginConfigDocument - JSON or MessagePack - CPEX pre/post tool hooks - - - - - Gateway dataplane - - - startup assembly - - - request middleware - claims.sub -> UserConfig - - - MCP validators - VirtualHostId -> VirtualHost - - - optional CPEX runtime - - - - - Selected VirtualHost - backend map key is namespace - backend URL builds upstream transport - - - CPEX Runtime - loaded at startup - watcher reloads every 10 minutes - - - - - - startup - - - - per request - - - - select route - - - - plugin document - - - - active hooks - - - - reload swaps runtime state - - diff --git a/docs/book/src/assets/session-ownership.svg b/docs/book/src/assets/session-ownership.svg deleted file mode 100644 index 67783238..00000000 --- a/docs/book/src/assets/session-ownership.svg +++ /dev/null @@ -1,106 +0,0 @@ - - Session ownership - Initialize creates local backend MCP running services keyed by principal, backend name, and downstream session id. Later calls use Mcp-session-id to find them, and DELETE removes local state. - - - - - - - - - MCP client - initialize - then Mcp-session-id - - - - - Gateway process - - - RMCP LocalSessionManager - creates downstream session id - - - SessionManager - principal + session + virtual host - - - BackendTransports - principal + backend + session - Arc<RunningService> - - - - - LocalUserSessionStore - local LRU mapping - Redis store exists, not wired by default - - - Backend MCP services - one running service per backend - local process ownership - - - - - DELETE cleanup removes local entries - - - - - - initialize - - - - session mapping - - - - create services - - - - store handles - - - - later calls - - - - successful DELETE - - diff --git a/docs/book/src/assets/system-shape.svg b/docs/book/src/assets/system-shape.svg deleted file mode 100644 index f783a1c6..00000000 --- a/docs/book/src/assets/system-shape.svg +++ /dev/null @@ -1,105 +0,0 @@ - - ContextForge Data Plane system shape - The ContextForge control plane writes runtime config into Redis. MCP clients call the Rust gateway dataplane. The gateway loads config, validates identity, calls backend MCP servers, receives backend responses, merges them, and returns one downstream response. - - - - - - - - - Control plane - management, UI, IAM - config and policy authoring - - - - - MCP client - streamable HTTP - JWT + MCP session - - - - - Rust gateway dataplane - one logical MCP server downstream - - - Axum listener stack - - - auth + request context - - - UserConfig lookup - - - MCP fanout + merge - - - hooks + telemetry - - - - - Redis config - UserConfig by JWT subject - plugin runtime config - - - - - Backend MCP - servers - tools, resources, prompts - per-backend sessions - - - - - - writes runtime config - - - - loads config - - - - - - MCP request - - - - merged response - - - - - - backend calls - - - - backend responses - - diff --git a/docs/book/src/authentication-and-user-config.md b/docs/book/src/authentication-and-user-config.md deleted file mode 100644 index 8ec177b4..00000000 --- a/docs/book/src/authentication-and-user-config.md +++ /dev/null @@ -1,143 +0,0 @@ -# Authentication And User Config Lookup - -> 🔐 **Boundary:** authentication proves who is calling. User config lookup -> decides which virtual hosts and backends that caller can reach. - -![Authentication and user config lookup](assets/auth-user-config.svg) - -This page follows the identity boundary in the request path. The gateway does -not let an MCP method choose arbitrary backend URLs. It validates the bearer -token, loads the caller's `UserConfig`, and only then lets MCP validators select -a virtual host from that config. - -## Request Order - -Authentication and config lookup happen before `McpService` handles the MCP -method: - -| Step | Code | Output | -| --- | --- | --- | -| Path context | `virtual_host_id_layer` | `VirtualHostId` extension. | -| JWT validation | `claims_layer` | `ContextForgeClaims` extension. | -| Session header | `session_id_layer` | Optional `SessionId` extension. | -| Config lookup | `user_config_store_layer` | `UserConfig` extension. | -| Virtual host check | `virtual_host_config_layer` | `404` when the path's virtual host id is not in the loaded config. | -| MCP validation | `InitializeCallValidator` or `AuthorizedCallValidator` | Selected `VirtualHost`, session id, and claims. | - -The order matters: `user_config_store_layer` needs `ContextForgeClaims`, and MCP -validators need both `UserConfig` and `VirtualHostId`. - -## Token Validation - -`claims_layer` reads `Authorization: Bearer ...` and decodes the JWT with the -algorithm declared in the JWT header. - -| Token property | Current behavior | -| --- | --- | -| Algorithm | Accepts RS256/RS384/RS512 when an RSA public key is configured, or HS256/HS384/HS512 when a shared secret is configured. | -| Issuer | Must match `mcpgateway`. | -| Audience | Must match `mcpgateway-api`. | -| Expiration | `exp` is validated. | -| Unsupported algorithm | Rejected before claims are inserted. | - -The decoded value is stored as `ContextForgeClaims`. The fields currently -important to routing are: - -| Claim | Routing role | -| --- | --- | -| `sub` | Becomes the user config key and the principal for backend session lookup. | -| `iss`, `aud`, `exp` | Authentication checks only. | -| `jti`, `token_use`, `iat`, `teams`, `user`, `scopes` | Carried in claims for future policy use; not currently used by MCP routing. `token_use`, `iat`, `teams`, and `scopes` are optional, as is `user.full_name`, so tokens without those fields still validate. | - -A concrete decoded payload for a local UUID subject looks like this (timestamps -shown as example Unix seconds). Of everything here, MCP routing depends only on -`sub` today; the email remains human-readable metadata. The optional fields are -included for illustration: - -```json -{ - "iss": "mcpgateway", - "aud": "mcpgateway-api", - "sub": "11111111-1111-1111-1111-111111111111", - "exp": 1717180800, - "iat": 1717177200, - "jti": "example-token", - "token_use": "api", - "teams": ["team_awesome"], - "user": { - "email": "admin@example.com", - "full_name": "API Token User", - "is_admin": true, - "auth_provider": "api_token" - }, - "scopes": { - "server_id": "my_id", - "permissions": ["tools.read", "servers.use"], - "ip_restrictions": ["192.169.1.0/24"], - "time_restrictions": null - } -} -``` - -## User Config Key - -`user_config_store_layer` turns the subject into a typed key: - -```text -ContextForgeClaims.sub - -> User::new(subject) - -> UserConfigStore::get_config(&user) -``` - -The Redis adapter serializes that `User` key with MessagePack. The key includes -both the key type and the subject, so user config data is not just stored under -the raw subject string. - -## Cache And Redis Lookup - -`RedisUserConfigStore` checks an in-process LRU cache before going to Redis: - -| Stage | Behavior | -| --- | --- | -| LRU hit | Clone the decoded `UserConfig` from the cache. | -| LRU miss | MessagePack-encode `User::new(subject)`, `GET` that Redis key, decode the MessagePack `UserConfig`, then cache it. | -| Cache size | 50,000 entries. | -| Cache expiry | `--user-config-cache-expiry-seconds`, default 60 seconds. `0` disables the cache and reads Redis on every request. | -| Redis retry setting | Connection manager is configured with 1,000 retries. | - -The cache is an implementation detail of `RedisUserConfigStore`. Routing code -depends on `UserConfigStore`, not Redis commands. - -## Failure Behavior - -Failures before RMCP method handling are HTTP responses: - -| Failure | Response | -| --- | --- | -| Missing `Authorization` header | `401 Unauthorized`. | -| Header does not start with `Bearer ` | `401 Unauthorized`. | -| JWT header or body cannot be decoded | `401 Unauthorized`. | -| JWT uses an unsupported algorithm | `401 Unauthorized`. | -| Required decoder key or secret is not configured | `401 Unauthorized`. | -| No user config exists for `claims.sub` | `400 Bad Request`. | -| Redis/config store error other than missing data | `500 Internal Server Error`. | -| `user_config_store_layer` runs without claims | `400 Bad Request`. | -| Virtual host id not present in the caller's config | `404 Not Found` with body `{"detail":"Server not found"}`. | - -A valid user config can still fail a request: `virtual_host_config_layer` -returns `404` before MCP method handling when the config does not contain the -path's `VirtualHostId`. - -## What This Boundary Does Not Do - -Authentication and user config lookup do not route to a backend by themselves. -They only establish: - -```text -caller identity - + caller UserConfig - + requested VirtualHostId -``` - -`McpService` still has to validate the MCP call, resolve the virtual host, and -choose either the initialize path, routed backend path, or local method path. diff --git a/docs/book/src/backend-connections-and-transports.md b/docs/book/src/backend-connections-and-transports.md deleted file mode 100644 index fb91f679..00000000 --- a/docs/book/src/backend-connections-and-transports.md +++ /dev/null @@ -1,134 +0,0 @@ -# Backend Connections And Transports - -> 🚚 **Transport boundary:** downstream listener traffic, upstream backend -> traffic, and config-store traffic are separate concerns. Keep them separate -> even when they all use TCP underneath. - -![Backend connections and transports](assets/backend-transports.svg) - -The gateway has three transport classes on the hot path. They are built in -different modules, configured from different fields, and serve different -architecture roles. - -## Transport Classes - -| Transport class | Current implementation | Main owner | Purpose | -| --- | --- | --- | --- | -| Downstream listener | Axum/Hyper over TCP and optional Rustls TLS. | `transports/` and `Gateway::run_gateway`. | Accept MCP streamable HTTP traffic from clients or the front door. | -| Upstream backend | Shared `reqwest::Client` plus RMCP `StreamableHttpClientTransport`. | `common.rs`, `gateway/mcp_service/initialization.rs`, and `gateway/backend_transports.rs`. | Open MCP client sessions to configured backend MCP servers. | -| Config store | Redis plain, TLS, or mTLS connection manager. | `common.rs` and `user_config_store/`. | Load `UserConfig` and plugin runtime config from control-plane authored storage. | - -The current MCP dataplane only uses streamable HTTP for backend MCP traffic. -`BackendMCPGateway.transport` already has `STREAMABLEHTTP`, `SSE`, and `STDIO`, -but upstream routing does not branch on that field yet. - -## Downstream Listeners - -`Gateway::run_gateway` builds one Axum router and can expose it through TCP, -TLS, or both. - -| Listener | Config fields | Behavior | -| --- | --- | --- | -| TCP | `address` | Binds a Tokio `TcpSocket`, sets reuse options and keepalive, listens with backlog `1024`, and serves Axum with graceful shutdown on `ctrl_c`. | -| TLS | `tls_address`, `server_certificate`, `server_private_key` | Builds a Rustls server config, accepts TLS by hand, then serves the same Axum router through Hyper. | - -TLS listener setup has two important constraints: - -| Constraint | Why | -| --- | --- | -| `tls_address` requires both certificate and private key. | The listener cannot build a Rustls server config without both. | -| `tls_address` cannot equal `address`. | TCP and TLS cannot bind the same socket in this process. | - -The downstream TLS listener currently uses `with_no_client_auth()`. Client -identity is established by the gateway's bearer JWT layer, not by downstream -mTLS. - -## Upstream Backend Client - -The upstream HTTP client is built once at gateway startup: - -```text -Config - -> reqwest::Client::try_from(&config) - -> clone per backend initialize task - -> StreamableHttpClientTransport::with_client(...) -``` - -The process-level upstream mode controls whether backend URLs may use plain -HTTP, HTTPS, or HTTPS with client identity: - -| Mode | `reqwest` behavior | -| --- | --- | -| unset | `https_only(true)`. Same as `TlsOnly`. | -| `TlsOnly` | HTTPS backends only. | -| `PlainTextOrTls` | HTTP or HTTPS backends. | -| `PlainTextOrMTls` | HTTP or HTTPS backends, with a client identity configured for TLS handshakes. | -| `MtlsOnly` | HTTPS backends only, with a client identity configured for TLS handshakes. | - -If `upstream_trust_bundle` is configured, the PEM bundle is merged into the -client's TLS trust roots. For mTLS modes, the upstream certificate and private -key are read from disk and combined into a `reqwest::Identity`. - -## Backend MCP Transport - -During `initialize`, the selected virtual host fans out to every configured -backend: - -```text -VirtualHost.backends - -> for each backend URL - -> build StreamableHttpClientTransportConfig - -> serve GatewayBackendClient over StreamableHttpClientTransport - -> store running service in BackendTransports -``` - -For HTTPS backend URLs, the gateway sets a `Host` header from the backend URL host and optional port. -After that, `apply_header_config` runs the backend's header config in order: passthrough named downstream -headers, inject static `add_headers`, then strip `remove_headers`. Protected headers are silently skipped -in all three phases: body-framing (`Content-Length`, `Content-Type`), hop-by-hop (`Connection`, -`Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `Proxy-Connection`, `TE`, `Trailer`, -`Trailers`, `Transfer-Encoding`, `Upgrade`), and RMCP-reserved (`Mcp-Session-Id`, `Accept`, -`Last-Event-Id`). The gateway-managed `Host` is likewise protected and never altered by config. - -Backend connection failures are not fatal to the whole initialize call. The -gateway stores the backend entry with no running service, so list calls can -continue with available backends and routed calls to that backend can fail -locally. - -## Config-Store Transport - -Redis is the current config-store transport. It is used for user config and, -when runtime plugins are enabled, plugin runtime config. - -| Redis mode | Connection behavior | -| --- | --- | -| `PlainText` | Connects to `host:port` over TCP. | -| `Tls` | Connects with `rediss://host:port` and a required trust bundle. | -| `Mtls` | Connects with `rediss://host:port`, required trust bundle, required client certificate, and required client key. | - -The Redis user config adapter stores: - -```text -MessagePack(User::new(claims.sub)) -> MessagePack(UserConfig) -``` - -The Redis connection manager is configured with `1,000` retries. The adapter -keeps an in-process LRU cache in front of Redis, but routing code should only -depend on the `UserConfigStore` trait. - -## What Should Move To Runtime Config - -Transport security is mostly process config today. That keeps startup simple, -but it is not the final shape for every backend-specific decision. - -| Setting | Today | Better long-term owner | -| --- | --- | --- | -| Downstream TLS certificate | Process config. | Process config. It belongs to the gateway listener. | -| Upstream trust bundle and mTLS identity | Process config. | Runtime config per backend or referenced secret material. | -| Backend auth headers | Delegated to `passthrough_headers` / `add_headers` / `remove_headers` in `BackendMCPGateway`. | — | -| Backend transport type | Model field exists, not routed yet. | Runtime config per backend. | -| Header pass-through policy | Implemented via `passthrough_headers`, `add_headers`, `remove_headers` on `BackendMCPGateway`. Headers are session-scoped (snapshotted at initialize); request-scoped propagation is future work. | — | - -The boundary to preserve is simple: listener code should not know Redis schema, -MCP routing code should not know Redis command details, and backend transport -creation should stay behind a small, explicit upstream boundary. diff --git a/docs/book/src/capability-flow.md b/docs/book/src/capability-flow.md deleted file mode 100644 index f38b0ae0..00000000 --- a/docs/book/src/capability-flow.md +++ /dev/null @@ -1,135 +0,0 @@ -# Capability Flow - -> **Migration note:** this page documents the current `initialize` capability -> path. The target downstream contract is MCP `2026-07-28`, where -> `server/discover` replaces this client-facing lifecycle. - -This page explains where MCP capabilities come from during `initialize`, how -the gateway stores them, and what the downstream client sees today. - -## Short Answer - -Upstream capabilities come from each backend's own `InitializeResult`. The -gateway captures them after opening the RMCP client service for that backend, -stores them with the backend transport state, and then separately builds one -downstream `InitializeResult` for the client. - -Today, the downstream response is gateway-defined. It is not a direct pass -through of one backend; it is a gateway-aware merge of backend capability -families the gateway can route. - -## Initialize Sequence - -`McpService::initialize` in -`crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs` -runs this capability-related flow: - -1. The gateway validates the call and resolves the selected virtual host. -2. It iterates over `virtual_host.backends` and starts one upstream RMCP client - service per configured backend. -3. Each upstream service performs that backend's `initialize` handshake. -4. After the service is running, the gateway reads the backend-advertised - capabilities from `rs.peer().peer_info().capabilities`. -5. It stores those capabilities inside `BackendTransportService` together with - the running backend service. -6. It returns a separate downstream `InitializeResult` to the caller. - -The code path that reads the upstream capabilities is: - -```rust,ignore -let server_capabilities = running_service - .as_ref() - .and_then(|rs| rs.peer().peer_info().as_ref().map(|pi| pi.capabilities.clone())); -``` - -Those values are then stored here: - -```rust,ignore -BackendTransportService::from((server_capabilities, running_service.map(Arc::new))) -``` - -## Where The Capabilities Come From - -The source of truth is the upstream backend server. For example, a backend test -server can return: - -```rust,ignore -InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) -``` - -When the gateway connects to that backend, RMCP exposes the backend's peer -information and the gateway copies `peer_info().capabilities` into local -transport state. - -## What The Client Sees Today - -The downstream client does not receive a backend-specific capability object. The -gateway returns one virtual-server capability set from `merge_and_build_capabilities(...)`. - -At the time of writing, that function behaves like this: - -1. It checks each upstream capability family independently. -2. It advertises a downstream family when at least one backend advertises that - family and the gateway supports routing it. -3. It preserves `resources.subscribe` when at least one backend advertises it, - because the gateway routes subscribe/unsubscribe and forwards resource update - notifications for active subscriptions. -4. It does not advertise `listChanged` sub-capabilities yet, because the gateway - does not currently emit downstream list-changed notifications when upstream - lists or control-plane allow maps change. -5. If no backend reports supported capabilities, it returns - `ServerCapabilities::default()`. - -This means the downstream capability response is a gateway policy over upstream -capabilities, not a literal copy of any one backend. - -## Should This Use One Backend Or A Merge? - -Do not initialize the downstream capability object from just one backend entry. - -Reasons: - -- The gateway fronts multiple backends but presents one downstream MCP server. -- `virtual_host.backends` is a `HashMap`, so a "first backend wins" choice is - not a stable contract. -- List methods already merge results across backends, so selecting one backend's - capabilities would under-report or over-report behavior depending on which - backend happened to be chosen. - -If the goal is to make the downstream capability response more accurate, prefer -merge semantics over single-backend selection. - -## What Kind Of Merge? - -The safe rule is not "copy one backend" and not even "blind union of every -field". The safe rule is: - -Advertise only capabilities that the gateway can support correctly end-to-end -for the downstream client. - -That usually means a gateway-aware merge such as: - -- enable a top-level capability when at least one backend supports it and the - gateway has a correct routing/merge story for that method family -- keep a capability disabled when the gateway cannot yet preserve the backend's - semantics across multiple backends -- merge subfields only when their meaning still holds after namespacing, - routing, filtering, and partial backend failure handling - -Examples: - -- `tools`, `prompts`, and `resources` fit the current gateway model because the - gateway already merges list calls and routes targeted calls. -- More specific sub-capabilities should only be surfaced if the gateway really - preserves their behavior in the aggregated downstream view. - -## Recommendation - -If you are deciding between these two approaches: - -- "use one capability object from the vector" -- "merge backend capabilities into one downstream capability object" - -choose merge. - -But implement it as a gateway-supported merge, not as a raw backend union. diff --git a/docs/book/src/concurrency-and-runtime.md b/docs/book/src/concurrency-and-runtime.md deleted file mode 100644 index 31d9ba6e..00000000 --- a/docs/book/src/concurrency-and-runtime.md +++ /dev/null @@ -1,64 +0,0 @@ -# Concurrency And Runtime Model - -> 🧵 **Execution lens:** the gateway is async Rust on Tokio with jemalloc as -> the global allocator. This page explains the two executor shapes, what -> state is shared under which locks, and where work fans out. - -## Executor Shapes - -`--single-runtime` selects between two models: - -| Mode | Shape | When | -| --- | --- | --- | -| `true` (default) | One multi-thread Tokio runtime with `--number-of-cpus` worker threads (default: host CPU count). All connections share one runtime and one set of gateway state. | The default for all stateful MCP traffic. | -| `false` | One OS thread per CPU, each running its own current-thread Tokio runtime, each executing the full gateway stack. Listeners bind with `SO_REUSEPORT`, so the kernel spreads incoming connections across the per-thread listeners. | A shared-nothing, per-core experiment shape for throughput work. | - -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 -> `BackendTransports` map and user-session store inside `run_gateway`. -> Backend session state is therefore per-runtime-thread, and `SO_REUSEPORT` -> gives no connection affinity — later requests in a streamable HTTP session -> arrive on new connections and can land on a thread that does not own the -> session. Treat single-runtime mode as the only mode that supports stateful -> MCP sessions today; this is the in-process version of the -> [load-balancing constraint](session-ownership.md#load-balancing-consequence). - -## Shared State And Locks - -| State | Lock | Contention profile | -| --- | --- | --- | -| `BackendTransports` map | `Arc>>` | Locked briefly on initialize insert, per-call borrow, and cleanup. Borrowing clones `Arc` handles so the lock is not held across backend calls. | -| Subscription set | `Arc>>` | Local `subscribe`/`unsubscribe` only. | -| User config LRU cache | `Arc>` 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. | - -The design rule: locks guard maps of handles, not I/O. Backend calls, Redis -reads, and plugin hooks all run outside any gateway lock. - -## Fanout And Cancellation - -- `initialize` opens 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 resolve exactly one backend service handle. -- `call_tool` watches 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. - -## 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, which holds up -better than the system allocator under the many small, short-lived -allocations of per-request JSON and header processing. diff --git a/docs/book/src/contributing.md b/docs/book/src/contributing.md deleted file mode 100644 index 08812431..00000000 --- a/docs/book/src/contributing.md +++ /dev/null @@ -1,83 +0,0 @@ -# Contributing To The Gateway - -> 🛠️ **Contribution rule:** put behavior in the crate that owns it, keep the -> client-visible contracts stable, and update the matching book page in the -> same change. - -## Where Changes Belong - -| Change | Home | -| --- | --- | -| Dataplane behavior: routing, middleware, sessions, transports | `contextforge-data-plane-lib` — almost everything goes here. | -| Process shell: CLI flags, logging, runtime shape, exporters | `contextforge-data-plane` (the binary crate). Do not add dataplane logic here. | -| Shared config shapes (`UserConfig`, `User`, plugin config document) | `contextforge-data-plane-apis`. Regenerate the JSON schemas after any change: `cargo run -p contextforge-data-plane-apis` (see [Control-Plane Integration](control-plane-integration.md)). | -| Plugin integration | `contextforge-data-plane-cpex`. | -| Load generation | `contextforge-load-test`. | - -Inside the library crate, keep the module boundaries from -[System Shape](system-shape.md#module-boundaries): config validation in -`common.rs`, extension extraction in `layers/`, MCP behavior in `gateway/`, -listeners in `transports/`, Redis details behind `UserConfigStore`. - -## Changing MCP Routing - -The backend prefix namespace is a client-visible contract -([MCP Routing Semantics](mcp-routing-semantics.md)). Any change to it must -update the merge logic, the split logic, and the tests in the same PR — and -the [Control-Plane Integration](control-plane-integration.md) if the -client-facing surface moves. - -The project is still early, with no external users: prefer the right -architecture over preserving unstable APIs or compatibility surfaces. - -## Adding Plugin Hooks - -New hook points need defined behavior for failure, timeout, cancellation, -streaming, and telemetry attribution before they land on the hot path — see -[Plugins And Policy](plugins-and-policy.md). Avoid ad hoc plugin calls in the -middle of routing code. - -## Branch And Pull Request Workflow - -Name contribution branches `user//`, where `` is the -contributor's GitHub username and `` is a short, kebab-case summary -of the change. For example: `user/alice/fix-session-cleanup`. - -Open pull requests as drafts while work or validation is still in progress. -Mark a pull request ready for review only after its implementation, -documentation, tests, and required checks are complete. - -## Validation - -The pre-commit hooks run these local gates: - -```bash -cargo fmt --all --check -cargo clippy --locked --workspace --all-targets -- -D warnings -cargo deny check advisories licenses -cargo nextest run --locked --workspace -cargo build --locked --workspace -cargo bench --locked --workspace --no-run -``` - -CI runs the same gates and additionally runs -`cargo shear --check-test-targets --deny-warnings --locked`. - -Expectations by change type: - -| Change type | Minimum validation | -| --- | --- | -| Docs only | `mdbook build docs/book` and `mdbook test docs/book`. | -| Routing or session behavior | New or updated integration tests under `crates/contextforge-data-plane-lib/tests/` against the mock backends. | -| Config shape | Schema regeneration plus a control-plane compatibility check. | -| Plugin behavior | `gateway_plugins.rs` coverage for the new hook path. | -| Performance-sensitive paths | A [load-test run](performance.md) before and after. | - -For end-to-end confidence against the real control plane, run the -[cf-integration lanes](testing.md#full-stack-integration-harness). - -## Keep The Book True - -Every page in this book states verifiable behavior. When a change makes a -page wrong — a flag, a status code, a lock, a boundary — fix the page in the -same PR. Stale architecture docs are worse than none. diff --git a/docs/book/src/control-plane-integration.md b/docs/book/src/control-plane-integration.md deleted file mode 100644 index ee60ef08..00000000 --- a/docs/book/src/control-plane-integration.md +++ /dev/null @@ -1,79 +0,0 @@ -# Control-Plane Integration - -> 🤝 **Provisional:** no formal contract with the control plane has been -> stipulated yet. This page is a snapshot of the current de facto integration -> surface with -> [IBM/mcp-context-forge](https://github.com/IBM/mcp-context-forge) as -> implemented today. Any row may change while the project is early; when a -> proper contract is agreed, this page should track it. - -## Current Integration Surface - -These are the values both sides currently rely on: - -| Agreement | Value today | -| --- | --- | -| Client-facing route | The public route remains `/servers/{virtual_host_id}/mcp`. The front door rewrites modern MCP `2026-07-28` Streamable HTTP traffic to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the dataplane. | -| Protocol compatibility | The dataplane target is MCP `2026-07-28` only. The control plane serves older MCP versions, legacy session initialization, and SSE on its own routes; that traffic is not forwarded to the dataplane. | -| Unknown virtual host | `404` with body `{"detail":"Server not found"}`, matching the control-plane response shape. | -| Token issuer and audience | `iss = mcpgateway`, `aud = mcpgateway-api` — the values the control plane mints. | -| Claims shape | `sub`, `jti`, `iss`, `aud`, `exp`, and `user` are required. `token_use`, `iat`, `teams`, and `scopes` are optional, as is `user.full_name`. The dataplane routes on `sub` only. | -| User config key | MessagePack-encoded `User::new(jwt_subject)` (key type plus subject). | -| User config value | MessagePack-encoded `UserConfig`; the JSON schema is generated into `schemas/user_config.json`. | -| Plugin config key | `ContextForgeGatewayRuntimePluginConfig`, JSON or MessagePack, `version: 1` with a `cpex` section. | - -Changing any of these is a cross-repo change: the dataplane, the control-plane -publisher, and the integration harness all need updating together. - -The protocol boundary is a target contract while the implementation migration -is in progress. Temporary session-oriented code inside the dataplane does not -move legacy compatibility ownership back into this repository. - -## Config Publishing - -The control plane owns durable config and publishes runtime snapshots to -Redis. With `DATAPLANE_PUBLISHER=true`, it rewrites the dataplane's -`UserConfig` keys on an interval — every 60 seconds by default, configurable -in newer control-plane images. - -Config staleness on the dataplane is bounded by two knobs: - -```text -worst-case staleness = publisher interval + user config cache expiry -``` - -The dataplane's in-process cache defaults to 60 seconds -(`--user-config-cache-expiry-seconds`; `0` disables it and reads Redis on -every request). Functional test setups shorten the publisher interval and -disable the cache; production keeps both at 60. - -## Schema Generation - -`contextforge-data-plane-apis` is the single source of truth for the shared -config shapes. It generates the JSON Schemas the control plane can validate -against: - -```bash -cargo run -p contextforge-data-plane-apis -``` - -This writes `schemas/user.json` and `schemas/user_config.json`. Regenerate and -commit them whenever `UserConfig`, `VirtualHost`, `BackendMCPGateway`, or the -`User` key type changes. - -## Front-Door Split - -Only modern MCP `2026-07-28` Streamable HTTP traffic comes to this process. -The repository's reference `docker/nginx.conf` proxies -`location ^~ /contextforge-rs` to the gateway. Legacy MCP and SSE traffic, -plus all UI, management API, and other ContextForge traffic, stays on the -control-plane paths and does not enter the dataplane. - -## Verifying The Integration - -The [`cf-integration`](https://github.com/contextforge-org/contextforge-dev-tools) -harness tests exactly this surface: it runs the stock upstream control-plane -stack with the nginx split and the dataplane publisher enabled, then drives -probe, live-test, and load lanes through the public route. When the -integration surface changes, its lanes are what prove both sides still agree. -See [Testing](testing.md#full-stack-integration-harness) for the commands. diff --git a/docs/book/src/deployment-notes.md b/docs/book/src/deployment-notes.md deleted file mode 100644 index 7105ff34..00000000 --- a/docs/book/src/deployment-notes.md +++ /dev/null @@ -1,91 +0,0 @@ -# Deployment Notes - -> 🏗️ **Deployment lens:** the gateway is one stateless-config, stateful-session -> process behind a front door. Everything here follows from that: route only -> MCP traffic to it, keep Redis close and trusted, and give stateful sessions -> affinity. - -## Front-Door Routing - -The reference `docker/nginx.conf` shows the intended split: - -- `location ^~ /contextforge-rs` proxies to the gateway upstream. -- Everything else — UI, management APIs, other ContextForge traffic — stays on - the existing control-plane paths. -- The reference listener uses `backlog=4096 reuseport` and configures upstream - retries (`error timeout http_502/503/504`, 2 tries, 10 s window). For MCP - `POST` bodies this effectively retries only connection-stage failures: - nginx does not re-send non-idempotent requests once they reached an - upstream, and MCP calls are not idempotent. - -There is no production health endpoint today: `/health` is a `with_tools` -bootstrap helper served at `/contextforge-rs/health`, and production builds -compile it out. Use TCP-level checks or the exported metrics for liveness -until a real health endpoint exists. (The reference nginx config's -`location = /health` predates this and does not match the gateway's route.) - -The [`cf-integration`](https://github.com/contextforge-org/contextforge-dev-tools) -harness runs the same split with the stock upstream control-plane stack and -rewrites public `/servers/{id}/mcp` to `/contextforge-rs/servers/{id}/mcp`. - -## TLS Choices - -| Leg | Options | -| --- | --- | -| Front door to gateway | Plain HTTP on a trusted private network (the 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 (see [Security Model](security-model.md)). | -| Gateway to backends | HTTPS-only by default; opt into plain HTTP or mTLS with `--upstream-connection-mode`. | - -## Session Affinity And Failover - -Backend MCP sessions are local process state -([Session Ownership](session-ownership.md)): - -- More than one replica requires sticky routing by `Mcp-session-id`; the - reference nginx config does not provide this, so today's safe shapes are a - single replica or a front door that adds stickiness. -- On restart or failover, sessions are gone; clients must re-run - `initialize`. Design clients to treat a session-not-found error as - "reinitialize", not "retry". -- A Redis-backed user session store exists in code, but live backend services - would still be process-local; a remote session story is future work. -- Inside one host, the same constraint applies to `--single-runtime false`; - see [Concurrency And Runtime Model](concurrency-and-runtime.md). - -## Redis Availability - -Redis is required at startup and on every uncached config lookup. The -connection manager retries heavily (1,000 retries) rather than failing fast, -and the in-process cache (default 60 s) rides out short blips for warm -subjects. A cold subject during a Redis outage fails at config lookup; the -current Redis adapter reports failed `GET` calls as missing data, so the layer -returns `400` until Redis returns. - -## Images And Sizing - -- CI builds `docker/Dockerfile` (a `rust:1.96.1` builder stage) on every push - to `main` and pushes both - `ghcr.io//contextforge-data-plane:v` and - `ghcr.io//contextforge-data-plane:latest`, where `` is the - Cargo package version. There is no unprefixed version tag. Pin the - `v`-prefixed tag for reproducible deployments; `latest` tracks `main`. -- The reference Compose stack runs the gateway with raised limits worth - copying to real deployments: `nofile` 65535 and TCP tuning - (`tcp_fin_timeout=15`, widened local port range) for high connection churn. -- Size CPU with `--number-of-cpus` (defaults to host CPU count) and keep the - default single multi-thread runtime for stateful traffic. Memory scales - with active sessions (live backend services) and the config caches (up to - 50,000 entries each). - -## Deployment Checklist - -1. Front door routes only `/contextforge-rs` here. -2. JWT verification key or secret in place and rotated with the control - plane's signing key. -3. Redis reachable, TLS/mTLS across trust zones, write access restricted to - the control plane; `DATAPLANE_PUBLISHER` enabled on the control plane. -4. Upstream connection mode matches the backend URL schemes. -5. One replica per `Mcp-session-id` (single replica or sticky routing). -6. `with_tools` disabled in the production build. -7. Telemetry export pointed at the collector; see - [Telemetry And Diagnostics](telemetry-and-diagnostics.md). diff --git a/docs/book/src/failure-modes.md b/docs/book/src/failure-modes.md deleted file mode 100644 index 4b46172e..00000000 --- a/docs/book/src/failure-modes.md +++ /dev/null @@ -1,68 +0,0 @@ -# Failure Modes - -> 🚧 **Boundary rule:** every failure should come from the layer that owns the -> missing fact. Identity and config failures are HTTP responses before MCP -> handling; routing and backend failures are JSON-RPC errors. - -## HTTP Layer Failures - -These happen in middleware, before any MCP method runs: - -| Failure | Response | Owning layer | -| --- | --- | --- | -| Inner path does not match `/servers/{virtual_host_id}/mcp` | `400 Bad Request` | `virtual_host_id_layer` | -| Missing `Authorization` header or non-`Bearer` scheme | `401 Unauthorized` | `claims_layer` | -| JWT cannot be decoded, uses an unsupported algorithm, or no matching decoder key/secret is configured | `401 Unauthorized` | `claims_layer` | -| Expired token, wrong issuer, or wrong audience | `401 Unauthorized` | `claims_layer` | -| No user config exists for `claims.sub`, or claims are absent | `400 Bad Request` | `user_config_store_layer` | -| Config store error other than missing data | `500 Internal Server Error` | `user_config_store_layer` | -| Virtual host id not present in the caller's config | `404 Not Found` with `{"detail":"Server not found"}` | `virtual_host_config_layer` | - -## MCP Validation Failures - -`InitializeCallValidator` and `AuthorizedCallValidator` re-check the request -context before method handling. These are defense-in-depth errors: in a -healthy stack the middleware has already established the context. - -| Failure | JSON-RPC error | -| --- | --- | -| Missing session id, user config, virtual host id, or claims extension | Internal error (`Routing problem...`). | -| Virtual host absent from the user config | `RESOURCE_NOT_FOUND` with message `No configuration`. Normally unreachable because `virtual_host_config_layer` already returned `404`. | - -## Routing Failures - -| Failure | Behavior | -| --- | --- | -| Prefixed name or completion reference does not start with a configured backend name plus `-` | Internal error (`wrong tool name` / `wrong resource name` / `wrong prompt name` / `wrong completion reference`). | -| No backend entry matches the split name | Internal error (`got no responses from backends`). | -| Backend entry exists but has no running service | Internal error. Happens when that backend failed during `initialize`. | -| More than one backend entry matches | `INVALID_REQUEST`, and the session's backend entries are removed via `cleanup_backends`. | - -## Backend Session Failures - -| Situation | Behavior | -| --- | --- | -| Backend unreachable during `initialize` | The backend is stored with no running service. `initialize` still succeeds with the remaining backends. | -| Backend unreachable during a routed call | The call returns an internal error; other backends are unaffected. | -| Gateway process restart | All backend session state is lost because it is local process state. Clients must re-run `initialize`. | -| Request lands on a gateway node that does not own the session | List calls return empty results and routed calls fail, because `BackendTransports` has no entries there. Stateful sessions need sticky routing; see [Session Ownership](session-ownership.md). | - -## Plugin Failures - -| Failure | Behavior | -| --- | --- | -| Plugin denies a tool call or response | The denial becomes an MCP error to the caller. | -| Soft plugin error | Logged; the call proceeds. | -| Invalid plugin config document (wrong version, missing `cpex` config, unsupported features) | Rejected at load. On an invalid reload, the runtime is marked failed and plugin calls return an internal MCP error until a valid config is applied. | - -## Config Store Failures - -| Failure | Behavior | -| --- | --- | -| Redis connection loss | The connection manager retries (configured with 1,000 retries). | -| User config missing | `400 Bad Request` from `user_config_store_layer`. | -| Redis `GET` returns an error | Currently reported by the Redis adapter as missing data, so the layer returns `400 Bad Request`. | -| User config undecodable, key encoding failure, or other non-missing store errors | `500 Internal Server Error` from `user_config_store_layer`. | - -For a symptom-first version of this table, see the troubleshooting section in -[Run the Gateway Locally](running-the-gateway.md#troubleshooting). diff --git a/docs/book/src/gateway-options.md b/docs/book/src/gateway-options.md deleted file mode 100644 index 26e77e9d..00000000 --- a/docs/book/src/gateway-options.md +++ /dev/null @@ -1,229 +0,0 @@ -# Configuration Reference - -This page is the reference for every gateway setting. The binary parses its -configuration with `clap`, so each option has both a CLI flag and an environment -variable, and both feed the same `Config` struct. When a setting is supplied as -a flag and an environment variable at the same time, the command-line flag wins. - -For the always-current list, ask the binary directly: - -```bash -cargo run -p contextforge-data-plane --bin contextforge-data-plane -- --help -``` - -The sections below group the options by concern: listeners, JWT, Redis, upstream -transport, runtime, telemetry, and logging. - -## Minimum Useful Configuration - -At startup, the CLI requires Redis location and Redis connection mode: - -```text ---redis-address ---redis-port ---redis-mode -``` - -To serve useful traffic, the process also needs: - -| Need | Typical flag | -| --- | --- | -| At least one downstream listener | `--address 127.0.0.1:8001` or `--tls-address 0.0.0.0:8443` | -| JWT verification material | `--token-verification-public-key` or `--token-verification-secret` | -| A compatible upstream client mode | `--upstream-connection-mode plain-text-or-tls` for local HTTP backends | -| Runtime user config in Redis | Written by the control plane or by local bootstrap helpers | - -If no token verification key or secret is configured, authenticated MCP requests -cannot pass the claims layer. - -## Listener Options - -| Flag | Env var | Required | Meaning | -| --- | --- | --- | --- | -| `--address ` | `CONTEXTFORGE_DATA_PLANE_ADDRESS` | No | Plain HTTP listener. Omit it when serving only TLS. | -| `--tls-address ` | `CONTEXTFORGE_DATA_PLANE_TLS_ADDRESS` | No | TLS listener address. Requires certificate and private key. | -| `--server-certificate ` | `CONTEXTFORGE_DATA_PLANE_TLS_SERVER_CERTIFICATE` | With `--tls-address` | PEM certificate chain for downstream TLS. | -| `--server-private-key ` | `CONTEXTFORGE_DATA_PLANE_TLS_SERVER_PRIVATE_KEY` | With `--tls-address` | PEM private key for downstream TLS. | - -`--address` and `--tls-address` may both be configured, but they must not use -the same socket address. - -## JWT Options - -| Flag | Env var | Required | Meaning | -| --- | --- | --- | --- | -| `--token-verification-public-key ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PUBLIC_KEY` | For RSA tokens | RSA public key used for `RS256`, `RS384`, or `RS512` tokens. | -| `--token-verification-secret ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET` | For HMAC tokens | Shared secret used for `HS256`, `HS384`, or `HS512` tokens. | -| `--token-verification-private-key ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY` | Local tools only | RSA private key used by the optional local token helper. Present only when `contextforge-data-plane-lib/with_tools` is enabled. | - -The claims layer validates issuer, audience, and expiration: - -| Claim | Expected value | -| --- | --- | -| `iss` | `mcpgateway` | -| `aud` | `mcpgateway-api` | -| `exp` | Present and not expired | - -The JWT `sub` claim selects the Redis user config key. The path virtual host id -then selects one virtual host inside that config. - -## Redis Options - -| Flag | Env var | Required | Meaning | -| --- | --- | --- | --- | -| `--redis-address ` | `CONTEXTFORGE_DATA_PLANE_REDIS_HOSTNAME` | Yes | Redis host name or IP. | -| `--redis-port ` | `CONTEXTFORGE_DATA_PLANE_REDIS_PORT` | Yes | Redis port. | -| `--redis-mode ` | `CONTEXTFORGE_DATA_PLANE_REDIS_CONNECTION_MODE` | Yes | Redis connection mode: `plain-text`, `tls`, or `mtls`. | -| `--redis-tls-trust-bundle ` | `CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_TRUST_BUNDLE` | TLS and mTLS | PEM trust bundle for Redis TLS. | -| `--redis-tls-client-certificate ` | `CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_CLIENT_CERTIFICATE` | mTLS | PEM client certificate for Redis mTLS. | -| `--redis-tls-client-private-key ` | `CONTEXTFORGE_DATA_PLANE_REDIS_TLS_REDIS_CLIENT_PRIVATE_KEY` | mTLS | PEM client private key for Redis mTLS. | -| `--user-config-cache-expiry-seconds ` | `CONTEXTFORGE_DATA_PLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS` | No, default `60` | Expiry for the in-process user config cache in front of Redis. `0` disables caching and reads Redis on every request. | - -Local Compose exposes plain Redis on `127.0.0.1:6379`, so local runs normally -use: - -```bash ---redis-address 127.0.0.1 \ ---redis-port 6379 \ ---redis-mode plain-text -``` - -Runtime config values are MessagePack encoded. Redis is the current transport -for config, not the routing model itself. - -## Upstream MCP Transport Options - -| Flag | Env var | Required | Meaning | -| --- | --- | --- | --- | -| `--upstream-connection-mode ` | `CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE` | No | Controls whether backend MCP URLs may be HTTP, HTTPS, or mTLS. | -| `--upstream-trust-bundle ` | `CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_TRUST_BUNDLE` | No | Additional PEM trust bundle for HTTPS upstreams. | -| `--upstream-certificate ` | `CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_CERTIFICATE` | mTLS modes | PEM client certificate for upstream mTLS. | -| `--upstream-private-key ` | `CONTEXTFORGE_DATA_PLANE_TLS_UPSTREAM_PRIVATE_KEY` | mTLS modes | PEM client private key for upstream mTLS. | - -Connection modes: - -| Mode | Behavior | -| --- | --- | -| omitted or `tls-only` | HTTPS upstreams only. This is the safe default. | -| `plain-text-or-tls` | Allows HTTP and HTTPS upstream URLs. Use this for the local Compose backends. | -| `plain-text-or-m-tls` | Allows HTTP and HTTPS with client identity configured. | -| `mtls-only` | Requires HTTPS and uses the configured client certificate and key. | - -If an upstream backend URL is `http://...` and the mode is omitted, calls fail -before reaching that backend because the reqwest client is HTTPS-only. - -## Runtime Options - -| Flag | Env var | Default | Meaning | -| --- | --- | --- | --- | -| `--number-of-cpus ` | `CONTEXTFORGE_DATA_PLANE_NUMBER_OF_CPUS` | Host CPU count | Worker thread count for the Tokio runtime shape. | -| `--single-runtime ` | `CONTEXTFORGE_DATA_PLANE_SINGLE_RUNTIME` | `true` | `true` uses one multi-thread runtime. `false` starts multiple current-thread runtimes. | -| `--runtime-plugins-enabled ` | `CONTEXTFORGE_DATA_PLANE_RUNTIME_PLUGINS_ENABLED` | `false` | Enables CPEX runtime plugin execution and Redis plugin config loading. | - -When runtime plugins are enabled, plugin config is read from Redis key -`ContextForgeGatewayRuntimePluginConfig`. That key is a control-plane trust -boundary because it decides which registered hooks run. The flag only enables -execution for plugin factories compiled into the binary. For the experimental -secrets detection plugin, also build the gateway with -`contextforge-data-plane/plugins`. - -## Telemetry Options - -| Flag | Env var | Default | Meaning | -| --- | --- | --- | --- | -| `--enable-open-telemetry ` | `CONTEXTFORGE_DATA_PLANE_ENABLE_OPEN_TELEMETRY` | `false` | Enables trace export. | -| `--enable-otel-metrics ` | `CONTEXTFORGE_DATA_PLANE_ENABLE_OTEL_METRICS` | `false` | Enables HTTP server metric export. | -| `--otlp-protocol ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http-protobuf`. | -| `--otlp-endpoint ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_ENDPOINT` | Protocol-specific | Trace export endpoint. | -| `--otlp-metrics-endpoint ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Protocol-specific | Metrics export endpoint. | -| `--otlp-headers ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_HEADERS` | none | Comma-separated `key=value` headers for OTLP export. | -| `--otlp-service-name ` | `CONTEXTFORGE_DATA_PLANE_OTEL_SERVICE_NAME` | `CONTEXTFORGE-DATA-PLANE` | OpenTelemetry `service.name`. | - -Default endpoints: - -| Protocol | Traces | Metrics | -| --- | --- | --- | -| `grpc` | `http://127.0.0.1:4317` | `http://127.0.0.1:4317` | -| `http-protobuf` | `http://127.0.0.1:4318/v1/traces` | `http://127.0.0.1:4318/v1/metrics` | - -`RUST_TRACE_LOG` controls which spans reach the OTLP trace layer. The HTTP -trace layer emits debug-level spans, so local trace verification usually needs: - -```bash -RUST_TRACE_LOG=debug -``` - -## Logging Options - -| Flag or env var | Default | Meaning | -| --- | --- | --- | -| `--log-name` / `CONTEXTFORGE_DATA_PLANE_LOG_NAME` | `contextforge-data-plane.log` | File log name in the current working directory. | -| `--log-rotation` / `CONTEXTFORGE_DATA_PLANE_LOG_ROTATION` | `hourly` | Rotation mode: `minutely`, `hourly`, `daily`, or `never`. | -| `RUST_LOG` | `debug` | Console event filter. | -| `RUST_FILE_LOG` | `debug` | File event filter. | -| `RUST_TRACE_LOG` | `info` | OpenTelemetry span filter. | - -## Common Flag Sets - -### Local HTTP Gateway And Plain Redis - -```bash ---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 \ ---upstream-connection-mode plain-text-or-tls -``` - -### Downstream TLS Listener - -```bash ---tls-address 0.0.0.0:8443 \ ---server-certificate assets/contextforgeCA/contextforge-server.cert.pem \ ---server-private-key assets/contextforgeCA/contextforge-server.key.pem -``` - -You may combine this with `--address` to expose both HTTP and HTTPS listeners. - -### HMAC Token Verification - -```bash ---token-verification-secret "${CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET}" -``` - -Use this only when downstream JWTs are signed with an `HS*` algorithm. RSA -tokens need `--token-verification-public-key`. - -### Redis TLS - -```bash ---redis-address 127.0.0.1 \ ---redis-port 16379 \ ---redis-mode tls \ ---redis-tls-trust-bundle assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem -``` - -### Upstream mTLS - -```bash ---upstream-connection-mode mtls-only \ ---upstream-trust-bundle assets/contextforgeCA/contextforge.intermediate.ca-chain.cert.pem \ ---upstream-certificate assets/contextforgeCA/contextforge-client.cert.pem \ ---upstream-private-key assets/contextforgeCA/contextforge-client.key.pem -``` - -The certificate and key paths must point to PEM files accepted by reqwest. - -## Startup Validation - -The gateway fails fast for these invalid combinations: - -| Invalid combination | Reason | -| --- | --- | -| `--tls-address` without server cert or key | Downstream TLS cannot be configured partially. | -| Same socket for `--address` and `--tls-address` | The process cannot bind both listeners to the same address. | -| `--redis-mode tls` without trust bundle | Redis TLS needs a root certificate bundle. | -| `--redis-mode mtls` without trust bundle, client cert, or client key | Redis mTLS needs all three pieces. | -| mTLS upstream mode without upstream cert and key | The reqwest identity cannot be built. | -| Plain HTTP backend with default upstream mode | Default upstream mode is HTTPS-only. | diff --git a/docs/book/src/local-docker-stack.md b/docs/book/src/local-docker-stack.md deleted file mode 100644 index 7d0d11f4..00000000 --- a/docs/book/src/local-docker-stack.md +++ /dev/null @@ -1,113 +0,0 @@ -# Local Docker Stack - -`docker/docker-compose.yml` brings up the full control-plane + dataplane -stack locally and drives an MCP session through it against the bundled -`fast_time_server` test backend. This is separate from the `cf-integration` -harness (see [Testing](testing.md)) — use it for a quick local smoke test of -the built dataplane image. - -## Quick Start - -```bash -make docker-prod -make testing-up -``` - -`make docker-prod` builds `contextforge-data-plane:latest` from `docker/Dockerfile`; -`testing-up` refuses to start if this image doesn't exist yet. `testing-up` -starts: `nginx`, `control-plane`, `redis`, `postgres`, `pgbouncer`, -`data-plane`, `fast_time_server`, `register_fast_time` (see `Makefile`). - -Resource budget for this stack (`docker/docker-compose.yml` `deploy.resources`): - -| Service | CPU Limit | Mem Limit | -| ---------------- | --------- | --------- | -| nginx | 0.5 | 256 M | -| control-plane | 2 | 2 G | -| redis | 1 | 1 G | -| postgres | 2 | 2 G | -| pgbouncer | 0.5 | 256 M | -| data-plane | 2 | 2 G | -| fast_time_server | 1 | 512 M | -| **Total** | **9 cores** | **8 G** | - -`register_fast_time` is a one-shot init container (no steady-state resources): -it logs into control-plane, registers `fast_time_server` as an upstream -gateway, and creates a virtual server with a fixed id -(`b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8`) exposing all of its tools. - -Watch it finish: - -```bash -docker compose -f docker/docker-compose.yml logs -f register_fast_time -``` - -Look for `Fast Time Server registration complete!`. - -## Config Propagation - -The control plane doesn't talk to the dataplane directly — it publishes -`UserConfig` snapshots into Redis (`DATAPLANE_PUBLISHER=true`), and the -dataplane reads/caches from Redis. Worst-case propagation delay: - -```text -publisher interval + dataplane user-config cache expiry -``` - -Both default to ~60s in this stack (see -[Control-Plane Integration](control-plane-integration.md)). After -`register_fast_time` finishes, give it up to a minute before the virtual host -resolves on the dataplane side. - -## Get A Bearer Token - -Data-plane can self-issue a test JWT (RS256, signed with the same -`assets/jwt.key` the control plane also signs with) — no need to shell into -any container: - -```bash -TOKEN=$(curl --silent --show-error --request GET \ - --url http://localhost:8080/contextforge-rs/admin/tokens/admin@example.com \ - --header 'accept: application/json') -``` - -Use `admin@example.com` — that's `PLATFORM_ADMIN_EMAIL`, the identity -`register_fast_time` used, so it's the subject the publisher's `UserConfig` -is keyed under. Token is valid for 1 hour. - -## Point mcp-inspector At It - -```bash -npx @modelcontextprotocol/inspector -``` - -| Field | Value | -| ----------- | ----- | -| URL | `http://localhost:8080/contextforge-rs/servers/b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8/mcp` | -| Transport | Streamable HTTP | -| Auth token | `$TOKEN` from the previous step | - -Note the `/contextforge-rs` prefix — that's what routes to the dataplane via -nginx (`docker/nginx.conf`, `location ^~ /contextforge-rs`). Without it, -the request goes to control-plane instead and you'll get a -`{"detail": "..."}`-shaped error from mcpgateway, not the dataplane. - -Run `tools/list` — you should get back the `fast_time_server` tools -(prefixed with the gateway name, e.g. `fast_time-`). - -## Tear Down - -```bash -make testing-down -``` - -Stops the stack (containers/volumes kept; rerun `testing-up` to resume). - -## Troubleshooting - -| Symptom | Cause | Fix | -| --- | --- | --- | -| `403 {"detail":"Token is invalid: User is no longer a member of the associated team"}` | Hit control-plane instead of dataplane — URL missing `/contextforge-rs` prefix. | Use `/contextforge-rs/servers//mcp`. | -| `400 "Problem occurred retrieving the configuration"` | Dataplane has no `UserConfig` yet for the token's subject (`register_fast_time`/publisher hasn't run or synced). | Re-check config propagation above; confirm `register_fast_time` logs completed successfully and wait out the publisher interval. | -| `tools/list` returns `{"tools": [], "resultType": "complete"}` | Dataplane resolved the `UserConfig` fine, but couldn't connect to a declared backend. | `docker compose logs data-plane \| grep -i "worker quit\|BadScheme\|backend"`. A `BadScheme` error means `CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE` isn't set to `plain-text-or-tls` for a plain-`http://` backend. | -| `401` on every request | Bad/expired token, or control-plane and dataplane are signing/verifying with different keys or algorithms. | Confirm both sides have matching `JWT_ALGORITHM`/key config — see [Control-Plane Integration](control-plane-integration.md). | diff --git a/docs/book/src/mcp-behavior.md b/docs/book/src/mcp-behavior.md deleted file mode 100644 index 4b6cd188..00000000 --- a/docs/book/src/mcp-behavior.md +++ /dev/null @@ -1,30 +0,0 @@ -# MCP Behavior - -This section describes what the gateway exposes as an MCP server and how it -maps downstream MCP calls onto configured backend MCP servers. - -## Protocol Support Direction - -The downstream dataplane contract is MCP `2026-07-28` over Streamable HTTP. -Modern clients use `server/discover` and provide the required client context on -each request. - -Older MCP versions, legacy session initialization, and the legacy SSE transport -are not target dataplane compatibility surfaces. The external ContextForge -control plane serves those clients on its own routes. Legacy traffic does not -enter the Rust dataplane. - -The implementation is still being migrated to this boundary. Pages that -describe `initialize`, `Mcp-Session-Id`, or local session state document -temporary current internals, not a client contract to preserve. New code, -tests, and examples should use MCP `2026-07-28`; remaining legacy paths should -be replaced or removed rather than extended. - -> 📋 **Use this section for protocol behavior.** It covers the public MCP -> surface, backend fanout, namespacing, targeted routing, pagination, and -> the modern Streamable HTTP path. - -| Page | What it covers | -| --- | --- | -| 📋 [MCP Method Reference](mcp-method-reference.md) | Initialize, list, call, read, and prompt behavior from the client-facing gateway point of view. | -| 🛣️ [MCP Routing Semantics](mcp-routing-semantics.md) | How backend prefixes become the public tool, resource, and prompt namespace. | diff --git a/docs/book/src/mcp-method-reference.md b/docs/book/src/mcp-method-reference.md deleted file mode 100644 index d378a853..00000000 --- a/docs/book/src/mcp-method-reference.md +++ /dev/null @@ -1,68 +0,0 @@ -# MCP Method Reference - -> 📋 **Reference lens:** this page lists what each MCP method does at the -> gateway today, from the client's point of view. For how identifiers are -> preserved, aliased, or namespaced, see [MCP Routing Semantics](mcp-routing-semantics.md). - -> **Migration note:** the supported target uses MCP `2026-07-28`, -> `server/discover`, and per-request client context. The legacy `initialize`, -> session, and subscription paths below are implementation inventory to replace, -> not compatibility contracts. - -Gateway methods fall into three groups: `initialize` creates backend sessions, -routed methods use them, and `ping` remains local to the gateway process. - -## Initialize - -| Aspect | Behavior | -| --- | --- | -| Required context | RMCP `DownstreamSessionId`, `UserConfig`, `VirtualHostId`, and `ContextForgeClaims`. The `Mcp-session-id` header is not required yet. | -| Fanout | One `StreamableHttpClientTransport` per configured backend in the selected virtual host, opened concurrently with `futures::future::join_all`. | -| Backend failure | Not fatal. A backend that fails to initialize is stored with no running service; list calls skip it and routed calls to it fail. | -| Stored state | The local user session mapping, plus one `BackendTransports` entry per backend keyed by principal, backend name, and downstream session id. | -| Result | `InitializeResult` with the gateway's merged capability set. A capability family is advertised when at least one initialized backend advertises it and the gateway has routing support for that family. Backend capabilities are also stored with transport state. | - -## Routed List Methods - -`list_tools`, `list_resources`, `list_prompts`, and `list_resource_templates` -share one fanout path: - -| Aspect | Behavior | -| --- | --- | -| Fanout | Concurrent call to every connected backend in the session. | -| Identifiers | A single backend preserves upstream identifiers; multiple backends use the backend map-key prefix. Explicit control-plane tool aliases are returned exactly as configured. Resource templates apply the same single-versus-multi rule to both names and URI templates. | -| Ordering | Merged output is sorted by name. | -| Failures | Failed or unavailable backends are logged and omitted from the merged result. | -| Pagination | Cursor-based, per the [MCP spec §Pagination](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination). The gateway cursor is an opaque JSON token encoding each active backend's position. On the first request (no cursor) all backends are queried; on resume only backends with remaining pages are queried. An undecodable cursor returns `−32602 Invalid params`. See [Federated Pagination](mcp-routing-semantics.md#federated-pagination). | - -## Routed Targeted Methods - -Targeted calls resolve exactly one backend using an explicit tool alias, a -single-backend pass-through, or a multi-backend prefix: - -| Method | Behavior | -| --- | --- | -| `call_tool` | Resolves an exact control-plane alias first, then falls back to the single-versus-multi rule. It runs the optional plugin hooks, tracks progress, forwards the backend-local tool name, and propagates downstream cancellation. | -| `read_resource` | Preserves a single-backend URI or strips a multi-backend prefix, then returns the selected backend's result. | -| `subscribe`, `unsubscribe` | Apply the same resource-URI routing, track the downstream subscription, and forward or stop forwarding matching resource-update notifications. | -| `get_prompt` | Preserves a single-backend prompt name or strips a multi-backend prefix, then returns the selected backend's result. | -| `complete` | Applies the same routing to the prompt name or resource URI in `ref` and returns the selected backend's completion result. | - -Routed failures are JSON-RPC errors: an identifier that cannot select a backend -or an unavailable backend returns an internal error, and duplicate backend -matches invalidate the session; see [Failure Modes](failure-modes.md). - -## Local Methods - -This method passes through the same HTTP middleware but does not touch backends: - -| Method | Current behavior | -| --- | --- | -| `ping` | Returns success. | - -## Session Delete - -A downstream `DELETE` with `Mcp-session-id` is handled by RMCP first. On a -successful response, `session_id_layer` removes the local user session mapping -and the `BackendTransports` entries for that principal and session id. See -[Session Ownership](session-ownership.md) for the cleanup rules. diff --git a/docs/book/src/mcp-routing-semantics.md b/docs/book/src/mcp-routing-semantics.md deleted file mode 100644 index 079d5ad0..00000000 --- a/docs/book/src/mcp-routing-semantics.md +++ /dev/null @@ -1,112 +0,0 @@ -# MCP Routing Semantics - -The gateway presents multiple backend MCP servers as one downstream MCP server. -It fans out list operations, preserves identifiers when only one backend is -configured, and adds a backend namespace when multiple backends need to be -disambiguated. Explicit tool aliases published by the control plane take -precedence over both forms. - -## Backend Prefixes - -Backend map keys are part of the public namespace only for multi-backend -virtual hosts without an explicit tool alias: - -```text -backend tool "increment" from backend "gateway-one" - -> "gateway-one-increment" - -backend resource "counter" from backend "gateway-one" - -> "gateway-one-counter" - -backend prompt "summarize" from backend "research" - -> "research-summarize" -``` - -For a single-backend virtual host, those identifiers remain `increment`, -`counter`, and `summarize`. Resource URIs and resource-template URI templates -follow the same rule. This keeps a transparent gateway from changing the MCP -contract when no collision is possible. - -For tools, `BackendMCPGateway.tool_name_aliases` maps an exact downstream alias -to the upstream original name. An alias is advertised and routed exactly as -published, including case, dots, and underscores. When no alias exists, -single-backend hosts preserve the original tool name and multi-backend hosts -fall back to `{backend-map-key}-{tool-name}`. - -When a prefix is required, it is a routing contract rather than a display -detail. Changing a backend map key changes downstream identifiers for that -multi-backend virtual host. - -## List Operations - -List operations fan out: - -```text -list_tools -> all connected backends -> merged sorted tools -list_resources -> all connected backends -> merged sorted resources -list_prompts -> all connected backends -> merged sorted prompts -list_resource_templates -> all connected backends -> merged sorted templates -``` - -For a single backend, each successful result is returned with its identifier -unchanged, except for explicit tool aliases. For multiple backends, resources, -prompts, and tools without aliases are rewritten with their backend map-key -prefix. Resource-template names and URI templates are both prefixed. Failed or -unavailable backends are logged and omitted from the current merged list -result. - -## Routed Operations - -Calls that target one object use the inverse rule. A single-backend host selects -its only backend and forwards the identifier unchanged. A multi-backend host -splits the prefixed identifier: - -```text -gateway-one-increment - -> backend_name = gateway-one - -> upstream tool name = increment -``` - -`read_resource`, `subscribe`, `unsubscribe`, `get_prompt`, and `complete` share -this conditional routing helper. For `complete`, the routed value is the prompt -name or resource URI inside its `ref`. Resource-update notifications apply the -same rule on the response path, so their URI matches the URI originally exposed -to the downstream client. - -`call_tool` first resolves an exact control-plane alias, then applies the same -single-versus-multi-backend fallback. Backend names can themselves contain `-` -(as in `gateway-one`), so the splitter does not cut on the first `-`. Instead it -walks the configured backend names, takes the first one the prefixed name starts -with, and then requires a `-` immediately after that name. That is why -`gateway-one-increment` resolves to backend `gateway-one` and tool `increment`, -while a malformed name such as `gateway-oneincrement` is rejected. - -After selecting a route, the gateway resolves exactly one connected backend -service for the principal and downstream session. Missing backends fail the -call. Duplicate matches are treated as invalid session state and trigger -backend cleanup. - -## Federated Pagination - -All four list operations support cursor-based pagination across backends. - -The gateway wraps per-backend cursors inside its own opaque token (JSON serialised, -treated as an opaque string by MCP clients per spec). On the first request (no cursor) -every backend is queried. On a resume request the gateway decodes its cursor to -recover per-backend positions, skips backends that have already been exhausted, and -forwards each remaining backend its own cursor. Results from the current page are -merged and sorted. A new gateway cursor is emitted when at least one backend still -has more pages; the cursor is omitted once all backends are exhausted. - -An undecodable cursor returns `-32602 Invalid params` (MCP spec §Pagination). - -**Known limitation:** if the backend set changes between pages (reconfiguration), the -cursor for a removed backend is silently dropped and its remaining items are not -returned. Add a cursor version field if stability under live reconfiguration is -required. - -## Known Gaps - -Streaming/SSE behavior is also still a tracked design area. The target is to -stream downstream as backend chunks arrive while preserving plugin behavior, -backpressure, cancellation, and telemetry attribution. diff --git a/docs/book/src/operations.md b/docs/book/src/operations.md deleted file mode 100644 index 0c44a37c..00000000 --- a/docs/book/src/operations.md +++ /dev/null @@ -1,18 +0,0 @@ -# Operations - -This section covers runtime behavior after the gateway is serving traffic: -plugins, diagnostics, failure modes, tests, load tests, and deployment -constraints. - -> 🧪 **Use this section when operating or verifying the gateway.** It focuses -> on plugin hooks, observability, failure boundaries, load testing, and -> deployment assumptions. - -| Page | What it covers | -| --- | --- | -| 🧩 [Plugins And Policy](plugins-and-policy.md) | Where request and response plugins fit, and why body access changes the runtime model. | -| 📈 [Telemetry And Diagnostics](telemetry-and-diagnostics.md) | Signals needed to debug authentication, config lookup, routing, upstream calls, and merged results. | -| 🚧 [Failure Modes](failure-modes.md) | Expected failures by boundary, including auth, Redis, virtual hosts, backend sessions, and upstream transport. | -| 🧪 [Testing](testing.md) | Workspace checks, in-repo integration tests, and the cf-integration full-stack test lanes. | -| ⚡ [Performance](performance.md) | Dataplane-only load testing, full-stack Locust runs, headless versus web UI, and benchmark settings. | -| 🏗️ [Deployment Notes](deployment-notes.md) | Front-door routing, GitHub Pages publication, session affinity, and cluster constraints. | diff --git a/docs/book/src/performance.md b/docs/book/src/performance.md deleted file mode 100644 index e9a4871e..00000000 --- a/docs/book/src/performance.md +++ /dev/null @@ -1,86 +0,0 @@ -# Performance - -> ⚡ **Two load paths:** `contextforge-load-test` measures the Rust dataplane -> alone, and the [`cf-integration`](https://github.com/contextforge-org/contextforge-dev-tools) -> harness measures the full nginx-to-control-plane-to-dataplane stack with -> Locust. Use the first to profile gateway changes and the second to measure -> what users would see. - -## Dataplane-Only Load Testing - -`crates/contextforge-load-test` is a [Goose](https://book.goose.rs/)-based -traffic driver that speaks the full streamable HTTP MCP flow against a running -gateway. Start the [local stack](running-the-gateway.md) with seeded user -config first, then: - -```bash -cargo run --release --bin contextforge-load-test -- \ - --host 'http://127.0.0.1:8001' \ - -u 120 -r 40 --run-time 120s \ - --report-file report.html -``` - -`-u` is concurrent users, `-r` is the spawn rate per second, and -`--report-file` writes an HTML report. This measures the Rust dataplane alone, -without a control plane or front door in the path. Curated run reports live in -the repository's `reports/` directory. - -## Full-Stack Load With Locust - -The [cf-integration harness](testing.md#full-stack-integration-harness) runs -Locust against the public nginx route with a streamable-HTTP-aware locustfile: - -| Command | What it runs | -| --- | --- | -| `scripts/cf-integration.sh smoke` | 1 user for 10 seconds — a quick sanity pass. | -| `scripts/cf-integration.sh locust` | The full load run, default 100 users for 5 minutes. | - -Tune with environment variables: - -```bash -LOCUST_USERS=20 LOCUST_SPAWN_RATE=5 LOCUST_RUN_TIME=2m \ - scripts/cf-integration.sh locust -``` - -`MCP_VIRTUAL_SERVER_ID` targets a UI-created virtual server instead of the -auto-registered Fast Time one, and `MCP_TOOL_NAMES` picks the tools to call. -Locust HTML/CSV output lands under `.integration/mcp-context-forge/reports/`; -curated run reports live in the harness `reports/` directory. - -## Headless Versus Locust Web UI - -The harness runs Locust headless by default (`LOCUST_MODE=headless`): a timed -run that writes the HTML and CSV reports and prints only the summary. Setting -`LOCUST_MODE` to any other value (for example `web`) switches the Locust -service to interactive mode: a master with the web UI on port `8089` and a -class picker (`LOCUST_EXPECT_WORKERS` controls the expected worker count). The -one-off `locust` command does not publish container ports, so for the web UI -start the Locust service through the stack's `testing` Compose profile — the -upstream stack maps `8089:8089` — then open `http://localhost:8089` and drive -the run from the browser. - -## Benchmark Settings - -The harness tunes config propagation for functional runs, not throughput: - -| 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) | - -Restore both to `60` before measuring throughput, or the fast publish loop and -per-request Redis reads distort the numbers. - -## Control-Plane Baseline Load - -To compare against the stack without the dataplane in the path: - -```bash -scripts/cf-integration.sh down # frees the shared host ports -scripts/cf-integration.sh controlplane-locust -``` - -The baseline run defaults to the non-UI Locust class subset (health, Fast -Time, Fast Test, version/meta). `CONTROLPLANE_LOCUST_CLASSES=all` adds the -admin/UI/mutating surfaces. `LOCUST_USERS`, `LOCUST_SPAWN_RATE`, and -`LOCUST_RUN_TIME` apply here too. diff --git a/docs/book/src/plugins-and-policy.md b/docs/book/src/plugins-and-policy.md deleted file mode 100644 index e6e647ad..00000000 --- a/docs/book/src/plugins-and-policy.md +++ /dev/null @@ -1,128 +0,0 @@ -# Plugins And Policy - -Plugins are a policy boundary, not just middleware. They can inspect and mutate -payloads, so the gateway keeps the supported hook surface narrow and explicit. - -## Runtime Enablement - -Runtime plugins are disabled by default. Enablement has two stages: - -1. Compile concrete Rust plugin factories into the data-plane binary with Cargo - features. -2. Start the data plane with runtime plugins enabled and provide plugin config - in Redis. - -The runtime flag activates already-registered factories; it does not load new -Rust code into a running process. When enabled, the binary creates a CPEX -runtime registry and the runtime initializes it before serving traffic. - -Plugin configuration is loaded from Redis at: - -```text -ContextForgeGatewayRuntimePluginConfig -``` - -The runtime registry builds an initialized immutable plugin manager from that -configuration. Reloading swaps the manager instead of mutating a live one. - -The bundled secrets detection plugin is experimental. Compile it into the data -plane with `contextforge-data-plane/plugins`, enable runtime plugins with -`--runtime-plugins-enabled true`, and configure the plugin kind -`validator/secrets-detection` in the Redis document. - -## Built-In Demo Factories - -The optional `test-plugins` feature compiles three demo factories from the -independently hosted `cpex-plugins-rs` repository. Redis configuration activates -factories already present in the binary; it never loads new Rust code into a -running process. - -Start the lightweight dependencies: - -```bash -docker compose -f docker/docker-compose-local.yaml up -d redis gateway-one gateway-two -``` - -Register the payload-marker configuration before starting the data plane: - -```bash -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 the demo factories and runtime execution enabled: - -```bash -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 supported hook path is -also covered by: - -```bash -cargo nextest run --locked -p contextforge-data-plane-lib --test gateway_plugins -``` - -## Supported Hooks - -The supported surface is deliberately narrow: - -```text -cmf.tool_pre_invoke -cmf.tool_post_invoke -``` - -The gateway rejects route-based plugin selection, plugin directories, global -policies/defaults, non-tool hooks, and plugin conditions. Those features need -clear behavior for streaming, failures, timeouts, backpressure, context -propagation, and observability before they belong on the hot path. - -## Tool Call 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 -- deny the call - -After the upstream backend returns, the post hook can: - -- leave the result unchanged -- rewrite the result payload -- 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. - -## Boundary Rules - -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. - -Future hook expansion should define behavior for streaming/SSE, cancellation, -timeouts, backpressure, and telemetry before adding new hook points. diff --git a/docs/book/src/project.md b/docs/book/src/project.md deleted file mode 100644 index dc55465b..00000000 --- a/docs/book/src/project.md +++ /dev/null @@ -1,11 +0,0 @@ -# Project - -This section covers how to work on the repository and the book itself. - -> 🛠️ **Use this section for repo workflow.** It keeps contribution and -> publishing guidance separate from runtime architecture. - -| Page | What it covers | -| --- | --- | -| 🛠️ [Contributing To The Gateway](contributing.md) | Repository layout, expected validation, branch hygiene, and how to keep dataplane changes scoped. | -| 📚 [Publishing This Book](publishing-this-book.md) | How the mdBook build feeds GitHub Pages and how to preview the output before pushing. | diff --git a/docs/book/src/publishing-this-book.md b/docs/book/src/publishing-this-book.md deleted file mode 100644 index 65a66001..00000000 --- a/docs/book/src/publishing-this-book.md +++ /dev/null @@ -1,63 +0,0 @@ -# Publishing This Book - -> 📚 **Publishing path:** mdBook renders `docs/book/src/` into static HTML, -> and the GitHub Pages workflow deploys that HTML from `main`. - -## Local Build And Preview - -The Pages workflow uses mdBook `0.5.3`; use the same version locally: - -```bash -cargo install mdbook --version 0.5.3 --locked -``` - -Build and preview: - -```bash -mdbook build docs/book -mdbook serve docs/book --hostname 127.0.0.1 --port 3000 --open -``` - -The generated HTML lands in `docs/book/book/`, which is ignored by git. Never -edit files there; edit `docs/book/src/` instead. - -## Validation Before Pushing - -```bash -mdbook build docs/book -mdbook test docs/book -git diff --check -``` - -`mdbook test` runs Rust code blocks as tests and confirms that every chapter -in `SUMMARY.md` parses. - -## Workflow Behavior - -`.github/workflows/pages.yml` runs when a change touches `docs/book/**` or the -workflow file itself: - -| Trigger | Jobs | -| --- | --- | -| Pull request | `build` only: install mdBook, build the book, upload `docs/book/book` as the Pages artifact. | -| Push to `main` | `build`, then `deploy` publishes the artifact to GitHub Pages. | -| Manual `workflow_dispatch` | Same as a pull request run. | - -Builds are cancelled when a newer run starts on the same ref; deploys are -serialized and never cancelled mid-flight. - -## Repository Settings - -Publishing requires the repository's GitHub Pages source to be set to -`GitHub Actions`. Without that setting, the deploy job cannot publish the -uploaded artifact. - -## Adding Or Renaming Chapters - -1. Add or rename the Markdown file under `docs/book/src/`. -2. Update `docs/book/src/SUMMARY.md`; its order is the reader's path through - the book. -3. Run the validation commands above. - -Draft chapters use the `> Status: draft. To be implemented.` marker followed -by a `## To implement` list, so unfinished pages stay visible and navigable. diff --git a/docs/book/src/request-flow.md b/docs/book/src/request-flow.md deleted file mode 100644 index de553445..00000000 --- a/docs/book/src/request-flow.md +++ /dev/null @@ -1,206 +0,0 @@ -# Request Flow - -> **Migration note:** the flow below documents the current session-oriented -> implementation. The downstream target is MCP `2026-07-28` with -> `server/discover` and per-request client context. Older clients and SSE remain -> on control-plane routes and do not enter this dataplane. - -> 🎯 **Flow invariant:** Axum builds request context before RMCP handlers route MCP -> methods. MCP handlers should read typed extensions, not parse headers, paths, -> or Redis keys directly. - -![Request flow](assets/request-flow.svg) - -## Graph Legend - -The graph uses color only to separate paths: - -| Color | Meaning | -| --- | --- | -| Blue | Request direction: listener, middleware, RMCP dispatch, and backend calls. | -| Green | Response direction: backend result, response unwind, and client response. | -| Amber | `initialize`, where backend MCP client sessions are created. | -| Purple | Authorized MCP calls after `Mcp-session-id` exists. | -| Red | Layer-local HTTP rejection before MCP method handling. | - -This page follows a normal streamable HTTP MCP request through the code. The -shape below is based on the current `main.rs`, `runtime.rs`, `Gateway::run_gateway`, -the request layers, and `McpService`. To watch the same flow with real requests, -follow [Run the Gateway Locally](running-the-gateway.md) alongside this page. - -## Startup Path - -Startup begins in `crates/contextforge-data-plane/src/main.rs`: - -```text -install rustls crypto provider - -> Config::parse() - -> logging::init_tracing_logging(&config) - -> Runtime::from(&config) - -> optional CpexRuntimeRegistry - -> Gateway::builder() - .with_config(config) - .with_user_config_store_type(UserConfigStoreType::Redis) - .with_session_manager(LocalSessionManager::default()) - .with_plugin_runtime(...) - .build() - -> runtime.execute(gateway, plugin_registry) -``` - -`runtime.execute` either runs one multi-thread Tokio runtime or starts multiple -current-thread runtimes. In both modes it initializes the optional CPEX runtime -and then calls `gateway.run_gateway()`. - -## HTTP Stack Order - -`Gateway::run_gateway` builds the service stack in `crates/contextforge-data-plane-lib/src/lib.rs`. -Tower layers execute from the outside in, so a normal MCP request reaches the -handler in this order: - -```text -TCP/TLS listener - -> HttpMetricsLayer - -> TraceLayer - -> /contextforge-rs nested router - -> CORS layer - -> virtual_host_id_layer - -> claims_layer - -> session_id_layer - -> user_config_store_layer - -> virtual_host_config_layer - -> /servers/{virtual_host_name}/mcp RMCP service -``` - -The inner Axum route is: - -```text -/servers/{virtual_host_name}/mcp -``` - -The public route is nested under: - -```text -/contextforge-rs/servers/{virtual_host_name}/mcp -``` - -The route segment is named `virtual_host_name`, but the layer stores the value -as `VirtualHostId`. - -## Flow Checkpoints - -| Checkpoint | Established fact | Next dependency | -| --- | --- | --- | -| Listener | The request reached the Rust dataplane over TCP or TLS. | Metrics, tracing, and nested routing can observe it. | -| Path extraction | The inner path matched `/servers/{virtual_host_id}/mcp`. | MCP handlers can resolve a `VirtualHost`. | -| Claims validation | The bearer token was accepted and `ContextForgeClaims` exists. | Config lookup can use `claims.sub`. | -| User config lookup | A `UserConfig` exists for the authenticated subject. | The virtual host check can run against that config. | -| Virtual host check | The path's virtual host id exists in the caller's config. | MCP validators can resolve the selected `VirtualHost`. | -| RMCP dispatch | The streamable HTTP request is mapped to an MCP method. | The handler chooses initialize, routed backend calls, or local behavior. | - -## Middleware Context - -The request layers insert the context used later by RMCP handlers: - -| Layer | Request behavior | Failure behavior | -| --- | --- | --- | -| `virtual_host_id_layer` | Extracts `/servers/{virtual_host_id}/mcp` and inserts `VirtualHostId`. | Returns `400` when the inner path does not match. | -| `claims_layer` | Validates `Authorization: Bearer ...` with configured RS/HMAC decoder, issuer, audience, and expiration. Inserts `ContextForgeClaims`. | Returns `401` for missing or invalid bearer auth. | -| `session_id_layer` | Reads `Mcp-session-id` and inserts `SessionId` when present. | Missing session id is allowed here; authorized MCP handlers reject it later when required. | -| `user_config_store_layer` | Uses `claims.sub` as `User::new(subject)`, loads `UserConfig`, and inserts it. | Returns `400` for missing config, `500` for other store failures, and `400` if claims are absent. | -| `virtual_host_config_layer` | Checks that the path's virtual host id exists in the loaded `UserConfig`. | Returns `404` with body `{"detail":"Server not found"}` when the virtual host is not in the caller's config. | - -For `DELETE`, `session_id_layer` also has response-side behavior. It lets RMCP -handle the request first. If the RMCP response succeeds and a session id exists, -it removes the local user session mapping and removes backend transports for -`principal + session_id`. - -## Initialize Flow - -`initialize` does not require the downstream `Mcp-session-id` header. RMCP -creates a `DownstreamSessionId` and places it in the request context. - -`McpService::initialize` runs this sequence: - -1. `InitializeCallValidator` reads `DownstreamSessionId`, `UserConfig`, - `VirtualHostId`, and `ContextForgeClaims`. -2. It resolves `user_config.virtual_hosts[virtual_host_id]`. -3. It reads the local user session mapping for `claims.sub + downstream_session_id`. -4. For every backend in the selected virtual host, it concurrently builds a - `StreamableHttpClientTransport` with the configured backend URL and serves a - `GatewayBackendClient` over that transport. -5. It collects backend capabilities and running RMCP client services. The - capabilities are stored with backend transport state and shape the downstream - initialize response through a gateway-aware merge. -6. It writes the local user session mapping. -7. It stores each backend service in `BackendTransports` keyed by principal, - backend name, and downstream session id. -8. It returns `InitializeResult` with the gateway's merged capability set. - -Backend initialization is concurrent through `futures::future::join_all`. - -## Authorized MCP Calls - -Routed MCP calls after initialization use `AuthorizedCallValidator`. It requires: - -```text -SessionId -UserConfig -VirtualHostId -ContextForgeClaims -``` - -The validator resolves the same virtual host from the authenticated user's -config, then `SessionManager` locates backend services for: - -```text -principal + backend_name + session_id -``` - -Current routed method families: - -| Method family | Flow | -| --- | --- | -| `list_tools`, `list_resources`, `list_prompts`, `list_resource_templates` | Decode the incoming gateway cursor (if present) to recover per-backend positions. On the first request (no cursor) all configured backend services are queried; on a resume request only backends that still have pages are queried. Call each selected backend concurrently via `fan_out_list`, passing its own per-backend cursor. Preserve identifiers for a single backend or namespace them for multiple backends. Sort the merged page output. Encode a new gateway cursor when at least one backend returned a `next_cursor`; omit it when all are exhausted. Explicit tool aliases are preserved exactly. An undecodable cursor returns `−32602 Invalid params`. | -| `call_tool` | Resolve an exact tool alias first; otherwise preserve the name for one backend or split `{backend_name}-{tool_name}` for multiple backends. Resolve one backend, run the optional tool hooks, track progress, call the backend, and return the result. | -| `read_resource`, `subscribe`, `unsubscribe`, `get_prompt` | Select the only backend and forward the identifier unchanged, or split and strip the prefix for a multi-backend host, then call the resolved backend. | -| `complete` | Apply the same conditional routing to the prompt name or resource URI in `ref`, then return the selected backend's completion result. | - -`GatewayBackendClient` handles backend progress notifications for `call_tool`. -RMCP assigns a new progress token to each backend request, so the gateway maps -that backend token to the corresponding downstream token. Request enqueue and -mapping publication are serialized against progress lookup so an immediate -backend notification cannot overtake registration. When a notification matches -an in-flight backend token, the gateway restores the downstream token, optionally -runs the stream-event post hook, and forwards the notification to the downstream -client. - -`call_backend_tool` also watches the downstream cancellation token. If the -downstream call is cancelled before the backend responds, the gateway sends a -cancel request to the backend handle. - -## Local MCP Methods - -Some MCP methods are currently local to the gateway implementation rather than -backend-routed: - -| Method | Current behavior | -| --- | --- | -| `ping` | Returns success. | - -This path still passes through the same HTTP middleware, but it does not use -backend fanout or identifier routing. - -## Response Path - -Backend responses return to `McpService` first. `call_tool` may run response -plugin hooks before returning. - -List calls decode the incoming gateway cursor, fan out to the active backends, -merge the current page of output (preserving single-backend identifiers and -namespacing multi-backend identifiers as needed), and encode a new gateway -cursor when more pages remain across any backend. - -The HTTP response then unwinds through `virtual_host_config_layer`, -`user_config_store_layer`, `session_id_layer`, `claims_layer`, -`virtual_host_id_layer`, CORS, trace, and metrics. On successful `DELETE`, `session_id_layer` performs local session and -backend transport cleanup during this unwind. diff --git a/docs/book/src/running-the-gateway.md b/docs/book/src/running-the-gateway.md deleted file mode 100644 index c57bdd70..00000000 --- a/docs/book/src/running-the-gateway.md +++ /dev/null @@ -1,213 +0,0 @@ -# Run the Gateway Locally - -Use one of the two local workflows below. The end-to-end stack uses the current -Fast Time MCP backend, while the lightweight stack uses counter and conformance -test fixtures. - -## Prerequisites - -- Docker Compose. -- A Rust toolchain matching the workspace `rust-version` for the host workflow. -- Test keys at `assets/jwt.key` and `assets/jwt.key.pub` for the host workflow. -- Free local ports `6379`, `16379`, `5555`, `5556`, and `8001`. - -## Recommended End-to-End Stack - -The supported smoke environment includes the external ContextForge control -plane, Redis, PostgreSQL, PgBouncer, the Rust data plane, and -`fast_time_server`: - -```bash -make docker-prod -make testing-up -``` - -Confirm the services and one-shot registration job: - -```bash -docker compose -f docker/docker-compose.yml ps -a -docker compose -f docker/docker-compose.yml logs register_fast_time -``` - -Continue with [Local Docker Stack](local-docker-stack.md) for token creation, -config propagation, and an MCP smoke test through the complete control-plane to -data-plane path. - -Stop the stack without deleting its containers or volumes: - -```bash -make testing-down -``` - -## Run the Rust Binary from Cargo - -For debugger, profiler, or rapid host-development loops, start Redis and the -counter and conformance fixtures: - -```bash -docker compose -f docker/docker-compose-local.yaml up -d -docker compose -f docker/docker-compose-local.yaml ps redis gateway-one gateway-two -``` - -The services are available at: - -| Service | Local 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 the local bootstrap helpers when direct token/config setup -is needed during development: - -```bash -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: - -```text -http://127.0.0.1:8001/contextforge-rs/servers/{virtual_host_id}/mcp -``` - -Keep the data-plane process running. Use another terminal for the remaining -steps. - -Runtime CPEX plugins need both a compile-time feature and runtime config. To -try the experimental secrets detection plugin locally, build with -`contextforge-data-plane/plugins`, start the data plane with -`--runtime-plugins-enabled true`, and write the Redis plugin config before -startup. - -The local command uses `--upstream-connection-mode plain-text-or-tls` because -the sample backend URLs are plain HTTP. Without that option, the default -upstream client is HTTPS-only. - -### Mint a Local Test Token - -The local token helper signs an RS256 token with `assets/jwt.key`. Its `sub` -claim becomes the Redis user-config key. - -```bash -USER_ID=11111111-1111-1111-1111-111111111111 -USER_EMAIL=admin@example.com - -TOKEN=$(curl --silent --show-error \ - --url "http://127.0.0.1:8001/contextforge-rs/admin/tokens/${USER_ID}?email=${USER_EMAIL}") -``` - -### Seed Local Runtime Configuration - -Create a virtual host that points at both lightweight MCP fixtures: - -```bash -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", - "transport": "STREAMABLEHTTP", - "passthrough_headers": [], - "allowed_tool_names": [], - "allowed_resource_names": [], - "allowed_prompt_names": [] - }, - "gateway-two": { - "name": "gateway-two", - "url": "http://127.0.0.1:5556/mcp", - "transport": "STREAMABLEHTTP", - "passthrough_headers": [], - "allowed_tool_names": [], - "allowed_resource_names": [], - "allowed_prompt_names": [] - } - } - } - } - }' -``` - -The identity and routing relationship is: - -```text -JWT subject - -> Redis UserConfig - -> virtual host id - -> configured backend MCP URLs -``` - -### Verify Modern Protocol Discovery - -Probe the client-facing route with MCP `2026-07-28`. Modern requests carry the -protocol version, client identity, and client capabilities in `_meta` on every -request: - -```bash -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": {} - } - } - }' -``` - -The response should advertise `2026-07-28` in `supportedVersions`. This probe -validates the modern HTTP envelope plus authentication, user-config lookup, and -virtual-host selection. Use the [recommended end-to-end stack](local-docker-stack.md) -for the complete backend tool-call smoke test. - -The target downstream contract is MCP `2026-07-28` over Streamable HTTP using -`server/discover` and per-request client metadata. Older protocol versions, -legacy session initialization, and SSE remain control-plane responsibilities; -see [MCP Behavior](mcp-behavior.md) and -[Control-Plane Integration](control-plane-integration.md). - -## Troubleshooting - -| Symptom | Likely cause | -| --- | --- | -| `401 Unauthorized` | Missing bearer token, invalid signature, expired token, or mismatched issuer/audience. | -| `400 Problem occurred retrieving the configuration` | Redis has no `UserConfig` for the token subject. Re-run the config POST with the same `USER_ID`. | -| `500 Problem occurred retrieving the configuration` | Redis access or stored config decoding failed; inspect data-plane and Redis logs. | -| `404` with `{"detail":"Server not found"}` | The URL virtual-host id does not exist in that user's config. | -| `400` mentioning request metadata | The MCP protocol header and `_meta` version differ, or required per-request client metadata is missing. | -| Backend calls fail | A backend URL is wrong, a fixture is down, or `--upstream-connection-mode` rejects plain HTTP. | - -Stop the host process with `Ctrl-C`, then remove the lightweight dependencies: - -```bash -docker compose -f docker/docker-compose-local.yaml down -``` diff --git a/docs/book/src/runtime-configuration.md b/docs/book/src/runtime-configuration.md deleted file mode 100644 index 01b1ad1c..00000000 --- a/docs/book/src/runtime-configuration.md +++ /dev/null @@ -1,197 +0,0 @@ -# Runtime Configuration - -> 🗂️ **Config boundary:** process config tells the gateway how to run. Runtime -> user config tells each request where it may route. Plugin config controls the -> optional CPEX hook runtime. - -![Runtime configuration](assets/runtime-config.svg) - -The gateway consumes configuration from three places. Keeping them separate is -important because they change at different times and are used by different -parts of the dataplane. - -## Config Surfaces - -| Surface | Source | Loaded | Main user | -| --- | --- | --- | --- | -| Process `Config` | CLI flags and `CONTEXTFORGE_DATA_PLANE_*` env vars parsed by `clap`. | Startup. | Listener setup, JWT decoder keys, Redis connection, upstream HTTP client, telemetry, runtime shape. | -| `UserConfig` | `UserConfigStore`, currently Redis through `RedisUserConfigStore`. | Per request after JWT validation. | Virtual host and backend selection. | -| `RuntimePluginConfigDocument` | Redis key `ContextForgeGatewayRuntimePluginConfig` when runtime plugins are enabled. | Startup and watcher reload. | CPEX tool pre/post hooks. | - -The control plane owns durable authoring. This repo owns reading those values -and applying them on the request path. - -## UserConfig Shape - -The API crate defines the shared runtime routing model: - -```text -UserConfig - virtual_hosts: HashMap - -VirtualHost - backends: HashMap - -BackendMCPGateway - name: String - url: Url - transport: Transport - passthrough_headers: Vec - allowed_tool_names: Vec - allowed_resource_names: Vec - allowed_prompt_names: Vec -``` - -`Transport` currently declares: - -```text -STREAMABLEHTTP -SSE -STDIO -``` - -The Rust types above are easier to picture as JSON. For a complete, working -`UserConfig` document, see the seed request body in -[Run the Gateway Locally](running-the-gateway.md). - -## What The MCP Dataplane Uses Today - -The struct is already wider than the current MCP routing code. That is useful, -but the distinction should stay explicit: - -| Config field | Current MCP dataplane behavior | -| --- | --- | -| `UserConfig.virtual_hosts` | Required. `VirtualHostId` from the path selects one entry. | -| `VirtualHost.backends` map key | Required. Selects backend session state and becomes the public prefix when a multi-backend identifier has no explicit alias. | -| `BackendMCPGateway.url` | Required. Used to build the upstream `StreamableHttpClientTransport`. | -| `BackendMCPGateway.name` | Present in the model. Current routing uses the backend map key, not this field, as the namespace. | -| `transport` | Present in the model. Current upstream code always builds a streamable HTTP client transport. | -| `passthrough_headers` | Applied during `initialize`: named downstream request headers are copied onto the upstream connection header map. Body-framing (`Content-Length`, `Content-Type`), hop-by-hop, non-standard hop-by-hop (`Proxy-Connection`), and RMCP-reserved headers are silently skipped. Propagation is session-scoped — headers are snapshotted from the initialize request; see note below. | -| `add_headers` | Static `{name: value}` headers injected onto the upstream connection after passthrough (override passthrough values). Body-framing, hop-by-hop, non-standard hop-by-hop (`Proxy-Connection`), and RMCP-reserved headers are silently skipped. | -| `remove_headers` | Header names stripped from the upstream connection after add (applied last). Body-framing, hop-by-hop, non-standard hop-by-hop (`Proxy-Connection`), and RMCP-reserved headers are silently skipped. | -| `allowed_tool_names` | Present in the model. Current list/call routing does not enforce it. | -| `tool_name_aliases` | Optional exact `{downstream_alias: upstream_original}` mapping used by tool list/call routing before the single-versus-multi-backend fallback. | -| `allowed_resource_names` | Present in the model. Current resource routing does not enforce it. | -| `allowed_prompt_names` | Present in the model. Current prompt routing does not enforce it. | - -> **Note — session-scoped header propagation:** `passthrough_headers` values are snapshotted from the -> downstream `initialize` request and baked into the backend transport connection for the lifetime of the -> session. Post-initialize requests (tool calls, list calls, etc.) reuse those headers. True -> request-scoped propagation requires either per-request transport reconstruction or SDK support for -> per-request header injection; this is the correct path as MCP moves toward stateless operation -> (SEP-2575, SEP-2567). - -The current route selection is: - -```text -JWT subject - -> User::new(subject) - -> UserConfig - -> path VirtualHostId - -> VirtualHost - -> backend map key - -> BackendMCPGateway.url -``` - -Expected config growth beyond the current fields: - -- route selection across multiple MCP endpoints -- principal/virtual-host filters for tools, resources, and prompts -- backend auth/TLS material references -- plugin/CPEX hook settings -- pagination/SSE behavior where protocol handling needs config -- future A2A and LLM routing/provider settings - -## Redis Storage - -`RedisUserConfigStore` stores user routing config as MessagePack: - -| Item | Encoding | -| --- | --- | -| Redis key | MessagePack-encoded `User::new(jwt_subject)`. | -| Redis value | MessagePack-encoded `UserConfig`. | -| Cache key | Raw subject string through `User::key()`. | -| Cache value | Decoded `UserConfig`. | - -The in-process cache is an implementation detail: - -| Setting | Value | -| --- | --- | -| Entries | 50,000 | -| Expiry | `--user-config-cache-expiry-seconds`, default 60 seconds; `0` disables caching | -| Redis connection retries | 1,000 | - -Routing code should stay behind the `UserConfigStore` trait. That keeps Redis, -MessagePack, and cache behavior out of MCP method handling. - -## Plugin Runtime Config - -When `runtime_plugins_enabled` is true, startup builds a -`CpexRuntimeRegistry::with_redis_config(...)`. The registry reads a separate -runtime plugin document: - -```text -Redis key: ContextForgeGatewayRuntimePluginConfig - -RuntimePluginConfigDocument - version: 1 - cpex: CpexConfig -``` - -Runtime config activates plugin factories that were compiled into the gateway -binary; it does not load new Rust code. The experimental secrets detection -plugin is included with the `contextforge-data-plane/plugins` Cargo feature and -activated with plugin kind `validator/secrets-detection`. - -The plugin config loader accepts JSON bytes or MessagePack bytes. It rejects -documents with the wrong version, missing `cpex` config, unsupported CPEX -features, or an unavailable config store. - -Current supported plugin scope is deliberately narrow: - -| CPEX feature | Current support | -| --- | --- | -| `TOOL_PRE_INVOKE` | Supported for `call_tool` before backend invocation. | -| `TOOL_POST_INVOKE` | Supported for `call_tool` responses and backend progress events. | -| CPEX routing | Rejected. Gateway routing is owned by `UserConfig` and MCP routing code. | -| Plugin conditions | Rejected. | -| Plugin dirs, global policies, global defaults | Rejected. | -| Other hook types | Rejected. | - -The registry has a watcher interval of 10 minutes. On valid reloads it swaps in -the new runtime. On invalid reloads it marks the runtime failed so new plugin -calls return an internal MCP error until a valid config is applied. - -## Process Config - -Process `Config` is not per-user routing state. It is parsed once and controls -the gateway shell: - -| Area | Examples | -| --- | --- | -| Listener | `address`, `tls_address`, downstream TLS certificate and private key. | -| Authentication | RSA public key path or HMAC secret for JWT verification. | -| Redis | Host, port, plain/TLS/mTLS mode, trust bundle, client cert, client key. | -| Upstream HTTP client | Plaintext/TLS/mTLS mode, upstream trust bundle, client cert, client key. | -| Telemetry | OpenTelemetry traces, metrics endpoint/protocol, OTLP headers, service name. | -| Runtime shape | CPU count and single-runtime versus multi-runtime mode. | -| Plugins | Whether runtime plugins are enabled. | -| Logging | Log name and rotation. | - -The process config decides whether the gateway can start and what dependencies -it can reach. It does not decide which backend a specific MCP caller may use; -that remains in `UserConfig`. - -## Selection Invariant - -Backend selection should only happen after these facts exist: - -```text -authenticated subject - + loaded UserConfig - + path VirtualHostId - + MCP session context -``` - -That invariant is why runtime config access is in middleware and MCP validators, -not hidden inside individual backend calls. diff --git a/docs/book/src/session-ownership.md b/docs/book/src/session-ownership.md deleted file mode 100644 index cff4b590..00000000 --- a/docs/book/src/session-ownership.md +++ /dev/null @@ -1,128 +0,0 @@ -# Session Ownership - -> 🧷 **Session invariant:** backend MCP services are local process state keyed -> by authenticated principal, backend namespace, and downstream MCP session id. - -![Session ownership](assets/session-ownership.svg) - -The current gateway keeps backend MCP client services in memory. That choice is -simple and fast, but it defines how initialized MCP sessions can be routed in a -deployment. - -> **Temporary RMCP v3 compatibility:** the gateway currently maps initialize -> and every downstream `Mcp-Session-Id` header to one fixed internal session -> key. RMCP still manages the real transport session; the fixed key only keeps -> the existing gateway-owned backend lookup working until that state is -> removed. It is not part of the supported MCP `2026-07-28` client contract; -> legacy clients remain on control-plane routes and do not enter the dataplane. - -## Owned State - -Several pieces of state participate in one MCP session. They do not all have -the same owner. - -| State | Key | Owner today | Lifetime | -| --- | --- | --- | --- | -| RMCP downstream session | RMCP `Mcp-Session-Id`; fixed internal gateway session key. | RMCP `LocalSessionManager`. | Local process. | -| User session mapping | `UserSession { principal, downstream_session_id }`. | `LocalUserSessionStore`. | Local LRU cache, 50,000 entries, 1 hour. | -| Backend running service | `principal + backend_name + session_id`. | `BackendTransports`. | Local process. | -| Backend upstream MCP session | Managed inside RMCP running client service. | Backend service handle. | Local process and backend server. | - -`RedisUserSessionStore` exists, but `Gateway::run_gateway` wires -`LocalUserSessionStore` today. Even if the user session mapping moved to Redis, -the live RMCP backend services in `BackendTransports` would still be local -unless that architecture changes too. - -## Initialize Creates Backend Services - -`initialize` is the ownership creation point: - -```text -InitializeCallValidator - -> selected VirtualHost - -> claims.sub - -> fixed internal gateway session key - -> one upstream client service per backend - -> BackendTransports entries -``` - -The backend transport key is: - -```text -BackendTransportKey - principal: claims.sub - backend_name: backend map key - session_id: downstream session id -``` - -The backend map key matters because one downstream MCP session can fan out to -many backend MCP sessions. The principal matters because two callers could -present the same downstream session id value. - -## Borrowing Backend Services - -`SessionManager` is a request-scoped view over the selected virtual host, -session id, principal, and shared backend transport map. - -| Operation | What it does | -| --- | --- | -| `get_backend_names()` | Reads backend namespaces from the selected `VirtualHost`. | -| `borrow_transports()` | Locks the map, finds entries for the current principal/session/backend names, and clones the shared `Arc`. | -| `cleanup_backends(reason)` | Removes backend entries for the current principal/session/backend names. | - -`borrow_transports()` does not move services out of the map in the current -code. It clones shared service handles, so there is no return step after a -request completes. - -## List Calls And Routed Calls - -Backend service lookup splits into two patterns: - -| MCP call shape | Session behavior | -| --- | --- | -| `list_tools`, `list_resources`, `list_prompts`, `list_resource_templates` | Borrow every available backend service for the selected virtual host, call them concurrently, then merge successful responses with aliases or conditional namespacing. | -| `call_tool`, `read_resource`, `subscribe`, `unsubscribe`, `get_prompt`, `complete` | Resolve an explicit tool alias, select the only backend without rewriting, or split a multi-backend prefix; then call the single resolved backend service. | - -An initialized backend entry can still contain no running service if backend -initialize failed. List calls skip unavailable backends. Routed calls to that -backend return a routing error. - -If routed lookup ever detects duplicate backend matches, the session is treated -as invalid and `cleanup_backends` removes the local backend entries for that -principal and session. - -## Delete Cleanup - -`session_id_layer` owns response-side cleanup for downstream `DELETE`: - -```text -DELETE with Mcp-session-id - -> let RMCP handle the request - -> if RMCP response is successful - -> remove LocalUserSessionStore entry - -> remove BackendTransports entries for principal + session id -``` - -Cleanup is intentionally tied to a successful downstream delete response. If -RMCP rejects the delete, the layer returns that response and leaves local -session state untouched. - -## Load-Balancing Consequence - -Random load balancing is not safe for stateful MCP sessions today. After -`initialize`, later requests with the same `Mcp-session-id` must reach the -process that owns the backend running services. - -Known deployment options are: - -| Option | Tradeoff | -| --- | --- | -| Sticky routing by `Mcp-session-id` | Simple, preserves local state, but couples sessions to one process. | -| External session ownership | More flexible, but backend running service state must move out of process or become reconstructible. | -| Reinitialize after failover | Operationally simple, but clients must tolerate session loss. | - -Until backend service ownership changes, design request handling as if a -stateful MCP session belongs to one gateway process. The same constraint -appears inside one host in multi-runtime mode, where each runtime thread owns -its own backend transports; see -[Concurrency And Runtime Model](concurrency-and-runtime.md). diff --git a/docs/book/src/system-shape.md b/docs/book/src/system-shape.md deleted file mode 100644 index 717dcfe1..00000000 --- a/docs/book/src/system-shape.md +++ /dev/null @@ -1,284 +0,0 @@ -# System Shape - -> **Migration note:** session-oriented components shown on this page describe -> current internals. The downstream target is MCP `2026-07-28` over Streamable -> HTTP. Legacy MCP and SSE stay on control-plane routes and do not enter the -> dataplane. - -> 🧭 **Architecture lens:** this page explains what the gateway is, what it is -> not, and which code owns each boundary. - -![System shape](assets/system-shape.svg) - -The ContextForge Data Plane is the Rust dataplane process for MCP traffic. It is -not a second ContextForge application. It accepts downstream streamable HTTP MCP -requests, builds request context, loads runtime config, opens or reuses backend -MCP client sessions, and returns one merged MCP server view to the caller. - -That shape matters because this repository sits on the traffic path. Its design -should bias toward predictable request behavior, explicit state ownership, and -small hot-path dependencies. - -## Three-Layer Model - -The gateway is easiest to reason about as three layers: - -| Layer | Owns | Does not own | -| --- | --- | --- | -| ContextForge control plane | Management workflows, UI, IAM lifecycle, durable config ownership, policy authoring, observability storage. | Hot MCP request handling or in-process MCP sessions. | -| Rust gateway dataplane | Auth, config lookup, virtual host selection, MCP fanout, plugin hooks, telemetry emission, upstream calls. | Control-plane APIs, customer IAM, UI, durable metrics storage. | -| Backend MCP servers | Actual tools, resources, prompts, and backend protocol behavior. | Caller auth, virtual host selection, gateway-level policy. | - -In text form, the same model is: - -```text -ContextForge control plane - -> writes runtime config and owns management workflows - -contextforge-data-plane process - -> authenticates, loads config, routes, fans out, applies hooks, emits signals - -backend MCP servers - -> own the actual tools, resources, and prompts -``` - -> **Boundary rule:** Redis is the current config-store transport, not the -> architecture. Routing code should depend on `UserConfigStore`, not Redis -> commands. - -## Control Plane Boundary - -The external ContextForge control plane owns the slow-changing product surface: - -| Area | Why it stays outside this repo | -| --- | --- | -| Management APIs and UI | They are user/admin workflows, not request-path forwarding. | -| Credential and policy authoring | The dataplane consumes policy; it should not become the policy editor. | -| Persistent config ownership | Durable storage and schema lifecycle belong to the control plane. | -| Durable observability storage | The gateway emits telemetry; it should not become the metrics database. | -| Customer IAM lifecycle | The gateway validates presented identity, not the customer identity product. | - -The Rust dataplane owns the hot path: - -| Area | Code-facing meaning | -| --- | --- | -| Listener setup | Expose downstream TCP and/or TLS endpoints. | -| JWT validation | Convert bearer auth into request claims. | -| Runtime config lookup | Load the caller's `UserConfig` by JWT subject. | -| MCP fanout and routing | Present multiple backends as one MCP server. | -| Request and response hooks | Run plugin/policy hooks at explicit pipeline points. | -| Telemetry emission | Emit logs, traces, and metrics around the path. | - -The front door can route only MCP traffic to this process while leaving other -ContextForge traffic on existing paths: - -```text -/contextforge-rs/servers/{virtual_host_id}/mcp -``` - -From a client point of view, that endpoint behaves like one ContextForge MCP -server. Internally, it is a focused proxy, fanout, policy, and merge dataplane. - -## Process Assembly - -Startup begins in `crates/contextforge-data-plane/src/main.rs`. The binary crate -does process-level assembly: - -```text -install rustls crypto provider - -> Config::parse() - -> logging::init_tracing_logging(&config) - -> runtime::Runtime::from(&config) - -> optional CpexRuntimeRegistry - -> Gateway::builder() - .with_config(config) - .with_user_config_store_type(UserConfigStoreType::Redis) - .with_session_manager(LocalSessionManager::default()) - .with_plugin_runtime(...) - .build() - -> runtime.execute(gateway, plugin_registry) -``` - -| Step | Owner | Result | -| --- | --- | --- | -| Parse config | Binary crate | A process config value used to build the gateway. | -| Initialize logging | Binary crate | Console/file logging and trace export are ready before serving traffic. | -| Select runtime | `runtime::Runtime` | Either one multi-thread Tokio runtime or multiple current-thread runtimes. | -| Start plugin runtime | CPEX registry path | Optional plugin manager and reloadable config loop. | -| Build gateway | `Gateway::builder()` | Dataplane dependencies are assembled and handed to the runtime. | - -`runtime::Runtime` changes executor shape, not gateway semantics. The default -path is one multi-thread Tokio runtime. The binary crate also sets -`tikv_jemallocator` as the global allocator and owns file logging, console -logging, trace export, and metrics export setup. - -## Gateway Assembly - -`crates/contextforge-data-plane-lib/src/lib.rs` builds the dataplane stack in -`Gateway::run_gateway`: - -```text -RedisUserConfigStore -LocalUserSessionStore -BackendTransports -reqwest::Client for backend MCP calls -RMCP StreamableHttpService -Axum middleware stack -TCP and/or TLS listeners -``` - -The resulting route tree is: - -```text -/contextforge-rs - /servers/{virtual_host_name}/mcp - /admin/... local bootstrap helpers when with_tools is enabled - /health local bootstrap helper when with_tools is enabled -``` - -The route segment is named `virtual_host_name` in Axum, but the gateway extracts -the actual path value as a `VirtualHostId`. Routing code should treat it as a -virtual host id. - -## Workspace Layers - -The repository is a Cargo workspace with narrow responsibilities: - -| Crate | Responsibility | -| --- | --- | -| `contextforge-data-plane` | Process shell: CLI config, logging, telemetry exporters, Tokio runtime, allocator, plugin registry startup, gateway construction. | -| `contextforge-data-plane-lib` | Dataplane library: Axum stack, request middleware, config lookup, MCP fanout/routing, backend sessions, upstream clients, downstream transports. | -| `contextforge-data-plane-apis` | Shared contract: `UserConfig`, `VirtualHost`, `BackendMCPGateway`, Redis user key, plugin config document, schema generation. | -| `contextforge-data-plane-cpex` | Plugin integration: CPEX runtime registry, Redis plugin config loading, supported tool pre/post hooks, stream event adaptation. | -| `contextforge-load-test` | Performance harness: end-to-end MCP traffic driver. | - -Keep those boundaries stable. Adding dataplane behavior to the binary crate -makes the process shell harder to test and reuse. Adding control-plane behavior -to the library crate makes the hot path harder to reason about. - -## Pipeline Shape - -The target shape is a bidirectional AI traffic pipeline: authentication, -rate limiting, routing and protocol selection, request mutation, optional -retrieval, and request guardrails on the way upstream; response guardrails, -response mutation, and telemetry on the way back. Upstreams may eventually be -MCP, A2A, or model providers. Preserve the ordering as pieces land: auth and -config before backend selection, request plugins before upstream calls, -response plugins before returning, telemetry around both sides. - -The current code implements the MCP subset of that pipeline: - -```text -downstream request - -> virtual host extraction - -> JWT validation - -> session extraction - -> user config lookup - -> MCP handler validation - -> request plugin hooks - -> backend MCP call - -upstream response - -> response plugin hooks - -> merge, namespace, or pass through - -> metrics, tracing, and logging - -> downstream response -``` - -The Axum layer registration order is important because Tower layers execute -from the outside in. The stack is built so the request reaches handlers with: - -| Context | Inserted by | Used by | -| --- | --- | --- | -| `VirtualHostId` | `virtual_host_id_layer` | Initialize and authorized MCP validators. | -| `ContextForgeClaims` | `claims_layer` | Config lookup, session cleanup, routing. | -| `SessionId` | `session_id_layer` | Authorized MCP calls after initialize. | -| `UserConfig` | `user_config_store_layer` | Virtual host selection and backend selection. | - -After those extensions exist, `virtual_host_config_layer` rejects requests with -`404` when the path's virtual host id is not present in the loaded -`UserConfig`, so MCP handlers only see resolvable virtual hosts. - -`initialize` is the special call. RMCP provides a downstream session id before -the gateway has a `Mcp-session-id` header. The gateway fans out to configured -backends, opens upstream MCP client sessions, and stores those running services -under: - -```text -principal + backend_name + downstream_session_id -``` - -Later calls use the `Mcp-session-id` header to find those stored backend -services. - -## State Ownership - -The architecture keeps request state, session state, runtime config, and process -state in separate ownership scopes. The current ownership model is: - -| State | Owner | Lifetime | -| --- | --- | --- | -| CLI `Config` | Binary startup and `Gateway` | Process lifetime. | -| JWT decoders | `ContextForgeDataPlaneAppState` | Process lifetime. | -| User config | `UserConfigStore`, currently Redis plus in-process LRU | Control-plane authored, request-path consumed. | -| Request identity | Request extensions | One HTTP request. | -| Virtual host id | Request extensions | One HTTP request. | -| Downstream session id | RMCP plus `SessionId` extension | MCP session. | -| Backend RMCP services | `BackendTransports` map | Local process, per principal/backend/session. | -| Local user session mapping | `LocalUserSessionStore` | Local process, per principal/session. | -| Runtime plugin manager | `CpexRuntimeRegistry` and handle | Process lifetime, reloadable. | -| Logs, traces, metrics | Logging setup and Axum/tower layers | Process lifetime. | - -> **Session rule:** backend MCP services are local process state today. -> Load-balanced deployments need sticky routing, external session state, or a -> reinitialize-after-failover story. - -Redis-backed user session storage exists in code, but the default gateway -assembly wires `LocalUserSessionStore`. Do not assume session state is durable -or shared across gateway nodes. - -## Module Boundaries - -The library crate has deliberately narrow internal module roles: - -| Module | Owns | -| --- | --- | -| `common.rs` | CLI config shape, JWT claim shape, Redis config validation, upstream `reqwest::Client` construction. | -| `layers/` | HTTP request extension extraction and request-bound validation. | -| `gateway/` | MCP server behavior, initialize fanout, list merging, prefixed routing, backend service state. | -| `gateway/session_store/` | Local and Redis-capable user session storage abstractions. | -| `user_config_store/` | `UserConfigStore` trait and Redis-backed runtime config store. | -| `transports/` | Downstream TCP and TLS listener setup. | -| `tools.rs` | Local bootstrap helpers compiled only with `with_tools`. | - -Concrete Redis commands stay behind `UserConfigStore`. Downstream listener -transport stays in `transports/`. MCP method behavior stays in `gateway/`. -Request extension extraction stays in `layers/`. - -## Reusable Shell - -The code is MCP-first today, but the repository is intentionally not named or -structured as only an MCP proxy. Authentication, configuration ingestion, TLS -handling, plugin execution, telemetry, runtime shape, and session strategy are -gateway-shell concerns. - -MCP-specific behavior should remain isolated to the current MCP modules so -future A2A or model-provider routing can reuse the shell instead of growing a -parallel stack. - -## Why These Architecture Pages Exist - -The architecture section is split into subpages because each page tracks a -boundary that already exists in modules or runtime ownership: - -| Subpage | Reason it exists | -| --- | --- | -| Request Flow | Shows the ordered path through startup, middleware, initialize, and authorized calls. | -| Authentication And User Config Lookup | Keeps identity and config selection separate from MCP method handling. | -| Runtime Configuration | Describes the data model the gateway consumes, independent of Redis transport details. | -| Backend Connections And Transports | Separates downstream, upstream, and config-store transports. | -| Session Ownership | Makes local backend session state and load-balancing constraints explicit. | -| Architectural Choices | Records tradeoffs that should not be changed accidentally. | - -This split should make later pages easier to fill in without turning the -architecture chapter into one long mixed-concern essay. diff --git a/docs/book/src/telemetry-and-diagnostics.md b/docs/book/src/telemetry-and-diagnostics.md deleted file mode 100644 index 73b15c87..00000000 --- a/docs/book/src/telemetry-and-diagnostics.md +++ /dev/null @@ -1,169 +0,0 @@ -# Telemetry And Diagnostics - -> 📈 **Observability lens:** the gateway emits three signals — logs, request -> traces, and HTTP metrics. This page explains how each is produced, how to -> verify them locally, and where to look when a request misbehaves. - -## Signal Overview - -| Signal | Produced by | Destination | -| --- | --- | --- | -| Logs | `tracing` events from layers, validators, and MCP handlers. | Console and a rotating file in the working directory. | -| Request traces | `tower_http::TraceLayer` spans around every HTTP request. | OTLP trace export when `--enable-open-telemetry` is set. | -| HTTP metrics | `axum-otel-metrics` (`HttpMetricsLayer`) request instruments. | OTLP metrics export when `--enable-otel-metrics` is set. | - -All export settings are process config; see the telemetry section of the -[Configuration Reference](gateway-options.md#telemetry-options). - -## Logging - -Console and file logging are always on. The file appender writes -`contextforge-data-plane.log` (configurable with `--log-name`) in the current -working directory and rotates hourly by default (`--log-rotation`). - -Three environment filters control verbosity independently: - -| Env var | Default | Controls | -| --- | --- | --- | -| `RUST_LOG` | `debug` | Console events. | -| `RUST_FILE_LOG` | `debug` | File events. | -| `RUST_TRACE_LOG` | `info` | Which spans reach the OTLP trace exporter. | - -Dataplane log messages use a stable `method_name - event text field = value` -shape, so boundary-specific prefixes are grep-friendly: `claims_layer`, -`user_config_store_layer`, `virtual_host_config_layer`, -`AuthorizedCallValidator::validate`, `initialize:`, `call_tool`, and so on. - -## Traces - -`TraceLayer` emits its request spans at `DEBUG` level. - -> ⚠️ **`RUST_TRACE_LOG=debug` is required for trace export.** The default span -> filter (`info`) drops the HTTP spans before they reach the OTLP exporter, so -> nothing arrives at the trace backend. - -Enable export with `--enable-open-telemetry true` plus the OTLP protocol, -endpoint, and header flags. Each handled HTTP request produces one span with -method, route, status, and latency. - -## Metrics - -`--enable-otel-metrics true` turns on the HTTP server instruments: request -duration histogram, active request gauge, and request/response body size -histograms, all labeled with method, status code, and `service.name`. - -Metrics are pushed by a `PeriodicReader` every **30 seconds**, so allow about -35 seconds after the first request before the first data point appears -downstream. - -## Local Verification Stack - -A complete local pipeline ships under `docker/` as overlays on top of the -[local run](running-the-gateway.md) stack: - -| Component | Role | Endpoint | -| --- | --- | --- | -| Langfuse | Trace backend and span viewer. | UI at `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`, raw dumps via `docker logs otel-collector`. | -| Prometheus | Scrapes the collector for browsable PromQL. | UI at `http://localhost:9090`. | - -### 1. Start the stack - -```bash -docker compose \ - -f docker/docker-compose-local.yaml \ - -f docker/docker-compose-langfuse.yaml \ - -f docker/docker-compose-otel-collector.yaml \ - up -d -``` - -Re-run with `ps` instead of `up -d` and wait until every container is healthy. - -### 2. Run the gateway with export enabled - -```bash -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-headers "Authorization=Basic cGstbGYtY29udGV4dGZvcmdlOnNrLWxmLWNvbnRleHRmb3JnZQ==" \ - --otlp-metrics-endpoint http://127.0.0.1:4318/v1/metrics \ - --otlp-service-name contextforge-data-plane -``` - -The `Authorization` header is Basic auth for the seeded Langfuse project keys -(`pk-lf-contextforge:sk-lf-contextforge`, base64-encoded). - -### 3. Generate traffic - -```bash -USER_ID=11111111-1111-1111-1111-111111111111 -USER_EMAIL=admin@example.com - -for i in {1..10}; do - curl -s -o /dev/null -w "%{http_code}\n" \ - "http://127.0.0.1:8001/contextforge-rs/admin/tokens/${USER_ID}?email=${USER_EMAIL}" -done -``` - -Any response counts: each request is traced and recorded as a metric sample. - -### 4. Inspect the data - -- **Langfuse:** open `http://localhost:3100` and check the project — one span - per request with method, route, status, and latency. -- **Prometheus:** open `http://localhost:9090`; `Status → Targets` should show - `otel-collector:8889` as **UP**, then try the starter queries below. -- **Collector stdout:** `docker logs otel-collector --tail 200` shows raw OTLP - trace and metric dumps. - -The data path is: - -```text -gateway :8001 - -> OTLP/HTTP traces -> Langfuse :3100 (span UI) - -> OTLP/HTTP metrics -> OTel Collector :4318 - -> stdout dump (docker logs) - -> Prometheus exposition :8889 -> Prometheus :9090 -``` - -Tear the stack down with the same three `-f` files and `down`. - -## Prometheus Starter Queries - -| Question | Query | -| --- | --- | -| Request count by method, status, and 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` | - -## Debugging Checklist - -Work down the boundaries in request order; each failure has an owning signal. -[Failure Modes](failure-modes.md) lists the exact responses per boundary. - -| Symptom | Where to look | -| --- | --- | -| `401` responses | `claims_layer` log lines: missing/invalid bearer token, unsupported algorithm, or no configured decoder key. | -| `400` config responses | `user_config_store_layer` log lines and Redis content for the JWT subject. | -| `404` `Server not found` | `virtual_host_config_layer` debug line showing the requested virtual host id and how many the caller's config has. | -| MCP routing errors | `AuthorizedCallValidator::validate` debug lines, then `call_tool`/`read_resource`/`get_prompt` warns for the split and backend resolution. | -| Backend failures | `initialize:` warns for backends that failed to connect; routed-call warns name the failing backend. | -| Plugin problems | CPEX pipeline error logs; an invalid reload marks the plugin runtime failed until a valid config is applied. | - -## Known Gaps - -Tracked upstream, not yet implemented here: - -- W3C trace-context propagation across gateway hops - ([mcp-context-forge#4723](https://github.com/IBM/mcp-context-forge/issues/4723)). -- MCP-semantic spans with tool names and JSON-RPC method attributes - ([mcp-context-forge#4722](https://github.com/IBM/mcp-context-forge/issues/4722)). diff --git a/docs/book/src/testing.md b/docs/book/src/testing.md deleted file mode 100644 index 4f804860..00000000 --- a/docs/book/src/testing.md +++ /dev/null @@ -1,117 +0,0 @@ -# Testing - -> 🧪 **Verification rings:** workspace checks prove the code compiles and unit -> behavior holds, in-repo integration tests prove MCP routing against mock -> backends, and the `cf-integration` harness proves the whole -> control-plane-to-dataplane path end to end. Load and benchmark runs have -> their own page: [Performance](performance.md). - -## Workspace Validation - -CI runs these on every change; run them locally before pushing: - -```bash -cargo fmt --all --check -cargo clippy --locked --workspace --all-targets -- -D warnings -cargo nextest run --locked --workspace -``` - -Use `cargo test` when nextest is unavailable. For book changes, also run -`mdbook build docs/book` and `mdbook test docs/book`. - -Protocol tests and fixtures should target MCP `2026-07-28`, use -`server/discover`, and include the required per-request client metadata. Do not -add new dataplane coverage for older protocol versions, legacy session -initialization, or SSE; those paths belong in control-plane tests and must not -route legacy traffic through the dataplane. Existing legacy-shaped tests are -migration inventory and should be replaced as the modern implementation lands. - -## In-Repo Integration Tests - -`crates/contextforge-data-plane-lib/tests/` exercises the gateway against -in-process mock MCP backends (shared helpers live in `tests/support/`): - -| Test file | Covers | -| --- | --- | -| `gateway_list_tools.rs` | List fanout, prefixing, and merged output. | -| `gateway_prompts.rs` | Prompt listing and prefixed `get_prompt` routing. | -| `gateway_resource_templates.rs` | Template fanout with prefixed names and URI templates, plus `read_resource` round-trips. | -| `gateway_plugins.rs` | CPEX pre/post tool hooks around `call_tool` and stream events. | - -These run in `cargo nextest run` with no Docker dependencies. - -## Full-Stack Integration Harness - -[`cf-integration`](https://github.com/contextforge-org/contextforge-dev-tools) -wires the external ContextForge control plane (`cf-controlplane`) to this -dataplane the way production intends: the stock upstream Compose stack, plus -exactly two intentional differences — nginx routes only -`/servers/{virtual_host_id}/mcp` to the dataplane (as -`/contextforge-rs/servers/{virtual_host_id}/mcp`), and the control plane runs -with `DATAPLANE_PUBLISHER=true` so virtual server configs reach the dataplane -through Redis. Because the stack otherwise matches upstream, test failures -measure dataplane behavior, not stack drift. - -### Quick Start - -```bash -scripts/cf-integration.sh up -``` - -This checks out the control plane under `.integration/mcp-context-forge`, -pulls the published dataplane image, and starts the combined stack plus a -local MCP counter backend. The admin UI is at -`http://localhost:8080/admin` (`admin@example.com` / `changeme`). A Fast Time -backend is auto-registered as a fixed virtual server, so the commands below -work with no manual UI step; backends added through the UI are published to -the dataplane by the control-plane publisher. - -### Route Probe - -```bash -scripts/cf-integration.sh probe -``` - -Verifies the public nginx-to-dataplane route end to end: a 401 negative check, -`initialize`, session reuse, `tools/list`, and `tools/call`. - -### Full Test Runs - -| Command | What it runs | -| --- | --- | -| `scripts/cf-integration.sh test-all` | Every live lane against the running stack, with per-test result rows and full output in a timestamped log under `.integration/test-logs/` (override with `CF_TEST_LOG_DIR`). | -| `CF_TEST_ALL_LOCUST=true scripts/cf-integration.sh test-all` | Same, plus the full Locust load run as a final lane. | -| `scripts/cf-integration.sh test-all-up` | Start or update the stack, then `test-all` without the load lane. | -| `scripts/cf-integration.sh test-all-up-load` | Start or update the stack, then `test-all` with the load lane. | - -Individual lanes are `live-mcp`, `live-rbac`, `live-protocol`, and `live-all`. -`live-mcp` is the green lane: the full MCP protocol end-to-end suite passes -against this harness. Remaining failures in the other lanes measure known -dataplane feature gaps; the harness `reports/` directory keeps the current -classification. - -### Control-Plane Baseline - -To separate dataplane regressions from upstream behavior, the harness can run -the stock control-plane-only stack (no dataplane, no nginx split, no -publisher) with the same commands: - -```bash -scripts/cf-integration.sh down # frees the shared host ports -scripts/cf-integration.sh controlplane-test-all # up + live core + locust -``` - -Individual steps are `controlplane-up`, `controlplane-live-core`, -`controlplane-live-all`, `controlplane-locust`, and `controlplane-down`. The -baseline load run is covered in [Performance](performance.md). - -### Key Settings - -| Variable | Purpose | -| --- | --- | -| `CF_DATAPLANE_IMAGE` / `CF_DATAPLANE_VERSION` | Which published dataplane image the stack runs. | -| `CF_CONTROLPLANE_IMAGE` / `CF_CONTROLPLANE_REF` | Which control-plane image and git ref to use. | -| `NGINX_PORT` | Public front-door port (default `8080`). | -| `CF_TEST_LOG_DIR` | Where `test-all` writes timestamped logs. | - -See the harness README for the full command and override list. diff --git a/docs/book/src/usage.md b/docs/book/src/usage.md deleted file mode 100644 index d44a7812..00000000 --- a/docs/book/src/usage.md +++ /dev/null @@ -1,30 +0,0 @@ -# Getting Started - -This section is for the first working run of the gateway: start the local -dependencies, launch the Rust dataplane, seed runtime config, and send real MCP -traffic through it. - -> 🚀 **Goal:** get from a clean checkout to one client-facing MCP endpoint at -> `/contextforge-rs/servers/{virtual_host_id}/mcp`. - -| Page | What it covers | -| --- | --- | -| 🚀 [Run the Gateway Locally](running-the-gateway.md) | Docker services, local bootstrap helpers, user config, MCP session setup, and smoke tests. | -| ⚙️ [Configuration Reference](gateway-options.md) | Listener settings, Redis wiring, JWT verification, upstream transport, telemetry, logging, and runtime knobs. | - -## What "started" means - -A useful local gateway run has these pieces: - -| Piece | Why it matters | -| --- | --- | -| Redis | Holds runtime `UserConfig` keyed by JWT subject. | -| Backend MCP servers | Provide the tools, resources, and prompts the gateway merges. | -| Gateway listener | Accepts downstream streamable HTTP MCP traffic. | -| JWT verification key or secret | Lets the gateway authenticate downstream requests. | -| User config | Maps the caller's JWT subject to virtual hosts and backend MCP URLs. | -| MCP session | Binds downstream session state to backend MCP client sessions. | - -If one of those is missing, the gateway should fail at the boundary that owns -that fact: authentication, config lookup, virtual host resolution, session -lookup, routing, or upstream transport. diff --git a/docs/book/src/what-is-contextforge-data-plane.md b/docs/book/src/what-is-contextforge-data-plane.md deleted file mode 100644 index 7404fdac..00000000 --- a/docs/book/src/what-is-contextforge-data-plane.md +++ /dev/null @@ -1,129 +0,0 @@ -# What is ContextForge Data Plane? - -Welcome to the ContextForge Data Plane book. - -`contextforge-data-plane` is the Rust **data plane** for ContextForge: a single -MCP entry point that sits in front of many backend MCP servers and makes them -look like one. If you have used an API gateway or reverse proxy for HTTP APIs, -this is the same idea for the Model Context Protocol (MCP). - -Concretely, the gateway accepts MCP streamable HTTP traffic from a client, -authenticates the caller, loads that caller's runtime configuration, opens MCP -client sessions to the backend MCP servers that configuration allows, and -presents those backends to the client as one merged MCP server. - -Throughout this book, *downstream* means the client side of the gateway and -*upstream* means the backend side. - -> **Protocol boundary:** the downstream dataplane target is MCP `2026-07-28` -> over Streamable HTTP. Older MCP versions, legacy session initialization, and -> SSE remain on external ContextForge control-plane routes and do not enter the -> dataplane. The current session-oriented internals are temporary migration -> state, not a compatibility promise for direct dataplane clients. - -![Gateway overview](assets/gateway-overview.svg) - -Blue arrows show request traffic. Green arrows show backend responses returning -to the gateway and the merged MCP response going back to the client. - -| Layer | What happens | -| --- | --- | -| Client edge | A modern MCP `2026-07-28` client calls `/contextforge-rs/servers/{virtual_host_id}/mcp` over Streamable HTTP with a bearer token and per-request client context. | -| Gateway hot path | The gateway validates identity, loads runtime config, selects the virtual host, and routes MCP methods. | -| Backend edge | The gateway opens or reuses MCP client sessions to configured backend MCP servers and merges what the client sees. | - -> **Key idea:** downstream clients interact with one logical MCP server. The -> gateway decides which configured backend sessions are involved. - -The gateway is not the management application. It does not own the UI, IAM -lifecycle, tenant administration, durable metrics storage, or long-lived -configuration editing. Those concerns stay in the external ContextForge control -plane. This repository owns the hot request path and the runtime pieces needed -to make that path fast, observable, and enforceable. - -Most operators should not think about backend MCP servers directly. They should -think about one client-facing MCP endpoint: - -```text -/contextforge-rs/servers/{virtual_host_id}/mcp -``` - -Behind that endpoint, the gateway has three main boundaries: - -- the downstream boundary, where clients connect over streamable HTTP and - present their bearer token and MCP `2026-07-28` per-request context -- the configuration boundary, where the gateway turns the JWT subject into a - runtime `UserConfig` -- the upstream boundary, where the gateway opens or reuses MCP client sessions - to the configured backend servers - -Most bugs in this service come from confusing those boundaries. A downstream -request is not allowed to pick arbitrary upstream backends. Redis is not the -routing model; it is the current config-store transport. Backend sessions are -not durable cluster state; they are local process state today. - -| Boundary | Input | Output | Owner | -| --- | --- | --- | --- | -| Downstream | HTTP request, JWT, MCP headers | request extensions and downstream session id | gateway | -| Configuration | JWT subject and virtual host id | `UserConfig` and selected `VirtualHost` | control plane data, gateway enforcement | -| Upstream | selected backend names and URLs | running backend MCP client services | gateway | - -## Key terms - -These words appear on almost every page. It is worth pinning them down once. - -| Term | Meaning in this book | -| --- | --- | -| Dataplane | The process on the live request path (this repository). It handles MCP traffic. | -| Control plane | The external ContextForge application that authors config, policy, and identity. It is not in this repository. | -| Downstream | The client side of the gateway: the calling MCP client and its requests. | -| Upstream | The backend side of the gateway: the configured backend MCP servers. | -| Virtual host | A named routing group inside the caller's config. The URL path selects exactly one. | -| Backend | One configured MCP server behind the gateway. Its map key becomes a routing prefix when a virtual host has multiple backends. | -| Principal | The authenticated caller identity, taken from the JWT `sub` claim. | -| Session | An initialized MCP session, tracked by `Mcp-session-id`, that owns the per-caller backend client sessions. | - -## Mental model - -The gateway is easiest to understand as one logical MCP server assembled from a -set of configured backend MCP servers. - -On `initialize`, the gateway validates the caller, resolves the requested -virtual host, and creates upstream MCP client sessions for that virtual host's -backends. On list calls, it fans out to those backends and merges the result. On -targeted calls, it uses an explicit tool alias, the only configured backend, or -a multi-backend prefix to route to exactly one backend. - -For a stateful MCP call to work, five facts must line up: - -```text -JWT subject - -> user config - -> virtual host id - -> downstream MCP session id - -> backend session map entries -``` - -If any part is missing, the gateway should fail at the layer that owns that -fact: auth, config lookup, virtual host resolution, session lookup, routing, or -upstream transport. - -> **Operational consequence:** stateful MCP traffic needs session affinity -> until backend session state moves out of the gateway process. - -## Non-goals - -This repository should not grow control-plane behavior by accident. It should -not become the admin UI, tenant management API, policy authoring system, -credential store, or durable observability backend. - -It also should not expose backend topology as more than the MCP routing -contract requires. Backend map keys are visible when multi-backend identifiers -need prefixes, but clients should still experience one gateway endpoint and -one logical MCP server. - -## Where to go next - -- To run the gateway yourself, start with [Getting Started](usage.md). -- To understand how requests move through it, read [Architecture](architecture.md). -- For the exact client-facing protocol behavior, see [MCP Behavior](mcp-behavior.md).