From 42a934e7bb9558e43f4bf67df4337d93191abe38 Mon Sep 17 00:00:00 2001 From: Lang-Akshay Date: Fri, 24 Jul 2026 13:48:05 +0100 Subject: [PATCH 1/6] feat: added docker build -t dataplane:latest -f docker/Dockerfile . and Signed-off-by: Lang-Akshay --- Makefile | 17 ++ docker/Dockerfile | 4 +- docker/docker-compose.yml | 445 ++++++++++++++++++++++++++++++++++++-- docker/nginx.conf | 44 +++- 4 files changed, 481 insertions(+), 29 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..03a6f86 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: help docker-prod testing-up testing-down + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' + +docker-prod: ## Build production Docker image (dataplane:latest) from docker/Dockerfile + docker build -t dataplane:latest -f docker/Dockerfile . + +testing-up: ## Launch testing stack: nginx, control plane, redis, postgres, pgbouncer, dataplane, fast_time_server + @docker image inspect dataplane:latest >/dev/null 2>&1 || { \ + echo "Image dataplane:latest not found. Run 'make docker-prod' first."; \ + exit 1; \ + } + docker compose -f docker/docker-compose.yml up -d nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time + +testing-down: ## Tear down the testing stack + docker compose -f docker/docker-compose.yml stop nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time diff --git a/docker/Dockerfile b/docker/Dockerfile index 94c1e36..a895486 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,8 +13,8 @@ RUN --mount=type=cache,id=cargo,target=/usr/local/cargo/registry \ --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ cargo fetch --locked RUN --mount=type=cache,id=cargo,target=/usr/local/cargo/registry \ - --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ - cargo build --release + --mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \ + cargo build --release --features contextforge-gateway-rs-lib/with_tools FROM debian:trixie-slim RUN </dev/null) + + python3 -c " + import json + import os + import time + import urllib.error + import urllib.request + + token = os.environ.get('MCPGATEWAY_BEARER_TOKEN', '') + + def api_request(method, path, data=None): + url = f'http://control-plane:4444{path}' + req = urllib.request.Request(url, method=method) + req.add_header('Authorization', f'Bearer {token}') + req.add_header('Content-Type', 'application/json') + if data: + req.data = json.dumps(data).encode('utf-8') + with urllib.request.urlopen(req, timeout=60) as response: + return json.loads(response.read().decode('utf-8')) + + def api_request_with_retry(method, path, data=None, retries=30, delay=2, retry_statuses=(401, 403, 502, 503)): + for attempt in range(1, retries + 1): + try: + return api_request(method, path, data) + except urllib.error.HTTPError as exc: + if exc.code in retry_statuses and attempt < retries: + print(f'Retrying {method} {path} after HTTP {exc.code} ({attempt}/{retries})') + time.sleep(delay) + continue + raise + except Exception: + if attempt < retries: + print(f'Retrying {method} {path} after transient error ({attempt}/{retries})') + time.sleep(delay) + continue + raise + + print('Waiting for authenticated control-plane readiness...') + for i in range(1, 61): + try: + gateways = api_request('GET', '/gateways') + print(f'Authenticated control-plane readiness confirmed ({len(gateways)} gateways visible)') + break + except urllib.error.HTTPError as exc: + print(f'Authenticated readiness not ready yet ({i}/60): HTTP {exc.code}') + except Exception as exc: + print(f'Authenticated readiness not ready yet ({i}/60): {exc}') + time.sleep(2) + else: + print('Control-plane authenticated readiness check failed') + exit(1) + + try: + gateways = api_request_with_retry('GET', '/gateways') + for gw in gateways: + if gw.get('name') == 'fast_time': + print(f'Deleting existing gateway {gw[\"id\"]}...') + api_request_with_retry('DELETE', f'/gateways/{gw[\"id\"]}') + except Exception as exc: + print(f'Note: {exc}') + + VIRTUAL_SERVER_ID = 'b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8' + + try: + api_request_with_retry('DELETE', f'/servers/{VIRTUAL_SERVER_ID}') + print(f'Deleted existing virtual server {VIRTUAL_SERVER_ID}') + except Exception as exc: + print(f'Note: No existing virtual server to delete (or error: {exc})') + + try: + result = api_request_with_retry('POST', '/gateways', { + 'name': 'fast_time', + 'url': 'http://fast_time_server:8880/mcp', + 'transport': 'STREAMABLEHTTP' + }) + gateway_id = result.get('id', '') + print(f'Registered fast_time_server (gateway_id: {gateway_id})') + except Exception as exc: + print(f'Registration failed: {exc}') + exit(1) + + print('Waiting for tools to sync...') + for i in range(30): + time.sleep(1) + try: + tools = api_request_with_retry('GET', '/tools') + fast_time_tools = [t for t in tools if t.get('gatewayId') == gateway_id] + if fast_time_tools: + print(f'Found {len(fast_time_tools)} tools from fast_time gateway') + break + except Exception: + pass + print(f'Waiting for sync... ({i + 1}/30)') + else: + print('Warning: No tools synced, continuing anyway...') + + tool_ids = [] + try: + tools = api_request_with_retry('GET', '/tools') + tool_ids = [t['id'] for t in tools if t.get('gatewayId') == gateway_id] + print(f'Tools: {[t[\"name\"] for t in tools if t.get(\"gatewayId\") == gateway_id]}') + except Exception as exc: + print(f'Failed to fetch tools: {exc}') + + print('Creating virtual server...') + try: + server_payload = { + 'server': { + 'id': VIRTUAL_SERVER_ID, + 'name': 'Fast Time Server', + 'description': 'Virtual server exposing Fast Time MCP tools', + 'associated_tools': tool_ids, + 'associated_resources': [], + 'associated_prompts': [] + } + } + api_request_with_retry('POST', '/servers', server_payload) + print(f'Virtual server created: {VIRTUAL_SERVER_ID} with {len(tool_ids)} tools') + except Exception as exc: + print(f'Failed to create virtual server: {exc}') + exit(1) + " + echo "Fast Time Server registration complete!" + profiles: ["testing", "monitoring", "sso"] diff --git a/docker/nginx.conf b/docker/nginx.conf index ba09282..252af27 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -26,14 +26,6 @@ http { add_header 'Access-Control-Allow-Headers' '*' 'always'; add_header 'Access-Control-Allow-Credentials' 'true' 'always'; - upstream contextforge-gateway-rs { - hash $remote_addr$remote_port consistent; - server contextforge-gateway-rs-contextforge-gateway-rs-1; - server contextforge-gateway-rs-contextforge-gateway-rs-2; - server contextforge-gateway-rs-contextforge-gateway-rs-3; - } - - include /etc/nginx/mime.types; default_type application/octet-stream; @@ -167,7 +159,11 @@ http { # across gateway container churn. map "" $contextforge_gateway_backend_url { - default "http://contextforge-gateway-rs:4445"; + default "http://data-plane:4445"; + } + + map "" $contextforge_control_backend_url { + default "http://control-plane:4444"; } @@ -288,6 +284,36 @@ http { } + # ============================================================ + # control-plane (ContextForge Python) - config, policy, identity, UI + # Catch-all: anything not matching /contextforge-rs, /health, + # /nginx_status goes to control-plane. + # ============================================================ + location / { + limit_req zone=api_limit burst=20000 nodelay; + limit_conn conn_limit 20000; + + proxy_pass $contextforge_control_backend_url; + + proxy_cache off; + + proxy_next_upstream error timeout http_502 http_503 http_504; + proxy_next_upstream_tries 2; + proxy_next_upstream_timeout 10s; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $forwarded_proto; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header Connection ""; + proxy_http_version 1.1; + + proxy_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + # ============================================================ # Health Check - No rate limiting (for monitoring during load tests) # ============================================================ From 0c455c44cc8376850f5ce95cb53db88f829b2f8a Mon Sep 17 00:00:00 2001 From: Lang-Akshay Date: Fri, 24 Jul 2026 14:40:48 +0100 Subject: [PATCH 2/6] feat: configured control-plane - data-plane comminication Signed-off-by: Lang-Akshay --- docker/docker-compose.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c5840d2..3b1f26a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -73,6 +73,7 @@ services: - CONTEXTFORGE_GATEWAY_RS_REDIS_HOSTNAME=redis - CONTEXTFORGE_GATEWAY_RS_REDIS_PORT=6379 - CONTEXTFORGE_GATEWAY_RS_REDIS_CONNECTION_MODE=plain-text + - CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE=plain-text-or-tls - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PUBLIC_KEY=/keys/jwt.key.pub - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PRIVATE_KEY=/keys/jwt.key - RUST_LOG=debug @@ -139,6 +140,7 @@ services: - CONTEXTFORGE_GATEWAY_RS_REDIS_HOSTNAME=redis - CONTEXTFORGE_GATEWAY_RS_REDIS_PORT=6379 - CONTEXTFORGE_GATEWAY_RS_REDIS_CONNECTION_MODE=plain-text + - CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE=plain-text-or-tls - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PUBLIC_KEY=/keys/jwt.key.pub - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PRIVATE_KEY=/keys/jwt.key - RUST_LOG=debug @@ -182,6 +184,7 @@ services: - CONTEXTFORGE_GATEWAY_RS_REDIS_HOSTNAME=redis - CONTEXTFORGE_GATEWAY_RS_REDIS_PORT=6379 - CONTEXTFORGE_GATEWAY_RS_REDIS_CONNECTION_MODE=plain-text + - CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE=plain-text-or-tls - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PUBLIC_KEY=/keys/jwt.key.pub - CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PRIVATE_KEY=/keys/jwt.key - RUST_LOG=debug @@ -359,6 +362,9 @@ services: - PORT=4444 - DATABASE_URL=postgresql+psycopg://postgres:${POSTGRES_PASSWORD:-mysecretpassword}@pgbouncer:6432/mcp - JWT_SECRET_KEY=${JWT_SECRET_KEY:-devsecret-change-me} + - JWT_ALGORITHM=RS256 + - JWT_PUBLIC_KEY_PATH=/keys/jwt.key.pub + - JWT_PRIVATE_KEY_PATH=/keys/jwt.key - AUTH_ENCRYPTION_SECRET=${AUTH_ENCRYPTION_SECRET:-devencryption-change-me} - BASIC_AUTH_USER=${BASIC_AUTH_USER:-admin} - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD:-changeme} @@ -370,6 +376,11 @@ services: - MCPGATEWAY_UI_ENABLED=true - MCPGATEWAY_ADMIN_API_ENABLED=true - SSRF_ALLOW_PRIVATE_NETWORKS=true + - REDIS_URL=redis://redis:6379/0 + - CACHE_TYPE=redis + - DATAPLANE_PUBLISHER=true + volumes: + - ../assets/:/keys:z depends_on: migration: condition: service_completed_successfully @@ -569,7 +580,11 @@ services: fast_time_server: condition: service_healthy environment: - - JWT_SECRET_KEY=${JWT_SECRET_KEY:-devsecret-change-me} + - JWT_ALGORITHM=RS256 + - JWT_PUBLIC_KEY_PATH=/keys/jwt.key.pub + - JWT_PRIVATE_KEY_PATH=/keys/jwt.key + volumes: + - ../assets/:/keys:z restart: "no" entrypoint: ["/bin/sh", "-c"] command: @@ -577,7 +592,7 @@ services: set -e echo "Registering fast_time_server with control-plane..." - export MCPGATEWAY_BEARER_TOKEN=$$(python3 -m mcpgateway.utils.create_jwt_token --username admin@example.com --admin --exp 10080 --secret $$JWT_SECRET_KEY --algo HS256 2>/dev/null) + export MCPGATEWAY_BEARER_TOKEN=$$(python3 -m mcpgateway.utils.create_jwt_token --username admin@example.com --admin --exp 10080 2>/dev/null) python3 -c " import json From f1fde7f0d3cce96acf20279ee02a40b52e778d21 Mon Sep 17 00:00:00 2001 From: Lang-Akshay Date: Fri, 24 Jul 2026 15:12:47 +0100 Subject: [PATCH 3/6] feat: optimise cpu and memory usage for make testing-up stack Signed-off-by: Lang-Akshay --- docker/docker-compose.yml | 70 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3b1f26a..6adb359 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -50,11 +50,11 @@ services: deploy: resources: limits: - cpus: '4' - memory: 1G + cpus: '0.5' + memory: 256M reservations: - cpus: '2' - memory: 512M + cpus: '0.25' + memory: 128M # ────────────────────────────────────────────────────────────────────── # mcp-gateway-rs @@ -116,11 +116,11 @@ services: replicas: ${GATEWAY_REPLICAS:-1} resources: limits: - cpus: '${GATEWAY_CPU_LIMIT:-8}' - memory: ${GATEWAY_MEM_LIMIT:-8G} + cpus: '${GATEWAY_CPU_LIMIT:-2}' + memory: ${GATEWAY_MEM_LIMIT:-2G} reservations: - cpus: '${GATEWAY_CPU_RESERVATION:-4}' - memory: ${GATEWAY_MEM_RESERVATION:-4G} + cpus: '${GATEWAY_CPU_RESERVATION:-1}' + memory: ${GATEWAY_MEM_RESERVATION:-512M} # ────────────────────────────────────────────────────────────────────── # Volume Mounts @@ -160,11 +160,11 @@ services: deploy: resources: limits: - cpus: '${GATEWAY_CPU_LIMIT:-8}' - memory: ${GATEWAY_MEM_LIMIT:-8G} + cpus: '${GATEWAY_CPU_LIMIT:-2}' + memory: ${GATEWAY_MEM_LIMIT:-2G} reservations: - cpus: '${GATEWAY_CPU_RESERVATION:-4}' - memory: ${GATEWAY_MEM_RESERVATION:-4G} + cpus: '${GATEWAY_CPU_RESERVATION:-1}' + memory: ${GATEWAY_MEM_RESERVATION:-512M} # ────────────────────────────────────────────────────────────────────── # Volume Mounts @@ -204,11 +204,11 @@ services: deploy: resources: limits: - cpus: '${GATEWAY_CPU_LIMIT:-8}' - memory: ${GATEWAY_MEM_LIMIT:-8G} + cpus: '${GATEWAY_CPU_LIMIT:-2}' + memory: ${GATEWAY_MEM_LIMIT:-2G} reservations: - cpus: '${GATEWAY_CPU_RESERVATION:-4}' - memory: ${GATEWAY_MEM_RESERVATION:-4G} + cpus: '${GATEWAY_CPU_RESERVATION:-1}' + memory: ${GATEWAY_MEM_RESERVATION:-512M} # ────────────────────────────────────────────────────────────────────── # Volume Mounts @@ -241,11 +241,11 @@ services: deploy: resources: limits: - cpus: '${GATEWAY_CPU_LIMIT:-8}' - memory: ${GATEWAY_MEM_LIMIT:-8G} + cpus: '${GATEWAY_CPU_LIMIT:-2}' + memory: ${GATEWAY_MEM_LIMIT:-2G} reservations: - cpus: '${GATEWAY_CPU_RESERVATION:-4}' - memory: ${GATEWAY_MEM_RESERVATION:-4G} + cpus: '${GATEWAY_CPU_RESERVATION:-1}' + memory: ${GATEWAY_MEM_RESERVATION:-512M} mcp-counter-tool-two: image: ghcr.io/mcp-counter-tool:latest @@ -271,11 +271,11 @@ services: deploy: resources: limits: - cpus: '${GATEWAY_CPU_LIMIT:-8}' - memory: ${GATEWAY_MEM_LIMIT:-8G} + cpus: '${GATEWAY_CPU_LIMIT:-2}' + memory: ${GATEWAY_MEM_LIMIT:-2G} reservations: - cpus: '${GATEWAY_CPU_RESERVATION:-4}' - memory: ${GATEWAY_MEM_RESERVATION:-4G} + cpus: '${GATEWAY_CPU_RESERVATION:-1}' + memory: ${GATEWAY_MEM_RESERVATION:-512M} ############################################################################### # CACHE @@ -314,11 +314,11 @@ services: deploy: resources: limits: - cpus: '2' - memory: 2G - reservations: cpus: '1' memory: 1G + reservations: + cpus: '0.5' + memory: 512M ############################################################################### # MIGRATION - one-shot Alembic upgrade + bootstrap, runs before control-plane @@ -470,11 +470,11 @@ services: deploy: resources: limits: - cpus: '4' - memory: 8G - reservations: cpus: '2' memory: 2G + reservations: + cpus: '1' + memory: 1G pgbouncer: image: edoburu/pgbouncer:latest @@ -522,10 +522,10 @@ services: deploy: resources: limits: - cpus: '1' + cpus: '0.5' memory: 256M reservations: - cpus: '0.5' + cpus: '0.25' memory: 128M ############################################################################### # TEST MCP SERVER - Fast Time Server @@ -560,11 +560,11 @@ services: deploy: resources: limits: - cpus: '4' - memory: 2G - reservations: cpus: '1' memory: 512M + reservations: + cpus: '0.5' + memory: 256M profiles: ["testing", "monitoring", "sso"] ############################################################################### From ae805bbc08088f81ea99c22bc4263ffea8aafc33 Mon Sep 17 00:00:00 2001 From: Lang-Akshay Date: Fri, 24 Jul 2026 15:22:56 +0100 Subject: [PATCH 4/6] feat: documented testing stack steps from create data-plane image to running entire tech stack Signed-off-by: Lang-Akshay --- README-TESTING.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 README-TESTING.md diff --git a/README-TESTING.md b/README-TESTING.md new file mode 100644 index 0000000..b68ab31 --- /dev/null +++ b/README-TESTING.md @@ -0,0 +1,121 @@ +# Testing the Full Stack Locally (Docker) + +Step-by-step process to build the dataplane image, bring up the full +control-plane + dataplane stack via `make`, and drive an MCP session through +it against the bundled `fast_time_server` test backend. + +See [`docs/book/src/testing.md`](docs/book/src/testing.md) for the +workspace/unit-test and `cf-integration` harness instead — this doc covers the +local `docker/docker-compose.yml` stack only. + +## 1. Build the dataplane image + +```bash +make docker-prod +``` + +Builds `dataplane:latest` from `docker/Dockerfile`. `testing-up` refuses to +start if this image doesn't exist yet. + +## 2. Launch the test stack + +```bash +make testing-up +``` + +Starts: `nginx`, `control-plane`, `redis`, `postgres`, `pgbouncer`, +`data-plane`, `fast_time_server`, `register_fast_time` (see `Makefile`). + +Resource budget for this stack (`docker/docker-compose.yml` `deploy.resources`): + +| Service | CPU Limit | Mem Limit | +| ---------------- | --------- | --------- | +| nginx | 0.5 | 256 M | +| control-plane | 2 | 2 G | +| redis | 1 | 1 G | +| postgres | 2 | 2 G | +| pgbouncer | 0.5 | 256 M | +| data-plane | 2 | 2 G | +| fast_time_server | 1 | 512 M | +| **Total** | **9 cores** | **8 G** | + +`register_fast_time` is a one-shot init container (no steady-state resources): +it logs into control-plane, registers `fast_time_server` as an upstream +gateway, and creates a virtual server with a fixed id +(`b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8`) exposing all of its tools. + +Watch it finish: + +```bash +docker compose -f docker/docker-compose.yml logs -f register_fast_time +``` + +Look for `Fast Time Server registration complete!`. + +## 3. Wait for the config to reach the dataplane + +The control plane doesn't talk to the dataplane directly — it publishes +`UserConfig` snapshots into Redis (`DATAPLANE_PUBLISHER=true`), and the +dataplane reads/caches from Redis. Worst-case propagation delay: + +```text +publisher interval + dataplane user-config cache expiry +``` + +Both default to ~60s in this stack (see +`docs/book/src/control-plane-integration.md`). After +`register_fast_time` finishes, give it up to a minute before the virtual host +resolves on the dataplane side. + +## 4. Get a bearer token + +Data-plane can self-issue a test JWT (RS256, signed with the same +`assets/jwt.key` the control plane also signs with) — no need to shell into +any container: + +```bash +TOKEN=$(curl --silent --show-error --request GET \ + --url http://localhost:8080/contextforge-rs/admin/tokens/admin@example.com \ + --header 'accept: application/json') +``` + +Use `admin@example.com` — that's `PLATFORM_ADMIN_EMAIL`, the identity +`register_fast_time` used, so it's the subject the publisher's `UserConfig` +is keyed under. Token is valid for 1 hour. + +## 5. Point mcp-inspector at it + +```bash +npx @modelcontextprotocol/inspector +``` + +| Field | Value | +| ----------- | ----- | +| URL | `http://localhost:8080/contextforge-rs/servers/b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8/mcp` | +| Transport | Streamable HTTP | +| Auth token | `$TOKEN` from step 4 | + +Note the `/contextforge-rs` prefix — that's what routes to the dataplane via +nginx (`docker/nginx.conf`, `location ^~ /contextforge-rs`). Without it, +the request goes to control-plane instead and you'll get a +`{"detail": "..."}`-shaped error from mcpgateway, not the dataplane. + +Run `tools/list` — you should get back the `fast_time_server` tools +(prefixed with the gateway name, e.g. `fast_time-`). + +## 6. Tear down + +```bash +make testing-down +``` + +Stops the stack (containers/volumes kept; rerun `testing-up` to resume). + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `403 {"detail":"Token is invalid: User is no longer a member of the associated team"}` | Hit control-plane instead of dataplane — URL missing `/contextforge-rs` prefix. | Use `/contextforge-rs/servers//mcp`. | +| `400 "Problem occurred retrieving the configuration"` | Dataplane has no `UserConfig` yet for the token's subject (`register_fast_time`/publisher hasn't run or synced). | Re-check step 3; confirm `register_fast_time` logs completed successfully and wait out the publisher interval. | +| `tools/list` returns `{"tools": [], "resultType": "complete"}` | Dataplane resolved the `UserConfig` fine, but couldn't connect to a declared backend. | `docker compose logs data-plane \| grep -i "worker quit\|BadScheme\|backend"`. A `BadScheme` error means `CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE` isn't set to `plain-text-or-tls` for a plain-`http://` backend. | +| `401` on every request | Bad/expired token, or control-plane and dataplane are signing/verifying with different keys or algorithms. | Confirm both sides have matching `JWT_ALGORITHM`/key config — see `docs/book/src/control-plane-integration.md`. | From 6a7e82793880013aefe37060285cd72664b16f62 Mon Sep 17 00:00:00 2001 From: Lang-Akshay Date: Fri, 24 Jul 2026 16:44:00 +0100 Subject: [PATCH 5/6] feat: implemented recomended updates to include m1, m2, p1 and p2 findings Signed-off-by: Lang-Akshay --- docker/Dockerfile | 12 +++++------- docker/docker-compose.yml | 3 ++- docker/nginx.conf | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a895486..db21aa6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,14 +1,12 @@ FROM rust:1.96.1 AS builder -WORKDIR /tmp/ +WORKDIR /app RUN < Date: Fri, 24 Jul 2026 17:00:04 +0100 Subject: [PATCH 6/6] feat: updated the documentation for make testing-up stack Signed-off-by: Lang-Akshay --- docs/book/src/SUMMARY.md | 1 + .../book/src/local-docker-stack.md | 44 ++++++++----------- 2 files changed, 19 insertions(+), 26 deletions(-) rename README-TESTING.md => docs/book/src/local-docker-stack.md (77%) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 5b42dca..a382a3f 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -23,6 +23,7 @@ - [Telemetry And Diagnostics](telemetry-and-diagnostics.md) - [Failure Modes](failure-modes.md) - [Testing](testing.md) + - [Local Docker Stack](local-docker-stack.md) - [Performance](performance.md) - [Deployment Notes](deployment-notes.md) - [🛠️ Project](project.md) diff --git a/README-TESTING.md b/docs/book/src/local-docker-stack.md similarity index 77% rename from README-TESTING.md rename to docs/book/src/local-docker-stack.md index b68ab31..06f9de1 100644 --- a/README-TESTING.md +++ b/docs/book/src/local-docker-stack.md @@ -1,29 +1,21 @@ -# Testing the Full Stack Locally (Docker) +# Local Docker Stack -Step-by-step process to build the dataplane image, bring up the full -control-plane + dataplane stack via `make`, and drive an MCP session through -it against the bundled `fast_time_server` test backend. +`docker/docker-compose.yml` brings up the full control-plane + dataplane +stack locally and drives an MCP session through it against the bundled +`fast_time_server` test backend. This is separate from the `cf-integration` +harness (see [Testing](testing.md)) — use it for a quick local smoke test of +the built dataplane image. -See [`docs/book/src/testing.md`](docs/book/src/testing.md) for the -workspace/unit-test and `cf-integration` harness instead — this doc covers the -local `docker/docker-compose.yml` stack only. - -## 1. Build the dataplane image +## Quick Start ```bash make docker-prod -``` - -Builds `dataplane:latest` from `docker/Dockerfile`. `testing-up` refuses to -start if this image doesn't exist yet. - -## 2. Launch the test stack - -```bash make testing-up ``` -Starts: `nginx`, `control-plane`, `redis`, `postgres`, `pgbouncer`, +`make docker-prod` builds `dataplane:latest` from `docker/Dockerfile`; +`testing-up` refuses to start if this image doesn't exist yet. `testing-up` +starts: `nginx`, `control-plane`, `redis`, `postgres`, `pgbouncer`, `data-plane`, `fast_time_server`, `register_fast_time` (see `Makefile`). Resource budget for this stack (`docker/docker-compose.yml` `deploy.resources`): @@ -52,7 +44,7 @@ docker compose -f docker/docker-compose.yml logs -f register_fast_time Look for `Fast Time Server registration complete!`. -## 3. Wait for the config to reach the dataplane +## Config Propagation The control plane doesn't talk to the dataplane directly — it publishes `UserConfig` snapshots into Redis (`DATAPLANE_PUBLISHER=true`), and the @@ -63,11 +55,11 @@ publisher interval + dataplane user-config cache expiry ``` Both default to ~60s in this stack (see -`docs/book/src/control-plane-integration.md`). After +[Control-Plane Integration](control-plane-integration.md)). After `register_fast_time` finishes, give it up to a minute before the virtual host resolves on the dataplane side. -## 4. Get a bearer token +## Get A Bearer Token Data-plane can self-issue a test JWT (RS256, signed with the same `assets/jwt.key` the control plane also signs with) — no need to shell into @@ -83,7 +75,7 @@ Use `admin@example.com` — that's `PLATFORM_ADMIN_EMAIL`, the identity `register_fast_time` used, so it's the subject the publisher's `UserConfig` is keyed under. Token is valid for 1 hour. -## 5. Point mcp-inspector at it +## Point mcp-inspector At It ```bash npx @modelcontextprotocol/inspector @@ -93,7 +85,7 @@ npx @modelcontextprotocol/inspector | ----------- | ----- | | URL | `http://localhost:8080/contextforge-rs/servers/b8e3f1a2c4d5e6f7a1b2c3d4e5f6a7b8/mcp` | | Transport | Streamable HTTP | -| Auth token | `$TOKEN` from step 4 | +| Auth token | `$TOKEN` from the previous step | Note the `/contextforge-rs` prefix — that's what routes to the dataplane via nginx (`docker/nginx.conf`, `location ^~ /contextforge-rs`). Without it, @@ -103,7 +95,7 @@ the request goes to control-plane instead and you'll get a Run `tools/list` — you should get back the `fast_time_server` tools (prefixed with the gateway name, e.g. `fast_time-`). -## 6. Tear down +## Tear Down ```bash make testing-down @@ -116,6 +108,6 @@ Stops the stack (containers/volumes kept; rerun `testing-up` to resume). | Symptom | Cause | Fix | | --- | --- | --- | | `403 {"detail":"Token is invalid: User is no longer a member of the associated team"}` | Hit control-plane instead of dataplane — URL missing `/contextforge-rs` prefix. | Use `/contextforge-rs/servers//mcp`. | -| `400 "Problem occurred retrieving the configuration"` | Dataplane has no `UserConfig` yet for the token's subject (`register_fast_time`/publisher hasn't run or synced). | Re-check step 3; confirm `register_fast_time` logs completed successfully and wait out the publisher interval. | +| `400 "Problem occurred retrieving the configuration"` | Dataplane has no `UserConfig` yet for the token's subject (`register_fast_time`/publisher hasn't run or synced). | Re-check config propagation above; confirm `register_fast_time` logs completed successfully and wait out the publisher interval. | | `tools/list` returns `{"tools": [], "resultType": "complete"}` | Dataplane resolved the `UserConfig` fine, but couldn't connect to a declared backend. | `docker compose logs data-plane \| grep -i "worker quit\|BadScheme\|backend"`. A `BadScheme` error means `CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE` isn't set to `plain-text-or-tls` for a plain-`http://` backend. | -| `401` on every request | Bad/expired token, or control-plane and dataplane are signing/verifying with different keys or algorithms. | Confirm both sides have matching `JWT_ALGORITHM`/key config — see `docs/book/src/control-plane-integration.md`. | +| `401` on every request | Bad/expired token, or control-plane and dataplane are signing/verifying with different keys or algorithms. | Confirm both sides have matching `JWT_ALGORITHM`/key config — see [Control-Plane Integration](control-plane-integration.md). |