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
claude[bot] 89c2c0452e
Validate all wire messages against the per-version spec JSON schema (#399)
* Vendor schema/{version}/schema.json alongside schema.ts

sync-schema now copies the spec repo's JSON Schema for each version into
src/spec-types/{version}.schema.json (same pinned SHA recorded in SOURCE:
modelcontextprotocol@f817239f4d6b1efff2c4dfc2f7af85c985d73076). These are
the inputs for runtime wire-message validation. Vendored verbatim, so they
join schema.ts in .prettierignore.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHNrpU3Vr6Q1rPZiji7zLY

* Validate all wire messages against the per-version spec JSON schema

Every JSON-RPC message the harness sends or receives is now validated
(ajv) against the vendored schema.json for the run's spec version, at the
wire choke points: sendStatelessRequest (outbound request, JSON response,
SSE events), connectStateful (request/result/notifications), and both mock
servers (inbound client requests, outbound responses/handler results).

Beyond the JSONRPCMessage envelope, messages are validated against the
most specific definition the schema has: typed requests/notifications by
method const, typed error responses by error.code const (e.g. -32021 ->
MissingRequiredClientCapabilityError, the #376 hallucination), and typed
results via the XxxRequest -> XxxResult pair, with SEP-2322
InputRequiredResult discriminated on resultType.

Violations are recorded per scenario and surfaced by the runners as a
synthetic wire-schema-valid check (FAILURE when the implementation under
test sent an invalid message) plus a wire-schema-harness-error check when
the invalid message was harness-authored. A vitest-wide afterEach guard
fails any self-test that recorded violations, so hallucinated fixtures
can't ship (#376, #271). Opt-outs are explicit and greppable: per-call
skipValidation for deliberately malformed traffic, takeWireViolations()
drains in tests that exercise deliberately broken fixtures.

Offenders the new validation caught, now fixed:
- everything-server stateless path omitted resultType (and the caching
  hints on server/discover) required by the draft schema
- sep-2164-empty-contents fixture omitted resultType/ttlMs/cacheScope
  (its intended violation, empty contents, is schema-valid prose-only)
- connection/mock-server unit-test fixtures used schema-invalid stubs
  (tools/call without name, ListToolsResult without required members,
  Tool without inputSchema, ProgressNotification without progressToken,
  ElicitRequest without message/requestedSchema)
- http-standard-headers test mocks returned bare { tools: [] } results
- input-required-result-validate-input deliberately sends malformed
  inputResponses; now marked skipValidation

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHNrpU3Vr6Q1rPZiji7zLY

* Validate the stateful wire at the SDK transport, close review-panel gaps

Address the review-panel findings on the wire-schema validation PR:

- Stateful wire: hook the SDK transport inside connectToServer, wrapping
  send/onmessage so every raw wire message in both directions is
  validated for the scenario's spec version -- including the initialize
  handshake, server->client requests (elicitation/sampling), and the
  harness's responses to them. Request ids are correlated per direction
  so responses validate against their typed result definition (e.g. an
  outbound ElicitResult). The reconstructed stand-ins in
  connectStateful (fake id: 0 envelopes, Zod-reparsed results) are
  removed; the hook sees the real bytes. The four scenarios that call
  connectToServer directly (elicitation-defaults, elicitation-enums,
  tools, lifecycle) now pass ctx.specVersion and get validation for
  free; the runner surfaces their violations through the existing
  recorder. Known remaining gap (documented in the module doc): the
  client-auth scenarios' bespoke express mock is not instrumented.

- skipValidation now skips only the request direction; the
  implementation's response to a deliberately malformed request is
  still validated, with a narrow carve-out tolerating the JSON-RPC 2.0
  id: null an error response must carry when the request could not be
  processed (the MCP schema's RequestId forbids null). Covered by new
  unit tests pinning both the still-validated response and the
  narrowness of the carve-out.

- negative-mrtr.test.ts: the file-wide drain now asserts every recorded
  violation is implementation-origin (the deliberately broken fixture)
  and throws on any harness-origin violation.

- everything-server: hoist the cacheable-methods set to module scope
  and replace the per-request res.json monkey-patch with an explicit
  sendStatelessJson(res, method, payload) helper applied at each
  dispatch site.

- Pin the extracted dispatch maps (methodDefs/errorDefs, e.g.
  -32021 -> MissingRequiredClientCapabilityError under the draft) per
  spec version in wire-schema.test.ts, so a schema sync that breaks the
  const extraction fails loudly instead of degrading to envelope-only
  validation. Also note the batch branch's requestMethod forwarding
  limitation.

Offender the transport hook flushed out: the everything-server's
test_elicitation_sep1034_defaults tool sent a number-typed elicitation
field with default: 95.5, which the 2025-11-25 schema forbids
(NumberSchema.default is typed integer there; widened to number only in
the draft). The fixture and the scenario's expected default are now the
integer-valued 95, with comments explaining why.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHNrpU3Vr6Q1rPZiji7zLY

* Condense comments to three lines max

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHNrpU3Vr6Q1rPZiji7zLY

* Re-vendor spec schemas at pinned SHA 71e30695

Regenerate src/spec-types/*.schema.json via npm run sync-schema at the
SHA already recorded in SOURCE. The previously vendored draft schema
predated spec PR #3002, so it still required DiscoverResult.serverInfo
at the top level; the mock server now correctly reports serverInfo via
result _meta, which the stale schema rejected.

* Scope the wire-violation recorder per scenario for parallel suite runs

Client --suite runs execute scenarios concurrently over what was a
module-global violation recorder, so scenarios could steal or wipe each
other's recorded violations. Give each suite scenario its own recorder
via AsyncLocalStorage; code outside a scope (single-scenario runs, the
sequential server suite, vitest hooks) falls back to the global
recorder. Also condense a four-line comment in connection/stateful.ts
to three lines.

* Patch the 2025-11-25 NumberSchema generator bug as a load-time erratum

The released 2025-11-25 schema.json types NumberSchema
minimum/maximum/default as integer, contradicting its own schema.ts
(default?: number) — a typescript-json-schema artifact, fixed for draft
in modelcontextprotocol#2710 and proposed for 2025-11-25 in
modelcontextprotocol#3139. Patch the schema at load time so SDK
fixtures with fractional number defaults (all tier-1 SDKs send 95.5)
are not flagged; restore the sep-1034 scenario and example-server
fixture to 95.5. Delete the erratum when #3139 lands and the schemas
are re-vendored.

Also scope the sequential server-suite loop with withWireRecorder so a
connection leaked by a failed scenario cannot attribute late traffic to
the next scenario.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>
2026-07-27 15:58:43 +01: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 Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +01:00
scripts Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +01:00
src Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +01: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 Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +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 Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +01:00
package.json Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +01:00
README.md feat(sdk-runner): expand KNOWN_SDKS to all tier 1 SDKs (#417) 2026-07-27 14:54:45 +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 Validate all wire messages against the per-version spec JSON schema (#399) 2026-07-27 15:58:43 +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)

Baselining a single check

A scenario is many checks — server-stateless alone is over twenty — so baselining the whole scenario to excuse one of them stops enforcing the other nineteen. An entry can instead name a single check, as <scenario>:<check-id>:

server:
  - tasks-lifecycle # whole scenario may fail
  - server-stateless:sep-2575-server-implements-discover # only this check may fail

The check id is the left-hand column the runner already prints for each check, so it can be copied straight out of a failing run.

With a per-check entry, every failing check in that scenario is judged on its own: the named one is excused, and any other failure is still an unexpected regression. The four exit-code rules above apply per check rather than per scenario, with one addition — a baselined check that is absent or skipped is tolerated, because a scenario that bails on a failed prerequisite legitimately never reaches its later checks, and the prerequisite reports its own failure anyway.

Two things to know:

  • A check id addresses every occurrence of that id. Ids repeat within a run (a loop, a retried flow), and the occurrences collapse to one verdict, most-severe first. So baselining a repeated id excuses all of its occurrences — coarser than ideal, still far narrower than baselining the scenario.
  • Mind the space. - scenario:check-id is a string; - scenario: check-id is YAML for a mapping and is rejected with an error.

A scenario cannot be listed both wholesale and per-check — the wholesale entry already excuses everything, so the pair is contradictory and is rejected.

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 built-in fields for a one-off run:

# e.g. a 2026-07-28 run against go-sdk's server — the built-in server command
# pins -stateless=false (what the dated-spec suites need), so override it to
# get the SEP-2575 stateless lifecycle:
npm start -- sdk go-sdk --mode server --suite all --spec-version 2026-07-28 \
  --server-cmd './.conformance-server -http=localhost:3000'

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.