Split draft-spec scenarios into their own suite (#317)

* feat: split draft-spec scenarios into their own suite

- add --suite draft to the server command, selecting scenarios whose
  source.introducedIn is the draft protocol version
- exclude draft scenarios from the default active suite until the draft
  spec is published as a dated release
- derive the client command's draft suite from source.introducedIn so the
  non-auth draft scenarios (SEP-2243/2575/2322/2106) are included alongside
  the auth ones
- keep everything-server self-test coverage for draft scenarios in
  all-scenarios.test.ts (active + non-pending draft)
- document available suites in the README and add suite-membership tests

* fix: repair broken specReferences URLs

- SEP-986: SEP/SEP-986.md no longer exists in the spec repo; point at the
  Tool Names section of the published 2025-11-25 tools page instead
- SEP-990: the ext-auth spec file is enterprise-managed-authorization.mdx,
  not enterprise-oauth.mdx

Fixes #313
This commit is contained in:
Paul Carleton 2026-05-26 18:00:44 +01:00 • committed by GitHub
commit e2e65e5595
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 117 additions and 14 deletions

View file

@ -3,3 +3,7 @@
# repo's `prettier --check .` would reformat the file and fight the generator's
# output (and the refresh workflow's `git diff` check).
src/seps/traceability.json
# Local tooling workspaces (not part of the repo).
.claude/
.sdk-under-test/

View file

@ -63,7 +63,7 @@ npx @modelcontextprotocol/conformance client --command "<client-command>" --scen
- `--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 (e.g., "auth")
- `--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`, `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
- `--expected-failures <path>` - Path to YAML baseline file of known failures (see [Expected Failures](#expected-failures))
- `--timeout` - Timeout in milliseconds (default: 30000)
@ -81,7 +81,7 @@ npx @modelcontextprotocol/conformance server --url <url> [--scenario <scenario>]
- `--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), "all", or "pending"
- `--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](#expected-failures))
- `--verbose` - Show verbose output

View file

@ -25,6 +25,7 @@ import {
listExtensionScenarios,
listBackcompatScenarios,
listDraftScenarios,
listDraftClientScenarios,
listScenariosForSpec,
listClientScenariosForSpec,
getScenarioSpecVersions,
@ -314,7 +315,7 @@ program
)
.option(
'--suite <suite>',
'Suite to run: "active" (default, excludes pending), "all", or "pending"',
'Suite to run: "active" (default, excludes pending and draft), "all", "draft", or "pending"',
'active'
)
.option(
@ -378,9 +379,13 @@ program
scenarios = listActiveClientScenarios();
} else if (suite === 'pending') {
scenarios = listPendingClientScenarios();
} else if (suite === 'draft') {
// Scenarios targeting the in-progress draft spec; excluded from
// 'active' until the draft is published as a dated release.
scenarios = listDraftClientScenarios();
} else {
console.error(`Unknown suite: ${suite}`);
console.error('Available suites: active, all, core, pending');
console.error('Available suites: active, all, core, draft, pending');
process.exit(1);
}

View file

@ -104,7 +104,7 @@ export const SpecReferences: { [key: string]: SpecReference } = {
},
SEP_990_ENTERPRISE_OAUTH: {
id: 'SEP-990-Enterprise-Managed-OAuth',
url: 'https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-oauth.mdx'
url: 'https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx'
},
SEP_2207_REFRESH_TOKEN_GUIDANCE: {
id: 'SEP-2207-Refresh-Token-Guidance',

View file

@ -206,13 +206,24 @@ const allClientScenariosList: ClientScenario[] = [
new InputRequiredResultValidateInputScenario()
];
// Active client scenarios (excludes pending)
// Scenarios that test requirements introduced in the in-progress draft spec.
// They run via `--suite draft` (or `--suite all`) and are excluded from the
// default `active` suite until the draft is published as a dated release.
const draftClientScenariosList: ClientScenario[] =
allClientScenariosList.filter(
(scenario) =>
'introducedIn' in scenario.source &&
scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION
);
// Active client scenarios (excludes pending and draft)
const activeClientScenariosList: ClientScenario[] =
allClientScenariosList.filter(
(scenario) =>
!pendingClientScenariosList.some(
(pending) => pending.name === scenario.name
)
) &&
!draftClientScenariosList.some((draft) => draft.name === scenario.name)
);
// Client scenarios map - built from list
@ -329,8 +340,21 @@ export function listClientScenariosForAuthorizationServer(): string[] {
return Array.from(clientScenariosForAuthorizationServer.keys());
}
// All client-testing scenarios that target the draft spec, derived from the
// declared `source.introducedIn` rather than a hand-maintained list (covers
// both the auth draft scenarios and the non-auth ones, e.g. SEP-2243/2575).
const draftSpecScenariosList: Scenario[] = scenariosList.filter(
(scenario) =>
'introducedIn' in scenario.source &&
scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION
);
export function listDraftScenarios(): string[] {
return draftScenariosList.map((scenario) => scenario.name);
return draftSpecScenariosList.map((scenario) => scenario.name);
}
export function listDraftClientScenarios(): string[] {
return draftClientScenariosList.map((scenario) => scenario.name);
}
export { listMetadataScenarios };

View file

@ -1,6 +1,11 @@
import { spawn, ChildProcess } from 'child_process';
import { createServer } from 'net';
import { getClientScenario, listActiveClientScenarios } from '../index';
import {
getClientScenario,
listActiveClientScenarios,
listDraftClientScenarios,
listPendingClientScenarios
} from '../index';
import path from 'path';
function getFreePort(): Promise<number> {
@ -116,8 +121,14 @@ describe('Server Scenarios', () => {
}
});
// Generate individual test for each scenario
const scenarios = listActiveClientScenarios();
// Generate individual test for each scenario: the active suite plus the
// draft-spec scenarios that aren't parked in `pending` — the same set this
// file covered before draft scenarios were split out of `active`.
const pendingScenarios = new Set(listPendingClientScenarios());
const scenarios = [
...listActiveClientScenarios(),
...listDraftClientScenarios().filter((name) => !pendingScenarios.has(name))
];
for (const scenarioName of scenarios) {
it(`${scenarioName}`, async () => {

View file

@ -21,7 +21,7 @@ const TOOLS_NAME_FORMAT_SPEC_REFS = [
},
{
id: 'SEP-986',
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/SEP/SEP-986.md'
url: 'https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names'
}
];

View file

@ -3,11 +3,14 @@ import {
listScenarios,
listScenariosForSpec,
listDraftScenarios,
listDraftClientScenarios,
listActiveClientScenarios,
listExtensionScenarios,
getScenarioSpecVersions,
resolveSpecVersion,
ALL_SPEC_VERSIONS,
scenarios
scenarios,
clientScenarios
} from './index';
import {
DATED_SPEC_VERSIONS,
@ -94,3 +97,51 @@ describe('specVersions helpers', () => {
}
});
});
describe('draft suite membership', () => {
it('every scenario introduced in the draft spec is selected by its draft suite', () => {
const draftClientTesting = new Set(listDraftScenarios());
for (const [name, scenario] of scenarios) {
if (
'introducedIn' in scenario.source &&
scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION
) {
expect(
draftClientTesting.has(name),
`client-testing scenario "${name}" should be in the draft suite`
).toBe(true);
}
}
const draftServerTesting = new Set(listDraftClientScenarios());
for (const [name, scenario] of clientScenarios) {
if (
'introducedIn' in scenario.source &&
scenario.source.introducedIn === DRAFT_PROTOCOL_VERSION
) {
expect(
draftServerTesting.has(name),
`server-testing scenario "${name}" should be in the draft suite`
).toBe(true);
}
}
});
it('the draft suite covers the non-auth draft client scenarios', () => {
const draft = new Set(listDraftScenarios());
expect(draft.has('request-metadata')).toBe(true);
expect(draft.has('http-standard-headers')).toBe(true);
expect(draft.has('sep-2322-client-request-state')).toBe(true);
});
it('draft server-testing scenarios are excluded from the active suite', () => {
const active = new Set(listActiveClientScenarios());
expect(listDraftClientScenarios().length).toBeGreaterThan(0);
for (const name of listDraftClientScenarios()) {
expect(
active.has(name),
`draft scenario "${name}" should not be in the active suite`
).toBe(false);
}
});
});

View file

@ -5,7 +5,15 @@ export default defineConfig({
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
exclude: ['**/node_modules/**', 'dist', '.sdk-under-test'],
exclude: [
'**/node_modules/**',
'dist',
'.sdk-under-test',
// Local tooling workspaces (Claude worktrees, SDK checkouts) must never
// be collected as test files.
'**/.claude/**',
'**/.sdk-under-test/**'
],
// Run test files sequentially to avoid port conflicts
fileParallelism: false,
// Increase timeout for server tests in CI