From 6ee121acdae6bcd72f35ee605e3912ccacb86ffa Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 11:16:04 +0100 Subject: [PATCH 01/11] Add live MCP conformance CI Signed-off-by: lucarlig --- .github/conformance/docker-compose.yml | 113 +++++++++ .github/conformance/expected-failures.yml | 45 ++++ .github/conformance/nginx.conf.template | 65 +++++ .github/workflows/mcp_conformance.yml | 275 ++++++++++++++++++++++ _context/wiki/testing.md | 37 +++ 5 files changed, 535 insertions(+) create mode 100644 .github/conformance/docker-compose.yml create mode 100644 .github/conformance/expected-failures.yml create mode 100644 .github/conformance/nginx.conf.template create mode 100644 .github/workflows/mcp_conformance.yml diff --git a/.github/conformance/docker-compose.yml b/.github/conformance/docker-compose.yml new file mode 100644 index 00000000..1da8cf69 --- /dev/null +++ b/.github/conformance/docker-compose.yml @@ -0,0 +1,113 @@ +name: contextforge-conformance + +services: + redis: + image: redis:latest + command: [redis-server, --save, "", --appendonly, "no"] + networks: [contextforge] + healthcheck: + test: [CMD, redis-cli, ping] + interval: 2s + timeout: 2s + retries: 30 + + control-plane: + image: ${CF_CONTROLPLANE_IMAGE:-ghcr.io/ibm/mcp-context-forge:latest} + ports: + - "127.0.0.1:4444:4444" + networks: [contextforge] + extra_hosts: + - host.docker.internal:host-gateway + environment: + HOST: 0.0.0.0 + PORT: "4444" + DATABASE_URL: sqlite:////tmp/contextforge-conformance.db + CACHE_TYPE: redis + REDIS_URL: redis://redis:6379/0 + JWT_SECRET_KEY: contextforge-conformance-jwt-secret-at-least-32-bytes + JWT_ALGORITHM: RS256 + JWT_PUBLIC_KEY_PATH: /keys/jwt.key.pub + JWT_PRIVATE_KEY_PATH: /keys/jwt.key + AUTH_ENCRYPTION_SECRET: contextforge-conformance-encryption-secret-at-least-32-bytes + AUTH_REQUIRED: "true" + BASIC_AUTH_USER: admin + BASIC_AUTH_PASSWORD: conformance-admin-password + PLATFORM_ADMIN_EMAIL: admin@example.com + PLATFORM_ADMIN_PASSWORD: conformance-admin-password + MCPGATEWAY_SKIP_MIGRATIONS: "false" + MCPGATEWAY_UI_ENABLED: "false" + MCPGATEWAY_ADMIN_API_ENABLED: "true" + SECURE_COOKIES: "false" + SSRF_ALLOW_PRIVATE_NETWORKS: "true" + DATAPLANE_PUBLISHER: "true" + DATAPLANE_PUBLISHER_INTERVAL_SECONDS: "2" + PASSWORD_CHANGE_ENFORCEMENT_ENABLED: "false" + ADMIN_REQUIRE_PASSWORD_CHANGE_ON_BOOTSTRAP: "false" + REQUIRE_PASSWORD_CHANGE_FOR_DEFAULT_PASSWORD: "false" + GATEWAY_TOOL_NAME_SEPARATOR: _ + GUNICORN_WORKERS: "1" + LOG_LEVEL: INFO + volumes: + - ../../assets:/keys:ro + depends_on: + redis: + condition: service_healthy + healthcheck: + test: [CMD, curl, --fail, http://127.0.0.1:4444/health] + interval: 5s + timeout: 5s + retries: 60 + start_period: 10s + + data-plane: + image: ${CF_DATAPLANE_IMAGE:-ghcr.io/contextforge-org/contextforge-data-plane:latest} + platform: linux/amd64 + networks: [contextforge] + extra_hosts: + - host.docker.internal:host-gateway + environment: + CONTEXTFORGE_DATA_PLANE_ADDRESS: 0.0.0.0:4445 + CONTEXTFORGE_DATA_PLANE_REDIS_HOSTNAME: redis + CONTEXTFORGE_DATA_PLANE_REDIS_PORT: "6379" + CONTEXTFORGE_DATA_PLANE_REDIS_CONNECTION_MODE: plain-text + CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE: plain-text-or-tls + CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PUBLIC_KEY: /keys/jwt.key.pub + CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY: /keys/jwt.key + CONTEXTFORGE_DATA_PLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS: "0" + CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS: >- + 127.0.0.1:${MCP_CONFORMANCE_PORT:-8080},localhost:${MCP_CONFORMANCE_PORT:-8080} + CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_ORIGINS: >- + http://127.0.0.1:${MCP_CONFORMANCE_PORT:-8080},http://localhost:${MCP_CONFORMANCE_PORT:-8080} + RUST_LOG: info + volumes: + - ../../assets:/keys:ro + depends_on: + redis: + condition: service_healthy + + nginx: + image: nginx:latest + ports: + - "127.0.0.1:${MCP_CONFORMANCE_PORT:-8080}:80" + networks: [contextforge] + environment: + MCP_CONFORMANCE_TOKEN: ${MCP_CONFORMANCE_TOKEN:-missing} + NGINX_ENVSUBST_TEMPLATE_DIR: /templates + NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template + NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx + volumes: + - ./nginx.conf.template:/templates/nginx.conf.template:ro + depends_on: + control-plane: + condition: service_healthy + data-plane: + condition: service_started + healthcheck: + test: [CMD, curl, --fail, http://127.0.0.1/health] + interval: 2s + timeout: 2s + retries: 30 + +networks: + contextforge: + driver: bridge diff --git a/.github/conformance/expected-failures.yml b/.github/conformance/expected-failures.yml new file mode 100644 index 00000000..4f5007b0 --- /dev/null +++ b/.github/conformance/expected-failures.yml @@ -0,0 +1,45 @@ +server: + - server-stateless:sep-2575-server-declares-prompts-in-discover + - server-stateless:sep-2575-discover-capabilities-match-handlers + - server-stateless:sep-2575-server-rejects-undeclared-capability + - server-stateless:sep-2575-missing-capability-http-400 + - server-stateless:sep-2575-http-server-no-independent-requests-on-stream + - server-stateless:sep-2575-server-no-log-without-loglevel + - completion-complete:completion-complete + - tools-list:tools-list + - tools-call-simple-text:tools-call-simple-text + - tools-call-image:tools-call-image + - tools-call-audio:tools-call-audio + - tools-call-embedded-resource:tools-call-embedded-resource + - tools-call-mixed-content:tools-call-mixed-content + - tools-call-error:tools-call-error + - tools-call-with-progress:tools-call-with-progress + - resources-list:resources-list + - resources-read-text:resources-read-text + - resources-read-binary:resources-read-binary + - resources-templates-read:resources-templates-read + - sep-2164-resource-not-found:sep-2164-error-code + - sep-2164-resource-not-found:sep-2164-data-uri + - prompts-list:prompts-list + - prompts-get-simple:prompts-get-simple + - prompts-get-with-args:prompts-get-with-args + - prompts-get-embedded-resource:prompts-get-embedded-resource + - prompts-get-with-image:prompts-get-with-image + - caching:sep-2549-tools-list-caching-hints + - caching:sep-2549-prompts-list-caching-hints + - caching:sep-2549-resources-list-caching-hints + - caching:sep-2549-resources-templates-list-caching-hints + - caching:sep-2549-ttl-non-negative + - caching:sep-2549-cache-scope-valid + - input-required-result-basic-elicitation:sep-2322-elicitation-incomplete + - input-required-result-basic-sampling:sep-2322-sampling-incomplete + - input-required-result-basic-list-roots:sep-2322-list-roots-incomplete + - input-required-result-request-state:sep-2322-request-state-incomplete + - input-required-result-multiple-input-requests:sep-2322-multiple-inputs-incomplete + - input-required-result-multi-round:sep-2322-multi-round-r1 + - input-required-result-missing-input-response:sep-2322-missing-response-rerequests + - input-required-result-non-tool-request:sep-2322-non-tool-incomplete + - input-required-result-result-type:sep-2322-result-type-included + - input-required-result-tampered-state:sep-2322-reject-tampered-state + - input-required-result-capability-check:sep-2322-respect-client-capabilities + - input-required-result-ignore-extra-params:sep-2322-ignore-unexpected-params diff --git a/.github/conformance/nginx.conf.template b/.github/conformance/nginx.conf.template new file mode 100644 index 00000000..404e8d17 --- /dev/null +++ b/.github/conformance/nginx.conf.template @@ -0,0 +1,65 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /var/log/nginx/access.log; + resolver 127.0.0.11 valid=5s ipv6=off; + + map $http_authorization $conformance_authorization { + default $http_authorization; + "" "Bearer ${MCP_CONFORMANCE_TOKEN}"; + } + + server { + listen 80 default_server; + server_name _; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Authorization $conformance_authorization; + proxy_set_header Mcp-Session-Id $http_mcp_session_id; + proxy_set_header Mcp-Protocol-Version $http_mcp_protocol_version; + proxy_set_header Origin $http_origin; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header Connection ""; + + location ~ ^/servers/([^/]+)/mcp/?$ { + set $dataplane http://data-plane:4445; + proxy_pass $dataplane/contextforge-rs/servers/$1/mcp$is_args$args; + proxy_request_buffering on; + proxy_buffering off; + proxy_cache off; + proxy_connect_timeout 30s; + proxy_send_timeout 1h; + proxy_read_timeout 1h; + add_header X-Accel-Buffering "no" always; + add_header X-CF-Conformance-Backend dataplane always; + } + + location = /health { + set $control_plane http://control-plane:4444; + proxy_pass $control_plane/health; + proxy_connect_timeout 5s; + proxy_read_timeout 5s; + } + + location / { + set $control_plane http://control-plane:4444; + proxy_pass $control_plane; + proxy_buffering off; + proxy_connect_timeout 30s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + } + } +} diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml new file mode 100644 index 00000000..c46fec47 --- /dev/null +++ b/.github/workflows/mcp_conformance.yml @@ -0,0 +1,275 @@ +name: MCP Conformance + +on: + workflow_dispatch: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: mcp-conformance-${{ github.ref }} + cancel-in-progress: true + +env: + MCP_CONFORMANCE_VERSION: 0.2.0-alpha.11 + MCP_CONFORMANCE_SOURCE_SHA: c321dd32035556e6769d3724a8ee97d87c3faaac + MCP_CONFORMANCE_SPEC_VERSION: 2026-07-28 + MCP_CONFORMANCE_SERVER_ID: 3f33286667d34b65a31c3bafd30e4c21 + CF_CONTROLPLANE_IMAGE: ghcr.io/ibm/mcp-context-forge:latest + CF_DATAPLANE_IMAGE: ghcr.io/contextforge-org/contextforge-data-plane:latest + +jobs: + conformance: + name: MCP 2026-07-28 through nginx and dataplane + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Check out data plane + uses: actions/checkout@v6.0.2 + + - name: Check out the alpha.11 conformance fixture + uses: actions/checkout@v6.0.2 + with: + repository: modelcontextprotocol/conformance + ref: ${{ env.MCP_CONFORMANCE_SOURCE_SHA }} + path: .conformance-suite + persist-credentials: false + + - name: Install Node.js 22 + uses: actions/setup-node@v6.5.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: .conformance-suite/package-lock.json + + - name: Install official conformance dependencies + working-directory: .conformance-suite + run: | + test "$(node -p "require('./package.json').version")" = "${MCP_CONFORMANCE_VERSION}" + npm ci --ignore-scripts + + - name: Pull the latest ContextForge images + run: >- + docker compose -f .github/conformance/docker-compose.yml + pull redis control-plane data-plane nginx + + - name: Start the fixture and control plane + run: | + mkdir -p conformance-logs conformance-results + + ( + cd .conformance-suite + PORT=3000 npm exec -- tsx examples/servers/typescript/everything-server.ts + ) > conformance-logs/reference-server.log 2>&1 & + echo "$!" > conformance-logs/reference-server.pid + + docker compose -f .github/conformance/docker-compose.yml \ + up -d --wait redis control-plane + + for _ in $(seq 1 120); do + if curl --silent --output /dev/null http://127.0.0.1:3000/mcp; then + exit 0 + fi + sleep 0.25 + done + echo "Timed out waiting for the official conformance fixture" >&2 + exit 1 + + - name: Register the official fixture through the control plane + run: | + bootstrap_token="$({ + docker compose -f .github/conformance/docker-compose.yml exec -T control-plane \ + python3 -m mcpgateway.utils.create_jwt_token \ + --username admin@example.com --admin --exp 120 + } 2>/dev/null | tail -n 1)" + test -n "${bootstrap_token}" + echo "::add-mask::${bootstrap_token}" + + api_request() { + local method="$1" + local path="$2" + local body="${3-}" + local args=( + --silent --show-error --fail-with-body + --request "${method}" + --header "Authorization: Bearer ${bootstrap_token}" + --header "Content-Type: application/json" + "http://127.0.0.1:4444${path}" + ) + if [ -n "${body}" ]; then + args+=(--data "${body}") + fi + curl "${args[@]}" + } + + gateway="$(api_request POST /gateways '{ + "name": "_", + "url": "http://host.docker.internal:3000/mcp", + "transport": "STREAMABLEHTTP", + "authType": "authheaders", + "authHeaders": [{"key": "Host", "value": "localhost:3000"}], + "description": "Official MCP alpha.11 conformance fixture" + }')" + gateway_id="$(jq --exit-status --raw-output '.id' <<< "${gateway}")" + + api_request POST \ + "/gateways/${gateway_id}/tools/refresh?include_resources=true&include_prompts=true" \ + '{}' > /dev/null + + tool_ids='[]' + resource_ids='[]' + prompt_ids='[]' + for _ in $(seq 1 120); do + tools="$(api_request GET /tools)" + resources="$(api_request GET /resources)" + prompts="$(api_request GET /prompts)" + + tool_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${tools}")" + resource_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${resources}")" + prompt_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${prompts}")" + + has_tool="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_text")] | length' \ + <<< "${tools}")" + has_resource="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .uri == "test://static-text")] | length' \ + <<< "${resources}")" + has_prompt="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_prompt")] | length' \ + <<< "${prompts}")" + if [ "${has_tool}" -gt 0 ] && [ "${has_resource}" -gt 0 ] && [ "${has_prompt}" -gt 0 ]; then + break + fi + sleep 0.5 + done + test "${has_tool}" -gt 0 + test "${has_resource}" -gt 0 + test "${has_prompt}" -gt 0 + + server_payload="$(jq --null-input --compact-output \ + --arg id "${MCP_CONFORMANCE_SERVER_ID}" \ + --argjson tools "${tool_ids}" \ + --argjson resources "${resource_ids}" \ + --argjson prompts "${prompt_ids}" \ + '{server: { + id: $id, + name: "Official MCP Conformance Server", + description: "Virtual server for alpha.11 conformance", + associated_tools: $tools, + associated_resources: $resources, + associated_prompts: $prompts + }}')" + api_request POST /servers "${server_payload}" > /dev/null + + token_response="$(api_request POST /v1/tokens '{ + "name": "MCP conformance CI", + "description": "Ephemeral dataplane token", + "expires_in_days": 1, + "user_email": "admin@example.com" + }')" + conformance_token="$(jq --exit-status --raw-output '.access_token' <<< "${token_response}")" + echo "::add-mask::${conformance_token}" + echo "MCP_CONFORMANCE_TOKEN=${conformance_token}" >> "${GITHUB_ENV}" + + - name: Start dataplane and nginx + run: | + docker compose -f .github/conformance/docker-compose.yml \ + up -d --wait data-plane nginx + + endpoint="http://127.0.0.1:8080/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" + request='{ + "jsonrpc": "2.0", + "id": 1, + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ci-route-probe", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } + }' + for _ in $(seq 1 120); do + curl --silent --show-error \ + --dump-header conformance-logs/route-probe-headers.txt \ + --output conformance-logs/route-probe-body.txt \ + --request POST \ + --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 "${request}" \ + "${endpoint}" || true + if grep --ignore-case --quiet '^X-CF-Conformance-Backend: dataplane' \ + conformance-logs/route-probe-headers.txt \ + && sed -n 's/^data: //p' conformance-logs/route-probe-body.txt \ + | jq --exit-status \ + '.result.supportedVersions | index("2026-07-28") != null' \ + > /dev/null 2>&1; then + exit 0 + fi + sleep 0.5 + done + echo "Modern MCP route did not reach the dataplane through nginx" >&2 + cat conformance-logs/route-probe-headers.txt >&2 + cat conformance-logs/route-probe-body.txt >&2 + exit 1 + + - name: Run MCP 2026-07-28 requirements + id: runner + run: | + set +e + ( + cd .conformance-suite + npm start -- \ + server \ + --url "http://127.0.0.1:8080/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" \ + --requirements "${MCP_CONFORMANCE_SPEC_VERSION}" \ + --expected-failures ../.github/conformance/expected-failures.yml \ + --output-dir ../conformance-results + ) 2>&1 | tee conformance-logs/runner.log + runner_status="${PIPESTATUS[0]}" + set -e + echo "status=${runner_status}" >> "${GITHUB_OUTPUT}" + + - name: Collect stack logs + if: always() + run: >- + docker compose -f .github/conformance/docker-compose.yml + logs --no-color > conformance-logs/stack.log 2>&1 || true + + - name: Upload conformance results + if: always() + uses: actions/upload-artifact@v6.0.0 + with: + name: mcp-conformance-results + path: | + conformance-results/ + conformance-logs/ + if-no-files-found: warn + retention-days: 14 + + - name: Stop the live stack + if: always() + run: | + if [ -f conformance-logs/reference-server.pid ]; then + kill "$(cat conformance-logs/reference-server.pid)" 2>/dev/null || true + fi + docker compose -f .github/conformance/docker-compose.yml \ + down --volumes --remove-orphans + + - name: Enforce conformance baseline + if: always() + env: + RUNNER_STATUS: ${{ steps.runner.outputs.status }} + run: test "${RUNNER_STATUS}" = "0" diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index 8d16a434..e55fb998 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -34,6 +34,43 @@ Protocol tests and fixtures should target MCP `2026-07-28`, use `server/discover These run in `cargo nextest run` with no Docker dependencies. +## MCP Conformance CI + +`.github/workflows/mcp_conformance.yml` runs the official +`@modelcontextprotocol/conformance@0.2.0-alpha.11` server requirements for MCP +`2026-07-28`. It uses `--requirements 2026-07-28`, rather than a moving suite, +so both the scenario set and the stateless wire version are fixed to that +specification revision. + +The job launches a deliberately small live stack: Redis, the published +`ghcr.io/ibm/mcp-context-forge:latest` control-plane image, the published +`ghcr.io/contextforge-org/contextforge-data-plane:latest` dataplane image, and +nginx. The control plane uses an ephemeral SQLite database, so PostgreSQL and +PgBouncer are not needed in this conformance lane. + +The official TypeScript everything server runs at the npm release's source +SHA and is registered through the real control-plane API. The control-plane +publisher writes the virtual-server snapshot to Redis. The official runner +then targets nginx at `/servers/{virtual_host_id}/mcp`; nginx rewrites that +modern route to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the +dataplane. A route probe requires the dataplane response marker before the +suite starts, so the test cannot silently fall back to the control plane. + +The conformance CLI has no bearer-header option. Nginx therefore adds a real +control-plane test token only when the runner did not supply one. There is no +separate authentication proxy or repository-owned JavaScript helper. + +This exercises nginx routing, dataplane authentication, Redis config lookup, +control-plane publication, virtual-host lookup, and upstream gateway routing. +Because both ContextForge images use the mutable `latest` tag, the job tests +the currently published images rather than the pull-request commit. + +The job prints the official runner's pass and failure details directly in its +GitHub Actions log and writes the raw results and process logs to the +`mcp-conformance-results` artifact. Known failures live in +`.github/conformance/expected-failures.yml`; a new failure or a now-stale +baseline entry fails CI. + ## Full-Stack Integration Harness [`cf-integration`](https://github.com/contextforge-org/contextforge-dev-tools) wires the external ContextForge control plane to this dataplane the way production intends: the stock upstream Compose stack, plus exactly two intentional differences — nginx routes only `/servers/{virtual_host_id}/mcp` to the dataplane (as `/contextforge-rs/servers/{virtual_host_id}/mcp`), and the control plane runs with `DATAPLANE_PUBLISHER=true` so virtual server configs reach the dataplane through Redis. From 3dd0371162cd325583074377250bae430f67e71a Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 11:34:27 +0100 Subject: [PATCH 02/11] Refactor conformance workflow scripts Signed-off-by: lucarlig --- .github/conformance/register-fixture.sh | 112 +++++++++++ .github/conformance/run-conformance.sh | 26 +++ .../conformance/start-dataplane-and-nginx.sh | 56 ++++++ .../start-fixture-and-control-plane.sh | 28 +++ .github/conformance/stop-live-stack.sh | 13 ++ .github/workflows/mcp_conformance.yml | 187 +----------------- _context/wiki/testing.md | 4 +- 7 files changed, 243 insertions(+), 183 deletions(-) create mode 100755 .github/conformance/register-fixture.sh create mode 100755 .github/conformance/run-conformance.sh create mode 100755 .github/conformance/start-dataplane-and-nginx.sh create mode 100755 .github/conformance/start-fixture-and-control-plane.sh create mode 100755 .github/conformance/stop-live-stack.sh diff --git a/.github/conformance/register-fixture.sh b/.github/conformance/register-fixture.sh new file mode 100755 index 00000000..ffae024f --- /dev/null +++ b/.github/conformance/register-fixture.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_ENV:?GITHUB_ENV must be set}" +: "${MCP_CONFORMANCE_SERVER_ID:?MCP_CONFORMANCE_SERVER_ID must be set}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +compose_file="${script_dir}/docker-compose.yml" + +bootstrap_token="$({ + docker compose -f "${compose_file}" exec -T control-plane \ + python3 -m mcpgateway.utils.create_jwt_token \ + --username admin@example.com --admin --exp 120 +} 2>/dev/null | tail -n 1)" +test -n "${bootstrap_token}" +if [ "${GITHUB_ACTIONS:-}" = "true" ]; then + echo "::add-mask::${bootstrap_token}" +fi + +api_request() { + local method="$1" + local path="$2" + local body="${3-}" + local args=( + --silent --show-error --fail-with-body + --request "${method}" + --header "Authorization: Bearer ${bootstrap_token}" + --header "Content-Type: application/json" + "http://127.0.0.1:4444${path}" + ) + if [ -n "${body}" ]; then + args+=(--data "${body}") + fi + curl "${args[@]}" +} + +gateway="$(api_request POST /gateways '{ + "name": "_", + "url": "http://host.docker.internal:3000/mcp", + "transport": "STREAMABLEHTTP", + "authType": "authheaders", + "authHeaders": [{"key": "Host", "value": "localhost:3000"}], + "description": "Official MCP alpha.11 conformance fixture" +}')" +gateway_id="$(jq --exit-status --raw-output '.id' <<< "${gateway}")" + +api_request POST \ + "/gateways/${gateway_id}/tools/refresh?include_resources=true&include_prompts=true" \ + '{}' > /dev/null + +tool_ids='[]' +resource_ids='[]' +prompt_ids='[]' +has_tool=0 +has_resource=0 +has_prompt=0 +for _ in $(seq 1 120); do + tools="$(api_request GET /tools)" + resources="$(api_request GET /resources)" + prompts="$(api_request GET /prompts)" + + tool_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${tools}")" + resource_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${resources}")" + prompt_ids="$(jq --compact-output --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${prompts}")" + + has_tool="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_text")] | length' \ + <<< "${tools}")" + has_resource="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .uri == "test://static-text")] | length' \ + <<< "${resources}")" + has_prompt="$(jq --arg id "${gateway_id}" \ + '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_prompt")] | length' \ + <<< "${prompts}")" + if [ "${has_tool}" -gt 0 ] && [ "${has_resource}" -gt 0 ] && [ "${has_prompt}" -gt 0 ]; then + break + fi + sleep 0.5 +done +test "${has_tool}" -gt 0 +test "${has_resource}" -gt 0 +test "${has_prompt}" -gt 0 + +server_payload="$(jq --null-input --compact-output \ + --arg id "${MCP_CONFORMANCE_SERVER_ID}" \ + --argjson tools "${tool_ids}" \ + --argjson resources "${resource_ids}" \ + --argjson prompts "${prompt_ids}" \ + '{server: { + id: $id, + name: "Official MCP Conformance Server", + description: "Virtual server for alpha.11 conformance", + associated_tools: $tools, + associated_resources: $resources, + associated_prompts: $prompts + }}')" +api_request POST /servers "${server_payload}" > /dev/null + +token_response="$(api_request POST /v1/tokens '{ + "name": "MCP conformance CI", + "description": "Ephemeral dataplane token", + "expires_in_days": 1, + "user_email": "admin@example.com" +}')" +conformance_token="$(jq --exit-status --raw-output '.access_token' <<< "${token_response}")" +if [ "${GITHUB_ACTIONS:-}" = "true" ]; then + echo "::add-mask::${conformance_token}" +fi +echo "MCP_CONFORMANCE_TOKEN=${conformance_token}" >> "${GITHUB_ENV}" diff --git a/.github/conformance/run-conformance.sh b/.github/conformance/run-conformance.sh new file mode 100755 index 00000000..010a9871 --- /dev/null +++ b/.github/conformance/run-conformance.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" +: "${MCP_CONFORMANCE_SERVER_ID:?MCP_CONFORMANCE_SERVER_ID must be set}" +: "${MCP_CONFORMANCE_SPEC_VERSION:?MCP_CONFORMANCE_SPEC_VERSION must be set}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +suite_dir="${MCP_CONFORMANCE_SUITE_DIR:-${repo_root}/.conformance-suite}" +conformance_port="${MCP_CONFORMANCE_PORT:-8080}" + +set +e +( + cd "${suite_dir}" + npm start -- \ + server \ + --url "http://127.0.0.1:${conformance_port}/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" \ + --requirements "${MCP_CONFORMANCE_SPEC_VERSION}" \ + --expected-failures "${script_dir}/expected-failures.yml" \ + --output-dir "${repo_root}/conformance-results" +) 2>&1 | tee "${repo_root}/conformance-logs/runner.log" +runner_status="${PIPESTATUS[0]}" +set -e + +echo "status=${runner_status}" >> "${GITHUB_OUTPUT}" diff --git a/.github/conformance/start-dataplane-and-nginx.sh b/.github/conformance/start-dataplane-and-nginx.sh new file mode 100755 index 00000000..c2a793e2 --- /dev/null +++ b/.github/conformance/start-dataplane-and-nginx.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${MCP_CONFORMANCE_SERVER_ID:?MCP_CONFORMANCE_SERVER_ID must be set}" + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +log_dir="${repo_root}/conformance-logs" +conformance_port="${MCP_CONFORMANCE_PORT:-8080}" + +docker compose -f "${script_dir}/docker-compose.yml" \ + up -d --wait data-plane nginx + +endpoint="http://127.0.0.1:${conformance_port}/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" +request='{ + "jsonrpc": "2.0", + "id": 1, + "method": "server/discover", + "params": { + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "ci-route-probe", + "version": "1.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": {} + } + } +}' + +for _ in $(seq 1 120); do + curl --silent --show-error \ + --dump-header "${log_dir}/route-probe-headers.txt" \ + --output "${log_dir}/route-probe-body.txt" \ + --request POST \ + --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 "${request}" \ + "${endpoint}" || true + if grep --ignore-case --quiet '^X-CF-Conformance-Backend: dataplane' \ + "${log_dir}/route-probe-headers.txt" \ + && sed -n 's/^data: //p' "${log_dir}/route-probe-body.txt" \ + | jq --exit-status \ + '.result.supportedVersions | index("2026-07-28") != null' \ + > /dev/null 2>&1; then + exit 0 + fi + sleep 0.5 +done + +echo "Modern MCP route did not reach the dataplane through nginx" >&2 +cat "${log_dir}/route-probe-headers.txt" >&2 +cat "${log_dir}/route-probe-body.txt" >&2 +exit 1 diff --git a/.github/conformance/start-fixture-and-control-plane.sh b/.github/conformance/start-fixture-and-control-plane.sh new file mode 100755 index 00000000..a255270a --- /dev/null +++ b/.github/conformance/start-fixture-and-control-plane.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +suite_dir="${MCP_CONFORMANCE_SUITE_DIR:-${repo_root}/.conformance-suite}" +log_dir="${repo_root}/conformance-logs" + +mkdir -p "${log_dir}" "${repo_root}/conformance-results" + +( + cd "${suite_dir}" + PORT=3000 npm exec -- tsx examples/servers/typescript/everything-server.ts +) > "${log_dir}/reference-server.log" 2>&1 & +echo "$!" > "${log_dir}/reference-server.pid" + +docker compose -f "${script_dir}/docker-compose.yml" \ + up -d --wait redis control-plane + +for _ in $(seq 1 120); do + if curl --silent --output /dev/null http://127.0.0.1:3000/mcp; then + exit 0 + fi + sleep 0.25 +done + +echo "Timed out waiting for the official conformance fixture" >&2 +exit 1 diff --git a/.github/conformance/stop-live-stack.sh b/.github/conformance/stop-live-stack.sh new file mode 100755 index 00000000..e88cf942 --- /dev/null +++ b/.github/conformance/stop-live-stack.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +pid_file="${repo_root}/conformance-logs/reference-server.pid" + +if [ -f "${pid_file}" ]; then + kill "$(cat "${pid_file}")" 2>/dev/null || true +fi + +docker compose -f "${script_dir}/docker-compose.yml" \ + down --volumes --remove-orphans diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index c46fec47..5ac2814c 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -58,189 +58,17 @@ jobs: pull redis control-plane data-plane nginx - name: Start the fixture and control plane - run: | - mkdir -p conformance-logs conformance-results - - ( - cd .conformance-suite - PORT=3000 npm exec -- tsx examples/servers/typescript/everything-server.ts - ) > conformance-logs/reference-server.log 2>&1 & - echo "$!" > conformance-logs/reference-server.pid - - docker compose -f .github/conformance/docker-compose.yml \ - up -d --wait redis control-plane - - for _ in $(seq 1 120); do - if curl --silent --output /dev/null http://127.0.0.1:3000/mcp; then - exit 0 - fi - sleep 0.25 - done - echo "Timed out waiting for the official conformance fixture" >&2 - exit 1 + run: .github/conformance/start-fixture-and-control-plane.sh - name: Register the official fixture through the control plane - run: | - bootstrap_token="$({ - docker compose -f .github/conformance/docker-compose.yml exec -T control-plane \ - python3 -m mcpgateway.utils.create_jwt_token \ - --username admin@example.com --admin --exp 120 - } 2>/dev/null | tail -n 1)" - test -n "${bootstrap_token}" - echo "::add-mask::${bootstrap_token}" - - api_request() { - local method="$1" - local path="$2" - local body="${3-}" - local args=( - --silent --show-error --fail-with-body - --request "${method}" - --header "Authorization: Bearer ${bootstrap_token}" - --header "Content-Type: application/json" - "http://127.0.0.1:4444${path}" - ) - if [ -n "${body}" ]; then - args+=(--data "${body}") - fi - curl "${args[@]}" - } - - gateway="$(api_request POST /gateways '{ - "name": "_", - "url": "http://host.docker.internal:3000/mcp", - "transport": "STREAMABLEHTTP", - "authType": "authheaders", - "authHeaders": [{"key": "Host", "value": "localhost:3000"}], - "description": "Official MCP alpha.11 conformance fixture" - }')" - gateway_id="$(jq --exit-status --raw-output '.id' <<< "${gateway}")" - - api_request POST \ - "/gateways/${gateway_id}/tools/refresh?include_resources=true&include_prompts=true" \ - '{}' > /dev/null - - tool_ids='[]' - resource_ids='[]' - prompt_ids='[]' - for _ in $(seq 1 120); do - tools="$(api_request GET /tools)" - resources="$(api_request GET /resources)" - prompts="$(api_request GET /prompts)" - - tool_ids="$(jq --compact-output --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${tools}")" - resource_ids="$(jq --compact-output --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${resources}")" - prompt_ids="$(jq --compact-output --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id) | .id]' <<< "${prompts}")" - - has_tool="$(jq --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_text")] | length' \ - <<< "${tools}")" - has_resource="$(jq --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id and .uri == "test://static-text")] | length' \ - <<< "${resources}")" - has_prompt="$(jq --arg id "${gateway_id}" \ - '[.[] | select((.gateway_id // .gatewayId) == $id and .name == "test_simple_prompt")] | length' \ - <<< "${prompts}")" - if [ "${has_tool}" -gt 0 ] && [ "${has_resource}" -gt 0 ] && [ "${has_prompt}" -gt 0 ]; then - break - fi - sleep 0.5 - done - test "${has_tool}" -gt 0 - test "${has_resource}" -gt 0 - test "${has_prompt}" -gt 0 - - server_payload="$(jq --null-input --compact-output \ - --arg id "${MCP_CONFORMANCE_SERVER_ID}" \ - --argjson tools "${tool_ids}" \ - --argjson resources "${resource_ids}" \ - --argjson prompts "${prompt_ids}" \ - '{server: { - id: $id, - name: "Official MCP Conformance Server", - description: "Virtual server for alpha.11 conformance", - associated_tools: $tools, - associated_resources: $resources, - associated_prompts: $prompts - }}')" - api_request POST /servers "${server_payload}" > /dev/null - - token_response="$(api_request POST /v1/tokens '{ - "name": "MCP conformance CI", - "description": "Ephemeral dataplane token", - "expires_in_days": 1, - "user_email": "admin@example.com" - }')" - conformance_token="$(jq --exit-status --raw-output '.access_token' <<< "${token_response}")" - echo "::add-mask::${conformance_token}" - echo "MCP_CONFORMANCE_TOKEN=${conformance_token}" >> "${GITHUB_ENV}" + run: .github/conformance/register-fixture.sh - name: Start dataplane and nginx - run: | - docker compose -f .github/conformance/docker-compose.yml \ - up -d --wait data-plane nginx - - endpoint="http://127.0.0.1:8080/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" - request='{ - "jsonrpc": "2.0", - "id": 1, - "method": "server/discover", - "params": { - "_meta": { - "io.modelcontextprotocol/protocolVersion": "2026-07-28", - "io.modelcontextprotocol/clientInfo": { - "name": "ci-route-probe", - "version": "1.0.0" - }, - "io.modelcontextprotocol/clientCapabilities": {} - } - } - }' - for _ in $(seq 1 120); do - curl --silent --show-error \ - --dump-header conformance-logs/route-probe-headers.txt \ - --output conformance-logs/route-probe-body.txt \ - --request POST \ - --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 "${request}" \ - "${endpoint}" || true - if grep --ignore-case --quiet '^X-CF-Conformance-Backend: dataplane' \ - conformance-logs/route-probe-headers.txt \ - && sed -n 's/^data: //p' conformance-logs/route-probe-body.txt \ - | jq --exit-status \ - '.result.supportedVersions | index("2026-07-28") != null' \ - > /dev/null 2>&1; then - exit 0 - fi - sleep 0.5 - done - echo "Modern MCP route did not reach the dataplane through nginx" >&2 - cat conformance-logs/route-probe-headers.txt >&2 - cat conformance-logs/route-probe-body.txt >&2 - exit 1 + run: .github/conformance/start-dataplane-and-nginx.sh - name: Run MCP 2026-07-28 requirements id: runner - run: | - set +e - ( - cd .conformance-suite - npm start -- \ - server \ - --url "http://127.0.0.1:8080/servers/${MCP_CONFORMANCE_SERVER_ID}/mcp" \ - --requirements "${MCP_CONFORMANCE_SPEC_VERSION}" \ - --expected-failures ../.github/conformance/expected-failures.yml \ - --output-dir ../conformance-results - ) 2>&1 | tee conformance-logs/runner.log - runner_status="${PIPESTATUS[0]}" - set -e - echo "status=${runner_status}" >> "${GITHUB_OUTPUT}" + run: .github/conformance/run-conformance.sh - name: Collect stack logs if: always() @@ -261,12 +89,7 @@ jobs: - name: Stop the live stack if: always() - run: | - if [ -f conformance-logs/reference-server.pid ]; then - kill "$(cat conformance-logs/reference-server.pid)" 2>/dev/null || true - fi - docker compose -f .github/conformance/docker-compose.yml \ - down --volumes --remove-orphans + run: .github/conformance/stop-live-stack.sh - name: Enforce conformance baseline if: always() diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index e55fb998..5b73969a 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -40,7 +40,9 @@ These run in `cargo nextest run` with no Docker dependencies. `@modelcontextprotocol/conformance@0.2.0-alpha.11` server requirements for MCP `2026-07-28`. It uses `--requirements 2026-07-28`, rather than a moving suite, so both the scenario set and the stateless wire version are fixed to that -specification revision. +specification revision. The workflow delegates stack setup, provisioning, +route verification, runner execution, and cleanup to the Bash scripts in +`.github/conformance/`. The job launches a deliberately small live stack: Redis, the published `ghcr.io/ibm/mcp-context-forge:latest` control-plane image, the published From 5f70be0b190623387c4b99d67a33311acc5504c4 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 11:39:43 +0100 Subject: [PATCH 03/11] Condense conformance CI documentation Signed-off-by: lucarlig --- _context/wiki/testing.md | 49 +++++++++++----------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index 5b73969a..ee4a0f77 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -36,42 +36,19 @@ These run in `cargo nextest run` with no Docker dependencies. ## MCP Conformance CI -`.github/workflows/mcp_conformance.yml` runs the official -`@modelcontextprotocol/conformance@0.2.0-alpha.11` server requirements for MCP -`2026-07-28`. It uses `--requirements 2026-07-28`, rather than a moving suite, -so both the scenario set and the stateless wire version are fixed to that -specification revision. The workflow delegates stack setup, provisioning, -route verification, runner execution, and cleanup to the Bash scripts in -`.github/conformance/`. - -The job launches a deliberately small live stack: Redis, the published -`ghcr.io/ibm/mcp-context-forge:latest` control-plane image, the published -`ghcr.io/contextforge-org/contextforge-data-plane:latest` dataplane image, and -nginx. The control plane uses an ephemeral SQLite database, so PostgreSQL and -PgBouncer are not needed in this conformance lane. - -The official TypeScript everything server runs at the npm release's source -SHA and is registered through the real control-plane API. The control-plane -publisher writes the virtual-server snapshot to Redis. The official runner -then targets nginx at `/servers/{virtual_host_id}/mcp`; nginx rewrites that -modern route to `/contextforge-rs/servers/{virtual_host_id}/mcp` on the -dataplane. A route probe requires the dataplane response marker before the -suite starts, so the test cannot silently fall back to the control plane. - -The conformance CLI has no bearer-header option. Nginx therefore adds a real -control-plane test token only when the runner did not supply one. There is no -separate authentication proxy or repository-owned JavaScript helper. - -This exercises nginx routing, dataplane authentication, Redis config lookup, -control-plane publication, virtual-host lookup, and upstream gateway routing. -Because both ContextForge images use the mutable `latest` tag, the job tests -the currently published images rather than the pull-request commit. - -The job prints the official runner's pass and failure details directly in its -GitHub Actions log and writes the raw results and process logs to the -`mcp-conformance-results` artifact. Known failures live in -`.github/conformance/expected-failures.yml`; a new failure or a now-stale -baseline entry fails CI. +`.github/workflows/mcp_conformance.yml` runs the pinned official conformance +suite `0.2.0-alpha.11` with `--requirements 2026-07-28`. Its small live path is +official runner → nginx → published `latest` dataplane → official fixture, +with the published `latest` control plane registering and publishing the +fixture through Redis. The control plane uses ephemeral SQLite, so PostgreSQL +is unnecessary. Workflow helpers live in `.github/conformance/`. + +Because this conformance CLI cannot set a bearer header, nginx adds an +ephemeral control-plane token when one is absent; there is no auth proxy or +repository-owned JavaScript. A route probe prevents control-plane fallback. +Counts appear in the Actions log, full output is uploaded as the +`mcp-conformance-results` artifact, and `expected-failures.yml` guards the +current baseline. ## Full-Stack Integration Harness From 14ba89b282fa80636116bea7256ca35493cd2953 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 11:46:26 +0100 Subject: [PATCH 04/11] Organize conformance harness and refresh Pages CI Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 14 +++++++------- .github/workflows/pages.yml | 6 +++--- _context/wiki/testing.md | 2 +- {.github => tests}/conformance/docker-compose.yml | 0 .../conformance/expected-failures.yml | 0 {.github => tests}/conformance/nginx.conf.template | 0 {.github => tests}/conformance/register-fixture.sh | 0 {.github => tests}/conformance/run-conformance.sh | 0 .../conformance/start-dataplane-and-nginx.sh | 0 .../conformance/start-fixture-and-control-plane.sh | 0 {.github => tests}/conformance/stop-live-stack.sh | 0 11 files changed, 11 insertions(+), 11 deletions(-) rename {.github => tests}/conformance/docker-compose.yml (100%) rename {.github => tests}/conformance/expected-failures.yml (100%) rename {.github => tests}/conformance/nginx.conf.template (100%) rename {.github => tests}/conformance/register-fixture.sh (100%) rename {.github => tests}/conformance/run-conformance.sh (100%) rename {.github => tests}/conformance/start-dataplane-and-nginx.sh (100%) rename {.github => tests}/conformance/start-fixture-and-control-plane.sh (100%) rename {.github => tests}/conformance/stop-live-stack.sh (100%) diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index 5ac2814c..f9b27ebb 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -54,26 +54,26 @@ jobs: - name: Pull the latest ContextForge images run: >- - docker compose -f .github/conformance/docker-compose.yml + docker compose -f tests/conformance/docker-compose.yml pull redis control-plane data-plane nginx - name: Start the fixture and control plane - run: .github/conformance/start-fixture-and-control-plane.sh + run: tests/conformance/start-fixture-and-control-plane.sh - name: Register the official fixture through the control plane - run: .github/conformance/register-fixture.sh + run: tests/conformance/register-fixture.sh - name: Start dataplane and nginx - run: .github/conformance/start-dataplane-and-nginx.sh + run: tests/conformance/start-dataplane-and-nginx.sh - name: Run MCP 2026-07-28 requirements id: runner - run: .github/conformance/run-conformance.sh + run: tests/conformance/run-conformance.sh - name: Collect stack logs if: always() run: >- - docker compose -f .github/conformance/docker-compose.yml + docker compose -f tests/conformance/docker-compose.yml logs --no-color > conformance-logs/stack.log 2>&1 || true - name: Upload conformance results @@ -89,7 +89,7 @@ jobs: - name: Stop the live stack if: always() - run: .github/conformance/stop-live-stack.sh + run: tests/conformance/stop-live-stack.sh - name: Enforce conformance baseline if: always() diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 52057582..82273583 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -24,13 +24,13 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - name: Install mdBook - uses: taiki-e/install-action@v2.75.27 + uses: taiki-e/install-action@v2.85.11 with: tool: mdbook@0.5.3 - name: Build docs run: mdbook build _context/wiki - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5.0.0 with: path: _context/wiki/book @@ -51,4 +51,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5.0.0 diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index ee4a0f77..bcab9ea6 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -41,7 +41,7 @@ suite `0.2.0-alpha.11` with `--requirements 2026-07-28`. Its small live path is official runner → nginx → published `latest` dataplane → official fixture, with the published `latest` control plane registering and publishing the fixture through Redis. The control plane uses ephemeral SQLite, so PostgreSQL -is unnecessary. Workflow helpers live in `.github/conformance/`. +is unnecessary. The harness lives in `tests/conformance/`. Because this conformance CLI cannot set a bearer header, nginx adds an ephemeral control-plane token when one is absent; there is no auth proxy or diff --git a/.github/conformance/docker-compose.yml b/tests/conformance/docker-compose.yml similarity index 100% rename from .github/conformance/docker-compose.yml rename to tests/conformance/docker-compose.yml diff --git a/.github/conformance/expected-failures.yml b/tests/conformance/expected-failures.yml similarity index 100% rename from .github/conformance/expected-failures.yml rename to tests/conformance/expected-failures.yml diff --git a/.github/conformance/nginx.conf.template b/tests/conformance/nginx.conf.template similarity index 100% rename from .github/conformance/nginx.conf.template rename to tests/conformance/nginx.conf.template diff --git a/.github/conformance/register-fixture.sh b/tests/conformance/register-fixture.sh similarity index 100% rename from .github/conformance/register-fixture.sh rename to tests/conformance/register-fixture.sh diff --git a/.github/conformance/run-conformance.sh b/tests/conformance/run-conformance.sh similarity index 100% rename from .github/conformance/run-conformance.sh rename to tests/conformance/run-conformance.sh diff --git a/.github/conformance/start-dataplane-and-nginx.sh b/tests/conformance/start-dataplane-and-nginx.sh similarity index 100% rename from .github/conformance/start-dataplane-and-nginx.sh rename to tests/conformance/start-dataplane-and-nginx.sh diff --git a/.github/conformance/start-fixture-and-control-plane.sh b/tests/conformance/start-fixture-and-control-plane.sh similarity index 100% rename from .github/conformance/start-fixture-and-control-plane.sh rename to tests/conformance/start-fixture-and-control-plane.sh diff --git a/.github/conformance/stop-live-stack.sh b/tests/conformance/stop-live-stack.sh similarity index 100% rename from .github/conformance/stop-live-stack.sh rename to tests/conformance/stop-live-stack.sh From a2925f328325c19ae60ead0c81b971b0fed9b574 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 12:33:35 +0100 Subject: [PATCH 05/11] Keep conformance results in Actions logs Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 17 ----------------- _context/wiki/testing.md | 5 ++--- tests/conformance/run-conformance.sh | 4 ++-- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index f9b27ebb..ffe0ed9c 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -70,23 +70,6 @@ jobs: id: runner run: tests/conformance/run-conformance.sh - - name: Collect stack logs - if: always() - run: >- - docker compose -f tests/conformance/docker-compose.yml - logs --no-color > conformance-logs/stack.log 2>&1 || true - - - name: Upload conformance results - if: always() - uses: actions/upload-artifact@v6.0.0 - with: - name: mcp-conformance-results - path: | - conformance-results/ - conformance-logs/ - if-no-files-found: warn - retention-days: 14 - - name: Stop the live stack if: always() run: tests/conformance/stop-live-stack.sh diff --git a/_context/wiki/testing.md b/_context/wiki/testing.md index bcab9ea6..c7135055 100644 --- a/_context/wiki/testing.md +++ b/_context/wiki/testing.md @@ -46,9 +46,8 @@ is unnecessary. The harness lives in `tests/conformance/`. Because this conformance CLI cannot set a bearer header, nginx adds an ephemeral control-plane token when one is absent; there is no auth proxy or repository-owned JavaScript. A route probe prevents control-plane fallback. -Counts appear in the Actions log, full output is uploaded as the -`mcp-conformance-results` artifact, and `expected-failures.yml` guards the -current baseline. +Counts appear directly in the Actions log, and `expected-failures.yml` guards +the current baseline. The job does not retain a separate conformance artifact. ## Full-Stack Integration Harness diff --git a/tests/conformance/run-conformance.sh b/tests/conformance/run-conformance.sh index 010a9871..8302110c 100755 --- a/tests/conformance/run-conformance.sh +++ b/tests/conformance/run-conformance.sh @@ -19,8 +19,8 @@ set +e --requirements "${MCP_CONFORMANCE_SPEC_VERSION}" \ --expected-failures "${script_dir}/expected-failures.yml" \ --output-dir "${repo_root}/conformance-results" -) 2>&1 | tee "${repo_root}/conformance-logs/runner.log" -runner_status="${PIPESTATUS[0]}" +) +runner_status="$?" set -e echo "status=${runner_status}" >> "${GITHUB_OUTPUT}" From 290a2deb7b415781674bae7dbdb29e3c0bcefef7 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 12:33:51 +0100 Subject: [PATCH 06/11] Bump rmcp to 3.1.2 Signed-off-by: lucarlig --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6abd30c3..28ebb8e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1018,7 +1018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2266,7 +2266,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2518,9 +2518,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094c075f6698deef5a657cf4df6b684dff65157d255978b92b552ec22503f17a" +checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8" dependencies = [ "async-trait", "base64 0.23.1", @@ -2552,9 +2552,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "737d947bcfd946fae6a179a4ef6487be6dcf25c930c2393856b820f1386e52a6" +checksum = "6898e24cd16342b59bfa8a53c2c04b9cf62fc8a2cfea57b9c038b09984bfc521" dependencies = [ "darling", "proc-macro2", @@ -2653,7 +2653,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3886,7 +3886,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index eacf3e93..a7841d8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ repository = "https://github.com/contextforge-org/contextforge-data-plane" # Keep dependencies here only when at least two workspace members inherit them. contextforge-data-plane-cpex = { path = "./crates/contextforge-data-plane-cpex" } contextforge-data-plane-apis = { path = "./crates/contextforge-data-plane-apis"} -rmcp = { version = "3.1.1", features = [ +rmcp = { version = "3.1.2", features = [ "server", "client", "auth", From 68887380d33a0c5f82e7675ca14794a953473db3 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Thu, 13 Aug 2026 12:16:50 +0100 Subject: [PATCH 07/11] ci: address conformance review feedback Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 32 ++++++++- .gitignore | 4 ++ Makefile | 5 +- tests/conformance/docker-compose.yml | 7 +- tests/conformance/run-local.sh | 97 +++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 5 deletions(-) create mode 100755 tests/conformance/run-local.sh diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index ffe0ed9c..13206891 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -2,7 +2,6 @@ name: MCP Conformance on: workflow_dispatch: - pull_request: push: branches: [main] @@ -53,14 +52,20 @@ jobs: npm ci --ignore-scripts - name: Pull the latest ContextForge images + env: + MCP_CONFORMANCE_TOKEN: pull-only run: >- docker compose -f tests/conformance/docker-compose.yml pull redis control-plane data-plane nginx - name: Start the fixture and control plane + env: + MCP_CONFORMANCE_TOKEN: bootstrap-only run: tests/conformance/start-fixture-and-control-plane.sh - name: Register the official fixture through the control plane + env: + MCP_CONFORMANCE_TOKEN: bootstrap-only run: tests/conformance/register-fixture.sh - name: Start dataplane and nginx @@ -70,8 +75,33 @@ jobs: id: runner run: tests/conformance/run-conformance.sh + - name: Print failed conformance checks + if: always() + run: | + if [ ! -d conformance-results ]; then + echo "No conformance results were produced." + exit 0 + fi + find conformance-results -type f -name checks.json -print0 | + while IFS= read -r -d '' checks_file; do + jq --arg file "${checks_file}" \ + '[.[] | select(.status == "FAILURE")] | + if length > 0 then {file: $file, failures: .} else empty end' \ + "${checks_file}" + done + + - name: Print live stack logs + if: always() + env: + MCP_CONFORMANCE_TOKEN: diagnostics-only + run: >- + docker compose -f tests/conformance/docker-compose.yml + logs --no-color || true + - name: Stop the live stack if: always() + env: + MCP_CONFORMANCE_TOKEN: cleanup-only run: tests/conformance/stop-live-stack.sh - name: Enforce conformance baseline diff --git a/.gitignore b/.gitignore index bea63b88..363ba80d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ target # Generated by gateway local runs contextforge-data-plane.log.* +# Generated by local MCP conformance runs +.conformance-suite/ +conformance-logs/ +conformance-results/ # Generated by mdBook (wiki) _context/wiki/book/ diff --git a/Makefile b/Makefile index 89645fb5..99724f37 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help docker-prod compose-up compose-down docs-serve +.PHONY: help docker-prod compose-up compose-down conformance docs-serve help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' @@ -16,5 +16,8 @@ compose-up: ## Launch stack: nginx, control plane, redis, postgres, pgbouncer, d compose-down: ## Tear down the stack docker compose -f docker/docker-compose.yml stop nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time +conformance: ## Run official MCP 2026-07-28 conformance locally + tests/conformance/run-local.sh + docs-serve: ## Serve the wiki book locally at http://127.0.0.1:3000 mdbook serve _context/wiki --hostname 127.0.0.1 --port 3000 --open diff --git a/tests/conformance/docker-compose.yml b/tests/conformance/docker-compose.yml index 1da8cf69..4d5a7ee4 100644 --- a/tests/conformance/docker-compose.yml +++ b/tests/conformance/docker-compose.yml @@ -2,7 +2,7 @@ name: contextforge-conformance services: redis: - image: redis:latest + image: redis:8.8.1-alpine3.23 command: [redis-server, --save, "", --appendonly, "no"] networks: [contextforge] healthcheck: @@ -86,12 +86,12 @@ services: condition: service_healthy nginx: - image: nginx:latest + image: nginx:1.30.4-alpine3.24 ports: - "127.0.0.1:${MCP_CONFORMANCE_PORT:-8080}:80" networks: [contextforge] environment: - MCP_CONFORMANCE_TOKEN: ${MCP_CONFORMANCE_TOKEN:-missing} + MCP_CONFORMANCE_TOKEN: ${MCP_CONFORMANCE_TOKEN:?} NGINX_ENVSUBST_TEMPLATE_DIR: /templates NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx @@ -100,6 +100,7 @@ services: depends_on: control-plane: condition: service_healthy + # Full readiness, routing, and auth are verified by start-dataplane-and-nginx.sh. data-plane: condition: service_started healthcheck: diff --git a/tests/conformance/run-local.sh b/tests/conformance/run-local.sh new file mode 100755 index 00000000..f190985d --- /dev/null +++ b/tests/conformance/run-local.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +compose_file="${script_dir}/docker-compose.yml" + +export MCP_CONFORMANCE_VERSION="${MCP_CONFORMANCE_VERSION:-0.2.0-alpha.11}" +export MCP_CONFORMANCE_SOURCE_SHA="${MCP_CONFORMANCE_SOURCE_SHA:-c321dd32035556e6769d3724a8ee97d87c3faaac}" +export MCP_CONFORMANCE_SPEC_VERSION="${MCP_CONFORMANCE_SPEC_VERSION:-2026-07-28}" +export MCP_CONFORMANCE_SERVER_ID="${MCP_CONFORMANCE_SERVER_ID:-3f33286667d34b65a31c3bafd30e4c21}" +export MCP_CONFORMANCE_SUITE_DIR="${MCP_CONFORMANCE_SUITE_DIR:-${repo_root}/.conformance-suite}" +export CF_CONTROLPLANE_IMAGE="${CF_CONTROLPLANE_IMAGE:-ghcr.io/ibm/mcp-context-forge:latest}" +export CF_DATAPLANE_IMAGE="${CF_DATAPLANE_IMAGE:-ghcr.io/contextforge-org/contextforge-data-plane:latest}" + +for command in curl docker git jq node npm; do + if ! command -v "${command}" > /dev/null 2>&1; then + echo "Required command not found: ${command}" >&2 + exit 1 + fi +done +docker compose version > /dev/null + +if [ -e "${MCP_CONFORMANCE_SUITE_DIR}" ] && [ ! -d "${MCP_CONFORMANCE_SUITE_DIR}/.git" ]; then + echo "MCP_CONFORMANCE_SUITE_DIR is not a git checkout: ${MCP_CONFORMANCE_SUITE_DIR}" >&2 + exit 1 +fi + +if [ ! -d "${MCP_CONFORMANCE_SUITE_DIR}/.git" ]; then + echo "Checking out the official conformance suite." + git clone --filter=blob:none \ + https://github.com/modelcontextprotocol/conformance.git \ + "${MCP_CONFORMANCE_SUITE_DIR}" + git -C "${MCP_CONFORMANCE_SUITE_DIR}" checkout --detach "${MCP_CONFORMANCE_SOURCE_SHA}" +fi + +suite_sha="$(git -C "${MCP_CONFORMANCE_SUITE_DIR}" rev-parse HEAD)" +if [ "${suite_sha}" != "${MCP_CONFORMANCE_SOURCE_SHA}" ]; then + echo "Conformance checkout is at ${suite_sha}; expected ${MCP_CONFORMANCE_SOURCE_SHA}." >&2 + echo "Use a checkout at the pinned commit or set MCP_CONFORMANCE_SUITE_DIR." >&2 + exit 1 +fi + +( + echo "Installing official conformance dependencies." + cd "${MCP_CONFORMANCE_SUITE_DIR}" + test "$(node -p "require('./package.json').version")" = "${MCP_CONFORMANCE_VERSION}" + npm ci --ignore-scripts +) + +state_dir="$(mktemp -d "${TMPDIR:-/tmp}/contextforge-conformance.XXXXXX")" +export GITHUB_ENV="${state_dir}/github-env" +export GITHUB_OUTPUT="${state_dir}/github-output" +touch "${GITHUB_ENV}" "${GITHUB_OUTPUT}" + +# shellcheck disable=SC2329 # Invoked by the trap below. +cleanup() { + local status="$?" + trap - EXIT INT TERM + if [ "${status}" -ne 0 ]; then + echo "Conformance run failed; printing live stack logs." >&2 + MCP_CONFORMANCE_TOKEN=diagnostics-only \ + docker compose -f "${compose_file}" logs --no-color || true + fi + MCP_CONFORMANCE_TOKEN="${MCP_CONFORMANCE_TOKEN:-cleanup-only}" \ + "${script_dir}/stop-live-stack.sh" || true + rm -f -- "${GITHUB_ENV}" "${GITHUB_OUTPUT}" + rmdir -- "${state_dir}" + exit "${status}" +} +trap cleanup EXIT INT TERM + +MCP_CONFORMANCE_TOKEN=pull-only \ + docker compose -f "${compose_file}" pull redis control-plane data-plane nginx +echo "Starting the fixture and control plane." +MCP_CONFORMANCE_TOKEN=bootstrap-only \ + "${script_dir}/start-fixture-and-control-plane.sh" +echo "Registering the fixture through the control plane." +MCP_CONFORMANCE_TOKEN=bootstrap-only \ + "${script_dir}/register-fixture.sh" + +set -a +# shellcheck disable=SC1090 +source "${GITHUB_ENV}" +set +a + +echo "Starting the dataplane and nginx." +"${script_dir}/start-dataplane-and-nginx.sh" +echo "Running MCP ${MCP_CONFORMANCE_SPEC_VERSION} conformance." +"${script_dir}/run-conformance.sh" + +runner_status="$(sed -n 's/^status=//p' "${GITHUB_OUTPUT}" | tail -n 1)" +if [ -z "${runner_status}" ]; then + echo "Conformance runner did not report a status." >&2 + exit 1 +fi +exit "${runner_status}" From 2596e0b8010475bd2c9bbb7fc8ff836a4dd9484a Mon Sep 17 00:00:00 2001 From: lucarlig Date: Thu, 13 Aug 2026 13:35:21 +0100 Subject: [PATCH 08/11] ci: allowlist conformance test credentials Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 2 +- tests/conformance/docker-compose.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index 13206891..8362ad73 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -14,7 +14,7 @@ concurrency: env: MCP_CONFORMANCE_VERSION: 0.2.0-alpha.11 - MCP_CONFORMANCE_SOURCE_SHA: c321dd32035556e6769d3724a8ee97d87c3faaac + MCP_CONFORMANCE_SOURCE_SHA: c321dd32035556e6769d3724a8ee97d87c3faaac # pragma: allowlist secret MCP_CONFORMANCE_SPEC_VERSION: 2026-07-28 MCP_CONFORMANCE_SERVER_ID: 3f33286667d34b65a31c3bafd30e4c21 CF_CONTROLPLANE_IMAGE: ghcr.io/ibm/mcp-context-forge:latest diff --git a/tests/conformance/docker-compose.yml b/tests/conformance/docker-compose.yml index 4d5a7ee4..5147af74 100644 --- a/tests/conformance/docker-compose.yml +++ b/tests/conformance/docker-compose.yml @@ -28,12 +28,12 @@ services: JWT_ALGORITHM: RS256 JWT_PUBLIC_KEY_PATH: /keys/jwt.key.pub JWT_PRIVATE_KEY_PATH: /keys/jwt.key - AUTH_ENCRYPTION_SECRET: contextforge-conformance-encryption-secret-at-least-32-bytes + AUTH_ENCRYPTION_SECRET: contextforge-conformance-encryption-secret-at-least-32-bytes # pragma: allowlist secret AUTH_REQUIRED: "true" BASIC_AUTH_USER: admin - BASIC_AUTH_PASSWORD: conformance-admin-password + BASIC_AUTH_PASSWORD: conformance-admin-password # pragma: allowlist secret PLATFORM_ADMIN_EMAIL: admin@example.com - PLATFORM_ADMIN_PASSWORD: conformance-admin-password + PLATFORM_ADMIN_PASSWORD: conformance-admin-password # pragma: allowlist secret MCPGATEWAY_SKIP_MIGRATIONS: "false" MCPGATEWAY_UI_ENABLED: "false" MCPGATEWAY_ADMIN_API_ENABLED: "true" @@ -72,7 +72,7 @@ services: CONTEXTFORGE_DATA_PLANE_REDIS_CONNECTION_MODE: plain-text CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE: plain-text-or-tls CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PUBLIC_KEY: /keys/jwt.key.pub - CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY: /keys/jwt.key + CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY: /keys/jwt.key # pragma: allowlist secret CONTEXTFORGE_DATA_PLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS: "0" CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS: >- 127.0.0.1:${MCP_CONFORMANCE_PORT:-8080},localhost:${MCP_CONFORMANCE_PORT:-8080} From b9a43af411e69ff7c7c320c5f3665d953bcdbd71 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Thu, 13 Aug 2026 15:53:40 +0100 Subject: [PATCH 09/11] ci: test conformance against PR image Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index 8362ad73..09c0a7a1 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -2,6 +2,8 @@ name: MCP Conformance on: workflow_dispatch: + pull_request: + branches: [main] push: branches: [main] @@ -18,7 +20,7 @@ env: MCP_CONFORMANCE_SPEC_VERSION: 2026-07-28 MCP_CONFORMANCE_SERVER_ID: 3f33286667d34b65a31c3bafd30e4c21 CF_CONTROLPLANE_IMAGE: ghcr.io/ibm/mcp-context-forge:latest - CF_DATAPLANE_IMAGE: ghcr.io/contextforge-org/contextforge-data-plane:latest + CF_DATAPLANE_IMAGE: contextforge-data-plane:conformance jobs: conformance: @@ -30,6 +32,11 @@ jobs: - name: Check out data plane uses: actions/checkout@v6.0.2 + - name: Build data plane from the checked-out source + run: >- + docker build . --file docker/Dockerfile + --tag "${CF_DATAPLANE_IMAGE}" + - name: Check out the alpha.11 conformance fixture uses: actions/checkout@v6.0.2 with: @@ -51,12 +58,12 @@ jobs: test "$(node -p "require('./package.json').version")" = "${MCP_CONFORMANCE_VERSION}" npm ci --ignore-scripts - - name: Pull the latest ContextForge images + - name: Pull external stack images env: MCP_CONFORMANCE_TOKEN: pull-only run: >- docker compose -f tests/conformance/docker-compose.yml - pull redis control-plane data-plane nginx + pull redis control-plane nginx - name: Start the fixture and control plane env: From 7bfef724c7bd2310c0f78661ace9a4e8ec669b45 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Thu, 13 Aug 2026 16:11:11 +0100 Subject: [PATCH 10/11] ci: align local conformance with PR build Signed-off-by: lucarlig --- .dockerignore | 3 +++ .github/workflows/mcp_conformance.yml | 4 +--- Makefile | 10 +++++++--- tests/conformance/docker-compose.yml | 1 - tests/conformance/run-local.sh | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 89532539..43f469d3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,6 @@ target/ assets/ resources/ releases/ +.conformance-suite/ +conformance-logs/ +conformance-results/ diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index 09c0a7a1..8cc35853 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -33,9 +33,7 @@ jobs: uses: actions/checkout@v6.0.2 - name: Build data plane from the checked-out source - run: >- - docker build . --file docker/Dockerfile - --tag "${CF_DATAPLANE_IMAGE}" + run: make conformance-image - name: Check out the alpha.11 conformance fixture uses: actions/checkout@v6.0.2 diff --git a/Makefile b/Makefile index 7a7a37f1..cf443962 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ IMAGE_NAME := contextforge-data-plane:latest +CF_DATAPLANE_IMAGE ?= contextforge-data-plane:conformance SERVICES ?= nginx control-plane redis postgres pgbouncer data-plane fast_time_server register_fast_time ARGS ?= @@ -6,7 +7,7 @@ ARGS ?= DETECT_SECRETS_SPEC ?= git+https://github.com/ibm/detect-secrets.git@076672a9a01abdfc7ecee2e7d14f08cdccb73976 DETECT_SECRETS_EXCLUDE := '(?x)(Cargo\.lock$$|\.lock$$)|^\.secrets\.baseline$$' -.PHONY: help docker-prod compose-up compose-down conformance docs-serve pre-commit secrets-scan-all configure-git +.PHONY: help docker-prod compose-up compose-down conformance-image conformance docs-serve pre-commit secrets-scan-all configure-git help: ## Show available commands @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' @@ -24,8 +25,11 @@ compose-up: ## Launch stack: nginx, control plane, redis, postgres, pgbouncer, d compose-down: ## Tear down the stack docker compose -f docker/docker-compose.yml stop $(SERVICES) $(ARGS) -conformance: ## Run official MCP 2026-07-28 conformance locally - tests/conformance/run-local.sh +conformance-image: ## Build the current checkout for MCP conformance + docker build -t "$(CF_DATAPLANE_IMAGE)" -f docker/Dockerfile . + +conformance: conformance-image ## Build and run official MCP 2026-07-28 conformance locally + CF_DATAPLANE_IMAGE="$(CF_DATAPLANE_IMAGE)" tests/conformance/run-local.sh docs-serve: ## Serve the wiki book locally at http://127.0.0.1:3000 mdbook serve _context/wiki --hostname 127.0.0.1 --port 3000 --open diff --git a/tests/conformance/docker-compose.yml b/tests/conformance/docker-compose.yml index 5147af74..f941f3c6 100644 --- a/tests/conformance/docker-compose.yml +++ b/tests/conformance/docker-compose.yml @@ -61,7 +61,6 @@ services: data-plane: image: ${CF_DATAPLANE_IMAGE:-ghcr.io/contextforge-org/contextforge-data-plane:latest} - platform: linux/amd64 networks: [contextforge] extra_hosts: - host.docker.internal:host-gateway diff --git a/tests/conformance/run-local.sh b/tests/conformance/run-local.sh index f190985d..86bf25dc 100755 --- a/tests/conformance/run-local.sh +++ b/tests/conformance/run-local.sh @@ -11,7 +11,7 @@ export MCP_CONFORMANCE_SPEC_VERSION="${MCP_CONFORMANCE_SPEC_VERSION:-2026-07-28} export MCP_CONFORMANCE_SERVER_ID="${MCP_CONFORMANCE_SERVER_ID:-3f33286667d34b65a31c3bafd30e4c21}" export MCP_CONFORMANCE_SUITE_DIR="${MCP_CONFORMANCE_SUITE_DIR:-${repo_root}/.conformance-suite}" export CF_CONTROLPLANE_IMAGE="${CF_CONTROLPLANE_IMAGE:-ghcr.io/ibm/mcp-context-forge:latest}" -export CF_DATAPLANE_IMAGE="${CF_DATAPLANE_IMAGE:-ghcr.io/contextforge-org/contextforge-data-plane:latest}" +export CF_DATAPLANE_IMAGE="${CF_DATAPLANE_IMAGE:-contextforge-data-plane:conformance}" for command in curl docker git jq node npm; do if ! command -v "${command}" > /dev/null 2>&1; then @@ -71,7 +71,7 @@ cleanup() { trap cleanup EXIT INT TERM MCP_CONFORMANCE_TOKEN=pull-only \ - docker compose -f "${compose_file}" pull redis control-plane data-plane nginx + docker compose -f "${compose_file}" pull redis control-plane nginx echo "Starting the fixture and control plane." MCP_CONFORMANCE_TOKEN=bootstrap-only \ "${script_dir}/start-fixture-and-control-plane.sh" From 828d47518cfb47de8226b29150331bd871182432 Mon Sep 17 00:00:00 2001 From: lucarlig Date: Fri, 14 Aug 2026 09:22:13 +0100 Subject: [PATCH 11/11] Print conformance warnings in CI diagnostics Signed-off-by: lucarlig --- .github/workflows/mcp_conformance.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mcp_conformance.yml b/.github/workflows/mcp_conformance.yml index 8cc35853..1a47c6bd 100644 --- a/.github/workflows/mcp_conformance.yml +++ b/.github/workflows/mcp_conformance.yml @@ -80,7 +80,7 @@ jobs: id: runner run: tests/conformance/run-conformance.sh - - name: Print failed conformance checks + - name: Print conformance failures and warnings if: always() run: | if [ ! -d conformance-results ]; then @@ -90,8 +90,8 @@ jobs: find conformance-results -type f -name checks.json -print0 | while IFS= read -r -d '' checks_file; do jq --arg file "${checks_file}" \ - '[.[] | select(.status == "FAILURE")] | - if length > 0 then {file: $file, failures: .} else empty end' \ + '[.[] | select(.status == "FAILURE" or .status == "WARNING")] | + if length > 0 then {file: $file, findings: .} else empty end' \ "${checks_file}" done