From 6ee121acdae6bcd72f35ee605e3912ccacb86ffa Mon Sep 17 00:00:00 2001 From: lucarlig Date: Tue, 11 Aug 2026 11:16:04 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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",