Skip to content

feat: version-aware MockServer abstraction for client scenarios#321

Draft
felixweinberger wants to merge 6 commits into
fweinberger/runcontextfrom
fweinberger/client-runcontext
Draft

feat: version-aware MockServer abstraction for client scenarios#321
felixweinberger wants to merge 6 commits into
fweinberger/runcontextfrom
fweinberger/client-runcontext

Conversation

@felixweinberger
Copy link
Copy Markdown
Collaborator

Stacked on #318. Mirrors the server-side Connection/RunContext abstraction for client conformance: scenarios that act as a mock server now get a version-aware ctx.createServer(handlers) instead of building the server inline.

Motivation and Context

Client-conformance scenarios spin up a mock server for the client-under-test to connect to. Today every scenario builds that server inline (express + SDK Server + StreamableHTTPServerTransport, or raw http.createServer), which hard-codes the 2025 lifecycle. A scenario that should run under --spec-version draft can't unless its mock server speaks the stateless lifecycle, and there are currently three different inline patterns for doing that.

What changes

  • src/mock-server/{index,stateful,stateless,select,testing}.tsMockServer = {url, recorded, close} with stateful (SDK-backed) and stateless (raw http.createServer, validates _meta, serves server/discover) impls. createServerFor(specVersion) picks. validateStatelessRequest() is exported for reuse.
  • Scenario.start()start(ctx: ScenarioContext) where ScenarioContext = {specVersion, createServer()}. Runner builds it from --spec-version and passes the resolved version to the spawned client process via MCP_CONFORMANCE_PROTOCOL_VERSION (now set unconditionally; runInteractiveMode also takes it).
  • isStatefulVersion() exported from connection/select.ts and reused by mock-server/select.ts and the auth helper, so the version boundary is defined once.
  • client/tools_call.ts migrated to ctx.createServer(); assertions read from srv.recorded.
  • removedIn: DRAFT on 3 client scenariosinitialize, sse-retry, elicitation-defaults.
  • client/auth/helpers/createServer.ts is version-aware via ctx.specVersion: for stateless versions it calls the shared validateStatelessRequest() and routes tools/list/tools/call directly. The 25 auth call sites now forward ctx. Full restructuring of the auth helper onto ctx.createServer() is deferred (would be ~200 more mechanical lines around the ServerLifecycle.start(app) pattern).
  • everything-client.ts: reads MCP_CONFORMANCE_PROTOCOL_VERSION; runBasicClient (handles tools_call) picks SDK Client vs a raw statelessRequest() helper accordingly. Also fixes a pre-existing registration mismatch ('tools-call' vs 'tools_call') that meant the carry-forward client test had never actually run against the fixture.
  • Stateful mock: capabilities auto-derived from handler method names so registering prompts/list doesn't trip the SDK's capability assertion; recorded captured at the express layer so it includes unregistered methods (parity with stateless).

How Has This Been Tested?

233/233 unit tests, typecheck/lint/build clean.

tools_call against everything-client: 1/1 under both --spec-version 2025-11-25 and --spec-version draft. client/auth/index.test.ts: 43/43.

A client/all-scenarios.test.ts mirror (drive everything-client against every client scenario under both spec versions) does not exist yet; that would be the fuller validation.

Per-scenario category split
Category Count Scenarios
Carry-forward → ctx.createServer() 1 tools_call
Lifecycle → removedIn: DRAFT 2 initialize, sse-retry
Mixed → removedIn: DRAFT 1 elicitation-defaults (server→client elicitation via SSE; MRTR-client sibling needed)
2026-native (introducedIn: DRAFT) 6 request-metadata, http-base, http-standard-headers, http-custom-headers, json-schema-ref-deref, mrtr-client
Auth (single seam: auth/helpers/createServer.ts) ~29 across 12 files all of client/auth/*

Breaking Changes

Scenario.start()start(ctx: ScenarioContext). All in-tree scenarios are updated.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Deferred:

  • Consolidating the 6 2026-native client scenarios (request-metadata, http-*, mrtr-client, json-schema-ref-deref) onto MockServer — same call as feat: version-aware Connection abstraction for server scenarios #318 (DRAFT-scenario coherence pass).
  • Full restructure of the auth helper onto ctx.createServer() — version-awareness is achieved via the shared validateStatelessRequest(); the express/ServerLifecycle shape is kept for now.

MockServer encapsulates the lifecycle scaffold a client-conformance
scenario presents to the client-under-test:

- createServerStateful: 2025-x lifecycle. SDK Server +
  StreamableHTTPServerTransport (sessionless mode); the SDK handles the
  initialize handshake.
- createServerStateless: 2026-x lifecycle (SEP-2575). Raw express app
  that validates _meta + MCP-Protocol-Version on every request, serves
  server/discover, routes other methods to the supplied handlers.

createServerFor(specVersion) picks the implementation. ScenarioContext
bundles specVersion and a bound createServer() for the runner to hand
to each scenario.

This is the client-conformance mirror of src/connection (PR #318).
Nothing uses it yet; wiring follows in the next commit.
Scenario.start() becomes start(ctx: ScenarioContext). The runner builds
the context from --spec-version (defaulting to LATEST_SPEC_VERSION) and
passes it through; scenarios receive it as _ctx and otherwise behave
identically.

No scenario uses ctx.createServer() yet, so behaviour is unchanged:
231/231 tests pass.

Test files use a testScenarioContext() helper. The runner already
threads MCP_CONFORMANCE_PROTOCOL_VERSION to the spawned client process,
so the fixture-side env wiring is unchanged.
…t scenarios

ToolsCallScenario now goes through ctx.createServer() instead of an
inline express + SDK Server build. Same handlers, same checks; the
assertion now reads from srv.recorded so it works regardless of which
lifecycle scaffold the runner picked.

initialize, sse-retry, and elicitation-defaults are tagged
removedIn: DRAFT (initialize/GET-SSE/SSE-embedded-elicitation are gone
in the 2026 lifecycle; the MRTR sibling for elicitation-defaults is a
follow-up).

spec-version.test.ts: the 'draft is a superset of latest' invariant no
longer holds once removedIn: DRAFT exists; the test now asserts that
any scenario in latest-but-not-draft is explicitly removedIn.
The auth helper now takes ctx: ScenarioContext as its first argument
and branches on ctx.specVersion inside the /mcp route: the stateful
path (SDK Server + StreamableHTTPServerTransport) is unchanged; under
the draft version a raw stateless handler validates _meta + the
MCP-Protocol-Version header, serves server/discover, and routes the
same tools/list and tools/call responses.

The PRM endpoint, bearer-auth middleware, and request logger sit above
the branch and are version-independent.

All 25 call sites across the 12 auth scenario files pass ctx through;
ServerLifecycle and the express.Application return type are unchanged
so stop()/getChecks() are untouched.

Deviation from the MockServer wrapper approach: keeping the helper's
return type as express.Application avoids restructuring 25 call sites'
ServerLifecycle handling in this PR. Folding the auth seam onto
ctx.createServer() fully is a follow-up once the lifecycle ownership
moves into MockServer.
…PROTOCOL_VERSION

Adds a statelessRequest(serverUrl, method, params) helper that POSTs
with _meta + MCP-Protocol-Version (the SEP-2575 lifecycle), shimming
around the SDK Client not yet supporting stateless mode. The
runRequestMetadataClient handler's meta constants are extracted to share
with the helper.

runBasicClient (initialize, tools_call, json-schema-ref-no-deref) now
branches on MCP_CONFORMANCE_PROTOCOL_VERSION: for the draft version it
uses statelessRequest to call tools/list then tools/call; for dated
versions it keeps the SDK Client path.

The runner already passes MCP_CONFORMANCE_PROTOCOL_VERSION to the
spawned client, so no runner change is needed.
…or, capability derivation, recorded parity, specVersion threading)

- MockServerOptions removed (capabilities/configure had zero callers); opts
  param dropped from createServerStateful/Stateless/For and ScenarioContext.
- validateStatelessRequest extracted from mock-server/stateless and exported;
  both the stateless MockServer and auth/helpers/createServer.ts call it so
  _meta/header/version validation cannot drift.
- isStatefulVersion exported from connection/select; mock-server/select uses
  it instead of duplicating the version set.
- runner/client.ts: env MCP_CONFORMANCE_PROTOCOL_VERSION set unconditionally
  to the resolved version; runInteractiveMode now takes specVersion and the
  CLI passes it.
- createServerStateful: capabilities derived from handler method prefixes;
  newServer() moved inside the try so a capability mismatch surfaces as
  JSON-RPC -32603 instead of an HTML 500. Recording moved to the express
  layer so unregistered methods are captured (parity with stateless).
- readFinalSseMessage return type now declares error.data.
- Tests added for the capability derivation and unregistered-method recording.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 27, 2026

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@321

commit: 9720252

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.

1 participant