Skip to content

test(compliance): MCP 2025-11-25 protocol compliance harness - #4301

Merged
jonpspri merged 7 commits into
mainfrom
jps-mcp-compliance
Apr 19, 2026
Merged

jonpspri merged 7 commits into
mainfrom
jps-mcp-compliance

Conversation

@jonpspri

Copy link
Copy Markdown
Collaborator

Summary

Adds a pytest-based MCP 2025-11-25 protocol-compliance harness, a FastMCP reference server to act as the known-good witness, and a matrix orchestrator that runs the harness across every runnable engine mode of the runtime-mutable MCP gateway (#4273, #4296).

  • tests/protocol_compliance/ — parametrized probe suite covering lifecycle, transport, tools, resources, prompts, logging, utilities (ping/progress/cancellation), roots, sampling, elicitation, notifications (list_changed, subscription updates), pagination, OAuth 2.1 via Keycloak, security best practices, runtime-mutable MCP mode, and JSON-RPC envelope invariants.
  • mcp-servers/python/compliance_reference_server/ — FastMCP-based reference server with explicit witnesses for every capability the harness probes, including new get_cancellation_count and _mutation_counter plumbing so end-to-end notification delivery is observable from tests.
  • scripts/compliance_matrix.py — orchestrator that flips runtime-mode, waits for data-plane convergence, invokes the harness per engine (reference / python / rust_edge / rust_full), and renders a console / markdown / JSON summary with XPASS-sidecar capture for detecting silently-closed gaps.
  • tests/protocol_compliance/COMPLIANCE_GAPS.md — tracked backlog of documented divergences (GAP-001 … GAP-012) with workflow for filing, narrowing on partial closure, and the canonical stream-attribution note for MCP 2025-11-25 § Listening for Messages.
  • Makefile — test-protocol-compliance* targets; testing-up now folds in the sso compose profile so Keycloak comes up with the stack.
  • infra/keycloak/realm-export.json — service-accounts enabled on the dev client so the client_credentials OAuth tests work out of the box.

Scope of changes

5 commits, all linear on top of the latest origin/main (post-#4296, post-#4299):

  1. test(compliance) — the harness, reference server, matrix script, rewritten test-mcp-protocol-e2e target.
  2. refactor(tests) — group test helpers under tests/*/helpers/ + verbose-regex precommit excludes.
  3. fix(keycloak) — enable service-accounts on the dev realm client.
  4. test(compliance) follow-ups — silent-failure fixes, type-design tightening, behavioral coverage, corrected stream attribution, 6 new tests.
  5. fix(e2e) — align test_oauth_jwks_e2e JWT secret with the gateway's 32-char minimum.

Current matrix status

After make testing-up-rust-full:

Engine Passed Failed XFailed XPassed Skipped
reference (stdio) 30 0 0 0 0
python (shadow flip) 53 0 34 0 11
rust_edge 53 0 34 0 11
rust_full 58 0 40 0 11

Zero unexpected failures, zero XPASS. Each xfail points at a documented GAP; 11 gaps currently open. Python and rust_edge ingress paths behave identically under the probe set (same pass/xfail/skip counts), which is the drift parity we want to pin.

Test plan

  • make test-protocol-compliance-reference — reference-only, green.
  • make test-protocol-compliance-gateway — gateway-only, green with expected xfails.
  • make test-protocol-compliance-matrix — full matrix under boot=edge and boot=full.
  • Reference-server suite: 20/20 passing.
  • Precommit hooks pass on both commits.
  • CI run on the PR (pending).

Follow-ups (filed / tracked)

Build a FastMCP-based test harness that probes ContextForge against the
MCP 2025-11-25 specification across three targets: reference stdio,
gateway proxy (federated), and gateway virtual server. Includes a new
reference server (mcp-servers/python/compliance_reference_server) with
tools/resources/prompts covering lifecycle, transport, pagination,
notifications, subscriptions, roots, sampling, elicitation, and logging.

Key additions:
- tests/protocol_compliance/: parametrized test suite with XPASS-capture,
  drift detection across targets, and gap-tracking workflow
- scripts/compliance_matrix.py: orchestrator that runs the suite across
  engine modes (python/shadow/edge/full) and aggregates results
- Runtime-mutable MCP mode tests covering shadow<->edge flip, boot-mode
  rails, publish/audit response contract, and data-plane runtime header
- OAuth 2.1 / Authorization tests against Keycloak (tier-gated skip)
- Security Best Practices probes (origin validation, bind address,
  confused-deputy)
- Rewrite tests/e2e/test_mcp_cli_protocol.py as test_mcp_protocol_e2e.py
  using FastMCP Client directly (no external CLI deps)
- Makefile: test-protocol-compliance* targets; testing-up folds in sso
  profile so Keycloak comes up with the gateway stack
- pre-commit: scope name-tests-test hook to tests/unit/ (integration,
  e2e, and compliance suites legitimately host non-test Python modules)

Closes #4273 (runtime-mutable MCP mode observability coverage).

Signed-off-by: Jonathan Springer <jps@s390x.com>
Move loose non-test Python modules into conventional helpers/ subdirs
so the test-naming hook can exclude a single pattern instead of
enumerating each file.

Moves:
- tests/e2e/mcp_test_helpers.py -> tests/e2e/helpers/mcp_test_helpers.py
- tests/protocol_compliance/_helpers.py ->
  tests/protocol_compliance/helpers/compliance.py
- tests/protocol_compliance/_drift.py ->
  tests/protocol_compliance/helpers/drift.py

Updates all import sites across tests/e2e, tests/e2e_rust, and
tests/protocol_compliance.

Config cleanup:
- name-tests-test now excludes tests/*/(helpers|fixtures|targets|pages)/
  as a single verbose-regex alternative instead of file-by-file.
- Top-level pre-commit exclude and detect-secrets excludes converted
  to verbose-regex with inline comments.
- Dropped non-existent excludes (scripts/sign_image.sh, scripts/zap,
  sonar-project.properties) and the redundant .secrets.baseline
  entry (detect-secrets skips its own baseline automatically).
- detect-secrets hook in pre-commit now carries its own exclude list,
  kept in sync with the Makefile invocation.

Signed-off-by: Jonathan Springer <jps@s390x.com>
…lient

The local Keycloak dev realm disables the client_credentials grant, so
compliance tests that exercise OAuth 2.1 token issuance against the
preconfigured `mcp-gateway` client fail with
`unauthorized_client: Client not enabled to retrieve service account`.

Flip `serviceAccountsEnabled` to `true` so fresh `docker compose` volume
imports come up with the grant available. Existing stacks can apply the
same change via the Keycloak Admin API against the running realm; the
stored volume overrides the import file once the realm exists, which is
why the change has to land here to stick across rebuilds.

Signed-off-by: Jonathan Springer <jps@s390x.com>
…oral coverage, type design, stream attribution

Bundles the fixes surfaced by running the pr-review-toolkit agents
(code-reviewer, pr-test-analyzer, comment-analyzer,
silent-failure-hunter, type-design-analyzer) plus two iterations of
Codex stop-review feedback.

Silent-failure fixes:
  - _parse_junit / _run_slice / stale-junit unlink /
    _wait_data_plane_converges return-value check close the
    "0/0/0/0 row" silent path in the matrix orchestrator.
  - Reference server stdout+stderr captured to a session tmp log; tail
    dumped on readiness timeout instead of swallowed.
  - flip_runtime_mode / _delete_if_exists / _wait_for_federation_sync
    / _probe_state / Keycloak token fetch surface last-status
    diagnostics instead of collapsing to None.
  - conftest fixture-resolution narrows catch-all so ImportError /
    NameError / SyntaxError from a broken fixture definition
    propagate; only runtime unreachability skips.
  - pytest_runtest_logreport wraps sidecar write in try/except so an
    unwriteable log doesn't poison the run.

Type design:
  - ComplianceTarget: __init_subclass__ enforces name and
    supported_transports on every concrete subclass; base client()
    validates transport support and dispatches to abstract
    _open_client(); per-subclass boilerplate deleted.
  - SliceResult: stored `ran` field removed (derive from
    skip_reason); __post_init__ rejects negative counts and the
    inconsistent "skipped slice with non-zero counts" state.
  - ReferenceUpstream: frozen, no raw Popen field — process handle
    stays in fixture closure so tests can't race teardown.
  - KeycloakConfig: token_endpoint is now a property; __repr__
    redacts client_secret.

Stream attribution:
  - MCP 2025-11-25 § Listening for Messages is explicit:
    server→client *requests* (roots/list, sampling/createMessage,
    elicitation/create) MUST NOT ride the standalone GET /mcp/
    stream; they travel on the POST-correlated stream. GAP-002..005
    and seven xfail reasons wrongly blamed #4205 (standalone-stream
    closure). Rewrote the Why sections and reasons to point at
    POST-correlated-stream relay instead.
  - GAP-011 (resource-subscription updates) is the only gap that
    legitimately points at #4205.
  - Added a canonical "Stream-attribution note" to COMPLIANCE_GAPS.md.
  - Filed GAP-011 (resource subscriptions) and GAP-012 (gateway
    returns -32000 where spec requires -32601).

Behavioral coverage (6 new tests + 4 reference-server witnesses):
  - notifications/cancelled end-to-end delivery.
  - logging/setLevel filter-threshold round-trip.
  - JSON-RPC reserved codes -32700 / -32601 / -32602.
  - tools/call argument-type validation (narrowed to McpError).
  - initialize-twice lifecycle rejection.
  - MCP-Protocol-Version mismatch handling.
  - Reference-server side-effect witnesses for mutate_resource_list /
    mutate_prompt_list / bump_subscribable notifications and the
    subscribe/unsubscribe handler round-trip.

  Each raw-httpx test fails on 5xx and accepts 4xx as HTTP-level
  rejection; no false-green paths where a server crash would read as
  compliance.

Bug fixes:
  - Reference server: mutator tools now use a dedicated
    _mutation_counter to prevent ephemeral_0 name-collisions.
  - test_drift._collect narrows to pytest.skip.Exception; other probe
    errors are recorded so drift diffs show the root cause.
  - Makefile PYTEST_IGNORE excludes tests/protocol_compliance.

Matrix rust_full slice: 58 passed / 0 failed / 40 xfailed / 0 xpassed
/ 11 skipped (was 53/0/34/0/11). Zero unexpected failures.

Signed-off-by: Jonathan Springer <jps@s390x.com>
Test defaulted its JWT secret to "my-test-key" (11 chars) when
JWT_SECRET_KEY was unset; the gateway's validator requires ≥32 chars
so every admin-API call the fixtures make would fail with
401 "Invalid authentication credentials" on default deployments.

Import JWT_SECRET from helpers.mcp_test_helpers — the 40-char default
the rest of the e2e suite already shares — so this test self-configures
without requiring the operator to export JWT_SECRET_KEY. Overriding
via the env var still works.

Signed-off-by: Jonathan Springer <jps@s390x.com>
@github-actions

github-actions Bot commented Apr 19, 2026 •

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

The following issues were found:
  • ❌ 1 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 7 package(s) with unknown licenses.
See the Details below.

Vulnerabilities

uv.lock

NameVersionVulnerabilitySeverity
authlib1.6.9Authlib: Cross-site request forging when using cachemoderate
Only included vulnerabilities with severity moderate or higher.

License Issues

mcp-servers/python/compliance_reference_server/pyproject.toml

PackageVersionLicenseIssue Type
fastmcp>= 2.0NullUnknown License

uv.lock

PackageVersionLicenseIssue Type
cyclopts4.10.2NullUnknown License
fastmcp3.2.2NullUnknown License
jsonschema-path0.4.5NullUnknown License
pathable0.5.0NullUnknown License
py-key-value-aio0.4.4NullUnknown License
uncalled-for0.3.1NullUnknown License
Denied Licenses: GPL-1.0, GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0, RPL-1.5, OSL-3.0, CPAL-1.0

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
pip/fastmcp >= 2.0 UnknownUnknown
pip/authlib 1.6.9 UnknownUnknown
pip/aiofile 3.9.0 🟢 3.1
Details
CheckScoreReason
Code-Review🟢 4Found 6/14 approved changesets -- score normalized to 4
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 9license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
pip/beartype 0.22.9 UnknownUnknown
pip/caio 0.9.25 UnknownUnknown
pip/cyclopts 4.10.2 UnknownUnknown
pip/docstring-parser 0.17.0 UnknownUnknown
pip/exceptiongroup 1.3.1 UnknownUnknown
pip/fastmcp 3.2.2 UnknownUnknown
pip/jsonref 1.1.0 UnknownUnknown
pip/jsonschema-path 0.4.5 UnknownUnknown
pip/openapi-pydantic 0.5.1 UnknownUnknown
pip/pathable 0.5.0 UnknownUnknown
pip/py-key-value-aio 0.4.4 UnknownUnknown
pip/pyperclip 1.11.0 UnknownUnknown
pip/rich-rst 1.3.2 UnknownUnknown
pip/uncalled-for 0.3.1 UnknownUnknown

Scanned Files

  • mcp-servers/python/compliance_reference_server/pyproject.toml
  • uv.lock

… lite config

The main pre-commit config already excludes these POM-style support
directories, but CI runs the lite config (Makefile:4175 —
`pre-commit run --config .pre-commit-lite.yaml`) which was a
near-duplicate that hadn't been kept in sync. Add `targets` to its
excluded-dir alternation so the four compliance-harness target modules
stop tripping the hook in CI.

Signed-off-by: Jonathan Springer <jps@s390x.com>
…ckage override

authlib is a transitive dep of fastmcp, which the compliance harness
uses as a test client — both live in the dev dependency group.
mcpgateway itself imports nothing from authlib, so the constraint goes
in `[dependency-groups] dev` rather than main project deps.

Release 1.7.0 (2026-04-18) ships security fixes but was blocked by the
repo-wide `exclude-newer = "10 days"` newness filter, leaving the
lockfile on 1.6.9. Adds an `exclude-newer-package` entry for authlib
(2026-04-19) so the filter allows the new release.

Signed-off-by: Jonathan Springer <jps@s390x.com>


def test_runtime_mode_state_is_readable(runtime_mode_state: dict) -> None:
"""GET /admin/runtime/mcp-mode returns a well-shaped state payload."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest, how is this related to MCP compliance ?

gcgoncalves pushed a commit that referenced this pull request Apr 23, 2026
* test(compliance): add MCP 2025-11-25 protocol compliance harness

Build a FastMCP-based test harness that probes ContextForge against the
MCP 2025-11-25 specification across three targets: reference stdio,
gateway proxy (federated), and gateway virtual server. Includes a new
reference server (mcp-servers/python/compliance_reference_server) with
tools/resources/prompts covering lifecycle, transport, pagination,
notifications, subscriptions, roots, sampling, elicitation, and logging.

Key additions:
- tests/protocol_compliance/: parametrized test suite with XPASS-capture,
  drift detection across targets, and gap-tracking workflow
- scripts/compliance_matrix.py: orchestrator that runs the suite across
  engine modes (python/shadow/edge/full) and aggregates results
- Runtime-mutable MCP mode tests covering shadow<->edge flip, boot-mode
  rails, publish/audit response contract, and data-plane runtime header
- OAuth 2.1 / Authorization tests against Keycloak (tier-gated skip)
- Security Best Practices probes (origin validation, bind address,
  confused-deputy)
- Rewrite tests/e2e/test_mcp_cli_protocol.py as test_mcp_protocol_e2e.py
  using FastMCP Client directly (no external CLI deps)
- Makefile: test-protocol-compliance* targets; testing-up folds in sso
  profile so Keycloak comes up with the gateway stack
- pre-commit: scope name-tests-test hook to tests/unit/ (integration,
  e2e, and compliance suites legitimately host non-test Python modules)

Closes #4273 (runtime-mutable MCP mode observability coverage).

Signed-off-by: Jonathan Springer <jps@s390x.com>

* refactor(tests): group test helpers under tests/*/helpers/ subdirs

Move loose non-test Python modules into conventional helpers/ subdirs
so the test-naming hook can exclude a single pattern instead of
enumerating each file.

Moves:
- tests/e2e/mcp_test_helpers.py -> tests/e2e/helpers/mcp_test_helpers.py
- tests/protocol_compliance/_helpers.py ->
  tests/protocol_compliance/helpers/compliance.py
- tests/protocol_compliance/_drift.py ->
  tests/protocol_compliance/helpers/drift.py

Updates all import sites across tests/e2e, tests/e2e_rust, and
tests/protocol_compliance.

Config cleanup:
- name-tests-test now excludes tests/*/(helpers|fixtures|targets|pages)/
  as a single verbose-regex alternative instead of file-by-file.
- Top-level pre-commit exclude and detect-secrets excludes converted
  to verbose-regex with inline comments.
- Dropped non-existent excludes (scripts/sign_image.sh, scripts/zap,
  sonar-project.properties) and the redundant .secrets.baseline
  entry (detect-secrets skips its own baseline automatically).
- detect-secrets hook in pre-commit now carries its own exclude list,
  kept in sync with the Makefile invocation.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(keycloak): enable service-accounts on the mcp-gateway dev realm client

The local Keycloak dev realm disables the client_credentials grant, so
compliance tests that exercise OAuth 2.1 token issuance against the
preconfigured `mcp-gateway` client fail with
`unauthorized_client: Client not enabled to retrieve service account`.

Flip `serviceAccountsEnabled` to `true` so fresh `docker compose` volume
imports come up with the grant available. Existing stacks can apply the
same change via the Keycloak Admin API against the running realm; the
stored volume overrides the import file once the realm exists, which is
why the change has to land here to stick across rebuilds.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* test(compliance): PR-review follow-ups — silent-failure fixes, behavioral coverage, type design, stream attribution

Bundles the fixes surfaced by running the pr-review-toolkit agents
(code-reviewer, pr-test-analyzer, comment-analyzer,
silent-failure-hunter, type-design-analyzer) plus two iterations of
Codex stop-review feedback.

Silent-failure fixes:
  - _parse_junit / _run_slice / stale-junit unlink /
    _wait_data_plane_converges return-value check close the
    "0/0/0/0 row" silent path in the matrix orchestrator.
  - Reference server stdout+stderr captured to a session tmp log; tail
    dumped on readiness timeout instead of swallowed.
  - flip_runtime_mode / _delete_if_exists / _wait_for_federation_sync
    / _probe_state / Keycloak token fetch surface last-status
    diagnostics instead of collapsing to None.
  - conftest fixture-resolution narrows catch-all so ImportError /
    NameError / SyntaxError from a broken fixture definition
    propagate; only runtime unreachability skips.
  - pytest_runtest_logreport wraps sidecar write in try/except so an
    unwriteable log doesn't poison the run.

Type design:
  - ComplianceTarget: __init_subclass__ enforces name and
    supported_transports on every concrete subclass; base client()
    validates transport support and dispatches to abstract
    _open_client(); per-subclass boilerplate deleted.
  - SliceResult: stored `ran` field removed (derive from
    skip_reason); __post_init__ rejects negative counts and the
    inconsistent "skipped slice with non-zero counts" state.
  - ReferenceUpstream: frozen, no raw Popen field — process handle
    stays in fixture closure so tests can't race teardown.
  - KeycloakConfig: token_endpoint is now a property; __repr__
    redacts client_secret.

Stream attribution:
  - MCP 2025-11-25 § Listening for Messages is explicit:
    server→client *requests* (roots/list, sampling/createMessage,
    elicitation/create) MUST NOT ride the standalone GET /mcp/
    stream; they travel on the POST-correlated stream. GAP-002..005
    and seven xfail reasons wrongly blamed #4205 (standalone-stream
    closure). Rewrote the Why sections and reasons to point at
    POST-correlated-stream relay instead.
  - GAP-011 (resource-subscription updates) is the only gap that
    legitimately points at #4205.
  - Added a canonical "Stream-attribution note" to COMPLIANCE_GAPS.md.
  - Filed GAP-011 (resource subscriptions) and GAP-012 (gateway
    returns -32000 where spec requires -32601).

Behavioral coverage (6 new tests + 4 reference-server witnesses):
  - notifications/cancelled end-to-end delivery.
  - logging/setLevel filter-threshold round-trip.
  - JSON-RPC reserved codes -32700 / -32601 / -32602.
  - tools/call argument-type validation (narrowed to McpError).
  - initialize-twice lifecycle rejection.
  - MCP-Protocol-Version mismatch handling.
  - Reference-server side-effect witnesses for mutate_resource_list /
    mutate_prompt_list / bump_subscribable notifications and the
    subscribe/unsubscribe handler round-trip.

  Each raw-httpx test fails on 5xx and accepts 4xx as HTTP-level
  rejection; no false-green paths where a server crash would read as
  compliance.

Bug fixes:
  - Reference server: mutator tools now use a dedicated
    _mutation_counter to prevent ephemeral_0 name-collisions.
  - test_drift._collect narrows to pytest.skip.Exception; other probe
    errors are recorded so drift diffs show the root cause.
  - Makefile PYTEST_IGNORE excludes tests/protocol_compliance.

Matrix rust_full slice: 58 passed / 0 failed / 40 xfailed / 0 xpassed
/ 11 skipped (was 53/0/34/0/11). Zero unexpected failures.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(e2e): align test_oauth_jwks_e2e JWT secret with gateway minimum

Test defaulted its JWT secret to "my-test-key" (11 chars) when
JWT_SECRET_KEY was unset; the gateway's validator requires ≥32 chars
so every admin-API call the fixtures make would fail with
401 "Invalid authentication credentials" on default deployments.

Import JWT_SECRET from helpers.mcp_test_helpers — the 40-char default
the rest of the e2e suite already shares — so this test self-configures
without requiring the operator to export JWT_SECRET_KEY. Overriding
via the env var still works.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* ci(pre-commit): exclude tests/**/targets/ from name-tests-test in the lite config

The main pre-commit config already excludes these POM-style support
directories, but CI runs the lite config (Makefile:4175 —
`pre-commit run --config .pre-commit-lite.yaml`) which was a
near-duplicate that hadn't been kept in sync. Add `targets` to its
excluded-dir alternation so the four compliance-harness target modules
stop tripping the hook in CI.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* chore(deps): pin authlib>=1.7.0 in the dev group via exclude-newer-package override

authlib is a transitive dep of fastmcp, which the compliance harness
uses as a test client — both live in the dev dependency group.
mcpgateway itself imports nothing from authlib, so the constraint goes
in `[dependency-groups] dev` rather than main project deps.

Release 1.7.0 (2026-04-18) ships security fixes but was blocked by the
repo-wide `exclude-newer = "10 days"` newness filter, leaving the
lockfile on 1.6.9. Adds an `exclude-newer-package` entry for authlib
(2026-04-19) so the filter allows the new release.

Signed-off-by: Jonathan Springer <jps@s390x.com>

---------

Signed-off-by: Jonathan Springer <jps@s390x.com>
brian-hussey pushed a commit that referenced this pull request May 5, 2026
* test(compliance): add MCP 2025-11-25 protocol compliance harness

Build a FastMCP-based test harness that probes ContextForge against the
MCP 2025-11-25 specification across three targets: reference stdio,
gateway proxy (federated), and gateway virtual server. Includes a new
reference server (mcp-servers/python/compliance_reference_server) with
tools/resources/prompts covering lifecycle, transport, pagination,
notifications, subscriptions, roots, sampling, elicitation, and logging.

Key additions:
- tests/protocol_compliance/: parametrized test suite with XPASS-capture,
  drift detection across targets, and gap-tracking workflow
- scripts/compliance_matrix.py: orchestrator that runs the suite across
  engine modes (python/shadow/edge/full) and aggregates results
- Runtime-mutable MCP mode tests covering shadow<->edge flip, boot-mode
  rails, publish/audit response contract, and data-plane runtime header
- OAuth 2.1 / Authorization tests against Keycloak (tier-gated skip)
- Security Best Practices probes (origin validation, bind address,
  confused-deputy)
- Rewrite tests/e2e/test_mcp_cli_protocol.py as test_mcp_protocol_e2e.py
  using FastMCP Client directly (no external CLI deps)
- Makefile: test-protocol-compliance* targets; testing-up folds in sso
  profile so Keycloak comes up with the gateway stack
- pre-commit: scope name-tests-test hook to tests/unit/ (integration,
  e2e, and compliance suites legitimately host non-test Python modules)

Closes #4273 (runtime-mutable MCP mode observability coverage).

Signed-off-by: Jonathan Springer <jps@s390x.com>

* refactor(tests): group test helpers under tests/*/helpers/ subdirs

Move loose non-test Python modules into conventional helpers/ subdirs
so the test-naming hook can exclude a single pattern instead of
enumerating each file.

Moves:
- tests/e2e/mcp_test_helpers.py -> tests/e2e/helpers/mcp_test_helpers.py
- tests/protocol_compliance/_helpers.py ->
  tests/protocol_compliance/helpers/compliance.py
- tests/protocol_compliance/_drift.py ->
  tests/protocol_compliance/helpers/drift.py

Updates all import sites across tests/e2e, tests/e2e_rust, and
tests/protocol_compliance.

Config cleanup:
- name-tests-test now excludes tests/*/(helpers|fixtures|targets|pages)/
  as a single verbose-regex alternative instead of file-by-file.
- Top-level pre-commit exclude and detect-secrets excludes converted
  to verbose-regex with inline comments.
- Dropped non-existent excludes (scripts/sign_image.sh, scripts/zap,
  sonar-project.properties) and the redundant .secrets.baseline
  entry (detect-secrets skips its own baseline automatically).
- detect-secrets hook in pre-commit now carries its own exclude list,
  kept in sync with the Makefile invocation.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(keycloak): enable service-accounts on the mcp-gateway dev realm client

The local Keycloak dev realm disables the client_credentials grant, so
compliance tests that exercise OAuth 2.1 token issuance against the
preconfigured `mcp-gateway` client fail with
`unauthorized_client: Client not enabled to retrieve service account`.

Flip `serviceAccountsEnabled` to `true` so fresh `docker compose` volume
imports come up with the grant available. Existing stacks can apply the
same change via the Keycloak Admin API against the running realm; the
stored volume overrides the import file once the realm exists, which is
why the change has to land here to stick across rebuilds.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* test(compliance): PR-review follow-ups — silent-failure fixes, behavioral coverage, type design, stream attribution

Bundles the fixes surfaced by running the pr-review-toolkit agents
(code-reviewer, pr-test-analyzer, comment-analyzer,
silent-failure-hunter, type-design-analyzer) plus two iterations of
Codex stop-review feedback.

Silent-failure fixes:
  - _parse_junit / _run_slice / stale-junit unlink /
    _wait_data_plane_converges return-value check close the
    "0/0/0/0 row" silent path in the matrix orchestrator.
  - Reference server stdout+stderr captured to a session tmp log; tail
    dumped on readiness timeout instead of swallowed.
  - flip_runtime_mode / _delete_if_exists / _wait_for_federation_sync
    / _probe_state / Keycloak token fetch surface last-status
    diagnostics instead of collapsing to None.
  - conftest fixture-resolution narrows catch-all so ImportError /
    NameError / SyntaxError from a broken fixture definition
    propagate; only runtime unreachability skips.
  - pytest_runtest_logreport wraps sidecar write in try/except so an
    unwriteable log doesn't poison the run.

Type design:
  - ComplianceTarget: __init_subclass__ enforces name and
    supported_transports on every concrete subclass; base client()
    validates transport support and dispatches to abstract
    _open_client(); per-subclass boilerplate deleted.
  - SliceResult: stored `ran` field removed (derive from
    skip_reason); __post_init__ rejects negative counts and the
    inconsistent "skipped slice with non-zero counts" state.
  - ReferenceUpstream: frozen, no raw Popen field — process handle
    stays in fixture closure so tests can't race teardown.
  - KeycloakConfig: token_endpoint is now a property; __repr__
    redacts client_secret.

Stream attribution:
  - MCP 2025-11-25 § Listening for Messages is explicit:
    server→client *requests* (roots/list, sampling/createMessage,
    elicitation/create) MUST NOT ride the standalone GET /mcp/
    stream; they travel on the POST-correlated stream. GAP-002..005
    and seven xfail reasons wrongly blamed #4205 (standalone-stream
    closure). Rewrote the Why sections and reasons to point at
    POST-correlated-stream relay instead.
  - GAP-011 (resource-subscription updates) is the only gap that
    legitimately points at #4205.
  - Added a canonical "Stream-attribution note" to COMPLIANCE_GAPS.md.
  - Filed GAP-011 (resource subscriptions) and GAP-012 (gateway
    returns -32000 where spec requires -32601).

Behavioral coverage (6 new tests + 4 reference-server witnesses):
  - notifications/cancelled end-to-end delivery.
  - logging/setLevel filter-threshold round-trip.
  - JSON-RPC reserved codes -32700 / -32601 / -32602.
  - tools/call argument-type validation (narrowed to McpError).
  - initialize-twice lifecycle rejection.
  - MCP-Protocol-Version mismatch handling.
  - Reference-server side-effect witnesses for mutate_resource_list /
    mutate_prompt_list / bump_subscribable notifications and the
    subscribe/unsubscribe handler round-trip.

  Each raw-httpx test fails on 5xx and accepts 4xx as HTTP-level
  rejection; no false-green paths where a server crash would read as
  compliance.

Bug fixes:
  - Reference server: mutator tools now use a dedicated
    _mutation_counter to prevent ephemeral_0 name-collisions.
  - test_drift._collect narrows to pytest.skip.Exception; other probe
    errors are recorded so drift diffs show the root cause.
  - Makefile PYTEST_IGNORE excludes tests/protocol_compliance.

Matrix rust_full slice: 58 passed / 0 failed / 40 xfailed / 0 xpassed
/ 11 skipped (was 53/0/34/0/11). Zero unexpected failures.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(e2e): align test_oauth_jwks_e2e JWT secret with gateway minimum

Test defaulted its JWT secret to "my-test-key" (11 chars) when
JWT_SECRET_KEY was unset; the gateway's validator requires ≥32 chars
so every admin-API call the fixtures make would fail with
401 "Invalid authentication credentials" on default deployments.

Import JWT_SECRET from helpers.mcp_test_helpers — the 40-char default
the rest of the e2e suite already shares — so this test self-configures
without requiring the operator to export JWT_SECRET_KEY. Overriding
via the env var still works.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* ci(pre-commit): exclude tests/**/targets/ from name-tests-test in the lite config

The main pre-commit config already excludes these POM-style support
directories, but CI runs the lite config (Makefile:4175 —
`pre-commit run --config .pre-commit-lite.yaml`) which was a
near-duplicate that hadn't been kept in sync. Add `targets` to its
excluded-dir alternation so the four compliance-harness target modules
stop tripping the hook in CI.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* chore(deps): pin authlib>=1.7.0 in the dev group via exclude-newer-package override

authlib is a transitive dep of fastmcp, which the compliance harness
uses as a test client — both live in the dev dependency group.
mcpgateway itself imports nothing from authlib, so the constraint goes
in `[dependency-groups] dev` rather than main project deps.

Release 1.7.0 (2026-04-18) ships security fixes but was blocked by the
repo-wide `exclude-newer = "10 days"` newness filter, leaving the
lockfile on 1.6.9. Adds an `exclude-newer-package` entry for authlib
(2026-04-19) so the filter allows the new release.

Signed-off-by: Jonathan Springer <jps@s390x.com>

---------

Signed-off-by: Jonathan Springer <jps@s390x.com>
Signed-off-by: Brian Hussey <brian.hussey@ie.ibm.com>
msureshkumar88 pushed a commit that referenced this pull request May 13, 2026
* test(compliance): add MCP 2025-11-25 protocol compliance harness

Build a FastMCP-based test harness that probes ContextForge against the
MCP 2025-11-25 specification across three targets: reference stdio,
gateway proxy (federated), and gateway virtual server. Includes a new
reference server (mcp-servers/python/compliance_reference_server) with
tools/resources/prompts covering lifecycle, transport, pagination,
notifications, subscriptions, roots, sampling, elicitation, and logging.

Key additions:
- tests/protocol_compliance/: parametrized test suite with XPASS-capture,
  drift detection across targets, and gap-tracking workflow
- scripts/compliance_matrix.py: orchestrator that runs the suite across
  engine modes (python/shadow/edge/full) and aggregates results
- Runtime-mutable MCP mode tests covering shadow<->edge flip, boot-mode
  rails, publish/audit response contract, and data-plane runtime header
- OAuth 2.1 / Authorization tests against Keycloak (tier-gated skip)
- Security Best Practices probes (origin validation, bind address,
  confused-deputy)
- Rewrite tests/e2e/test_mcp_cli_protocol.py as test_mcp_protocol_e2e.py
  using FastMCP Client directly (no external CLI deps)
- Makefile: test-protocol-compliance* targets; testing-up folds in sso
  profile so Keycloak comes up with the gateway stack
- pre-commit: scope name-tests-test hook to tests/unit/ (integration,
  e2e, and compliance suites legitimately host non-test Python modules)

Closes #4273 (runtime-mutable MCP mode observability coverage).

Signed-off-by: Jonathan Springer <jps@s390x.com>

* refactor(tests): group test helpers under tests/*/helpers/ subdirs

Move loose non-test Python modules into conventional helpers/ subdirs
so the test-naming hook can exclude a single pattern instead of
enumerating each file.

Moves:
- tests/e2e/mcp_test_helpers.py -> tests/e2e/helpers/mcp_test_helpers.py
- tests/protocol_compliance/_helpers.py ->
  tests/protocol_compliance/helpers/compliance.py
- tests/protocol_compliance/_drift.py ->
  tests/protocol_compliance/helpers/drift.py

Updates all import sites across tests/e2e, tests/e2e_rust, and
tests/protocol_compliance.

Config cleanup:
- name-tests-test now excludes tests/*/(helpers|fixtures|targets|pages)/
  as a single verbose-regex alternative instead of file-by-file.
- Top-level pre-commit exclude and detect-secrets excludes converted
  to verbose-regex with inline comments.
- Dropped non-existent excludes (scripts/sign_image.sh, scripts/zap,
  sonar-project.properties) and the redundant .secrets.baseline
  entry (detect-secrets skips its own baseline automatically).
- detect-secrets hook in pre-commit now carries its own exclude list,
  kept in sync with the Makefile invocation.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(keycloak): enable service-accounts on the mcp-gateway dev realm client

The local Keycloak dev realm disables the client_credentials grant, so
compliance tests that exercise OAuth 2.1 token issuance against the
preconfigured `mcp-gateway` client fail with
`unauthorized_client: Client not enabled to retrieve service account`.

Flip `serviceAccountsEnabled` to `true` so fresh `docker compose` volume
imports come up with the grant available. Existing stacks can apply the
same change via the Keycloak Admin API against the running realm; the
stored volume overrides the import file once the realm exists, which is
why the change has to land here to stick across rebuilds.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* test(compliance): PR-review follow-ups — silent-failure fixes, behavioral coverage, type design, stream attribution

Bundles the fixes surfaced by running the pr-review-toolkit agents
(code-reviewer, pr-test-analyzer, comment-analyzer,
silent-failure-hunter, type-design-analyzer) plus two iterations of
Codex stop-review feedback.

Silent-failure fixes:
  - _parse_junit / _run_slice / stale-junit unlink /
    _wait_data_plane_converges return-value check close the
    "0/0/0/0 row" silent path in the matrix orchestrator.
  - Reference server stdout+stderr captured to a session tmp log; tail
    dumped on readiness timeout instead of swallowed.
  - flip_runtime_mode / _delete_if_exists / _wait_for_federation_sync
    / _probe_state / Keycloak token fetch surface last-status
    diagnostics instead of collapsing to None.
  - conftest fixture-resolution narrows catch-all so ImportError /
    NameError / SyntaxError from a broken fixture definition
    propagate; only runtime unreachability skips.
  - pytest_runtest_logreport wraps sidecar write in try/except so an
    unwriteable log doesn't poison the run.

Type design:
  - ComplianceTarget: __init_subclass__ enforces name and
    supported_transports on every concrete subclass; base client()
    validates transport support and dispatches to abstract
    _open_client(); per-subclass boilerplate deleted.
  - SliceResult: stored `ran` field removed (derive from
    skip_reason); __post_init__ rejects negative counts and the
    inconsistent "skipped slice with non-zero counts" state.
  - ReferenceUpstream: frozen, no raw Popen field — process handle
    stays in fixture closure so tests can't race teardown.
  - KeycloakConfig: token_endpoint is now a property; __repr__
    redacts client_secret.

Stream attribution:
  - MCP 2025-11-25 § Listening for Messages is explicit:
    server→client *requests* (roots/list, sampling/createMessage,
    elicitation/create) MUST NOT ride the standalone GET /mcp/
    stream; they travel on the POST-correlated stream. GAP-002..005
    and seven xfail reasons wrongly blamed #4205 (standalone-stream
    closure). Rewrote the Why sections and reasons to point at
    POST-correlated-stream relay instead.
  - GAP-011 (resource-subscription updates) is the only gap that
    legitimately points at #4205.
  - Added a canonical "Stream-attribution note" to COMPLIANCE_GAPS.md.
  - Filed GAP-011 (resource subscriptions) and GAP-012 (gateway
    returns -32000 where spec requires -32601).

Behavioral coverage (6 new tests + 4 reference-server witnesses):
  - notifications/cancelled end-to-end delivery.
  - logging/setLevel filter-threshold round-trip.
  - JSON-RPC reserved codes -32700 / -32601 / -32602.
  - tools/call argument-type validation (narrowed to McpError).
  - initialize-twice lifecycle rejection.
  - MCP-Protocol-Version mismatch handling.
  - Reference-server side-effect witnesses for mutate_resource_list /
    mutate_prompt_list / bump_subscribable notifications and the
    subscribe/unsubscribe handler round-trip.

  Each raw-httpx test fails on 5xx and accepts 4xx as HTTP-level
  rejection; no false-green paths where a server crash would read as
  compliance.

Bug fixes:
  - Reference server: mutator tools now use a dedicated
    _mutation_counter to prevent ephemeral_0 name-collisions.
  - test_drift._collect narrows to pytest.skip.Exception; other probe
    errors are recorded so drift diffs show the root cause.
  - Makefile PYTEST_IGNORE excludes tests/protocol_compliance.

Matrix rust_full slice: 58 passed / 0 failed / 40 xfailed / 0 xpassed
/ 11 skipped (was 53/0/34/0/11). Zero unexpected failures.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix(e2e): align test_oauth_jwks_e2e JWT secret with gateway minimum

Test defaulted its JWT secret to "my-test-key" (11 chars) when
JWT_SECRET_KEY was unset; the gateway's validator requires ≥32 chars
so every admin-API call the fixtures make would fail with
401 "Invalid authentication credentials" on default deployments.

Import JWT_SECRET from helpers.mcp_test_helpers — the 40-char default
the rest of the e2e suite already shares — so this test self-configures
without requiring the operator to export JWT_SECRET_KEY. Overriding
via the env var still works.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* ci(pre-commit): exclude tests/**/targets/ from name-tests-test in the lite config

The main pre-commit config already excludes these POM-style support
directories, but CI runs the lite config (Makefile:4175 —
`pre-commit run --config .pre-commit-lite.yaml`) which was a
near-duplicate that hadn't been kept in sync. Add `targets` to its
excluded-dir alternation so the four compliance-harness target modules
stop tripping the hook in CI.

Signed-off-by: Jonathan Springer <jps@s390x.com>

* chore(deps): pin authlib>=1.7.0 in the dev group via exclude-newer-package override

authlib is a transitive dep of fastmcp, which the compliance harness
uses as a test client — both live in the dev dependency group.
mcpgateway itself imports nothing from authlib, so the constraint goes
in `[dependency-groups] dev` rather than main project deps.

Release 1.7.0 (2026-04-18) ships security fixes but was blocked by the
repo-wide `exclude-newer = "10 days"` newness filter, leaving the
lockfile on 1.6.9. Adds an `exclude-newer-package` entry for authlib
(2026-04-19) so the filter allows the new release.

Signed-off-by: Jonathan Springer <jps@s390x.com>

---------

Signed-off-by: Jonathan Springer <jps@s390x.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants