Mirror of https://github.com/modelcontextprotocol/conformance — Conformance Tests for MCP
  • TypeScript 98.1%
  • JavaScript 1.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Robin Otto 1ca3bc301b
feat(auth): client conformance for Workload Identity Federation (SEP-1933) (#268)
* feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933)

Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable,
tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario
(PR #2). Also extracts JWT_BEARER_GRANT_TYPE constant and migrates
cross-app-access.ts to use it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933)

Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable,
tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario
(PR #2). Also extracts JWT_BEARER_GRANT_TYPE constant and migrates
cross-app-access.ts to use it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auth): add WIF JWT-bearer scenario and negative tests (SEP-1933)

Adds the auth/wif-jwt-bearer client conformance scenario using the RFC 7523
JWT-bearer grant (urn:ietf:params:oauth:grant-type:jwt-bearer).

The scenario pre-signs valid, wrong-audience, and expired JWTs on start()
to simulate cloud workload identity tokens. The conformance AS verifies the
assertion and emits per-class checks (wif-assertion-verified,
wif-assertion-missing, wif-assertion-audience, wif-assertion-expired,
wif-assertion-malformed). Broken example clients exercise the missing-assertion
and wrong-audience failure paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: apply prettier formatting to wif-jwt-bearer.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): add expired-assertion negative test; clarify wif-jwt-bearer comments

- Add runWifJwtBearerExpiredAssertion + auth-test-wif-expired-assertion.ts
  to exercise the wif-assertion-expired check path (was dead code)
- Clarify WifAssertionVerified description to note iss is not validated
- Add comment explaining clockTolerance: 5 is intentional (same-run keypair)
- Add comment explaining numeric expiresIn is absolute epoch seconds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): disable DCR, detect retries, improve getChecks description

- Add WIF_CLIENT_ID constant; pre-seed provider's clientInformation() so
  the SDK skips Dynamic Client Registration entirely (disableDynamicRegistration
  on the auth server + pre-seeded client_id on the provider side)
- Add failedOnce/tokenRequestReceived tracking on the scenario; reject and
  record wif-no-retry FAILURE if the client attempts a second token request
  after the first fails
- Add hasAttempted guard in WifJwtBearerProvider.prepareTokenRequest() to
  throw on retry from the client side
- Fix getChecks() sentinel description: distinguish "no request made" from
  "request made but verification failed"
- Add client_id to context and Zod schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: prettier formatting on everything-client.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): let errors propagate from broken example clients

The try/catch blocks were silently swallowing auth failures, so the
negative tests passed purely because expectedFailureSlugs found the
AS-emitted check — not because client error-surfacing was verified.
allowClientError: true on the test cases handles the non-zero exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): tag as draft, add scope-rejected check, add SEP traceability

- Change specVersions from ['extension'] to [DRAFT_PROTOCOL_VERSION] and add
  source = { introducedIn: DRAFT_PROTOCOL_VERSION } so the scenario is
  reachable via --spec-version draft (extension tag excluded it from all
  spec-version runs)
- Move registration from extensionScenariosList to draftScenariosList in
  index.ts to match the tag/list convention enforced by spec-version.test.ts
- Add wif-assertion-scope-rejected check: AS returns invalid_scope for a
  valid JWT when the client requests the reserved 'wif.rejected' scope;
  verify client surfaces the error and does not retry
- Add runWifJwtBearerScopeRejected to everything-client.ts; add optional
  scope param to WifJwtBearerProvider; add CLI entry point and vitest case
- Add src/seps/sep-1933.yaml with requirements mapped to each check ID,
  covering the three deferred checks (iss, sub, jti) with exclusion rationale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: prettier formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wif-jwt-bearer): add wif-grant-fallback check for unauthorized_client fallback

Client bug class: WIF client receives unauthorized_client from JWT-bearer grant
and silently switches to authorization_code instead of surfacing the error.

The MCP SDK retries after UnauthorizedClientError (auth.js:152-154), calling
prepareTokenRequest() a second time. WifGrantFallbackProvider exploits this by
returning authorization_code params on the second call.

Spec anchor: RFC 7523 §2.1 — clients MUST use the JWT-bearer grant type;
silent grant-type switching hides authentication failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: rename unused scope param in WifGrantFallbackProvider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wif-jwt-bearer): use realistic SPIFFE and K8s PSAT token formats

valid_jwt uses a SPIFFE JWT-SVID subject (spiffe://conformance-test.local/...)
issued by a SPIRE-style issuer. wrong_audience_jwt uses a Kubernetes PSAT
subject and kubernetes.io claims, mirroring how K8s projected service-account
tokens look in production. Token verification and client behaviour are
unchanged; the formats ground the scenario in real workload identity platforms.

k8s_issuer and k8s_subject added to context and schema for external clients
that want to construct their own K8s-style assertions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: prettier formatting on wif-jwt-bearer.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(wif-jwt-bearer): drop informational k8s_issuer/k8s_subject from context

These fields were not used by any client handler and are redundant since
the K8s claims are already baked into wrong_audience_jwt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): address pre-merge review feedback

- Downgrade wif-no-retry, wif-grant-fallback, and wif-assertion-scope-rejected
  to WARNING; RFC 7523 is silent on client retry and grant-type switching, so
  FAILURE status was not traceable to spec text
- Add WifRetryProvider broken client that re-sends JWT-bearer after
  unauthorized_client, making wif-no-retry actually exercisable as a WARNING
- Drop getChecks() sentinel (non-standard pattern that overloaded the
  wif-assertion-verified check ID); test harness detects empty check sets
- Add comment on DRAFT_PROTOCOL_VERSION workaround for runner limitation
- Add comment explaining why both slash/no-slash audience forms are accepted
- Tighten schema comments for audience URL constraint and ES256 literal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): drop decorative fields and neutralise JWT fixtures

- Remove issuer and subject from context schema and scenario return value;
  neither is validated by the AS or used by the client, making them
  misleading. The iss exclusion rationale is updated to explain why.
- Replace SPIFFE/K8s-PSAT constants and the kubernetes.io additionalClaims
  with neutral values across all three JWT fixtures; no check inspects
  token format, so format flavour was decorative and invited false
  assumptions about fixture semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): remove workaround comment, drop signing_algorithm, annotate retry provider

- Remove self-admitted workaround comment on DRAFT_PROTOCOL_VERSION; the
  classification stands on its own and the comment was inviting challenge
- Drop signing_algorithm from context schema and scenario return; the field
  was redundant once the value was fixed to ES256, and the assertion is opaque
  to the client regardless
- Add comment to WifRetryProvider explaining it deliberately omits the
  hasAttempted guard so the SDK retry reaches the AS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): export scope constants, remove unused AS field, annotate state machine

- Export WIF_TRIGGER_UNAUTHORIZED_SCOPE and WIF_REJECTED_SCOPE from
  createWorkloadJwt.ts and import them in everything-client.ts; removes
  duplicate bare-string declarations that could silently diverge
- Remove tokenEndpointAuthSigningAlgValuesSupported from mock AS config;
  the SDK does not consume this field in the JWT-bearer flow
- Add comment to onTokenRequest clarifying wif-no-retry and wif-grant-fallback
  fire on any second request after any failure, not only post-unauthorized_client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): address pcarleton review comments

- Move all broken WIF provider classes and runners to wif-broken-clients.ts;
  everything-client.ts now contains only the conformant WifJwtBearerProvider
- Add sentinel in getChecks() for !tokenRequestReceived to prevent a silent
  pass when the client never sends a request
- Drop wif-assertion-scope-rejected check; the no-retry behaviour after
  invalid_scope is already covered by wif-no-retry, and sep-1933.yaml updated
  accordingly
- Remove audience from context schema and scenario return; the field was
  unused by any client implementation
- Expand scenario description to tell a client implementer what to implement
- Apply suggested description fix to wif-grant-fallback (does not assume
  unauthorized_client was the specific error that preceded the fallback)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(wif-jwt-bearer): drop number type from expiresIn, use jose duration strings

Jose supports negative duration strings ('-60s', '60 seconds ago') that produce
already-expired tokens without needing absolute epoch arithmetic. Drop the
number overload, update the expired_jwt call site to '-60s', and update the
unit test accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:13:14 +00:00
.claude/skills docs: add review-scenario skill for SEP scenario PR review (#276) 2026-05-15 14:38:23 +01:00
.github/workflows ci(traceability): tolerate SDK conformance failures in the run step (#290) 2026-05-19 23:06:39 +01:00
examples feat(auth): client conformance for Workload Identity Federation (SEP-1933) (#268) 2026-07-02 13:13:14 +00:00
scripts feat: version-aware Connection abstraction for server scenarios (#318) 2026-06-01 17:09:13 +01:00
src feat(auth): client conformance for Workload Identity Federation (SEP-1933) (#268) 2026-07-02 13:13:14 +00:00
.gitattributes chore: enforce LF line endings for x-plat consistency (#74) 2025-12-04 17:32:07 +00:00
.gitignore feat: version-aware Connection abstraction for server scenarios (#318) 2026-06-01 17:09:13 +01:00
.prettierignore feat: version-aware Connection abstraction for server scenarios (#318) 2026-06-01 17:09:13 +01:00
.prettierrc.json chore: enforce LF line endings for x-plat consistency (#74) 2025-12-04 17:32:07 +00:00
action.yml fix: use $GITHUB_ACTION_PATH to avoid bash backslash escaping on Windows runners (#298) 2026-06-24 15:46:09 +01:00
AGENTS.md fix: fail checks whose prerequisite is missing instead of skipping them (#372) 2026-06-30 11:42:01 +01:00
CONTRIBUTING.md doc: Add example of testing against TS SDK (#237) 2026-04-24 11:45:09 +01:00
eslint.config.mjs adjust prettier to use more defaults (#8) 2025-11-04 15:34:27 +00:00
lefthook-local.example.yml Add sep-835 limit retry scenario, add suite options to server command and update pending scenarios (#49) 2025-11-21 16:49:38 +00:00
lefthook.yml Add sep-835 limit retry scenario, add suite options to server command and update pending scenarios (#49) 2025-11-21 16:49:38 +00:00
LICENSE chore: update licensing to Apache 2.0 for new contributions (#100) 2026-01-19 16:47:12 +00:00
MAINTAINERS.md docs: add MAINTAINERS.md and remove experimental warning (#199) 2026-03-27 19:25:07 +00:00
package-lock.json chore: bump version to 0.2.0-alpha.9 (#377) 2026-07-01 19:09:47 +01:00
package.json chore: bump version to 0.2.0-alpha.9 (#377) 2026-07-01 19:09:47 +01:00
README.md fix: update the draft wire version to 2026-07-28 (#331) 2026-06-08 16:47:17 +01:00
SDK_INTEGRATION.md docs: add SDK integration guide and fix spec references (#121) 2026-01-23 12:50:11 +00:00
tsconfig.json chore(deps): bump the all-dependencies group across 1 directory with 10 updates (#252) 2026-04-24 12:51:39 +01:00
vitest.config.ts Split draft-spec scenarios into their own suite (#317) 2026-05-26 18:00:44 +01:00

MCP Conformance Test Framework

A framework for testing MCP (Model Context Protocol) client and server implementations against the specification.

For SDK maintainers: See SDK Integration Guide for a streamlined guide on integrating conformance tests into your SDK repository.

Quick Start

Testing Clients

# Using the everything-client (recommended)
npx @modelcontextprotocol/conformance client --command "tsx examples/clients/typescript/everything-client.ts" --scenario initialize

# Run an entire suite of tests
npx @modelcontextprotocol/conformance client --command "tsx examples/clients/typescript/everything-client.ts" --suite auth

Testing Servers

# Run all server scenarios (default)
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp

# Run a single scenario
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --scenario server-initialize

List Available Scenarios

npx @modelcontextprotocol/conformance list

Overview

The conformance test framework validates MCP implementations by:

For Clients:

  1. Starting a test server for the specified scenario
  2. Running the client implementation with the test server URL
  3. Capturing MCP protocol interactions
  4. Running conformance checks against the specification
  5. Generating detailed test results

For Servers:

  1. Connecting to the running server as an MCP client
  2. Sending test requests and capturing responses
  3. Running conformance checks against server behavior
  4. Generating detailed test results

Usage

Client Testing

npx @modelcontextprotocol/conformance client --command "<client-command>" --scenario <scenario-name> [options]

Options:

  • --command - The command to run your MCP client (can include flags)
  • --scenario - The test scenario to run (e.g., "initialize")
  • --suite - Run a suite of tests in parallel: all, core, extensions, backcompat, auth, metadata, draft (scenarios targeting the in-progress draft spec), or sep-835
  • --spec-version <version> - Filter scenarios by spec version (e.g., 2025-11-25, 2026-07-28; draft is accepted as an alias for the current draft identifier). The draft version selects the latest dated release plus any draft-only scenarios. When omitted, the version is inferred from the scenario's spec applicability (draft-only scenarios run at the draft version, everything else at the latest dated release); an explicitly requested version outside a scenario's applicability window skips the scenario (exit 0) unless --force is passed
  • --force - Run a scenario even if it is not applicable at the requested --spec-version
  • --expected-failures <path> - Path to YAML baseline file of known failures (see Expected Failures)
  • --timeout - Timeout in milliseconds (default: 30000)
  • --verbose - Show verbose output

The framework appends <server-url> as an argument to your command and sets the MCP_CONFORMANCE_SCENARIO environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the MCP_CONFORMANCE_CONTEXT environment variable contains a JSON object with scenario-specific data. When --spec-version is passed, its resolved value is forwarded to the client process as MCP_CONFORMANCE_PROTOCOL_VERSION; example clients can use this value directly as their protocolVersion. SDKs that hard-code their protocol version can ignore it. Clients under test must derive the lifecycle from the protocol version they are asked to run: dated versions through 2025-11-25 use the stateful lifecycle (initialize handshake), while the 2026 draft (2026-07-28) uses the stateless lifecycle (per-request _meta).

Server Testing

npx @modelcontextprotocol/conformance server --url <url> [--scenario <scenario>]

Options:

  • --url - URL of the server to test
  • --scenario <scenario> - Test scenario to run (e.g., "server-initialize"). Runs all available scenarios by default
  • --suite <suite> - Suite to run: "active" (default; excludes pending and draft-spec scenarios), "all", "draft" (scenarios targeting the in-progress draft spec), or "pending"
  • --expected-failures <path> - Path to YAML baseline file of known failures (see Expected Failures)
  • --verbose - Show verbose output

Test Results

Client Testing - Results are saved to results/<scenario>-<timestamp>/:

  • checks.json - Array of conformance check results with pass/fail status
  • stdout.txt - Client stdout output
  • stderr.txt - Client stderr output

Server Testing - Results are saved to results/server-<scenario>-<timestamp>/:

  • checks.json - Array of conformance check results with pass/fail status

Expected Failures

SDKs that don't yet pass all conformance tests can specify a baseline of known failures. This allows running conformance tests in CI without failing, while still catching regressions.

Create a YAML file listing expected failures by mode:

# conformance-baseline.yml
server:
  - tools-call-with-progress
  - resources-subscribe
client:
  - sse-retry

Then pass it to the CLI:

npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --expected-failures ./conformance-baseline.yml

Exit code behavior:

Scenario Result In Baseline? Outcome
Fails Yes Exit 0 — expected failure
Fails No Exit 1 — unexpected regression
Passes Yes Exit 1 — stale baseline, remove the entry
Passes No Exit 0 — normal pass

This ensures:

  • CI passes when only known failures occur
  • CI fails on new regressions (unexpected failures)
  • CI fails when a fix lands but the baseline isn't updated (stale entries)

GitHub Action

This repo provides a composite GitHub Action so SDK repos don't need to write their own conformance scripts.

Server Testing

steps:
  - uses: actions/checkout@v4

  # Start your server (SDK-specific)
  - run: |
      my-server --port 3001 &
      timeout 15 bash -c 'until curl -s http://localhost:3001/mcp; do sleep 0.5; done'

  - uses: modelcontextprotocol/conformance@v0.1.11
    with:
      mode: server
      url: http://localhost:3001/mcp
      expected-failures: ./conformance-baseline.yml # optional

Client Testing

steps:
  - uses: actions/checkout@v4

  - uses: modelcontextprotocol/conformance@v0.1.11
    with:
      mode: client
      command: 'python tests/conformance/client.py'
      expected-failures: ./conformance-baseline.yml # optional

Action Inputs

Input Required Description
mode Yes server or client
url Server mode URL of the server to test
command Client mode Command to run the client under test
expected-failures No Path to YAML baseline file
suite No Test suite to run
scenario No Run a single scenario by name
timeout No Timeout in ms for client tests (default: 30000)
verbose No Show verbose output (default: false)
node-version No Node.js version (default: 20)

Example Clients

  • examples/clients/typescript/everything-client.ts - Single client that handles all scenarios based on scenario name (recommended)
  • examples/clients/typescript/test1.ts - Simple MCP client (for reference)
  • examples/clients/typescript/auth-test.ts - Well-behaved OAuth client (for reference)

Available Scenarios

Client Scenarios

  • initialize - Tests MCP client initialization handshake
    • Validates protocol version
    • Validates clientInfo (name and version)
    • Validates server response handling
  • tools-call - Tests tool invocation
  • auth/basic-dcr - Tests OAuth Dynamic Client Registration flow
  • auth/basic-metadata-var1 - Tests OAuth with authorization metadata

Server Scenarios

Run npx @modelcontextprotocol/conformance list --server to see all available server scenarios, including:

  • server-initialize - Tests server initialization and capabilities
  • tools-list - Tests tool listing endpoint
  • tools-call-* - Various tool invocation scenarios
  • resources-* - Resource management scenarios
  • prompts-* - Prompt management scenarios

Running Against an SDK at a Specific Ref

The sdk subcommand clones an SDK repository at a given ref, builds it, and runs the local conformance build against it. This is the inner-loop tool for scenario authors and the basis for cross-SDK CI. Examples below use npm start -- so they run from source — no npm run build between edits.

--mode client or --mode server is required — each invocation tests exactly one side, so client and server are run (and pass/fail) independently.

# Run the client conformance suite against typescript-sdk @main (v2)
npm start -- sdk typescript-sdk --mode client

# Run the server conformance suite (separate invocation)
npm start -- sdk typescript-sdk --mode server

# A specific main-line SHA or branch (v2 monorepo)
npm start -- sdk typescript-sdk@abc123f --mode client
npm start -- sdk typescript-sdk@some-branch --mode server

# The published v1.x line — separate entry (npm build), defaults to the v1.x branch
npm start -- sdk typescript-sdk-v1 --mode client
npm start -- sdk typescript-sdk-v1@v1.29.0 --mode server

# Use an existing local checkout (no clone, no fetch)
npm start -- sdk --path ../typescript-sdk --skip-build --mode client

# Narrow to one scenario / suite
npm start -- sdk --path ../typescript-sdk --mode server --scenario server-initialize
npm start -- sdk typescript-sdk --mode client --suite auth

# Target a specific spec version (passed through to the underlying run).
# When omitted, the SDK's `specVersion` from KNOWN_SDKS is used, if set —
# e.g. typescript-sdk-v1 defaults to 2025-11-25.
npm start -- sdk typescript-sdk --mode client --spec-version draft

Build/run commands for each official SDK are looked up by name from src/sdk-runner/known-sdks.ts — no config file is required in the SDK repo. Resolution order is CLI flag > built-in entry, so any field can be overridden on the command line for refs that diverge from the built-in.

An SDK can have more than one entry when its layout differs across major versions — e.g. typescript-sdk (v2, the main monorepo) and typescript-sdk-v1 (the published npm v1.x line). An entry may set defaultRef (the branch used when you don't pass @<ref>) and repo (the real clone target when the entry name is an alias). Overriding for a one-off ref:

npm start -- sdk owner/go-sdk@some-branch \
  --mode client \
  --build-cmd 'go build -tags mcp_go_client_oauth -o ./.conformance-client ./conformance/everything-client' \
  --client-cmd './.conformance-client'

To add a new SDK to the matrix, add an entry to KNOWN_SDKS.

Clones are cached under .sdk-under-test/ and reused (fetched) on subsequent runs.

SDK Tier Assessment

The tier-check subcommand evaluates an MCP SDK repository against SEP-1730 (the SDK Tiering System):

# Without conformance tests (fastest)
gh auth login
npm run --silent tier-check -- --repo modelcontextprotocol/typescript-sdk --skip-conformance

# With conformance tests (start the everything server first)
npm run --silent tier-check -- \
  --repo modelcontextprotocol/typescript-sdk \
  --conformance-server-url http://localhost:3000/mcp

For a full AI-assisted assessment with remediation guide, use Claude Code:

/mcp-sdk-tier-audit <local-sdk-path> <conformance-server-url>

See .claude/skills/mcp-sdk-tier-audit/README.md for full documentation.

Architecture

See src/runner/DESIGN.md for detailed architecture documentation.

Key Components

  • Runner (src/runner/) - Orchestrates test execution and result generation
    • client.ts - Client testing implementation
    • server.ts - Server testing implementation
    • utils.ts - Shared utilities
    • index.ts - Public API exports
  • CLI (src/index.ts) - Command-line interface using Commander.js
  • Scenarios (src/scenarios/) - Test scenarios with expected behaviors
  • Checks (src/checks/) - Conformance validation functions
  • Types (src/types.ts) - Shared type definitions

Adding New Scenarios

  1. Create a new directory in src/scenarios/<scenario-name>/
  2. Implement the Scenario interface with start(), stop(), and getChecks()
  3. Register the scenario in src/scenarios/index.ts

See src/scenarios/initialize/ for a reference implementation.