Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/mcp_conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 tests/conformance/docker-compose.yml
pull redis control-plane data-plane nginx

- name: Start the fixture and control plane
run: tests/conformance/start-fixture-and-control-plane.sh

- name: Register the official fixture through the control plane
run: tests/conformance/register-fixture.sh

- name: Start dataplane and nginx
run: tests/conformance/start-dataplane-and-nginx.sh

- name: Run MCP 2026-07-28 requirements
id: runner
run: tests/conformance/run-conformance.sh

- name: Stop the live stack
if: always()
run: tests/conformance/stop-live-stack.sh

- name: Enforce conformance baseline
if: always()
env:
RUNNER_STATUS: ${{ steps.runner.outputs.status }}
run: test "${RUNNER_STATUS}" = "0"
6 changes: 3 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions _context/wiki/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ 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 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. 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 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

[`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.
Expand Down
113 changes: 113 additions & 0 deletions tests/conformance/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions tests/conformance/expected-failures.yml
Original file line number Diff line number Diff line change
@@ -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
Loading