feat: support "draft" as a first-class spec version target (#255)

* feat: support "draft" as a first-class spec version target

- Add LATEST_SPEC_VERSION and DATED_SPEC_VERSIONS constants so the next
  spec release is a one-line change in types.ts.
- Accept "draft" as a valid protocolVersion in the initialize check and
  mock-server response.
- --spec-version draft now selects latest-dated scenarios plus draft-tagged
  ones, so SEP authors can run the full suite against an SDK tracking the
  in-progress spec without retagging core scenarios.
- Forward --spec-version to the client process via
  MCP_CONFORMANCE_SPEC_VERSION so SDK examples can pick the matching
  protocolVersion.

Closes #253

* review: collapse SpecVersion to wire strings; dedup NEGOTIABLE_PROTOCOL_VERSIONS; split out 'extension'

Addresses review feedback from @felixweinberger and @mikekistler:

- SpecVersion is now always a wire protocolVersion string
  (DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION). The separate
  'draft' tag literal is gone from the type system; 'draft' survives
  only as a CLI input alias in resolveSpecVersion. Removes the
  tag-vs-wire confusion and the specVersionToProtocolVersion mapping.
- NEGOTIABLE_PROTOCOL_VERSIONS in types.ts is the single source for
  what the mock server accepts on initialize (was duplicated in
  checks/client.ts and scenarios/client/initialize.ts).
- 'extension' moved out of SpecVersion into a separate ScenarioSpecTag
  type. --spec-version extension is no longer valid; extension scenarios
  remain reachable via --suite extensions.
- Draft-tagged scenarios now use the DRAFT_PROTOCOL_VERSION constant
  so a draft revision bump is a one-line change in types.ts.
This commit is contained in:
Paul Carleton 2026-04-30 11:37:32 +01:00 • committed by GitHub
commit 17f1f93cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,7 +1,7 @@
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import { ConformanceCheck } from '../types';
import { ConformanceCheck, SpecVersion } from '../types';
import { getScenario } from '../scenarios';
import { createResultDir, formatPrettyChecks } from './utils';
@ -17,7 +17,8 @@ async function executeClient(
scenarioName: string,
serverUrl: string,
timeout: number = 30000,
context?: Record<string, unknown>
context?: Record<string, unknown>,
specVersion?: SpecVersion
): Promise<ClientExecutionResult> {
const commandParts = command.split(' ');
const executable = commandParts[0];
@ -34,6 +35,9 @@ async function executeClient(
// 3. Semantic separation: scenario identifies "which test", context provides "test data"
const env = { ...process.env };
env.MCP_CONFORMANCE_SCENARIO = scenarioName;
if (specVersion) {
env.MCP_CONFORMANCE_PROTOCOL_VERSION = specVersion;
}
if (context) {
// Include scenario name in context for discriminated union parsing
env.MCP_CONFORMANCE_CONTEXT = JSON.stringify({
@ -92,7 +96,8 @@ export async function runConformanceTest(
clientCommand: string,
scenarioName: string,
timeout: number = 30000,
outputDir?: string
outputDir?: string,
specVersion?: SpecVersion
): Promise<{
checks: ConformanceCheck[];
clientOutput: ClientExecutionResult;
@ -123,7 +128,8 @@ export async function runConformanceTest(
scenarioName,
urls.serverUrl,
timeout,
urls.context
urls.context,
specVersion
);
// Print stdout/stderr if client exited with nonzero code