Skip to content

Commit 7a87bf7

Browse files
committed
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.
1 parent 78514c2 commit 7a87bf7

15 files changed

Lines changed: 121 additions & 95 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ npx @modelcontextprotocol/conformance client --command "<client-command>" --scen
6464
- `--command` - The command to run your MCP client (can include flags)
6565
- `--scenario` - The test scenario to run (e.g., "initialize")
6666
- `--suite` - Run a suite of tests in parallel (e.g., "auth")
67-
- `--spec-version <version>` - Filter scenarios by spec version (e.g., `2025-11-25`, `draft`). `draft` runs the latest dated release plus any draft-only scenarios
67+
- `--spec-version <version>` - Filter scenarios by spec version (e.g., `2025-11-25`, `DRAFT-2026-v1`; `draft` is accepted as an alias for the current draft identifier). The draft version selects the latest dated release plus any draft-only scenarios
6868
- `--expected-failures <path>` - Path to YAML baseline file of known failures (see [Expected Failures](#expected-failures))
6969
- `--timeout` - Timeout in milliseconds (default: 30000)
7070
- `--verbose` - Show verbose output
7171

72-
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, the corresponding wire protocol version is forwarded as `MCP_CONFORMANCE_PROTOCOL_VERSION` (e.g., `--spec-version draft` sets it to the current draft identifier such as `DRAFT-2026-v1`); example clients can use this value directly as their `protocolVersion`. SDKs that hard-code their protocol version can ignore it.
72+
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.
7373

7474
### Server Testing
7575

src/checks/client.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
ConformanceCheck,
33
CheckStatus,
44
LATEST_SPEC_VERSION,
5-
DRAFT_PROTOCOL_VERSION
5+
NEGOTIABLE_PROTOCOL_VERSIONS
66
} from '../types';
77

88
export function createServerInfoCheck(serverInfo: {
@@ -28,23 +28,18 @@ export function createServerInfoCheck(serverInfo: {
2828
};
2929
}
3030

31-
// Protocol versions the mock server will accept on initialize.
32-
const VALID_PROTOCOL_VERSIONS = [
33-
'2025-06-18',
34-
LATEST_SPEC_VERSION,
35-
DRAFT_PROTOCOL_VERSION
36-
];
37-
3831
export function createClientInitializationCheck(
3932
initializeRequest: any,
4033
expectedSpecVersion: string = LATEST_SPEC_VERSION
4134
): ConformanceCheck {
4235
const protocolVersionSent = initializeRequest?.protocolVersion;
4336

4437
// Accept known valid versions OR custom expected version (for backward compatibility)
45-
const validVersions = VALID_PROTOCOL_VERSIONS.includes(expectedSpecVersion)
46-
? VALID_PROTOCOL_VERSIONS
47-
: [...VALID_PROTOCOL_VERSIONS, expectedSpecVersion];
38+
const validVersions = NEGOTIABLE_PROTOCOL_VERSIONS.includes(
39+
expectedSpecVersion
40+
)
41+
? NEGOTIABLE_PROTOCOL_VERSIONS
42+
: [...NEGOTIABLE_PROTOCOL_VERSIONS, expectedSpecVersion];
4843
const versionMatch = validVersions.includes(protocolVersionSent);
4944

5045
const errors: string[] = [];

src/runner/client.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { spawn } from 'child_process';
22
import { promises as fs } from 'fs';
33
import path from 'path';
4-
import {
5-
ConformanceCheck,
6-
SpecVersion,
7-
specVersionToProtocolVersion
8-
} from '../types';
4+
import { ConformanceCheck, SpecVersion } from '../types';
95
import { getScenario } from '../scenarios';
106
import { createResultDir, formatPrettyChecks } from './utils';
117

@@ -39,11 +35,8 @@ async function executeClient(
3935
// 3. Semantic separation: scenario identifies "which test", context provides "test data"
4036
const env = { ...process.env };
4137
env.MCP_CONFORMANCE_SCENARIO = scenarioName;
42-
const protocolVersion = specVersion
43-
? specVersionToProtocolVersion(specVersion)
44-
: undefined;
45-
if (protocolVersion) {
46-
env.MCP_CONFORMANCE_PROTOCOL_VERSION = protocolVersion;
38+
if (specVersion) {
39+
env.MCP_CONFORMANCE_PROTOCOL_VERSION = specVersion;
4740
}
4841
if (context) {
4942
// Include scenario name in context for discriminated union parsing

src/scenarios/client/auth/client-credentials.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
Scenario,
55
ConformanceCheck,
66
ScenarioUrls,
7-
SpecVersion
7+
ScenarioSpecTag
88
} from '../../../types';
99
import { createAuthServer } from './helpers/createAuthServer';
1010
import { createServer } from './helpers/createServer';
@@ -37,7 +37,7 @@ async function generateTestKeypair(): Promise<{
3737
*/
3838
export class ClientCredentialsJwtScenario implements Scenario {
3939
name = 'auth/client-credentials-jwt';
40-
specVersions: SpecVersion[] = ['extension'];
40+
specVersions: ScenarioSpecTag[] = ['extension'];
4141
description =
4242
'Tests OAuth client_credentials flow with private_key_jwt authentication (SEP-1046)';
4343

@@ -256,7 +256,7 @@ export class ClientCredentialsJwtScenario implements Scenario {
256256
*/
257257
export class ClientCredentialsBasicScenario implements Scenario {
258258
name = 'auth/client-credentials-basic';
259-
specVersions: SpecVersion[] = ['extension'];
259+
specVersions: ScenarioSpecTag[] = ['extension'];
260260
description =
261261
'Tests OAuth client_credentials flow with client_secret_basic authentication';
262262

src/scenarios/client/auth/cross-app-access.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
Scenario,
66
ConformanceCheck,
77
ScenarioUrls,
8-
SpecVersion
8+
ScenarioSpecTag
99
} from '../../../types';
1010
import { createAuthServer } from './helpers/createAuthServer';
1111
import { createServer } from './helpers/createServer';
@@ -60,7 +60,7 @@ async function createIdpIdToken(
6060
*/
6161
export class CrossAppAccessCompleteFlowScenario implements Scenario {
6262
name = 'auth/cross-app-access-complete-flow';
63-
specVersions: SpecVersion[] = ['extension'];
63+
specVersions: ScenarioSpecTag[] = ['extension'];
6464
description =
6565
'Tests complete SEP-990 flow: token exchange + JWT bearer grant (Enterprise Managed OAuth)';
6666

src/scenarios/client/auth/offline-access.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Scenario, ConformanceCheck } from '../../../types';
2-
import { ScenarioUrls, SpecVersion } from '../../../types';
2+
import {
3+
ScenarioUrls,
4+
SpecVersion,
5+
DRAFT_PROTOCOL_VERSION
6+
} from '../../../types';
37
import { createAuthServer } from './helpers/createAuthServer';
48
import { createServer } from './helpers/createServer';
59
import { ServerLifecycle } from './helpers/serverLifecycle';
@@ -23,7 +27,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier';
2327
*/
2428
export class OfflineAccessScopeScenario implements Scenario {
2529
name = 'auth/offline-access-scope';
26-
specVersions: SpecVersion[] = ['draft'];
30+
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
2731
description =
2832
'Tests that a client that wants a refresh token handles offline_access scope and refresh_token grant type when AS supports them (SEP-2207)';
2933

@@ -227,7 +231,7 @@ export class OfflineAccessScopeScenario implements Scenario {
227231
*/
228232
export class OfflineAccessNotSupportedScenario implements Scenario {
229233
name = 'auth/offline-access-not-supported';
230-
specVersions: SpecVersion[] = ['draft'];
234+
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
231235
description =
232236
'Tests that client does not request offline_access when AS does not list it in scopes_supported (SEP-2207)';
233237

src/scenarios/client/auth/resource-mismatch.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Scenario, ConformanceCheck } from '../../../types.js';
2-
import { ScenarioUrls, SpecVersion } from '../../../types.js';
2+
import {
3+
ScenarioUrls,
4+
SpecVersion,
5+
DRAFT_PROTOCOL_VERSION
6+
} from '../../../types.js';
37
import { createAuthServer } from './helpers/createAuthServer.js';
48
import { createServer } from './helpers/createServer.js';
59
import { ServerLifecycle } from './helpers/serverLifecycle.js';
@@ -27,7 +31,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier.js';
2731
*/
2832
export class ResourceMismatchScenario implements Scenario {
2933
name = 'auth/resource-mismatch';
30-
specVersions: SpecVersion[] = ['draft'];
34+
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
3135
description =
3236
'Tests that client rejects when PRM resource does not match server URL';
3337
allowClientError = true;

src/scenarios/client/initialize.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ConformanceCheck,
66
SpecVersion,
77
LATEST_SPEC_VERSION,
8-
DRAFT_PROTOCOL_VERSION
8+
NEGOTIABLE_PROTOCOL_VERSIONS
99
} from '../../types';
1010
import { clientChecks } from '../../checks/index';
1111

@@ -119,13 +119,8 @@ export class InitializeScenario implements Scenario {
119119
this.checks.push(clientChecks.createServerInfoCheck(serverInfo));
120120

121121
// Echo back client's version if valid, otherwise use latest
122-
const VALID_VERSIONS = [
123-
'2025-06-18',
124-
LATEST_SPEC_VERSION,
125-
DRAFT_PROTOCOL_VERSION
126-
];
127122
const clientVersion = initializeRequest?.protocolVersion;
128-
const responseVersion = VALID_VERSIONS.includes(clientVersion)
123+
const responseVersion = NEGOTIABLE_PROTOCOL_VERSIONS.includes(clientVersion)
129124
? clientVersion
130125
: LATEST_SPEC_VERSION;
131126

src/scenarios/index.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
ClientScenario,
44
ClientScenarioForAuthorizationServer,
55
SpecVersion,
6+
ScenarioSpecTag,
67
DATED_SPEC_VERSIONS,
8+
DRAFT_PROTOCOL_VERSION,
79
LATEST_SPEC_VERSION
810
} from '../types';
911
import { InitializeScenario } from './client/initialize';
@@ -258,31 +260,35 @@ export function listDraftScenarios(): string[] {
258260
export { listMetadataScenarios };
259261

260262
// All valid spec versions, used by the CLI to validate --spec-version input.
263+
// 'extension' is intentionally excluded — extension scenarios are off-timeline
264+
// and selected via `--suite extensions`, not `--spec-version`.
261265
export const ALL_SPEC_VERSIONS: SpecVersion[] = [
262266
...DATED_SPEC_VERSIONS,
263-
'draft',
264-
'extension'
267+
DRAFT_PROTOCOL_VERSION
265268
];
266269

267270
export function resolveSpecVersion(value: string): SpecVersion {
271+
if (value === 'draft') return DRAFT_PROTOCOL_VERSION;
268272
if (ALL_SPEC_VERSIONS.includes(value as SpecVersion)) {
269273
return value as SpecVersion;
270274
}
271275
console.error(`Unknown spec version: ${value}`);
272-
console.error(`Valid versions: ${ALL_SPEC_VERSIONS.join(', ')}`);
276+
console.error(
277+
`Valid versions: ${ALL_SPEC_VERSIONS.join(', ')} (or 'draft' as an alias for ${DRAFT_PROTOCOL_VERSION})`
278+
);
273279
process.exit(1);
274280
}
275281

276-
// `draft` selects everything in the latest dated release plus scenarios tagged
277-
// draft-only, so SEP authors can run the full suite against an SDK tracking the
278-
// in-progress spec without retagging core scenarios.
282+
// The draft version selects everything in the latest dated release plus
283+
// scenarios tagged draft-only, so SEP authors can run the full suite against an
284+
// SDK tracking the in-progress spec without retagging core scenarios.
279285
function matchesSpecVersion(
280-
scenario: { specVersions: SpecVersion[] },
286+
scenario: { specVersions: ScenarioSpecTag[] },
281287
version: SpecVersion
282288
): boolean {
283-
if (version === 'draft') {
289+
if (version === DRAFT_PROTOCOL_VERSION) {
284290
return (
285-
scenario.specVersions.includes('draft') ||
291+
scenario.specVersions.includes(DRAFT_PROTOCOL_VERSION) ||
286292
scenario.specVersions.includes(LATEST_SPEC_VERSION)
287293
);
288294
}
@@ -311,12 +317,12 @@ export function listClientScenariosForAuthorizationServerForSpec(
311317

312318
export function getScenarioSpecVersions(
313319
name: string
314-
): SpecVersion[] | undefined {
320+
): ScenarioSpecTag[] | undefined {
315321
return (
316322
scenarios.get(name)?.specVersions ??
317323
clientScenarios.get(name)?.specVersions ??
318324
clientScenariosForAuthorizationServer.get(name)?.specVersions
319325
);
320326
}
321327

322-
export type { SpecVersion };
328+
export type { SpecVersion, ScenarioSpecTag };

src/scenarios/server/resources.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
* Resources test scenarios for MCP servers
33
*/
44

5-
import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types';
5+
import {
6+
ClientScenario,
7+
ConformanceCheck,
8+
SpecVersion,
9+
DRAFT_PROTOCOL_VERSION
10+
} from '../../types';
611
import { connectToServer } from './client-helper';
712
import {
813
TextResourceContents,
@@ -438,7 +443,7 @@ Example request:
438443

439444
export class ResourcesNotFoundErrorScenario implements ClientScenario {
440445
name = 'sep-2164-resource-not-found';
441-
specVersions: SpecVersion[] = ['draft'];
446+
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
442447
description = `Test error handling for non-existent resources (SEP-2164).
443448
444449
**Server Implementation Requirements:**

0 commit comments

Comments
 (0)