diff --git a/_context/wiki/architecture.md b/_context/wiki/architecture.md index e36a668d..2fc8d141 100644 --- a/_context/wiki/architecture.md +++ b/_context/wiki/architecture.md @@ -1,260 +1,171 @@ # Architecture -> This page describes the **current Rust implementation**, including temporary -> backend fan-out and session behavior. The tentative configuration-driven end -> state is in -> [ContextForge 2.0 Target Architecture and Roadmap](mcp-capability-allocation.md). +This page describes the current Rust implementation. Proposed catalog and +policy compilation is in the [ContextForge 2.0 roadmap](mcp-capability-allocation.md). ## Middleware Stack Order -Tower layers execute outside-in. A request reaches MCP handlers with these extensions already set: +Tower layers execute outside-in: ```text TCP/TLS listener -> HttpMetricsLayer - -> TraceLayer + -> TraceLayer (extract incoming trace context) -> /contextforge-rs nested router - -> mcp_origin_layer → validates Origin (403 when invalid/disallowed) + -> mcp_origin_layer validates Origin (403) -> CORS layer - -> mcp_header_limits_layer → MCP standard header budgets (431 when exceeded) - -> virtual_host_id_layer → inserts VirtualHostId (400 on path mismatch) - -> claims_layer → inserts ContextForgeClaims (401 on bad/missing JWT) - -> session_id_layer → inserts SessionId if present - -> user_config_store_layer → inserts UserConfig (400 no config, 500 store error) - -> virtual_host_config_layer → rejects unknown vhost (404 "Server not found") - -> /servers/{virtual_host_name}/mcp RMCP service → validates Host, then dispatches MCP + -> mcp_header_limits_layer bounds MCP headers (431) + -> virtual_host_id_layer inserts VirtualHostId from path (400) + -> claims_layer verifies JWT, inserts AuthorizationClaims (401) + -> PrincipalExtractorLayer inserts AuthorizedPrincipal (401) + -> user_config_store_layer loads UserConfig (400 missing, 500 decode/error) + -> virtual_host_config_layer checks caller's virtual host (404) + -> /servers/{virtual_host_name}/mcp RMCP service + Host validation -> HTTP/MCP validation -> method dispatch ``` -DNS-rebinding validation is split by behavior. `mcp_origin_layer` rejects any -present Origin that is malformed or not allowlisted; requests without Origin -continue. RMCP validates the optional Host allowlist at the MCP service -boundary. See [Security](security.md#mcp-origin-and-host-validation). -`mcp_header_limits_layer` rejects excessive MCP standard headers before JWT -validation, config lookup, session creation, backend fanout, or RMCP body -parsing. +Origin is checked before authentication. The optional Host allowlist is checked +at the RMCP boundary, so earlier middleware may return first. MCP header budgets +apply before JWT verification, configuration reads, and body parsing. See +[Security](security.md#mcp-origin-and-host-validation). -MCP handlers read typed extensions and never parse paths or Redis keys directly. -`tools/call` reads the downstream header map from RMCP's request-context -`Parts` extension for parameter-header validation. +Health and development helper routes are registered outside the MCP auth/config +layers. MCP handlers consume typed extensions; they do not parse Redis keys. +`tools/call` also reads the HTTP headers from the request-context `Parts`. ## Pipeline Shape ```text -downstream request - -> Origin validation → MCP header limits → virtual host extraction → JWT validation → session extraction - -> user config lookup → RMCP request validation → MCP handler validation - -> request plugin hooks - -> backend MCP call (concurrent via join_all for initialize/list) - -upstream response - -> response plugin hooks → merge/namespace/passthrough - -> metrics, tracing, logging → downstream response +modern MCP request + -> header, JWT, and principal checks + -> user config and virtual-host check + -> published object/backend route + -> recognized tool parameter-header validation + -> pre-hook + -> connect to one backend -> call -> close + -> post-hook on the successful response + -> MCP 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\nRMCP · MCP validate"] - P1["request plugins\ntool_pre_invoke"] - B["backend MCP call\njoin_all for init/list"] - P2["response plugins\ntool_post_invoke"] - M["merge · namespace\npassthrough"] - T["metrics · tracing · logging"] - U(["downstream response"]) - D --> A --> C --> P1 --> B --> P2 --> M --> T --> U -``` +Tools, resources, and prompts use explicit routing tables. There is no catalog +fan-out or prefix splitting. Resource pre-hooks may rewrite a URI only to an +unambiguous target published in the caller's virtual host. +For `tools/call`, a published input schema enables local `Mcp-Param-*` +validation before plugins and backend I/O. The gateway does not fetch +`tools/list`. Without a schema, parameter headers are forwarded without local +validation. A plugin that changes an annotated argument does not change the +original parameter header; the backend may reject a resulting mismatch. -RMCP enforces its configured request-body cap and validates modern standard -headers before dispatch. The `tools/call` handler then resolves the request's -backend and original tool name. When `UserConfig` contains that tool's input -schema, it validates recognized `Mcp-Param-*` headers against the request body; -it does not call backend `tools/list`. Without a published schema, parameter -headers are unrecognized and forwarded without local validation. -Published annotations are validated for MCP token, uniqueness, primitive type, -and properties-only reachability constraints. Nested annotations read the exact -argument path. Present non-null values require a matching header; absent or -null values require no header. -Parameter headers are forwarded unchanged; request plugins run afterward, so a -plugin that changes an annotated argument also owns any resulting upstream -mismatch. +Backend cleanup occurs before post-hooks. A cleanup failure is logged and does +not replace the operation's result. Error and cancellation paths do not turn +into successful response hooks. See [Routing](routing.md) and +[Failure Modes](failure-modes.md) for method-specific details. -Order is invariant: auth/config before backend selection; request plugins before upstream; response plugins before returning. +## Module Boundaries -## Module Boundaries (`contextforge-data-plane-lib`) - -| Module | Owns | +| Module / crate | 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) | +| Binary `main.rs`, `logging.rs` | Startup wiring and telemetry providers. | +| Library `common.rs` | CLI configuration, Redis/TLS validation, upstream HTTP client construction. | +| Library `authorization/` | JWKS verification and principal extraction. | +| Library `layers/` | Request metadata, authentication/configuration boundaries, and validation. | +| Library `gateway/` | MCP method handlers, explicit routing, per-request backend clients, progress forwarding. | +| Library `user_config_store/` | `UserConfigStore` and Redis/cache implementation. | +| Library `transports/` | TCP and TLS listeners. | +| Library `tools.rs` | Development bootstrap routes, gated by `with_tools`. | +| `contextforge-data-plane-apis` | Published configuration models and schemas. | +| `contextforge-data-plane-cpex` | Registry, runtime reloads, request hook state, and CMF adapters. | ## State Ownership | State | Owner | Lifetime | | --- | --- | --- | -| CLI `Config` | Binary startup + `Gateway` | Process | -| JWT decoders | `ContextForgeDataPlaneAppState` | Process | -| User config | `RedisUserConfigStore` (LRU + Redis) | Request-path consumed; control-plane authored | -| Request identity / VirtualHostId | Request extensions | One HTTP request | -| Downstream session id | RMCP + `SessionId` extension | MCP session | -| Backend RMCP services (initialize, list ops) | `BackendTransports` map | Local process, per principal/backend/session | -| Backend RMCP services (call_tool) | Per-request connection | Single HTTP request | -| Local user session mapping | `LocalUserSessionStore` | Local LRU, 50k entries, 1 hour | -| Plugin manager | `CpexRuntimeRegistry` | Process, reloadable | - -> **Session rule:** backend MCP services are local process state. Sticky routing required for load-balanced deployments. - -## Executor Shapes - -| `--single-runtime` | Shape | -| --- | --- | -| `true` (default) | One multi-thread Tokio runtime, `--number-of-cpus` workers. All connections share one `BackendTransports`. | -| `false` | One OS thread per CPU, each with its own current-thread Tokio runtime and own `BackendTransports`. `SO_REUSEPORT` spreads connections — no session affinity. **Stateful MCP sessions need `--single-runtime true`**. | - -In multi-runtime mode, the first thread initializes the optional CPEX plugin runtime before the others start; the current-thread builders are tuned with a global queue interval of `1024` and `4` I/O events per tick. - -> **Multi-runtime consequence:** each runtime thread builds its own `BackendTransports` map and user-session store. Backend session state is per-runtime-thread, and `SO_REUSEPORT` gives no connection affinity — later requests in a streamable HTTP session can land on a thread that does not own the session. Treat single-runtime as the only mode supporting stateful MCP sessions today. - -## Lock Design - -| State | Lock | Contention profile | -| --- | --- | --- | -| `BackendTransports` map | `Arc>>` | Locked briefly on initialize insert, list-op borrow, and cleanup. Borrowing clones `Arc` handles so the lock is not held across backend calls. `call_tool` bypasses this map entirely. | -| 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. | - -Design rule: locks guard maps of handles, not I/O. Backend calls, Redis reads, and plugin hooks all run outside any gateway lock. +| Parsed config and shared upstream HTTP client | Gateway | Process. | +| JWKS keys | JWT authorization service | Five-minute cache; fetched when verification needs them. | +| User config | Redis store and optional local LRU | Redis is authoritative; local capacity 50,000, default expiry 60 seconds. | +| Principal, claims, virtual-host ID, config snapshot | HTTP request extensions | One request. | +| Backend RMCP service | Routed operation | One request; explicitly closed after the call. | +| Tool progress-token mapping | Request's backend client | While the tool call is in flight. | +| Active CPEX runtime | Registry | Reloadable; in-flight hook state pins its selected runtime. | +| RMCP session manager | RMCP service (`LocalSessionManager`) | Transport implementation detail; no session is required by the supported modern request contract. | + +The library no longer has `BackendTransports`, `SessionId` middleware, or a +`LocalUserSessionStore`. Modern routing never reuses backend session state from +a prior request and does not require load-balancer affinity. + +Cache hits do not extend a user-config entry's expiry. A miss releases the LRU +lock before reading Redis. `--user-config-cache-expiry-seconds 0` disables this +cache. Development config writes update the local process's cache immediately; +other replicas see the write after their own expiry. + +## Executor and Locks + +The binary starts one Tokio runtime through `#[tokio::main]`. The parsed +`--number-of-cpus` and `--single-runtime` fields are currently not used to +construct it. Do not use those flags to tune workers or select per-CPU runtimes. + +Locks have specific scopes: + +- The user-config LRU mutex protects cache access, not Redis I/O. +- JWKS refresh and plugin-config connection management synchronize their own + shared state; do not assume all network I/O is globally lock-free. +- Tool progress tracking holds a write guard while enqueuing the backend call + so an early notification cannot race registration. +- Tool hook state uses a mutex to serialize progress and final-response plugin + context updates. Prompt/resource state belongs to one request. + +There is no shared map of live backend transports to lock during routing. ## 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 TCP listener uses socket reuse options, keepalive, and backlog `1024`, then +serves Axum with Ctrl-C graceful shutdown. The TLS listener accepts through +Rustls and serves the same router through Hyper. See the transport implementation +before relying on identical shutdown behavior between the two listener types. -The binary sets `tikv_jemallocator` as the global allocator. jemalloc holds up better than the system allocator under the many small, short-lived allocations of per-request JSON and header processing. +The binary uses `tikv_jemallocator` as its global allocator. Performance claims +about it require a measured workload; see [Performance](performance.md). -## Fanout And Cancellation +## Cancellation, Progress, and Plugins -- `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 (except `call_tool`) resolve exactly one backend service handle from `BackendTransports`. -- Targeted tool, prompt, and resource calls run configured pre/post plugin hooks after backend routing. `call_tool` creates a fresh per-request backend connection via `connect_backend_for_request`, then explicitly closes it before returning. -- `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. +`tools/call` observes the downstream cancellation token and forwards cancellation +to the in-flight backend request. Progress notifications translate the generated +backend token to the caller's token; unknown tokens are dropped. Configured tool +post-hooks can inspect stream events, and a denied notification is dropped. +Prompt/resource operations should not be assumed to have the same explicit +cancellation relay. -Resource reads carry a concrete, request-owned hook state across backend I/O. It pins the runtime selected before the read, or records that no post hook was configured. Post processing consumes that state without type erasure, downcasts, or a second registry lookup. +Pre-hooks select a runtime before backend I/O. Typed hook state retains both +that runtime and whether a post-hook was enabled. Reloads affect subsequent +requests; they cannot add a hook or change policy halfway through a call. +Invalid reloads mark the registry failed for new calls while already pinned +requests can finish. Details are in [Plugin Config](config.md#plugin-config-redis-key-contextforgegatewayruntimepluginconfig). -## Startup And Response Flow - -Startup sequence (`main.rs` → `Gateway::run_gateway`): +## Startup ```text -install rustls crypto provider +Tokio main + -> 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 (tool, prompt, and resource calls) - -> merge / namespace / pass through - -> virtual_host_config_layer response side - -> user_config_store_layer response side - -> session_id_layer response side ← on DELETE success: remove session + backend transports - -> claims_layer response side - -> virtual_host_id_layer response side - -> CORS, mcp_origin_layer, TraceLayer, HttpMetricsLayer - -> downstream response + -> initialize logging and optional telemetry providers + -> optional CPEX registry and compiled factory registration + -> construct JWKS authorization service + -> build Gateway with Redis config store and RMCP session manager + -> initialize CPEX runtime from Redis, if enabled + -> run_gateway(): build router and start configured listeners ``` -Flow checkpoints — each must exist before the next dependency runs: - -| Checkpoint | Fact established | Next dependency | -| --- | --- | --- | -| Listener | Request reached the ContextForge external dataplane over TCP/TLS. | Metrics, tracing, nested routing. | -| Path extraction | Inner path matched `/servers/{virtual_host_id}/mcp`. | MCP handlers can resolve a `VirtualHost`. | -| Claims validation | Bearer token accepted; `ContextForgeClaims` exists. | Config lookup can use `claims.sub`. | -| User config lookup | `UserConfig` exists for the authenticated subject. | Virtual host check can run. | -| Virtual host check | Path's virtual host id exists in the caller's config. | MCP validators can resolve the selected `VirtualHost`. | -| RMCP dispatch | Streamable HTTP request mapped to an MCP method. | Handler chooses initialize, routed call, or local behavior. | - -## MCP-First, Not MCP-Only - -The current code implements MCP behavior, but the gateway shell is broader: - -```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 cover tool, prompt, and resource pre/post lifecycles. Before adding any new hook point, define all of the following: - -| Requirement | Why | -| --- | --- | -| Failure behavior | Does a plugin error abort the call, degrade gracefully, or log and continue? | -| Timeout behavior | What happens when a plugin takes too long on the hot path? | -| Cancellation behavior | Can the downstream cancel propagate through the plugin? | -| Streaming/SSE behavior | Does the hook fire once or per-chunk? What is the backpressure model? | -| Telemetry attribution | Which span/metric owns plugin latency and errors? | - -Avoid ad hoc plugin calls in routing code. New hook points belong at explicit, documented pipeline positions. - -## Architecture-Change Follow-Through Matrix +Some checks are lazy: JWKS retrieval occurs during token verification, and +backend connectivity is checked when an operation selects that backend. Health +is HTTP liveness, not dependency readiness. -Changing a load-bearing choice requires updating more than one file: +## Architecture-Change Follow-Through | Change | Required follow-through | | --- | --- | -| Downstream MCP version | Coordinate with the ContextForge control plane and built-in dataplane; update the `2026-07-28`/`2025-11-25` compatibility matrix, protocol tests, examples, and front-door routing. The ContextForge built-in dataplane handles both stateful and stateless traffic; the ContextForge external dataplane handles both supported Streamable HTTP versions statelessly. | -| Backend namespace / prefix contract | Update merge logic, split logic, tests, docs, and control-plane integration if client-facing surface moves. | -| Session state moves external | Update `SessionManager`, cleanup behavior, load-balancing docs, and failure-mode tests. | -| Config transport changes | Keep `UserConfigStore` as the boundary; update adapter tests. | -| Plugin hook surface expands | Document ordering, failure, timeout, cancellation, streaming, and telemetry before landing. | -| New protocol joins the gateway | Keep shared shell protocol-neutral; isolate new protocol-specific routing. | +| MCP behavior | Update modern protocol tests, examples, and front-door coordination. Do not expand legacy compatibility. | +| Published names, routes, or config shapes | Update publisher/consumer contracts, schemas, and integration tests. | +| Config transport | Keep user routing behind `UserConfigStore`; update adapter tests. | +| Plugin hook surface | Define ordering, failure, timeout, cancellation, streaming, and telemetry behavior; update validation and factory registration together. | +| Request lifecycle | Verify cancellation, backend cleanup, progress correlation, and replica independence. | diff --git a/_context/wiki/config.md b/_context/wiki/config.md index 10d57bf2..dfa53b99 100644 --- a/_context/wiki/config.md +++ b/_context/wiki/config.md @@ -3,10 +3,11 @@ ## Minimum Required Flags ```text ---redis-address --redis-port --redis-mode +--redis-address --redis-port --redis-mode --jwks-url ``` -Plus at least: `--address` or `--tls-address`, `--token-verification-public-key` or `--token-verification-secret`. +Plus at least one listener: `--address` or `--tls-address`. Development builds +with `with_tools` also require `--token-verification-private-key`. ## Complete CLI and Environment Reference @@ -26,13 +27,18 @@ Origin and Host settings retain the explicitly configured | Flag | Environment variable | Default / requirement | Purpose | | --- | --- | --- | --- | -| `--address ` | `CONTEXTFORGE_DATA_PLANE_ADDRESS` | Optional | Plain HTTP listener. | -| `--tls-address ` | `CONTEXTFORGE_DATA_PLANE_TLS_ADDRESS` | Optional | TLS listener; requires server certificate and key. | +| `--address ` | `CONTEXTFORGE_DATA_PLANE_ADDRESS` | Optional | Plain HTTP listener. | +| `--tls-address ` | `CONTEXTFORGE_DATA_PLANE_TLS_ADDRESS` | Optional | TLS listener; requires server certificate and 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. | -| `--token-verification-public-key ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PUBLIC_KEY` | For RSA tokens | Verifies `RS256`, `RS384`, and `RS512` tokens. | -| `--token-verification-secret ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET` | For HMAC tokens | Verifies `HS256`, `HS384`, and `HS512` tokens. | -| `--token-verification-private-key ` | `CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY` | Required when built with `with_tools` | Signs tokens for the optional local bootstrap helper. | +| `--jwks-url ` | `CONTEXTFORGE_DATA_PLANE_JWKS_URL` | Required | Fetches RSA/EC JWT verification keys. HTTPS required except for loopback HTTP testing. | +| `--jwks-ca-cert-path ` | `CONTEXTFORGE_DATA_PLANE_JWKS_CA_PATH` | Optional | PEM CA bundle trusted by the JWKS HTTP client. | +| `--token-verification-private-key ` | None (CLI only) | Required when built with `with_tools` | Signs local test tokens and supplies the public key served by the local JWKS helper. | +| `--cel-principal-extractor-path ` | None (CLI only) | Optional | CEL principal mapping for custom claim layouts; otherwise uses the default user/tenant claim mapping below. | + +The former `--token-verification-public-key` and `--token-verification-secret` +flags are no longer accepted. For the local signing/JWKS setup, follow +[Getting Started](getting-started.md#local-cargo-dev-workflow). ### MCP request validation @@ -82,8 +88,8 @@ the HTTP transport. | Flag | Environment variable | Default | Purpose | | --- | --- | --- | --- | -| `--number-of-cpus ` | `CONTEXTFORGE_DATA_PLANE_NUMBER_OF_CPUS` | Host CPU count | Tokio worker/runtime thread count. | -| `--single-runtime ` | `CONTEXTFORGE_DATA_PLANE_SINGLE_RUNTIME` | `true` | `false` creates per-CPU runtimes without session affinity. | +| `--number-of-cpus ` | `CONTEXTFORGE_DATA_PLANE_NUMBER_OF_CPUS` | Unset | Parsed but currently unused by the Tokio entry point. | +| `--single-runtime ` | `CONTEXTFORGE_DATA_PLANE_SINGLE_RUNTIME` | Unset | Parsed but currently unused; the binary starts one Tokio runtime. | | `--runtime-plugins-enabled ` | `CONTEXTFORGE_DATA_PLANE_RUNTIME_PLUGINS_ENABLED` | `false` | Enables compiled-in CPEX hooks and Redis plugin config loading. | ### Telemetry and logging @@ -91,27 +97,42 @@ the HTTP transport. | Flag | Environment variable | Default | Purpose | | --- | --- | --- | --- | | `--enable-open-telemetry ` | `CONTEXTFORGE_DATA_PLANE_ENABLE_OPEN_TELEMETRY` | `false` | Enables OTLP trace export. | -| `--enable-otel-metrics ` | `CONTEXTFORGE_DATA_PLANE_ENABLE_OTEL_METRICS` | `false` | Enables OTLP HTTP-server metric export. | +| `--enable-otel-metrics ` | `CONTEXTFORGE_DATA_PLANE_ENABLE_OTEL_METRICS` | `false` | Enables OTLP HTTP-server metric export when `--enable-open-telemetry true` is also set. | | `--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 endpoint; defaults to `http://127.0.0.1:4317` for gRPC or `http://127.0.0.1:4318/v1/traces` for HTTP. | | `--otlp-metrics-endpoint ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Protocol-specific | Metrics endpoint; defaults to `http://127.0.0.1:4317` for gRPC or `http://127.0.0.1:4318/v1/metrics` for HTTP. | | `--otlp-headers ` | `CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_HEADERS` | None | Comma-separated `key=value` exporter headers. | | `--otlp-service-name ` | `CONTEXTFORGE_DATA_PLANE_OTEL_SERVICE_NAME` | `CONTEXTFORGE-DATA-PLANE` | OpenTelemetry `service.name`. | -| `--log-name ` | `CONTEXTFORGE_DATA_PLANE_LOG_NAME` | `contextforge-data-plane.log` | File log name in the current directory. | -| `--log-rotation ` | `CONTEXTFORGE_DATA_PLANE_LOG_ROTATION` | `hourly` | `minutely`, `hourly`, `daily`, or `never`. | +| `--log-name ` | `CONTEXTFORGE_DATA_PLANE_LOG_NAME` | Unset | Parsed but currently unused; no file logger is installed. | +| `--log-rotation ` | `CONTEXTFORGE_DATA_PLANE_LOG_ROTATION` | Unset | Parsed but currently unused; no file rotation is installed. | ## 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`. +The JWT signature is checked against the configured JWKS. The default principal +extractor then requires user and tenant IDs at the top level of the claims: -> **No revocation:** a leaked token is valid until `exp`. Rotate the signing key and restart to invalidate all outstanding tokens. +| Claim | Current behavior | +| --- | --- | +| `sub`, `user_id`, `UserId` | First present alias must be a string; supplies the user ID used for Redis config lookup. | +| `tenantId`, `tenant_id` | First present alias must be a string; supplies the principal's tenant ID. | +| `exp` | Checked when present; the local helper sets a one-hour expiry. | +| `nbf` | Checked when present; rejects tokens that are not yet valid, subject to verifier leeway. | +| `iss`, `aud` | No fixed issuer or audience is currently enforced by the JWKS verifier. | + +The default extractor does not infer the tenant from `teams`, email, or a nested +`user` object. Use `--cel-principal-extractor-path` for a custom mapping. +An earlier alias with a non-string value prevents fallback to a later alias. +The tenant ID is required by extraction but is not currently included in the +user-config Redis/cache key. JWT scopes and RBAC are not enforced here; object +visibility comes from the published routing maps. + +The local `GET /contextforge-rs/admin/tokens/{tenant_id}/{user_id}` helper sets +`tenant_id` and `sub` from the path. Its raw JWT response belongs in the +`Authorization: Bearer ...` header; it is not a JSON token object. + +There is no per-token revocation. Verification keys are cached for five minutes; +removing a key from JWKS is not immediate invalidation of cached keys. Restart +the dataplane after removing a key if that cache must be cleared immediately. ## UserConfig Shape (from `contextforge-data-plane-apis`) @@ -120,21 +141,33 @@ UserConfig virtual_hosts: HashMap VirtualHost - backends: HashMap ← map key = routing prefix + backends: HashMap ← backend key, not a parsed prefix + tools: HashMap ← public tool name → route + resources: HashMap ← public resource URI → route + resource_templates: HashMap + prompts: HashMap ← public prompt name → route + +ServiceRoute + backend_name: String ← key in backends + upstream_name: String ← backend-local name or URI BackendMCPGateway name: String url: Url - passthrough_headers: Vec ← snapshotted at initialize; session-scoped - add_headers: HashMap ← injected after passthrough - remove_headers: Vec ← stripped after add - allowed_tool_names: Vec ← model exists, NOT currently enforced - tool_schemas: HashMap ← optional, defaults to {}; upstream name → input schema - tool_name_aliases: HashMap ← downstream_alias → upstream_original - allowed_resource_names: Vec ← model exists, NOT currently enforced - allowed_prompt_names: Vec ← model exists, NOT currently enforced + mcp_protocol_version: ProtocolVersion ← required + passthrough_headers: Vec ← required; current request's headers + add_headers: HashMap ← defaults to {} + remove_headers: Vec ← defaults to [] + completion: HashMap ← defaults to {}; completion is not implemented + tool_schemas: HashMap ← defaults to {}; upstream tool name → schema ``` +The virtual-host object maps default to empty. A backend must be referenced by +an explicit object route to be callable. `resource_templates` is part of the +published model, but template listing is rejected and resource reads currently +use exact entries in `resources`. + + `tool_schemas` lets the dataplane recognize and validate `x-mcp-header` annotations without calling backend `tools/list`. The control plane may omit the field or individual unannotated tools. Without a published schema, parameter @@ -146,9 +179,14 @@ exact property path. For a recognized annotation, a non-null argument requires an equal header; an absent or null argument requires the header to be absent. Integer values are limited to the IEEE 754 safe range. -**Header apply order:** `passthrough_headers` → `add_headers` (override passthrough) → `remove_headers` (applied last). +**Header apply order:** backend Host for HTTPS → configured passthrough → +automatic `Mcp-Param-*` forwarding → `add_headers` → `remove_headers` → current +trace-context injection. RMCP generates the outbound method, name, and protocol +headers for the routed request. -**`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). +Passthrough values come from the current HTTP request. A new backend transport +is constructed per routed operation; no initialization-time header snapshot is +reused across requests. **Protected headers** — silently skipped in all three phases (passthrough/add/remove): @@ -164,7 +202,7 @@ Integer values are limited to the IEEE 754 safe range. authentication through `passthrough_headers` or `add_headers` is intentional runtime configuration. -Redis storage: `MessagePack(User::new(sub))` → `MessagePack(UserConfig)`. +Redis storage: `MessagePack(User::new(principal.user_id))` → `MessagePack(UserConfig)`. Two schemas are generated — both must be regenerated and committed when `UserConfig`, `VirtualHost`, `BackendMCPGateway`, or the `User` key type changes: @@ -186,13 +224,22 @@ RuntimePluginConfigDocument ``` Supported: tool, prompt, and resource pre/post CMF hooks. -Rejected: routing-based selection, plugin dirs, global policies, LLM hooks, plugin conditions. +Rejected: routing-based selection, routes, plugin directories, global policies/defaults, +`plugin_settings.fail_on_plugin_error`, plugin conditions, and unsupported hooks +(including LLM hooks). Config validation and `CmfPluginFactory` registration must agree on that list: a hook accepted by validation but not registered leaves the plugin loaded and silently inert. Reload watcher: 10-minute interval. Invalid reload → runtime marked failed. +Compile bundled factories with `--features plugins` and enable execution with +`--runtime-plugins-enabled true`. A valid document must exist before startup; +a missing document fails initialization. The +[quick start](getting-started.md#2-seed-plugin-configuration-before-startup) +seeds a secrets-detection policy. `test-plugins` additionally compiles the demo +factories used below; it is not needed for bundled secrets detection. + ### Tool Call Hook Behavior -For `call_tool`, the pre hook runs after backend routing has selected the backend and stripped the public prefix. The hook sees the backend name, routed tool name, and arguments. It can leave arguments unchanged, replace arguments, or deny the call. +For `call_tool`, the pre hook runs after backend routing has resolved the published route to its backend and upstream tool name. The hook sees the backend name, routed tool name, and arguments. It can leave arguments unchanged, replace arguments, or deny the call. After the upstream backend returns, the post hook can leave the result unchanged, rewrite the result payload, or deny the response. Hook state is carried across the upstream call so pre and post hooks can share CPEX context for the same logical tool call. @@ -200,7 +247,7 @@ Plugin execution must not poison shared gateway state. A plugin denial becomes a ### Prompt Fetch Hook Behavior -For `get_prompt`, the pre hook runs after backend routing, so the plugin sees the backend-local prompt name and the owning backend separately rather than the gateway-prefixed identifier. It can leave the arguments unchanged, replace them, or deny the fetch before the backend renders anything. +For `get_prompt`, the pre hook runs after backend routing, so the plugin sees the backend-local prompt name and the owning backend separately from the published route. It can leave the arguments unchanged, replace them, or deny the fetch before the backend renders anything. The post hook receives the rendered prompt as one CMF message per rendered MCP message, each carrying its role and its content block: text, image, audio, embedded resource, or resource link. A plugin can inspect or rewrite any of them, so a policy can act on a file interpolated into a prompt rather than only on the surrounding text. @@ -226,13 +273,10 @@ The pre call returns an opaque, concrete `ResourceHookState` consumed by the pos The optional `test-plugins` feature compiles demo factories from the `cpex-plugins-rs` repository. Redis configuration activates factories already present in the binary; it never loads new Rust code into a running process. -Start lightweight dependencies: - -```bash -docker compose -f docker/docker-compose-local.yaml up -d redis gateway-one gateway-two -``` - -Register payload-marker configuration before starting the ContextForge external dataplane: +Start Redis and the counter fixture using the +[quick-start dependency command](getting-started.md#1-start-redis-and-the-counter-fixture). +The following command replaces the local plugin document with payload-marker +configuration; run it before starting the ContextForge external dataplane: ```bash docker compose -f docker/docker-compose-local.yaml exec -T redis \ @@ -254,13 +298,13 @@ Build and run with demo factories and runtime execution enabled: ```bash cargo run -p contextforge-data-plane \ - --features 'contextforge-data-plane-lib/with_tools,test-plugins' \ + --features with_tools,plugins,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 \ + --jwks-url http://127.0.0.1:8001/contextforge-rs/admin/.well-known/jwks.json \ --token-verification-private-key assets/jwt.key \ --upstream-connection-mode plain-text-or-tls \ --runtime-plugins-enabled true @@ -269,19 +313,26 @@ cargo run -p contextforge-data-plane \ Startup should log successful CPEX initialization. The payload marker appends `[cpex:payload-marker]` to successful tool results. The hook path is also covered by: ```bash -cargo nextest run --locked -p contextforge-data-plane-lib --test gateway_plugins +cargo nextest run --locked -p contextforge-data-plane-lib --test gateway -E 'test(plugins::)' ``` -## Startup Validation (fails fast) +## Configuration Validation and Readiness -| Invalid combo | Reason | +| Configuration / dependency | When it is checked | | --- | --- | -| `--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 | +| Missing required flags | CLI parsing. | +| No HTTP or TLS listener | Gateway startup. | +| TLS listener without certificate/key | Listener setup. Use different sockets for HTTP and TLS. | +| Redis TLS without trust bundle, or mTLS without client certificate/key | Redis configuration/connection setup. | +| mTLS upstream without certificate/key | Upstream HTTP client construction. | +| Invalid JWKS URL scheme or non-loopback plain HTTP URL | Authorization-service construction. | +| Missing/invalid plugin document when enabled | CPEX initialization, before serving requests. | +| Unreachable JWKS endpoint | Token verification when keys must be fetched. | +| Unreachable backend or HTTP URL with default HTTPS-only mode | When a request selects that backend. | + +Redis connection setup retries rather than failing immediately. The local +signing-key file is used by helper requests. A successful health probe does +not prove Redis, JWKS, signing helpers, or backends are ready. ## Upstream Connection Modes @@ -294,130 +345,111 @@ cargo nextest run --locked -p contextforge-data-plane-lib --test gateway_plugins ## 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) | +| Variable | Controls | +| --- | --- | +| `RUST_LOG` | Console event filter. | +| `RUST_TRACE_LOG` | OTLP span filter. | +Both fall back to `debug` with quieter dependency directives: +`hyper_util=off,tower_http=off,rmcp=warn,reqwest=warn,rustls=warn,h2=warn,opentelemetry_sdk=warn,opentelemetry-otlp=warn`. +`RUST_FILE_LOG` is not read by the current logger. The parsed `--log-name` and +`--log-rotation` fields also have no effect. Redirect console output or use the +process/container log collector for persisted logs. ## 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 | +HTTP request spans are emitted at `info`; `RUST_TRACE_LOG=info` includes them. +`debug` is optional for additional instrumentation, not a requirement for +export. `--enable-open-telemetry true` installs the trace provider and W3C +propagator. In the current startup implementation, metrics initialization is +inside that same branch: set **both** telemetry enable flags to export metrics. -**Debugging by symptom:** +Metrics are pushed every **30 seconds**. The supplied Prometheus scrape interval +is **15 seconds**; allow up to about 45–60 seconds after generating traffic. | 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 | +| `401` | Bearer header, `validate: unable to refresh SaaS JWKS`, `validate_and_decode_claims`, and `Can't extract the principal` logs. | +| `400` config error | `user_config_store_layer` and whether the publisher used the extracted user ID. A Redis GET failure also maps here. | +| `404 Server not found` | `virtual_host_config_layer`; requested vhost versus caller's published configuration. | +| MCP routing errors | `AuthorizedCallValidator::validate` log prefix (from `validate_stateless`), then `call_tool`, `read_resource`, or `get_prompt` diagnostics. | +| Backend failures | Per-request connection/call diagnostics; no initialization fan-out exists. | +| Plugin problems | CPEX initialization, pipeline, and reload logs. | +| Missing traces | Enable flag, `RUST_TRACE_LOG`, exporter endpoint/protocol, and exporter error logs. | +| Missing metrics | Both enable flags, metrics endpoint, 30-second export interval, then collector/Prometheus scrape status. | ## 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 +The collector overlay receives OTLP/HTTP on `:4318`, writes traces to its own +logs, and exposes metrics on `:8889` for Prometheus. It does **not** forward +traces to Langfuse. Prometheus is available at `http://localhost:9090`. - 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"] +```text +external dataplane + -> OTLP/HTTP :4318 -> OTel Collector -> trace logging exporter + ^ + | scrape metrics :8889 + Prometheus :9090 ``` +First complete the [local quick start](getting-started.md#local-cargo-dev-workflow), +including plugin configuration. Add only the telemetry services: -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 + up -d otel-collector prometheus ``` -Run the gateway with export enabled (RUST_TRACE_LOG=debug required for trace export): +Stop the quick-start Cargo process, then restart it 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 \ +RUST_LOG=info RUST_TRACE_LOG=info \ +cargo run --release -p contextforge-data-plane --features with_tools,plugins \ + --bin contextforge-data-plane -- \ + --address 127.0.0.1:8001 \ + --redis-port 6379 --redis-address 127.0.0.1 --redis-mode plain-text \ + --jwks-url http://127.0.0.1:8001/contextforge-rs/admin/.well-known/jwks.json \ + --token-verification-private-key assets/jwt.key \ + --upstream-connection-mode plain-text-or-tls \ + --runtime-plugins-enabled true \ + --user-config-cache-expiry-seconds 0 \ --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-endpoint http://127.0.0.1:4318/v1/traces \ --otlp-metrics-endpoint http://127.0.0.1:4318/v1/metrics \ --otlp-service-name contextforge-data-plane ``` +Repeat the quick-start MCP requests, then inspect collector output and +Prometheus. The optional `docker-compose-langfuse.yaml` provides a separate +trace backend; using it requires an authenticated Langfuse OTLP exporter +configuration. Merely starting that overlay does not connect the collector to it. + ## Prometheus Starter Queries +These names match the supplied collector overlay. Other collector versions or +translation settings may add unit suffixes; check the exposed metric names. +Rate/quantile queries need recent traffic and at least two exported samples. +Allow about 60–90 seconds of traffic for two 30-second export cycles plus +scraping; the five-minute query window tolerates sparse samples. + | 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])))` | +| Request count | `http_server_request_duration_count` | +| p95 latency across requests | `histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_bucket[5m])))` | | In-flight requests | `http_server_active_requests` | -| Payload throughput | `http_server_request_body_size_bytes_sum` / `http_server_response_body_size_bytes_sum` | +| Cumulative body bytes | `http_server_request_body_size_sum` / `http_server_response_body_size_sum` | -## Known Telemetry Gaps +## Telemetry Coverage and Gaps -Tracked upstream, not yet implemented in the ContextForge external dataplane: +Incoming W3C trace context is extracted by `ExtractingMakeSpan` and current +context is injected into each backend request after configured header changes. +This propagation is implemented, not a future gap. -| 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) | +HTTP spans carry method, URI, and version. Authentication, configuration, +routed operations, and CPEX also have instrumentation. A complete MCP semantic +attribute set and coverage of every operation are still separate work; do not +interpret HTTP tracing alone as full MCP observability. diff --git a/_context/wiki/deployment.md b/_context/wiki/deployment.md index 876b3026..165fa335 100644 --- a/_context/wiki/deployment.md +++ b/_context/wiki/deployment.md @@ -1,78 +1,124 @@ # Deployment -> This page describes **current deployment requirements**, including session -> affinity. The tentative target removes live aggregate fan-out and durable -> upstream-session dependence; see -> [ContextForge 2.0 Target Architecture and Roadmap](mcp-capability-allocation.md). +Modern requests are independent and use a fresh backend MCP service per +operation. There is no sticky-session requirement. Follow +[Getting Started](getting-started.md) for a local development environment. ## Checklist -1. Front door routes only `/contextforge-rs` to the ContextForge external dataplane. -2. JWT verification key/secret matches the control plane's signing material; clients use control-plane API tokens whose `sub` matches the published user-config 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). +1. Route the configured `/contextforge-rs` prefix to the external dataplane and + keep older clients and legacy SSE on Python routes. +2. Configure a reachable trusted `--jwks-url` and a principal mapping matching + the publisher's user IDs and tenant claims. +3. Provide Redis connectivity and control-plane publication + (`DATAPLANE_PUBLISHER=true` in the control-plane deployment). Restrict writes + to trusted publishers; use TLS/mTLS across trust zones. +4. Match the upstream connection mode to backend URL schemes and TLS identities. +5. Exclude `with_tools` from production builds. Compile `plugins` when needed, + publish a valid plugin document before startup, and enable runtime execution. +6. Configure Origin and Host allowlists for the public deployment. +7. Configure telemetry collectors and both enable flags when exporting metrics. +8. Size file-descriptor limits, CPU, and memory for measured concurrent traffic. ## 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. +`GET /contextforge-rs/health` returns HTTP `200` and `{"status":"healthy"}` +without authentication in every build. It checks HTTP liveness, not Redis, +JWKS, plugin reload health, or backend readiness. Verify an authenticated routed +request separately when checking deployment readiness. ## nginx Front-Door Routing -Reference `docker/nginx.conf` split: -- `location ^~ /contextforge-rs` → proxies to the ContextForge external dataplane. -- UI and management traffic → ContextForge control plane. -- Other MCP routes, including stateful and legacy/SSE compatibility routes → ContextForge built-in dataplane. -- Upstream retries on `error timeout http_502/503/504`: 2 tries, 10-second window. Non-idempotent MCP `POST` bodies are not re-sent after they reached an upstream — only connection-stage failures retry. +The reference `docker/nginx.conf` sends `/contextforge-rs` to the external +service and other paths to the Python service. It routes by prefix and does +not inspect protocol versions. The Python service owns its management and +built-in MCP routes; an ingress must keep legacy clients off the external route. -## Session Affinity And Failover +Its upstream retry policy allows connection-stage failover, with two tries +within ten seconds for configured error/timeout/502/503/504 conditions. It does +not enable retrying non-idempotent POSTs after they have been sent upstream. +Do not add blind retries of `tools/call`: a lost response does not prove the +backend operation failed to execute. -Backend MCP sessions are **local process state** — see [routing.md](routing.md). +## Replicas and Failover -- >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". +Each request verifies identity, loads configuration, resolves its published +route, and opens its own backend connection. Replicas need consistent JWKS +trust, compatible compiled plugin factories, and the same published configuration; +they do not need affinity by `Mcp-Session-Id`. + +A process failure can interrupt an in-flight call. Subsequent modern requests +can go to another healthy replica without initialization, subject to its own +configuration-cache freshness and dependency availability. ## 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. +- Redis is needed for initial configuration-store setup and uncached reads. +- Connection setup uses a manager configured for 1,000 retries. +- Warm configuration entries can survive an outage until their expiry (default 60 seconds). +- A missing entry or Redis GET error currently produces HTTP `400`; undecodable + configuration produces `500`. See [Failure Modes](failure-modes.md). +- Enabled CPEX also needs its initial plugin document and checks for reloads + every ten minutes. An invalid reload fails new plugin calls closed. + +## Builds and Images + +Build a production binary with bundled plugin factories and without local +bootstrap helpers: + +```bash +cargo build --locked --release -p contextforge-data-plane --features plugins +``` + +That feature compiles factories; `--runtime-plugins-enabled true` and a valid +Redis plugin document are still required to execute them. Production uses the +issuer's JWKS endpoint and does not supply a local token-signing private key. -## Images +**The current reference `docker/Dockerfile` includes `with_tools`.** The image +workflow uses that Dockerfile, so its published images and the `docker-prod` +Compose example are not production-hardened builds. Package the production +binary above in a deployment image that excludes helpers before using it in a +real environment. The all-features CI conformance artifact includes helpers too. -- 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). +The image workflow publishes `ghcr.io//contextforge-data-plane:latest` +and `:v` on pushes to `main`, using the Cargo package version. Repeated +builds can overwrite either tag; **pin an image digest** for reproducibility. +The current Docker builder is `rust:1.96.1`. + +The reference Compose stack sets `nofile 65535` and TCP tuning, but its resource +reservations must fit the host. These are example settings, not measured +requirements for every deployment. ## 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`. | +| Front door to gateway | HTTP on a trusted private network, or `--tls-address` with server certificate/key. HTTP and TLS listeners can run on distinct sockets. | +| Gateway to JWKS | HTTPS, optionally with `--jwks-ca-cert-path`. Plain HTTP is restricted to loopback testing. | +| Gateway to Redis | `--redis-mode plain-text`, `tls`, or `mtls`; use TLS/mTLS across trust zones. | +| Gateway to backends | HTTPS-only by default; explicitly select HTTP or mTLS modes as needed. | ## Config Propagation Delay +With healthy publication and reads, a useful staleness budget is: + ```text -worst-case staleness = publisher interval + user-config cache expiry +publisher interval + user-config cache expiry + publication/read latency ``` -Both default to ~60s. For functional tests, shorten the publisher interval and disable the cache. For throughput benchmarks, keep both at 60s. - +The Rust cache defaults to 60 seconds; check the deployed publisher's actual +interval. For functional tests, shorten publication and use cache expiry `0`. +For benchmarks, report both values and keep them consistent between runs. +CPEX reloads use a separate ten-minute interval. ## Security Posture -| Concern | Current state | -| --- | --- | -| JWT revocation | None. A leaked token is valid until `exp`. Rotate the key and restart to invalidate. | -| CORS / Origin | CORS response headers are permissive. `mcp_origin_layer` validates Origin before authentication, and RMCP validates Host at the MCP service boundary. Configure both `--mcp-allowed-hosts` and `--mcp-allowed-origins` for production. | -| Local bootstrap routes | `/contextforge-rs/admin/tokens/{user}`, `/admin/userconfigs/{user}`, `/health` are **outside auth middleware — unauthenticated by design.** Only exist with `with_tools`. Production builds must not enable `with_tools`. | -| Redis trust | Whoever can write Redis controls routing (arbitrary backend URLs receive caller traffic) AND which registered plugin hooks execute on payloads. Protect with TLS/mTLS and restrict write access to the control plane. | -| Downstream TLS | Optional. Plain HTTP is acceptable only behind a trusted front door on a private network. Identity is always the bearer JWT, not mTLS. | -| Plugin code | Fully trusted, in-process. Redis config activates compiled-in factories only — it cannot inject new Rust code. | +JWT verification currently does not enforce a fixed issuer/audience, mandatory +expiration, scopes, or tenant-partitioned config keys. Ensure the issuer and +publisher contracts fit those limitations. JWKS keys can remain cached for +five minutes after removal. See [Security](security.md) for the full boundary. + +Development token, JWKS, and config-write helpers are unauthenticated and +compiled only with `with_tools`. Health is unauthenticated in all builds. +Redis writers control routes and plugin configuration, and plugin code is fully +trusted in-process code. diff --git a/_context/wiki/failure-modes.md b/_context/wiki/failure-modes.md index 94b25416..7c7c42cf 100644 --- a/_context/wiki/failure-modes.md +++ b/_context/wiki/failure-modes.md @@ -1,58 +1,72 @@ # 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. +Identity/config failures are HTTP responses before MCP handling. Once a request +reaches an MCP handler, routing and backend failures are JSON-RPC errors. Earlier +layers may return before the layer listed below is reached. -## HTTP Layer (middleware, before MCP) +## HTTP Layer -| Failure | Response | Layer | +| Failure | Response | Owner | | --- | --- | --- | -| 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` | +| Present Origin is malformed or not allowed | `403` | `mcp_origin_layer`. | +| MCP standard-header count or byte budget exceeded | `431` | `mcp_header_limits_layer`. | +| A request reaching virtual-host extraction does not match `/servers/{id}/mcp` | `400` | `virtual_host_id_layer`; unrelated router paths may instead be `404`. | +| Missing Authorization or non-Bearer scheme | `401` | `claims_layer`. | +| Bad JWT, unsupported algorithm, no matching JWKS key, fetch failure, or invalid time claim | `401` `Invalid token` | `claims_layer`. | +| Missing/non-string mapped user or tenant | `401` `Invalid token. Unable to extract the principal from claims` | `PrincipalExtractorLayer`. | +| Missing user configuration | `400` | `user_config_store_layer`, keyed by extracted user ID. | +| Config cannot be decoded / key cannot be encoded | `500` | Config store / `user_config_store_layer`. | +| Virtual host absent from caller's config | `404` `{"detail":"Server not found"}` | `virtual_host_config_layer`. | +| Missing/malformed authority with Host allowlist enabled | `400` | RMCP. | +| Authority not in configured Host allowlist | `403` | RMCP. | +| Invalid modern HTTP/MCP request envelope or standard headers | Rejected by RMCP before method dispatch | Exact response depends on the failed transport/protocol check. | -## MCP Validation (defense-in-depth, normally unreachable) +## MCP Validation and Routing | 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` | +| Required request context/config/claims/vhost extension missing | Internal error `-32603`; defense-in-depth after middleware. | +| Virtual host absent at handler validation | Resource not found `-32002`, `No configuration`. | +| Tool, resource URI, or prompt absent from published routing map | Invalid params `-32602`, routing problem naming the missing object. | +| Route refers to a missing backend | Invalid params `-32602`, routing problem naming the missing backend. | +| Resource pre-hook rewrites to an unpublished or ambiguous target | Invalid params `-32602`; no backend connection opened. | +| Recognized tool parameter header or its schema annotation is invalid | Header mismatch `-32020`; no backend call. | +| Modern `initialize` | Invalid request `-32600`: initialization is not supported for `2026-07-28`. | +| Catalog lists, resource subscriptions, or completion | Invalid request `-32600`, `Fan out not supported at the moment. Go to control plane`. | -## Routing +There is no prefix-splitting, list-pagination, or session-lookup failure path in +modern routing. A configured backend alone does not make its objects callable. -| 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 +## Backend Calls | 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 | +| Selected backend cannot be connected | Internal MCP error `-32603`; other backends are not contacted. | +| Backend returns an MCP error | Routed back as an MCP error. | +| Backend transport fails during a call | Internal MCP error. | +| Tool result has `isError: true` | Successful JSON-RPC response carrying the MCP tool error result, not a protocol error. | +| Backend close fails | Warning; the close failure does not replace the call result. | +| Process restart or request lands on another replica | A new request resolves its own config and connection; no reinitialization or sticky routing is required. An in-flight call on the failed process can still be lost. | ## 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 | +| Pre-hook denies | MCP error; no upstream call. | +| Post-hook denies | MCP error; backend operation may already have completed. | +| Plugin supplies an error code | That code is used; a denial without one defaults to invalid request `-32600`. | +| Soft plugin error | Logged; execution can continue under the runtime's soft-error behavior. | +| Missing or invalid initial plugin config | Runtime initialization fails; gateway startup does not complete. | +| Invalid plugin reload | New plugin calls fail closed until valid configuration is loaded; already pinned requests keep their runtime. | +| Plugin edits cannot be represented faithfully as the operation's MCP result | MCP error; no fallback to the original unredacted response. | -## Config Store (Redis) +See [Configuration](config.md#plugin-config-redis-key-contextforgegatewayruntimepluginconfig) +for operation-specific conversion rules. -| 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` | +## Redis + +The connection manager is configured for 1,000 retries. Valid warm cache entries +can serve requests until expiry; a cache miss needs Redis. A Redis `GET` error is +currently mapped to missing data and therefore HTTP `400`, not `503`. +Malformed MessagePack is a separate HTTP `500` path. Health remains `200` during +these dependency failures because it only checks HTTP liveness. diff --git a/_context/wiki/getting-started.md b/_context/wiki/getting-started.md index 47983e2f..04e2c75b 100644 --- a/_context/wiki/getting-started.md +++ b/_context/wiki/getting-started.md @@ -1,91 +1,123 @@ # Getting Started -## Full Docker Stack - -```bash -make docker-prod # build contextforge-data-plane:latest from docker/Dockerfile -make compose-up # start nginx, Python control/built-in components, Redis, Postgres, external dataplane, fast_time_server -``` - -Wait for `register_fast_time` to finish, then allow ~60s config propagation: - -```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` | +## Local Cargo Dev Workflow -> **Critical**: `/contextforge-rs` prefix → ContextForge external dataplane. -> Without it, MCP routes reach the ContextForge built-in dataplane (you'll get -> `{"detail":"..."}` from mcpgateway, not an external-dataplane response). +Run these commands from the repository root. You need the repository's Rust +toolchain, Docker Compose, and `curl`. Keep the dataplane running in one terminal +and use a second terminal for the requests below. -Teardown: `make compose-down` (stops containers; volumes kept). +This quick start enables the bundled secrets-detection plugin, runtime plugin +execution, and local bootstrap helpers. `with_tools` exposes unauthenticated +token and config helpers for development only; keep the dataplane on loopback. +Production builds must omit `with_tools` and use the deployment's trusted JWT +issuer and published configuration. -## cf-integration Conformance +### 1. Start Redis and the counter fixture ```bash -cargo binstall cf-integration@0.1.0 --no-confirm -make conformance +GATEWAY_CPU_LIMIT=2 GATEWAY_CPU_RESERVATION=1 \ +GATEWAY_MEM_LIMIT=1G GATEWAY_MEM_RESERVATION=256M \ +docker compose -f docker/docker-compose-local.yaml up --build -d redis gateway-one +docker compose -f docker/docker-compose-local.yaml ps redis gateway-one +docker compose -f docker/docker-compose-local.yaml exec -T redis redis-cli PING ``` -This runs the modern client and modern server eras through the committed -external-dataplane `HEAD`, including fixture-direct server comparison and the -scoped client suite. Use `make conformance-bless` to replace all selected -baselines transactionally after a fully successful run. Generated checkouts, -results, reports, and logs stay under `.integration/`. +Redis should reply `PONG`. The first fixture build can take several minutes. +These fixture limits fit a small local Docker VM; adjust them for load testing. -## Local Cargo Dev Workflow +| Service | Endpoint | Role | +| --- | --- | --- | +| `redis` | `127.0.0.1:6379` | Runtime configuration store. | +| `gateway-one` | `http://127.0.0.1:5555/mcp` | MCP `2026-07-28` counter fixture. | +| Local dataplane | `http://127.0.0.1:8001/contextforge-rs` | Started with Cargo below. | -For debugger/profiler/rapid iteration, start Redis and the counter/conformance fixtures: +### 2. Seed plugin configuration before startup + +Runtime plugin execution requires a valid +`ContextForgeGatewayRuntimePluginConfig` document in Redis. This enables +secrets detection before and after tool calls, blocking detected secrets: ```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 +docker compose -f docker/docker-compose-local.yaml exec -T redis \ + redis-cli SET ContextForgeGatewayRuntimePluginConfig '{ + "version": 1, + "cpex": { + "plugins": [{ + "name": "secrets-detection", + "kind": "validator/secrets-detection", + "hooks": ["cmf.tool_pre_invoke", "cmf.tool_post_invoke"], + "config": {"block_on_detection": true} + }] + } + }' NX ``` -| 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. | +`NX` preserves an existing plugin document. `OK` means the example was inserted; +an empty reply means an existing document remains in use. Its plugin kinds must +be compiled into the binary. See [Plugin Config](config.md#plugin-config-redis-key-contextforgegatewayruntimepluginconfig) +for configuration and the optional [demo plugins](config.md#demo-plugin-workflow). -Run the binary with bootstrap helpers: +### 3. Start the dataplane ```bash -cargo run -p contextforge-data-plane \ - --features contextforge-data-plane-lib/with_tools \ +RUST_LOG=info \ +cargo run -p contextforge-data-plane --features with_tools,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 \ + --jwks-url http://127.0.0.1:8001/contextforge-rs/admin/.well-known/jwks.json \ --token-verification-private-key assets/jwt.key \ --upstream-connection-mode plain-text-or-tls \ - --number-of-cpus 4 + --runtime-plugins-enabled true \ + --user-config-cache-expiry-seconds 0 ``` -The client-facing route is `http://127.0.0.1:8001/contextforge-rs/servers/{virtual_host_id}/mcp`. +The `plugins` feature compiles bundled factories; `--runtime-plugins-enabled true` +loads their Redis configuration. The user-config cache is disabled for immediate +feedback when reseeding local routes. Use the default 60-second cache for normal +deployments. Telemetry export is optional and needs a collector; follow the +[local telemetry setup](config.md#local-telemetry-verification-stack) when needed. + +`--features with_tools` forwards to `contextforge-data-plane-lib/with_tools`; +either spelling enables the same helpers. JWT verification now uses +`--jwks-url`; the former `--token-verification-public-key` and +`--token-verification-secret` flags are no longer accepted. The local JWKS +helper serves the public key corresponding to `assets/jwt.key`. -### Mint a local test token +### 4. Check health and mint a local test token + +In the second terminal: ```bash +BASE_URL=http://127.0.0.1:8001/contextforge-rs +TENANT_ID=team_awesome 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") +VIRTUAL_HOST_ID=c0ffee00f001f00df00ddeadbeefdead + +curl --fail --silent --show-error "${BASE_URL}/health" +curl --fail --silent --show-error "${BASE_URL}/admin/.well-known/jwks.json" + +TOKEN=$(curl --fail --silent --show-error \ + "${BASE_URL}/admin/tokens/${TENANT_ID}/${USER_ID}?email=admin@example.com") ``` -### Seed runtime configuration +Expect `{"status": "healthy"}` and a JWKS document containing a `keys` array. +The token response is a raw JWT, stored in `TOKEN` without printing it. Tokens +expire after one hour; repeat the token command to refresh. Both tenant and user +path segments are required. The helper sets top-level `tenant_id` and `sub` +claims; the optional email does not select the user's Redis configuration. + +Keep the listener, JWKS, token, and MCP URLs on the same instance. If you use +port `9090`, change all four together. Port `8080` belongs to the full Docker +stack's nginx front door and does not automatically reach this Cargo process. + +### 5. Seed runtime routes for the token's user ```bash -VIRTUAL_HOST_ID=c0ffee00f001f00df00ddeadbeefdead -curl --silent --show-error --request POST \ - --url "http://127.0.0.1:8001/contextforge-rs/admin/userconfigs/${USER_ID}" \ +curl --fail --silent --show-error --request POST \ + "${BASE_URL}/admin/userconfigs/${USER_ID}" \ --header 'content-type: application/json' \ --data '{ "virtual_hosts": { @@ -94,14 +126,14 @@ curl --silent --show-error --request POST \ "gateway-one": { "name": "gateway-one", "url": "http://127.0.0.1:5555/mcp", - "passthrough_headers": [], "allowed_tool_names": [], - "allowed_resource_names": [], "allowed_prompt_names": [] - }, - "gateway-two": { - "name": "gateway-two", - "url": "http://127.0.0.1:5556/mcp", - "passthrough_headers": [], "allowed_tool_names": [], - "allowed_resource_names": [], "allowed_prompt_names": [] + "mcp_protocol_version": "2026-07-28", + "passthrough_headers": [] + } + }, + "tools": { + "counter-get_value": { + "backend_name": "gateway-one", + "upstream_name": "get_value" } } } @@ -109,37 +141,131 @@ curl --silent --show-error --request POST \ }' ``` -### 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` | +Expect `Added` (HTTP `202`). The `USER_ID` must match the token's `sub`. +The backend protocol version and explicit tool route are required for the tool +call below. The public tool name maps to the backend's original `get_value`. -### Modern protocol probe (server/discover) +### 6. Discover the server and call the counter ```bash -curl --silent --show-error \ - --url "http://127.0.0.1:8001/contextforge-rs/servers/${VIRTUAL_HOST_ID}/mcp" \ +curl --fail --silent --show-error \ + "${BASE_URL}/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":{}}}}' + --header 'mcp-name: quickstart' \ + --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": {} + } + } +}' + +curl --fail --silent --show-error \ + "${BASE_URL}/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: tools/call' \ + --header 'mcp-name: counter-get_value' \ + --data '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "counter-get_value", + "arguments": {}, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "curl", + "version": "0.1.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +}' ``` +Expect a discovery `result` and a successful tool `result` containing the counter +value. The tool call exercises the configured pre/post plugin hooks. An MCP +`error` can arrive with HTTP `200`, so inspect the JSON response as well. +Requests carry client metadata independently; no `initialize` or session header +is needed. Catalog listing is owned by the control plane, so use the published +tool name directly instead of expecting `tools/list` here. + ### Troubleshooting -| Symptom | Likely cause | +| Symptom | Check | | --- | --- | -| `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. | +| `unexpected argument --token-verification-public-key` | Use the JWKS startup command above. | +| Startup fails with a plugin configuration error | Seed the plugin document before startup; its kinds must match compiled factories. Add `test-plugins` only for a document using demo factories. | +| Token/JWKS helper does not return the expected body | Enable `with_tools`, include both tenant and user in the token URL, and check that the port reaches this process. | +| `401` / `Invalid token` | Send `Authorization: Bearer ${TOKEN}`; mint a fresh token and check that the configured JWKS URL is reachable and serves its signing key. | +| `401` / `Unable to extract the principal from claims` | The default extractor needs a string user ID (`sub`) and string tenant ID (`tenant_id`); see [JWT Claims](config.md#jwt-claims-validated-by-claims_layer). | +| `400 Problem occurred retrieving the configuration` | Seed `UserConfig` for the same user ID as the token. | +| `404 {"detail":"Server not found"}` | The URL's virtual-host ID must exist in that user's config. | +| `400` mentioning request metadata | Include the matching MCP protocol header, method/name headers, and per-request `_meta`. | +| MCP error for an unpublished tool | Add the tool's explicit route to the virtual host before calling it. | +| Backend unavailable | Check `gateway-one` logs, port `5555`, and `--upstream-connection-mode plain-text-or-tls`. | + +For JWT diagnostics, restart with `RUST_LOG=debug` and look for +`unable to refresh SaaS JWKS`, `validate_and_decode_claims`, or +`Can't extract the principal`. Do not share bearer tokens in logs or reports. + +Stop Cargo with Ctrl-C. Stop the local dependencies when finished: + +```bash +docker compose -f docker/docker-compose-local.yaml down +``` + +## Full Docker Stack + +The repository also contains a full Compose topology with nginx, the Python +control plane/built-in dataplane, Postgres, Redis, and automatic registration of +a Fast Time backend. **It needs configuration updates before it is a runnable +JWKS-based setup.** Use the local Cargo steps above for the working quick start. + +Before using the full topology: + +- Replace the old token public-key/secret settings with a reachable + `CONTEXTFORGE_DATA_PLANE_JWKS_URL` trusted by the Rust service. +- The current reference image includes `with_tools`, so it also needs an + explicit `--token-verification-private-key` path and mounted development key. + For production, package a build without helpers instead. +- Check that the control-plane publisher emits the current backend protocol + field and explicit object routes, with keys matching the extracted user ID. +- Match CPU and memory reservations to the Docker host and account for both + publisher and Rust-cache delay when verifying route changes. + +The Compose lifecycle targets are `make docker-prod`, `make compose-up`, and +`make compose-down`; their names do not imply production readiness. See +[Deployment](deployment.md#builds-and-images) for the current image boundary. +The reference nginx listener is `http://localhost:8080`, with external MCP at +`/contextforge-rs/servers/{virtual_host_id}/mcp`. Other MCP paths reach the +Python service. `fast_time_server` is a sample backend, not a gateway dependency. + +## cf-integration Conformance + +```bash +cargo binstall cf-integration@0.3.1 --no-confirm +make conformance +``` + +This runs the modern client and modern server eras through the committed +external-dataplane `HEAD`, including fixture-direct server comparison and the +scoped client suite. Use `make conformance-bless` to replace all selected +baselines transactionally after a fully successful run. Generated checkouts, +results, reports, and logs stay under `.integration/`. diff --git a/_context/wiki/index.md b/_context/wiki/index.md index d3f8b6a3..364399ea 100644 --- a/_context/wiki/index.md +++ b/_context/wiki/index.md @@ -8,17 +8,17 @@ then follow only the links that are relevant. | File | What it covers | | --- | --- | -| [getting-started.md](getting-started.md) | Full docker stack, local cargo dev, cf-integration — commands and URIs | +| [getting-started.md](getting-started.md) | Local Cargo quick start with plugins and JWKS, MCP probes, full Docker stack, cf-integration | | [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) | Current middleware stack order, pipeline shape, module boundaries, state ownership, executor shapes | +| [architecture.md](architecture.md) | Current middleware stack order, pipeline shape, module boundaries, state ownership, startup, and request lifecycle | | [routing.md](routing.md) | Stateless routing model: VirtualHost routing tables, per-request backend lifecycle, method quick reference, header forwarding, plugin hooks | | [mcp-capability-allocation.md](mcp-capability-allocation.md) | Tentative ContextForge 2.0 target topology, ownership, state model, Phase 1-4 roadmap, and Phase 3 flows | | [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) | External-dataplane deployment checklist, health endpoint caveat, nginx routing, TLS choices, session affinity, Redis availability, image pinning | +| [deployment.md](deployment.md) | External-dataplane deployment checklist, JWKS trust, health checks, nginx routing, TLS choices, replicas, Redis availability, and production build/image limitations | | [security.md](security.md) | Trust boundaries among the control plane, built-in dataplane, and external dataplane; Origin/Host validation; transport security; secrets handling | -| [performance.md](performance.md) | Control-plane Locust load runs, benchmark settings, and built-in-dataplane baseline | +| [performance.md](performance.md) | Current cf-integration load commands, standalone/full-stack comparisons, and benchmark controls | | [testing.md](testing.md) | Workspace checks, in-repo integration tests, full-stack harness lanes, settings, and control-plane baseline | ## Quick orientation diff --git a/_context/wiki/mcp-capability-allocation.md b/_context/wiki/mcp-capability-allocation.md index c5fe1cf5..bc05e623 100644 --- a/_context/wiki/mcp-capability-allocation.md +++ b/_context/wiki/mcp-capability-allocation.md @@ -14,109 +14,58 @@ names. ## Vision and Constraints -- ContextForge supports MCP `2026-07-28` and `2025-11-25` over Streamable HTTP - on both the client-facing and backend-facing sides of the ContextForge - external dataplane. -- Same-version client/backend paths are supported directly. Cross-version - `2026-07-28` → `2025-11-25` and `2025-11-25` → `2026-07-28` adaptation is - best effort. -- All external-dataplane request/response handling is stateless for both - versions. The target request path does not depend on an MCP session, session - affinity, or a retained backend transport. -- `initialize` remains supported for compatibility, but it is a stateless - request: the external dataplane generates its response from effective - configuration and does not use it to establish state required by later - requests. -- Legacy SSE transport is not part of the external-dataplane target. -- Fan-out and other one-to-many MCP work is limited to the control plane. The - built-in and external dataplanes generate discovery, capability, and list - responses from control-plane-authored effective configuration. -- Effective configuration flows one way from the control plane to the built-in - and external dataplanes through externally shared state. A process-local - cache may speed reads but is never the source of truth. -- MCP subscriptions and notifications remain Phase 4 work. - -## Stateless Protocol Compatibility - -[IBM/mcp-context-forge issue #6327](https://github.com/IBM/mcp-context-forge/issues/6327) -tracks the first targeted-operation slice for `tools/call`. The issue calls the -incoming/client-facing side “upstream” and the selected backend-facing side -“downstream”; this wiki uses the explicit names below. - -| Incoming client | Selected backend | Target behavior | -| --- | --- | --- | -| `2026-07-28` | `2026-07-28` | Supported directly as one stateless request. | -| `2026-07-28` | `2025-11-25` | Best-effort protocol adaptation within one stateless request. | -| `2025-11-25` | `2026-07-28` | Best-effort protocol adaptation within one stateless request. | -| `2025-11-25` | `2025-11-25` | Supported directly as one stateless request. | - -For every row, the external dataplane authenticates and authorizes the request, reads -the principal-bound effective configuration, validates that the requested -object is visible and permitted, resolves exactly one backend, adapts the -protocol when necessary, and closes the request-scoped backend connection after -the response. A client may call `initialize`, but later operations neither -require nor reuse state created by it. - -“Best effort” never permits hidden session state. If a semantic difference or -backend requirement cannot be handled within the current request, the -external dataplane returns an explicit error instead of creating affinity or -retaining a backend transport for a later request. - -For the initial `tools/call` slice, issue #6327 assumes that the selected -backend needs neither application authentication nor mTLS and that its server -certificate chains to the system CA. Those are issue-scope assumptions, not a -change to the external dataplane's broader transport-security model. +- The external dataplane targets MCP `2026-07-28` over Streamable HTTP, using + `server/discover` and per-request client metadata. Older protocol versions, + legacy initialization/session flows, and SSE remain on Python routes. +- External request handling must not depend on session affinity or a retained + backend transport. Existing legacy code is migration state, not a target to expand. +- Fan-out belongs in control-plane reconciliation. The proposed dataplanes + generate discovery, capability, and list responses from published effective + configuration, rather than querying every backend during a client request. +- Effective configuration flows from the control plane to shared storage. + Process-local caches accelerate reads but do not own policy. +- Subscription and notification delivery beyond current in-flight tool progress + remains Phase 4 work. + +## Protocol Scope and Implementation Checkpoint + +Earlier planning considered a four-way modern/legacy compatibility matrix. +That is not this repository's current contract: new work targets modern MCP, +and legacy clients stay on control-plane/built-in routes. Historical targeted +routing work is linked from +[IBM/mcp-context-forge #6327](https://github.com/IBM/mcp-context-forge/issues/6327). +Do not treat that historical issue as authorization to expand Rust compatibility. + +The current implementation already routes `tools/call`, `resources/read`, and +`prompts/get` through explicit per-user object maps, with request-scoped backend +connections and CPEX hooks. It rejects aggregate lists, completion, and +subscriptions. `server/discover` currently returns RMCP's local response, not +an effective catalog compiled for the principal. Tenant claims are required, +but Redis/cache keys contain only user ID; token scopes and compiled RBAC are +not enforced. The richer snapshots, authorization, and list responses below +are proposed work, not completed phases. ## Target End State -The front door separates management traffic from MCP traffic and chooses the -built-in or external dataplane by deployment route and session model, not only -by protocol version. The built-in dataplane can handle either supported version in stateful -or stateless mode. The external dataplane can handle either supported version -only in stateless mode. PostgreSQL remains the durable management store; the -shared runtime store carries compiled configuration to both dataplanes. - -```mermaid -flowchart TB - subgraph Clients[Traffic] - direction LR - AdminClient([Admin or User]) - CompatClient([MCP 2025-11-25 Client]) - ModernClient([MCP 2026-07-28 Client]) - end - - FrontDoor[Load Balancer and Router] - - subgraph ContextForge[ContextForge 2.0] - direction LR - subgraph PythonRepo[IBM mcp-context-forge Python Repository] - direction TB - Control[ContextForge Control Plane] - Builtin[ContextForge Built-In Dataplane] - end - External[ContextForge External Dataplane - Rust] - end - - Postgres[(PostgreSQL Management State)] - RuntimeStore[(Shared Effective Configuration)] - Upstreams[MCP 2026-07-28 and 2025-11-25 Servers] - - AdminClient -->|Management API| FrontDoor - CompatClient -->|Streamable HTTP MCP 2025-11-25| FrontDoor - ModernClient -->|Streamable HTTP MCP 2026-07-28| FrontDoor - - FrontDoor -->|Management routes| Control - FrontDoor -->|Stateful or built-in MCP routes| Builtin - FrontDoor -->|Stateless external MCP routes| External - - Control -->|Persist administrative state| Postgres - Control -->|Publish effective configuration| RuntimeStore - RuntimeStore -->|Read shared configuration| Builtin - RuntimeStore -->|Read-only configuration| External - - Control -->|Discover catalogs and poll liveness| Upstreams - Builtin -->|Stateful or stateless MCP calls| Upstreams - External -->|Stateless targeted MCP calls| Upstreams +The front door separates management and MCP routes. It may select the built-in +or external dataplane for modern requests; older clients and stateful/legacy +flows stay on the Python side. The external route handles modern independent +requests. PostgreSQL remains the durable management store, while shared runtime +storage carries proposed compiled configuration to both dataplanes. + +```text +client -> front door -> management -> control plane -> PostgreSQL + | | + | +-> compile shared effective config + | +-> reconcile modern/legacy backends + | + +-> Python MCP route -> built-in dataplane + | |-> read effective config + | +-> modern/legacy backend calls + | + +-> modern external route -> Rust external dataplane + |-> read effective config + +-> one modern backend call ``` Redis is the current external-dataplane configuration store and the preferred @@ -129,13 +78,13 @@ or affinity design; stateless behavior must not rely on process memory. | Component | Target responsibility | | --- | --- | -| Front door | Route management APIs to the ContextForge control plane. Route MCP to the built-in dataplane when the built-in route or stateful behavior is required, and to the external dataplane when the configured stateless external route is selected. Protocol version alone does not identify the component. | +| Front door | Route management to the control plane and legacy/stateful MCP to Python routes. Select either dataplane for compatible modern traffic by deployment route. | | ContextForge control plane | Manage the virtual-server lifecycle and upstream assignments; connect to heterogeneous upstreams; retrieve and page through capabilities, tools, resources, prompts, completions, and other catalogs; normalize and persist them; let administrators select exposed objects and rules; compile effective runtime configuration; poll upstream liveness and changes. | | PostgreSQL | Persist administrative source data such as virtual servers, upstream definitions, normalized catalogs, selections, and policies. It is not on the external-dataplane request path. | | Configuration synchronization | Publish effective configuration one way from the control plane to externally shared state. The built-in and external dataplanes should consume the same shape where practical. | | ContextForge built-in dataplane | Handle `2026-07-28` and `2025-11-25` MCP requests in Python, including stateful and stateless behavior. It is the MCP request path shipped in the same repository as the control plane, not the control plane itself. | -| ContextForge external dataplane | Handle `2026-07-28` and `2025-11-25` Streamable HTTP requests statelessly in Rust. Read effective configuration, serve aggregate and `initialize` responses locally, and route a targeted method to exactly one selected backend. Cross-version adaptation is best effort. It does not own IAM, UI, management APIs, or durable metrics storage. | -| Backend MCP servers | May use `2026-07-28` or `2025-11-25`, independently of the incoming client version. Connections and any required negotiation are request-scoped and leave no reusable session; the architecture does not require backend session affinity. | +| ContextForge external dataplane | Handle modern `2026-07-28` Streamable HTTP requests independently. In the proposed end state, read effective configuration, serve discovery and aggregates locally, and route targeted operations to one backend. It does not own IAM, UI, management APIs, or metrics storage. | +| Backend MCP servers | External routing targets modern backends with request-scoped connections. The control plane can reconcile heterogeneous catalogs, and Python routes own legacy protocol handling. | ## Administrative State and Effective Configuration @@ -177,7 +126,8 @@ authorization context. - The exact tenant/team claim mapping and token-scope-to-RBAC rules are a cross-repository contract that the control plane, publisher, schemas, external dataplane, and integration tests must define together. The current - coarse `sub`-only implementation is not the Phase 3 target. + user-ID-keyed routing maps, without tenant partitioning or scope/RBAC + enforcement, are not the Phase 3 target. ## MCP Work Allocation @@ -185,9 +135,9 @@ authorization context. | --- | --- | | Virtual-server creation and upstream assignment | Control plane persists management state and connects to assigned upstreams. | | Upstream discovery, initialization where required, catalog pagination, capability aggregation, filtering, and liveness polling | Control plane only; this is the intentional fan-out boundary. | -| `server/discover`, `initialize`, and effective capabilities | After per-request authorization, the built-in or external dataplane generates the response from principal-bound effective configuration. The built-in dataplane may support a stateful flow; the external dataplane treats `initialize` as stateless compatibility and creates no state required by later requests. | +| `server/discover` and effective capabilities | After per-request authorization, generate the modern response from principal-bound effective configuration. Legacy `initialize` stays on Python routes. | | `tools/list`, `resources/list`, `prompts/list`, resource-template listing, and similar aggregate methods | After method-scope and compiled-RBAC enforcement, the built-in or external dataplane generates the visible response from principal-bound effective configuration with no live upstream fan-out. | -| `tools/call`, `resources/read`, `prompts/get`, completion, and similar targeted methods | The built-in or external dataplane resolves the effective entry under the trusted authorization key, applies default-deny scope and object policy, and calls exactly one selected backend only when authorized. The external dataplane adapts protocol versions when necessary and leaves no reusable session; the built-in dataplane may use its stateful or stateless execution model. | +| `tools/call`, `resources/read`, `prompts/get`, completion, and similar targeted methods | The built-in or external dataplane resolves the effective entry under the trusted authorization key, applies default-deny scope and object policy, and calls exactly one selected backend only when authorized. The external dataplane handles modern requests without a reusable session; the built-in dataplane owns legacy/stateful execution. | | Plugins for trusted aggregate responses | Prefer policy compiled by the control plane; avoid mandatory per-request plugin calls for a response already produced from trusted effective configuration. | | Plugins for targeted calls | May run on the external-dataplane request path when request or response inspection is required. Exact hook allocation remains an implementation decision. | | Subscriptions, server notifications, and downstream list-change notifications | Deferred to Phase 4 because their state and delivery model do not fit the request/response simplification. | @@ -197,191 +147,84 @@ authorization context. | Phase | Scope | | --- | --- | | **1. Separate control-plane and built-in-dataplane responsibilities** | Establish a clear boundary between the ContextForge control plane and built-in dataplane inside the Python repository. The control plane writes effective configuration per user, team, or other principal to shared state; the built-in dataplane reads it and handles MCP requests. | -| **2. Route targeted calls through the external dataplane** | Make the built-in and external dataplanes follow the same configuration-driven contract. Send selected targeted operations such as `tools/call`, `resources/read`, `prompts/get`, and completion to the external dataplane. For each operation, support both same-version `2026-07-28`/`2025-11-25` paths and attempt both cross-version paths on a best-effort basis, always without reusable session state. The `tools/call` slice is tracked by [#6327](https://github.com/IBM/mcp-context-forge/issues/6327). | -| **3. Route all stateless request/response MCP methods through the external dataplane** | Serve discovery, stateless `initialize`, capabilities, aggregate lists, and targeted calls for both supported protocol versions from the external dataplane. Aggregate responses come from effective configuration; targeted calls reach exactly one backend. The built-in dataplane continues to support both stateful and stateless behavior. | +| **2. Route targeted calls through the external dataplane** | Use published object routes for modern calls. Tools, resources, and prompts already have request-scoped routing; completion remains unimplemented. Align the publisher and both consumers on the configuration contract. | +| **3. Serve modern request/response methods from effective configuration** | Add principal-bound discovery, capabilities, aggregate lists, and scope/RBAC enforcement for `2026-07-28`. Aggregate responses use compiled configuration; targeted calls reach one backend. This does not add legacy initialization or compatibility to Rust. | | **4. Implement subscriptions and notifications** | Add the state, routing, and delivery model for upstream subscriptions, resource notifications, and list-change notifications after the request/response architecture is complete. | ## Phase 3 Reference Flows The examples below use tools, but the same ownership applies to resources, prompts, completions, and other aggregate or targeted request/response methods. -“Supported MCP client” and “supported MCP server” mean either `2026-07-28` or -`2025-11-25`; when the two sides differ, adaptation is best effort. +External clients and selected external backends in these flows use `2026-07-28`. +The control plane may also manage legacy servers for Python routes. ### 1. Create a Virtual Server and Select Capabilities -```mermaid -sequenceDiagram - autonumber - actor User - participant UI as Admin UI or API - participant CP as ContextForge Control Plane - participant DB as Control Plane DB - participant MCP1 as MCP 2026-07-28 Server - participant MCP2 as MCP 2025-11-25 Server - participant Store as Shared Config Store (Redis) - participant DP as ContextForge External Dataplane - - User->>UI: Create virtual server - UI->>CP: Submit virtual server - CP->>DB: Store virtual server - - User->>UI: Assign MCP Server 1 and MCP Server 2 - UI->>CP: Update backend associations - CP->>DB: Store backend associations - - par Inspect 2026-07-28 backend - CP->>MCP1: Discover capabilities and retrieve catalogs - MCP1-->>CP: Capabilities and catalog - and Inspect 2025-11-25 backend - CP->>MCP2: Initialize or discover and retrieve catalogs - MCP2-->>CP: Capabilities and catalog - end - - CP->>DB: Reconcile normalized catalog - User->>UI: View available catalog entries - UI->>CP: Request reconciled catalog - CP->>DB: Read catalog - DB-->>CP: inc, sum, dec, diff - CP-->>UI: Display available catalog entries - - User->>UI: Allow inc and sum - UI->>CP: Update virtual server policy - CP->>DB: Store selected tools and policy - - CP->>CP: Compile snapshot by tenant, principal and vhost - CP->>Store: Atomically publish revision N - Store-->>DP: Configuration revision available - DP->>Store: Load revision N - DP->>DP: Replace local cache atomically - - Note over CP,MCP2: Control Plane handles upstream protocol and pagination - Note over CP,DP: Effective configuration flows one way from CP to DP - Note over CP,DP: Snapshot carries compiled scopes, RBAC and visible objects +```text +user -> admin UI/API -> control plane -> persist virtual server/associations + | + +-> discover both modern backend catalogs + +-> exhaust pagination and reconcile catalogs + +-> display available entries (inc, sum, dec, diff) +user -> select inc and sum -> persist selected tools and policy + | + +-> compile by tenant, principal, and virtual host + +-> atomically publish snapshot revision N +shared store -> external dataplane -> load/cache revision N + +The proposed snapshot contains visible objects, compiled scopes, and RBAC. +Only the control plane performs catalog fan-out and reconciliation. ``` -### 2. Initialize or Discover the Server and List Tools - -```mermaid -sequenceDiagram - autonumber - participant Client as Supported MCP Client - participant Ingress - participant DP as ContextForge External Dataplane - participant Cache as Local Cache - participant Store as Shared Config Store (Redis) - - Client->>Ingress: initialize or server/discover - Ingress->>DP: Forward supported MCP request - DP->>DP: Verify JWT, metadata and server route - DP->>DP: Derive authorization key from trusted context - DP->>Cache: Get snapshot by authorization key - - alt Snapshot available - Cache-->>DP: Snapshot revision N - else Snapshot missing or expired - Cache-->>DP: Cache miss - DP->>Store: Read by authorization key - Store-->>DP: Snapshot revision N or not found - opt Authorized snapshot returned - DP->>Cache: Store under authorization key - end - end - - DP->>DP: Enforce discovery scope and compiled RBAC - alt Snapshot mapped and authorized - DP-->>Client: Version-appropriate identity and visible capabilities - else Missing, unmapped or denied - DP-->>Client: Authorization error without catalog details - end - - Client->>Ingress: tools/list as independent request - Ingress->>DP: Forward supported MCP request - DP->>DP: Reverify and derive authorization key - DP->>DP: Enforce tools/list scope and compiled RBAC - alt Snapshot mapped and authorized - DP->>Cache: Read visible tools by authorization key - Cache-->>DP: inc and sum - DP-->>Client: tools/list result - else Missing, unmapped or denied - DP-->>Client: Authorization error without catalog details - end - - Note over DP,Store: The shared store distributes compiled state - Note over DP: No live upstream call for discovery or aggregate lists - Note over Client,DP: initialize does not create required session state - Note over Client,DP: Client-supplied identity or routing metadata is untrusted +### 2. Discover the Server and List Tools + +```text +client -> server/discover with per-request metadata -> ingress -> dataplane + dataplane: verify JWT, metadata, and route; derive trusted authorization key + cache hit: use snapshot revision N under that key + cache miss/expiry: read shared store and cache the authorized snapshot + enforce discovery scope and compiled RBAC + allow: return identity and visible capabilities from the snapshot + deny/missing: return an error without catalog or backend details + +client -> tools/list as an independent request -> ingress -> dataplane + reverify identity and derive authorization key + enforce tools/list scope and compiled RBAC + allow: return visible tools (inc, sum) from the snapshot + deny/missing: return an error without catalog details + +Neither operation calls a live backend. Client-supplied identity or routing +metadata cannot override the trusted authorization context. ``` ### 3. Call a Tool -```mermaid -sequenceDiagram - autonumber - participant Client as Supported MCP Client - participant Ingress - participant DP as ContextForge External Dataplane - participant Cache as Local Cache - participant CPEX as Policy and CPEX - participant MCP as Selected Supported MCP Server - - Client->>Ingress: tools/call name inc - Ingress->>DP: Forward supported MCP request - DP->>DP: Verify JWT, metadata and server route - DP->>DP: Derive authorization key from trusted context - DP->>Cache: Resolve inc under authorization key - Cache-->>DP: Backend mapping, protocol version and policy or missing - DP->>DP: Enforce tools/call scope and compiled RBAC - - alt Tool mapped and authorized - DP->>CPEX: Run pre-call policy - CPEX-->>DP: Allow or modify request - DP->>DP: Adapt client protocol to backend protocol - opt Backend negotiation is required - DP->>MCP: Request-scoped initialize - MCP-->>DP: Initialize result - end - DP->>MCP: tools/call name inc - MCP-->>DP: Tool result - DP->>MCP: Close request-scoped connection - DP->>CPEX: Run post-call policy - CPEX-->>DP: Allow or modify result - DP-->>Client: Return version-appropriate tool result - else Missing, unmapped or denied - DP-->>Client: Authorization error with no upstream call - end - - Note over DP,MCP: Exactly one backend is called - Note over DP,MCP: Client and backend versions are independently 2026-07-28 or 2025-11-25 - Note over DP,MCP: No durable backend MCP session is required - Note over DP: Control Plane, DB and Redis are not on this result path - Note over Client,DP: Client-supplied identity or backend selection is untrusted +```text +client -> tools/call inc with per-request metadata -> ingress -> dataplane + verify JWT, metadata, and route; derive trusted authorization key + resolve inc in the principal-bound snapshot + enforce tools/call scope and compiled RBAC + missing/denied: return an error; make no upstream call + allowed: + pre-call CPEX policy -> allow/modify arguments + build request-scoped modern client -> call one selected backend + backend result -> close connection -> post-call CPEX policy + allow/modify result -> return MCP result + +Client and backend use MCP 2026-07-28. No durable backend session is required. +After route resolution, results do not pass through the control plane or Redis. +A pre/post policy denial ends the corresponding path with an MCP error. ``` ### 4. Reconcile an Upstream Catalog Change -```mermaid -sequenceDiagram - autonumber - participant MCP as Supported MCP Server - participant CP as Control Plane Reconciler - participant DB as Control Plane DB - participant Store as Shared Config Store (Redis) - participant DP as ContextForge External Dataplane - participant Client as Supported MCP Client - - CP->>MCP: Poll liveness and refresh discovery and lists - MCP-->>CP: Updated catalog - CP->>DB: Reconcile catalog changes - CP->>CP: Recompile affected snapshots - CP->>Store: Atomically publish revision N plus 1 - - Store-->>DP: Configuration revision available - DP->>Store: Load revision N plus 1 - DP->>DP: Replace local cache atomically - - Client->>DP: tools/list - DP-->>Client: Updated list from local snapshot - - Note over DP,Client: Phase 4 owns MCP list-change notifications +```text +control-plane reconciler -> poll backend liveness, discovery, and catalogs + -> reconcile administrative catalog changes + -> compile affected snapshots + -> atomically publish revision N+1 to shared storage +external dataplane -> load revision N+1 -> replace local cached snapshot +client -> tools/list -> receive updated visible catalog from the snapshot + +Phase 4 owns downstream MCP list-change notifications. ``` diff --git a/_context/wiki/performance.md b/_context/wiki/performance.md index fe782bf0..f17a55f5 100644 --- a/_context/wiki/performance.md +++ b/_context/wiki/performance.md @@ -1,50 +1,87 @@ -# Performance And Load Testing +# Performance and Load Testing -## Full-Stack Load (Locust via cf-integration) +Load testing is owned by [`cf-integration`](https://crates.io/crates/cf-integration). +The commands below match **0.3.1**, the version pinned by this repository's +conformance workflow. The old `scripts/cf-integration.sh` wrapper is no longer +in this repository. -Performance testing uses the control-plane Locust suite through -[`cf-integration`](https://crates.io/crates/cf-integration). -It measures the nginx → external dataplane → backend request path while the -ContextForge control plane publishes configuration. +## Setup and Smoke Test -| 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: +Use Docker with enough resources for the selected topology. Install the pinned +CLI and check its command reference: ```bash -LOCUST_USERS=20 LOCUST_SPAWN_RATE=5 LOCUST_RUN_TIME=2m \ - scripts/cf-integration.sh locust +cargo binstall cf-integration@0.3.1 --no-confirm +cf-integration load --help ``` -- `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). +The standalone lane starts the external dataplane, Redis, nginx, and a fixture +without the control plane. It supplies a known published catalog so load setup +does not depend on the unsupported external `tools/list` method: -## Headless vs Web UI +```bash +CF_INTEGRATION_DIR="$PWD/.integration" \ +CF_DATAPLANE_REPO="$PWD" CF_DATAPLANE_REF="$(git rev-parse HEAD)" \ + cf-integration load --lane external --protocol-version modern --standalone \ + --users 1 --spawn-rate 1 --run-time 10s +``` -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`. +The harness checks out the selected committed revision; commit changes before +comparing them. All external examples target modern MCP `2026-07-28`. -## Benchmark Settings +## Load Settings -Restore both to `60` before measuring throughput — fast publish + per-request Redis reads distort numbers: +```bash +CF_INTEGRATION_DIR="$PWD/.integration" \ +CF_DATAPLANE_REPO="$PWD" CF_DATAPLANE_REF="$(git rev-parse HEAD)" \ + cf-integration load --lane external --protocol-version modern --standalone \ + --users 20 --spawn-rate 5 --run-time 2m +``` -| Variable | Functional default | Benchmark value | +| Setting | CLI flag | Default | | --- | --- | --- | -| `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) | +| Concurrent users | `--users` | `100` | +| Spawn rate | `--spawn-rate` | `10` users/second | +| Duration | `--run-time` | `5m` | +| Short workload preset | `--smoke` | Off; explicit load settings take precedence. | +| Diagnostic observability stack | `--observability` | Off, to avoid changing benchmark overhead. | -## Built-In-Dataplane Baseline +`LOCUST_USERS`, `LOCUST_SPAWN_RATE`, and `LOCUST_RUN_TIME` also configure the +workload; CLI values take precedence. Durations accept ordered `h`, `m`, and `s` +components, such as `2m30s`. The pinned CLI runs Locust headless; the old +`LOCUST_MODE=web` recipe does not switch this command into a web UI. -Compare against the stock Python repository, where MCP traffic uses the -ContextForge built-in dataplane and the ContextForge external dataplane is -absent: +HTML, CSV, and `locust.log` are written beneath +`CF_INTEGRATION_DIR/reports/load/`. Record the exact dataplane and harness +versions alongside the report. + +## Full-Stack and Built-In Comparisons + +Omit `--standalone` to include the control plane and its publication path: ```bash -scripts/cf-integration.sh down # free shared ports -scripts/cf-integration.sh controlplane-locust +cf-integration load --lane external --protocol-version modern \ + --users 20 --spawn-rate 5 --run-time 2m +cf-integration load --lane builtin --protocol-version modern \ + --users 20 --spawn-rate 5 --run-time 2m ``` -`CONTROLPLANE_LOCUST_CLASSES=all` adds admin/UI/mutating surfaces. `LOCUST_USERS`, `LOCUST_SPAWN_RATE`, and `LOCUST_RUN_TIME` apply here too. +Use equivalent backends, tools, hardware, authentication, policy, cache settings, +and client metadata for comparisons. A full-stack failure while discovering or +publishing the catalog is a setup failure, not a throughput measurement. +Standalone measurements omit control-plane publication and must be labeled as +such. See the [pinned harness documentation](https://github.com/contextforge-org/contextforge-dev-tools/blob/v0.3.1/README.md) +for topology and source selection. + +## Benchmark Controls + +Report the publisher interval and +`CF_DATAPLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS` explicitly. Cache expiry `0` +forces a Redis read on every request and is useful for functional checks; the +Rust default is `60`. Keep caching, publication, plugins, and telemetry identical +between before/after measurements. A publisher interval is irrelevant to a +standalone snapshot that is not being republished during the run. + +Record request rate, latency percentiles, failures, and resource usage after +warmup. Do not infer correctness or protocol coverage from successful load; +run [workspace and conformance checks](testing.md) separately. diff --git a/_context/wiki/preferences.md b/_context/wiki/preferences.md index 49e88342..cc75c8a6 100644 --- a/_context/wiki/preferences.md +++ b/_context/wiki/preferences.md @@ -5,21 +5,22 @@ 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. +3. `cargo nextest run --locked --workspace --all-features` passes (fallback: `cargo test`). +4. `cargo deny check advisories bans licenses` passes (CI; pre-commit runs advisories and licenses). +5. `cargo build --locked --workspace --all-features` 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`. +CI additionally runs `cargo shear --check-test-targets --deny-warnings --locked` +and `cargo bench --no-run`. **By change type:** | Change type | Minimum extra validation | | --- | --- | | Docs only | Run `mdbook build _context/wiki` and `mdbook test _context/wiki`; inspect affected headings, tables, and code blocks in the rendered output | -| Routing or session behavior | New/updated integration tests in `crates/contextforge-data-plane-lib/tests/` against mock backends | +| Routing or request lifecycle | 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 | +| Plugin behavior | `tests/gateway/plugins.rs` integration coverage plus CPEX/runtime or concrete-plugin tests as appropriate | | Performance-sensitive paths | Load-test run before and after | ## Code style @@ -49,16 +50,16 @@ CI additionally runs `cargo shear --check-test-targets --deny-warnings --locked` - The ContextForge external 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 stateful session behavior are being **removed**. `initialize` remains supported as a stateless compatibility method; do not use it to create affinity, persist client state, or retain backend transports between requests. +- Preserve the published client-facing naming contract. Routing now uses explicit `ServiceRoute` maps; changes must update the publisher, schemas, and tests rather than reintroducing prefix splitting. +- Legacy SSE and older-client initialization/session behavior stay on Python routes. Remaining Rust compatibility code is migration state; do not build new behavior on it. - Prefer the right architecture over backward compatibility; this project has no external users yet. ## Protocol target -- The ContextForge external-dataplane target supports MCP **`2026-07-28`** and **`2025-11-25`** over **Streamable HTTP**. -- Every request is independent for both versions. Do not require `Mcp-Session-Id`, session affinity, or a previously retained backend transport. -- Retain `initialize` for clients that use it, but generate its response from effective configuration and do not treat it as session establishment. `2026-07-28` tests and examples should continue to exercise `server/discover` and per-request client metadata. -- Protocol-sensitive tests cover both same-version paths and the best-effort cross-version paths (`2026-07-28` → `2025-11-25` and the reverse). Do not add SSE or versions earlier than `2025-11-25` without a separate architecture decision. +- The external-dataplane contract targets MCP **`2026-07-28`** over **Streamable HTTP**. +- Each request is independent. Do not require a session ID, affinity, or a retained backend transport. +- Tests and examples use `server/discover` and per-request client metadata. +- Do not add compatibility for older protocol versions, legacy `initialize`/session behavior, or legacy SSE. Keep those clients on control-plane/built-in routes. ## AI interaction preferences diff --git a/_context/wiki/project.md b/_context/wiki/project.md index 1a96797c..4e12fd07 100644 --- a/_context/wiki/project.md +++ b/_context/wiki/project.md @@ -1,208 +1,141 @@ # Project Overview -> This page describes the **current implementation**. The tentative product -> end state and Phase 1-4 migration are documented in -> [ContextForge 2.0 Target Architecture and Roadmap](mcp-capability-allocation.md). +This page describes the current external-dataplane implementation and integration +boundary. Proposed work is in the [ContextForge 2.0 roadmap](mcp-capability-allocation.md). ## What this project is -`contextforge-data-plane` is the Rust-based **ContextForge external dataplane**. -It is a scalable, separately deployable MCP (Model Context Protocol) gateway -that routes AI tool calls from MCP clients to backend MCP servers. - -The [`IBM/mcp-context-forge`](https://github.com/IBM/mcp-context-forge) -Python repository contains two different product components: the ContextForge -control plane and the ContextForge built-in dataplane. This Rust repository is -the third component: - -| Layer | Owns today | -| --- | --- | -| **ContextForge control plane** (Python) | IAM, UI, management APIs, durable administrative state, policy/catalog compilation, metrics storage, and external-dataplane configuration publishing. | -| **ContextForge built-in dataplane** (Python) | MCP request handling shipped in the same repository as the control plane. Supports `2026-07-28` and `2025-11-25`, including stateful and stateless behavior. | -| **ContextForge external dataplane** (Rust, this repo) | Separately deployed MCP request routing and authorization enforcement. The target supports both protocol versions without session state; cross-version adaptation is best effort. | - -The ContextForge external dataplane must never take on control-plane concerns. +`contextforge-data-plane` is the Rust **ContextForge external dataplane**. It +routes MCP requests to backend servers using configuration published by the +ContextForge control plane. It has no IAM, UI, management database, or metrics +storage responsibilities. ## Terminology -Use the full component names in product-wide architecture and deployment -documentation: - -- **ContextForge control plane** means the Python management plane in - `IBM/mcp-context-forge`. It owns administrative workflows and publishes - effective runtime configuration; it is not the name for every process or MCP - route in that repository. -- **ContextForge built-in dataplane** means the Python MCP request path in the - same `IBM/mcp-context-forge` repository. “Built-in” describes where it ships, - not a legacy-only or slow-path role. It handles the old and new protocol - versions and can serve stateful or stateless clients. -- **ContextForge external dataplane** means this independently deployable Rust - repository. “External” means external to the Python repository/deployment, - not untrusted or third-party. Its target request path is stateless for both - supported protocol versions. -- **Stateful** means later MCP requests can depend on session context established - by `initialize` or a session identifier. **Stateless** means every request is - independently authenticated, authorized, resolved, and completed without - reusable MCP session state. - -Always use one of the three canonical names. Do not use unqualified -“dataplane,” “local dataplane,” “slow dataplane,” or “fast dataplane” as a -product component name. - -```mermaid -flowchart LR - C(["MCP Client\nold/new · stateful/stateless"]) - - subgraph Infra["Infrastructure"] - N["nginx\nTLS termination\nrouting fan-out"] - end - - subgraph EDP["ContextForge External Dataplane (Rust, this repo)"] - direction TB - MW["Middleware stack\nvirtual host · JWT · session · user config"] - RT["MCP Routing\nfan-out · prefix namespace\nlist merge · capability merge"] - PL["Plugin hooks\ncmf.tool_pre_invoke\ncmf.tool_post_invoke\ncmf.prompt_pre_fetch\ncmf.prompt_post_fetch"] - MW --> RT --> PL - end - - subgraph PythonRepo["IBM/mcp-context-forge (Python repo)"] - direction TB - CP["ContextForge control plane\nIAM · UI · management"] - BDP["ContextForge built-in dataplane\nold/new · stateful/stateless"] - PUB["dataplane_publisher.py\nwrites UserConfig to Redis"] - CP --> PUB - end - - R[("Redis\nUserConfig store\nMessagePack")] - BE["Backend MCP Servers"] - - C --> N - N -->|"external route - currently 2026-07-28"| EDP - N -->|"UI / IAM / management"| CP - N -->|"built-in MCP routes"| BDP - PUB --> R - EDP -->|"read-only UserConfig"| R - EDP -->|"MCP calls"| BE - BDP -->|"MCP calls"| BE -``` +| Component | Responsibility | +| --- | --- | +| **ContextForge control plane** | Management, identity, upstream registration, catalog and policy publication in [IBM/mcp-context-forge](https://github.com/IBM/mcp-context-forge). | +| **ContextForge built-in dataplane** | MCP handling shipped in the Python repository. Older clients, legacy initialization/session flows, and SSE stay on those routes. It can also serve modern clients. | +| **ContextForge external dataplane** | This independently deployed Rust service. Its supported downstream contract is MCP `2026-07-28` over Streamable HTTP with `server/discover` and per-request client metadata. | +Use these component names in product-wide documentation. “External” means +separately deployed, not untrusted. A stateless request independently establishes +its identity, configuration, and backend route; it does not need a session or +backend transport retained from a previous request. ## Goals and objectives -- Provide a **production-grade, low-latency routing layer** between MCP clients and backend MCP servers. -- Support MCP `2026-07-28` and `2025-11-25` over Streamable HTTP as stateless downstream contracts. -- Enforce a clean **ContextForge external dataplane/control plane boundary** — no IAM, UI, or metrics storage logic in this repo. -- Keep config access behind the **`UserConfigStore` abstraction** (backed by Redis/MessagePack). -- Remain in the right architectural shape during early development, prioritising correctness over backward compatibility. +- Provide a low-latency routing layer between modern MCP clients and backends. +- Keep control-plane responsibilities outside this repository. +- Keep persistent user configuration behind `UserConfigStore`. +- Prefer correct architecture over preserving unstable APIs during early development. ## Key stakeholders and users -- **Platform teams** — deploy and operate the gateway as infrastructure. -- **AI application developers** — use the gateway as the MCP proxy layer for their applications. -- **Internal contributors** — engineers evolving the ContextForge external dataplane toward stateless `2026-07-28` and `2025-11-25` protocol support. +Platform teams deploy the service, application developers consume its MCP +routes, and contributors evolve its routing, security, and protocol behavior. ## Key modules and architecture -Architecture context lives in the wiki. Key pages: - -| Wiki page | Covers | +| 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 | -| [mcp-capability-allocation.md](mcp-capability-allocation.md) | Tentative ContextForge 2.0 end state, responsibility allocation, and Phase 1-4 roadmap | -| [config.md](config.md) | JWT validation, config keying, UserConfig shape, cache behavior | -| [security.md](security.md) | Trust boundaries, invariants, and tradeoffs | +| [Architecture](architecture.md) | Middleware, startup, state ownership, and module boundaries. | +| [Routing](routing.md) | Published routes and per-request backend lifecycle. | +| [Configuration](config.md) | CLI, principal mapping, Redis documents, plugins, and telemetry. | +| [Security](security.md) | Trust boundaries and current authorization limits. | +| [Roadmap](mcp-capability-allocation.md) | Proposed effective catalogs and stronger authorization. | ## Crate ownership | Crate | Purpose | | --- | --- | -| `contextforge-data-plane-lib` | All ContextForge external-dataplane behavior: routing, middleware, sessions, transports. Almost everything goes here. | -| `contextforge-data-plane` (binary) | Process shell only: CLI flags, logging, runtime shape. No ContextForge external-dataplane logic. | -| `contextforge-data-plane-apis` | Shared config shapes (`UserConfig`, `User`, plugin config). Regenerate JSON schemas after any change: `cargo run -p contextforge-data-plane-apis`. | -| `contextforge-data-plane-cpex` | Plugin integration (CPEX hook factories). | - -**Key invariants:** -- Redis/config access goes through `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**: support same-version `2026-07-28` and `2025-11-25` paths over Streamable HTTP, provide best-effort translation in either cross-version direction, and replace stateful session paths with request-scoped handling. -- Legacy SSE transport and session affinity are **being removed** from the ContextForge external dataplane. `initialize` is retained as a stateless compatibility request and must not create persistent external-dataplane or backend session state. -- Protocol-sensitive tests must cover the two direct and two best-effort cross-version combinations. Modern examples should continue to use `server/discover` and per-request client metadata; compatibility examples may use `initialize` without relying on later session reuse. +| `contextforge-data-plane-lib` | Gateway behavior, middleware, routing, configuration access, and transports. | +| `contextforge-data-plane` | Process startup, CLI parsing, logging, and wiring the library. | +| `contextforge-data-plane-apis` | Shared configuration shapes and JSON schema generation. | +| `contextforge-data-plane-cpex` | Plugin registry, runtime, and MCP/CMF adapters. | + +User routing reads go through `UserConfigStore`. Runtime plugin configuration +has its own store in the CPEX crate. Routing uses explicit `ServiceRoute` +entries; it does not split or merge backend prefixes. Any change to published +names or routes must be coordinated with the publisher and tested. Hot-path +behavior changes must update the matching wiki page in the same change. + +## Protocol migration + +New behavior, tests, and examples target `2026-07-28`, `server/discover`, and +per-request metadata. Remaining legacy `initialize` code and tests are migration +artifacts, not supported client contracts or a reason to add compatibility. +Modern targeted calls already use request-scoped backend connections and do +not require sticky routing. Aggregate list methods and completion are currently +rejected; proposed snapshot-backed catalogs are future work. ## ContextForge Integration Contract -> **Provisional.** No formal contract has been stipulated yet. This section documents the current de-facto integration surface with [IBM/mcp-context-forge](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. +The integration is evolving. These are the contracts consumed by the current +Rust source; publisher changes must be checked against them. -| Agreement | Value today | +| Agreement | Current Rust behavior | | --- | --- | -| Client-facing route | `/servers/{virtual_host_id}/mcp`. Front door rewrites modern MCP `2026-07-28` Streamable HTTP traffic to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the ContextForge external dataplane. | -| Protocol compatibility | Today the external-dataplane route accepts MCP `2026-07-28`; the built-in dataplane handles `2026-07-28` and `2025-11-25`, including stateful and stateless behavior and legacy SSE compatibility. The external-dataplane target handles both supported Streamable HTTP versions statelessly, with cross-version adaptation on a best-effort basis. | -| Unknown virtual host | `404` with body `{"detail":"Server not found"}`, matching the control-plane response shape. | -| Token issuer and audience | `iss = mcpgateway`, `aud = mcpgateway-api`. | -| Claims shape | `sub`, `jti`, `iss`, `aud`, `exp`, and `user` required. `token_use`, `iat`, `teams`, `scopes`, and `user.full_name` optional. The ContextForge external dataplane routes on `sub` only. | -| User config Redis key | `MessagePack(User::new(jwt_subject))` — key type plus subject, not the raw subject string. | -| User config Redis value | `MessagePack(UserConfig)`. JSON schema at `schemas/user_config.json`. | -| User key Redis schema | `schemas/user.json`. | -| Plugin config key | `ContextForgeGatewayRuntimePluginConfig`, JSON or MessagePack, `version: 1` with a `cpex` section. | - -**Coordination rule:** changing any row above is a cross-repo change. The external dataplane, the control-plane publisher (`dataplane_publisher.py`), and the `cf-integration` harness all need updating together. - -Regenerate both schemas after any struct change to `UserConfig`, `VirtualHost`, `BackendMCPGateway`, or the `User` key type: +| Direct MCP route | `/contextforge-rs/servers/{virtual_host_id}/mcp`. An ingress can expose a different public prefix if explicitly configured to rewrite it. | +| Protocol | Modern Streamable HTTP. Deployments must keep older clients and legacy SSE on Python routes. | +| Unknown virtual host | HTTP `404` with `{"detail":"Server not found"}`. | +| JWT trust | RSA/EC keys from `--jwks-url`; no fixed issuer or audience is enforced. See [JWT claims](config.md#jwt-claims-validated-by-claims_layer). | +| Principal | Default user aliases: `sub`, `user_id`, `UserId`; tenant aliases: `tenantId`, `tenant_id`. Both IDs must be strings. CEL can supply a custom mapping. | +| User config key | MessagePack-encoded `User::new(principal.user_id)`, not a raw user-ID string. Tenant ID is currently absent from the storage/cache key. | +| User config value | MessagePack `UserConfig`, including virtual hosts, backend definitions, and explicit object routes. | +| Schemas | `schemas/user.json` and `schemas/user_config.json`. | +| Plugin document | `ContextForgeGatewayRuntimePluginConfig`, JSON or MessagePack, with `version: 1` and `cpex`. | + +Publishing a backend alone does not publish its tools: each callable object +needs a route. The user ID extracted from the token must match the publisher's +key. Requiring a tenant claim does not provide tenant partitioning of that key; +see [Security](security.md#authentication-and-authorization). + +Coordinate changes with the control-plane publisher and `cf-integration`. +Regenerate both schemas after changes to their shared model types: + ```bash cargo run -p contextforge-data-plane-apis ``` -## System topology (current) - -All external traffic enters through **nginx**, which routes management traffic -to the control plane and MCP traffic to either the built-in or external -dataplane: - -```mermaid -flowchart LR - client(["client"]) --> nginx["nginx"] - nginx --> external["external dataplane\nRust · this repo"] - nginx --> builtin["built-in dataplane\nPython repo"] - nginx --> control["control plane\nPython repo"] - external --> redis["redis"] - control --> redis - control --> postgres["postgres\n(via pgbouncer)"] - external --> fastts["fast_time_server"] +## System topology + +A typical deployment places nginx in front of both repositories: + +```text +client -> nginx -> management route -> control plane -> management database + | | + | +-> publish config -> Redis + | + +-> Python MCP route -> built-in dataplane -> backend + | + +-> modern external route -> Rust external dataplane + |-> read config from Redis + |-> fetch trusted JWKS + +-> call selected backend ``` -### How the control plane publishes config to the external dataplane +The reference `docker/nginx.conf` routes by URL prefix; it does not inspect the +MCP protocol version. Selecting compatible clients and routes is a deployment +responsibility. See [Deployment](deployment.md#nginx-front-door-routing). -The control plane and external 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 external-dataplane configuration (user config, backend definitions, etc.) into Redis. -2. The external dataplane reads that config from Redis via the **`UserConfigStore`** abstraction (MessagePack-encoded `UserConfig`). - -This means: -- The external dataplane is a **pure reader** of Redis config. It never writes back to the control plane's Redis keys. -- The control plane is the **sole writer** of external-dataplane config; the external dataplane has no direct dependency on the control-plane process at runtime. -- Config changes from the control plane are picked up by the external dataplane through normal cache refresh / Redis reads — no restart or direct RPC required. +### How the control plane publishes config to the external dataplane -### Per-component responsibilities +The control-plane `dataplane_publisher.py` writes runtime documents to Redis. +The Rust service reads user configuration through `UserConfigStore` and refreshes +its local cache after expiry. Plugin configuration uses a separate reload +watcher. Neither mechanism calls a control-plane management API per MCP request. +JWKS retrieval is a separate HTTP dependency and may be hosted by the issuer +or control plane. -| Component | Role | Persistence | -| --- | --- | --- | -| **nginx** | TLS termination, routing fan-out | — | -| **ContextForge external dataplane** (`contextforge-data-plane`) | MCP routing, auth enforcement, and backend calls; current session-backed paths are migration state, while the target is stateless | Redis (read-only for config) | -| **ContextForge built-in dataplane** (`IBM/mcp-context-forge`) | Python MCP request handling for old/new protocols and stateful/stateless clients | Python repository runtime state and stores | -| **ContextForge control plane** (`IBM/mcp-context-forge`) | IAM, UI, management APIs, metrics, and external-dataplane config publishing | Redis (write) + PostgreSQL (via pgbouncer) | -| **redis** | Runtime config store, inter-component pub/sub channel | In-memory + persistence | -| **postgres** (via pgbouncer) | Control-plane relational store | Durable | -| **fast_time_server** | High-resolution time source used by the ContextForge external dataplane | — | +Production builds read published configuration. The development-only +`with_tools` feature also exposes unauthenticated token and config-write +helpers; it must be excluded from production builds. ## External dependencies and integration points -- **Redis** — runtime config store (MessagePack-encoded `UserConfig`). Populated by `dataplane_publisher.py` on the control plane; read by the external dataplane via `UserConfigStore`. -- **ContextForge control plane** (`IBM/mcp-context-forge`) — owns management workflows and publishes external-dataplane config via `dataplane_publisher.py`. -- **ContextForge built-in dataplane** (`IBM/mcp-context-forge`) — owns the Python repository's MCP request paths, including old/new and stateful/stateless handling. Requests sent there do not route through the external dataplane. -- **fast_time_server** — high-resolution time source consumed by the ContextForge external dataplane. -- **Tokio + Axum** — fixed async runtime and web framework. +- **Redis** distributes user routing and plugin configuration. +- **JWKS endpoint** supplies public token verification keys. +- **Backend MCP servers** execute selected operations. `fast_time_server` and + the counter server are test fixtures, not required gateway clock services. +- **Tokio, Axum, and RMCP** provide asynchronous execution, HTTP middleware, and MCP transport handling. diff --git a/_context/wiki/routing.md b/_context/wiki/routing.md index 0dc2e105..fc8393dc 100644 --- a/_context/wiki/routing.md +++ b/_context/wiki/routing.md @@ -1,10 +1,15 @@ # MCP Routing Semantics -The external dataplane is a **pure stateless router**. No session state, no `BackendTransports`, no sticky-routing requirement. +The modern external-dataplane request path is stateless. It has no retained +backend transports or sticky-routing requirement. RMCP still supplies a local +session manager internally; legacy implementation paths are not the supported +client contract. ## How a request is routed -1. `validate_stateless` extracts `VirtualHost` from request extensions (set by `virtual_host_config` layer from the JWT virtual-host ID). +1. Middleware extracts the virtual-host ID from the URL path, verifies the JWT, + extracts the principal, and loads that user's configuration. + `validate_stateless` resolves the selected `VirtualHost` from request context. 2. Downstream name is looked up in `VirtualHost::tools`, `::resources`, or `::prompts` — an O(1) table lookup. 3. `connect_backend_for_request` opens a fresh `StreamableHttpClientTransport`, runs the call, closes the connection. @@ -23,29 +28,36 @@ ServiceRoute { backend_name: String, // key into VirtualHost::backends upstream_name: String } // name/URI forwarded to the backend ``` -Source: [`user_store.rs`](../../crates/contextforge-data-plane-apis/src/user_store.rs) +Source: [`user_store.rs`](https://github.com/contextforge-org/contextforge-data-plane/blob/main/crates/contextforge-data-plane-apis/src/user_store.rs) ## Method quick reference | Method | Behavior | | --- | --- | +| `server/discover` | Local RMCP discovery response. It is not yet generated from a principal-bound effective catalog. | | `initialize` (`2026-07-28`) | `INVALID_REQUEST` — not supported by this dataplane. | -| `initialize` (legacy) | Stub `InitializeResult`; no backend fanout. Supports older clients during migration. | -| `list_tools`, `list_resources`, `list_resource_templates`, `list_prompts` | `INVALID_REQUEST` — delegated to control plane. | -| `call_tool` | Lookup in `tools` map → pre-hook → fresh connection → call → post-hook → close. Forwards cancellation; tracks progress tokens. | -| `read_resource` | Lookup in `resources` map → fresh connection → call with upstream URI → close. | -| `get_prompt` | Lookup in `prompts` map → pre-hook → fresh connection → call → post-hook → close. | -| `subscribe`, `unsubscribe`, `complete` | `INVALID_REQUEST` — delegated to control plane. | -| `ping` | Local success; no backend fanout. | -| `DELETE` | RMCP handles; `session_id_layer` removes the `LocalUserSessionStore` entry. No backend state to clean up. | +| Legacy `initialize` code | Stub result without backend fan-out; temporary migration behavior, not a supported client contract. | +| `tools/list`, `resources/list`, `resources/templates/list`, `prompts/list` | `INVALID_REQUEST` — catalog operations remain on control-plane routes. | +| `tools/call` | Route → parameter-header validation → pre-hook → connect → call → close → post-hook. Explicit cancellation relay and progress correlation. | +| `resources/read` | Route → pre-hook and permitted URI rewrite → connect → read → close → post-hook. | +| `prompts/get` | Route → pre-hook → connect → get prompt → close → post-hook. | +| `resources/subscribe`, `resources/unsubscribe`, `completion/complete` | `INVALID_REQUEST` — not implemented in this dataplane. | +| `ping` | Local success; no backend fan-out. | + +Post-hooks process successful backend responses. No `initialize`, session ID, +or DELETE cleanup request is required for the modern call lifecycle. Resource +reads resolve exact published URIs; the presence of a `resource_templates` map +does not implement template listing or dynamic URI matching. ## Header forwarding -Applied in order per upstream call: Host (from backend URL, HTTPS only) → passthrough (`BackendMCPGateway::passthrough_headers`) → `Mcp-Param-*` auto-forward → trace context → add (`add_headers`, overrides passthrough) → remove (`remove_headers`, applied last). +Applied in order per upstream call: Host (from backend URL, HTTPS only) → passthrough (`BackendMCPGateway::passthrough_headers`) → `Mcp-Param-*` auto-forward → add (`add_headers`, overrides passthrough) → remove (`remove_headers`) → current trace-context injection. Protected headers that config can never touch: `Host`, `Content-Length`, `Content-Type`, all RFC 7230 hop-by-hop headers, `Mcp-Session-Id`, `Accept`, `Last-Event-Id`, and all computed MCP standard headers (`Mcp-Method`, `Mcp-Name`, `Mcp-Protocol-Version`, `Mcp-Param-*`). -For clients on `≥ 2026-07-28`, `call_tool` validates `Mcp-Param-*` headers against `BackendMCPGateway::tool_schemas` before contacting the backend. +For clients on `≥ 2026-07-28`, `call_tool` validates `Mcp-Param-*` headers against `BackendMCPGateway::tool_schemas` when a schema is published, before +plugins or backend I/O. Without a schema the headers pass through without local +validation. ## Plugin hooks diff --git a/_context/wiki/security.md b/_context/wiki/security.md index ff261aa5..e5b73446 100644 --- a/_context/wiki/security.md +++ b/_context/wiki/security.md @@ -2,53 +2,55 @@ ## Trust Boundaries -| Boundary | Trust level | Enforced by | -| --- | --- | --- | -| Downstream client | Untrusted. Every request must present a valid bearer JWT; session id alone grants nothing without matching principal state. | `claims_layer`, validators, and principal-scoped backend session keys. | -| JWT verification material | Trust anchor. The RSA public key or HMAC secret in process config decides which tokens are accepted. | Process config; loaded at startup. | -| Redis | Control-plane trust boundary. Whoever can write Redis controls routing (`UserConfig`) and, when runtime plugins are enabled, which registered hooks execute (`ContextForgeGatewayRuntimePluginConfig`). | Redis TLS/mTLS connection modes; the external dataplane never writes user config in production builds. | -| Backend MCP servers | Trusted per configured URL. The gateway forwards caller traffic to them and merges their responses. | `UserConfig` backend URLs plus the upstream connection mode. | -| Plugins | Fully trusted code. Hooks run in-process and can read and mutate tool payloads. | Compiled-in factories only; Redis config activates registered factories, it cannot load new code. | +| Boundary | Current contract | +| --- | --- | +| Downstream client | Untrusted. Every MCP request needs a valid JWT, extracted principal, published virtual host, and an explicit route for targeted objects. | +| JWKS endpoint | Trust anchor for RSA/EC public verification keys. HTTPS is required except loopback HTTP for local testing. | +| Redis | Trusted configuration. Writers control backend URLs, object routes, backend credentials, and enabled compiled-in plugin policies. | +| Backend MCP servers | Receive requests selected by published routing and control their responses. Transport security follows the configured upstream mode. | +| Plugins | Fully trusted in-process code that can inspect and modify payloads. Redis activates registered factories; it cannot load new Rust code. | ## Authentication And Authorization -| Plane | Current responsibility | -| --- | --- | -| ContextForge control plane | Owns login/SSO, users, teams, IAM, API-token issuance and revocation, and external-dataplane configuration publication. `dataplane_publisher.py` writes visibility-filtered `UserConfig` snapshots to Redis by user email. | -| ContextForge built-in dataplane | Owns the Python repository's MCP request routes, including old/new protocol and stateful/stateless behavior. | -| ContextForge external dataplane | Has no IAM or user database. It currently verifies modern MCP bearer JWTs locally, loads `UserConfig` by `sub`, and requires the requested virtual host to exist. No runtime control-plane call occurs. | - -External-dataplane request path: control-plane API token (`sub` = email) → Origin check → -`claims_layer` → Redis config lookup → virtual-host check → RMCP Host check → -MCP routing. -Browser/login session tokens are management-plane credentials, not the -external-dataplane contract. - -- JWT validation accepts `RS256/384/512` or `HS256/384/512` and requires a valid - signature, `iss=mcpgateway`, `aud=mcpgateway-api`, and `exp`. `jti` and `user` - are required fields; `token_use`, `iat`, `teams`, and `scopes` are optional. -- Failures: bad/missing JWT → `401`; no user config → `400`; unavailable virtual - host → `404`. -- Authorization is currently coarse: valid JWT plus published virtual host. - JWT scopes/teams and object allowlists are not enforced; publishing a backend - exposes all objects returned by it. -- This coarse current behavior does not meet the tentative Phase 3 target. The - target requires principal- and isolation-bound snapshots, per-request scope - and compiled-RBAC enforcement, and default denial for missing or unauthorized - entries. See [Target Authorization Invariants](mcp-capability-allocation.md#target-authorization-invariants). -- External-dataplane requests do not consult the control-plane token blocklist. - Revoked tokens pass JWT validation until `exp` or signing-key rotation/restart. - Removing a subject's config eventually blocks all its tokens after publisher - and cache expiry. +The control plane owns identity management and token issuance. The external +dataplane verifies tokens and reads published configuration; it has no IAM or +user database. Configuration does not require a management API call per request, +but verification can fetch keys from the trusted issuer's JWKS endpoint. + +The request path is Origin/header checks → JWT verification → principal +extraction → user configuration → virtual-host check → RMCP validation → +published object route → backend call. + +- JWT verification uses RSA/EC JWKS keys. HMAC secrets and the old public-key + CLI flag are not supported. `exp` and `nbf` are validated when present; no + fixed issuer/audience or mandatory expiration claim is enforced today. +- The default extractor requires a string user ID (`sub`, `user_id`, or + `UserId`) and tenant ID (`tenantId` or `tenant_id`). The first present alias + wins and must have the right type. User IDs need not be emails. CEL can + define a custom mapping; see [Configuration](config.md#jwt-claims-validated-by-claims_layer). +- The Redis/cache key currently contains **only the extracted user ID**, not + the tenant. Identical user IDs in different tenants resolve to the same + stored configuration. Tenant extraction alone is not an isolation boundary. +- The virtual host and each targeted tool, resource, or prompt must exist in + that user's published routing maps. Publishing a backend alone does not + expose all its objects. The dataplane does not derive routes by prefix. +- JWT scopes, teams, and compiled RBAC are not independently enforced on this + path. Stronger isolation and policy checks in the + [target authorization model](mcp-capability-allocation.md#target-authorization-invariants) + are proposed work, not current guarantees. +- There is no per-token blocklist/revocation lookup. Keys are cached for five + minutes; removing a JWKS key is not immediate invalidation until refresh or + restart. A token without `exp` has no expiration enforced by this verifier. + Removing a user's published configuration blocks access after cache expiry. ## What Compromise Means -| If this is compromised | Impact | +| Compromise | Impact | | --- | --- | -| JWT signing key or HMAC secret | Attacker mints tokens for any subject and reaches that subject's backends. Rotate the key and restart; no revocation exists. | -| Redis write access | Attacker rewrites routing (arbitrary backend URLs receive caller traffic) and, if runtime plugins are enabled, chooses which registered hooks run on payloads. Protect Redis with TLS/mTLS and control-plane-only write access. | -| A backend MCP server | Attacker sees requests routed to that backend and controls its responses; the namespace prefix limits blast radius to that backend's objects. | -| The gateway process | Full compromise: it holds the decoding keys in memory and live backend sessions. | +| Trusted JWT signing key | Tokens can be forged for user IDs with published configuration. Remove the compromised key from trusted JWKS and clear cached keys; rotate issuer signing material. | +| Redis write access | Routing and plugin policy can be replaced, including routing caller payloads and configured credentials to attacker-controlled URLs. | +| Backend MCP server | It sees requests routed to it and controls their results. Published routes constrain selection; a naming prefix is not a security boundary. | +| Gateway process or plugin code | Access to live payloads, bearer tokens, configured backend credentials, and in-process state. Production verification uses public keys, but development helpers additionally load a signing private key. | ## Transport Security @@ -110,13 +112,19 @@ remains and the upstream server may reject the mismatch. ## Local Bootstrap Helpers (`with_tools`) -The `contextforge-data-plane-lib/with_tools` feature compiles in: -- `/contextforge-rs/admin/tokens/{user}` -- `/contextforge-rs/admin/userconfigs/{user}` -- `/contextforge-rs/health` +The binary's `with_tools` feature forwards to +`contextforge-data-plane-lib/with_tools` and compiles in: + +- `GET` / `POST /contextforge-rs/admin/tokens/{tenant_id}/{user_id}` +- `GET /contextforge-rs/admin/.well-known/jwks.json` +- `POST /contextforge-rs/admin/userconfigs/{user_id}` 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. +`GET /contextforge-rs/health` is also unauthenticated, but is available in every +build without `with_tools`. The local token and JWKS helpers use the same RSA +private key; see [Getting Started](getting-started.md#local-cargo-dev-workflow). + ## Secrets Handling - JWT validation keys are fetched from a remote JWKS endpoint; TLS certificate material is read from disk paths at startup. diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index a8b85662..37f7c902 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -4,7 +4,7 @@ - **Workspace checks** — code compiles and unit behavior holds. - **In-repo integration tests** — MCP routing against mock backends. -- **`cf-integration` harness** — full control-plane publication and external-dataplane request path end to end. +- **`cf-integration` harness** — standalone dataplane checks or full control-plane publication and routed requests, depending on the selected lane. - **Load and benchmark** — see [Performance](performance.md). ## Workspace Validation @@ -13,8 +13,11 @@ CI runs these on every change; run them locally before pushing: ```bash cargo fmt --all --check -cargo clippy --locked --workspace --all-targets --all-features -- -D warnings +cargo clippy --locked --workspace --all-targets -- -D warnings cargo nextest run --locked --workspace --all-features +cargo deny check advisories bans licenses +cargo build --locked --workspace --all-features +cargo bench --no-run cargo shear --check-test-targets --deny-warnings --locked ``` @@ -22,9 +25,9 @@ Use `cargo test` when nextest is unavailable. For wiki changes, also run `mdbook New protocol-sensitive tests target MCP `2026-07-28`, connect through `server/discover`, and send the required per-request client metadata. A small -`compatibility` module retains the active `2025-11-25`/`initialize` cases until -that production compatibility surface is removed in a dedicated change; do not -add new behavior to that lane. Every case must remain request-independent, with +`compatibility` module retains `2025-11-25`/`initialize` migration cases. These +are not a supported production contract; do not add new behavior to that lane. +Every case must remain request-independent, with no required `Mcp-Session-Id`, session affinity, or retained backend transport. SSE remains outside the external-dataplane contract. @@ -74,8 +77,10 @@ repository keeps only the CI invocation, Make targets, and expected findings. Comment exactly `/conformance` on a pull request to run the **Conformance** Actions workflow. Only repository owners, members, and collaborators can start -it. The workflow acknowledges the command, tests the pull request head commit, -and reports the final result back to the pull request. CI builds and names the +it. The workflow sets a pending `conformance` commit status, tests the pull request +head commit, and updates that status with the result. The `issue_comment` +workflow itself is read from the default branch, so a PR edit to that workflow +does not change the command handling until merged. CI builds and names the conformance binary artifact using that same head SHA and retains it for 90 days, so changes to `main` do not invalidate the artifact. It runs the modern client and modern server eras through the external dataplane in standalone mode. This diff --git a/docker/mcp_counter.Dockerfile b/docker/mcp_counter.Dockerfile index 0c481daf..2a22794e 100644 --- a/docker/mcp_counter.Dockerfile +++ b/docker/mcp_counter.Dockerfile @@ -18,7 +18,7 @@ RUN \ RUN --mount=type=cache,target=/app/target \ --mount=type=cache,id=cargo,target=/usr/local/cargo/registry \ --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ - cargo build --release --example servers_counter_streamhttp + cargo build --release -p mcp-server-examples --example servers_counter_streamhttp FROM debian:trixie-slim RUN <