feat: add specVersion classification to conformance scenarios (#147)

* feat: add specVersions classification to conformance scenarios

Each scenario declares which spec versions it applies to as a list.
Scenarios that carry forward (e.g. initialize) list all applicable versions
['2025-06-18', '2025-11-25']. Scenarios removed from newer specs (e.g.
backcompat auth) only list their original version ['2025-03-26'].

- specVersions list on Scenario and ClientScenario interfaces
- --spec-version CLI filter uses simple .includes()
- Tier-check conformance matrix (Server / Client: Core / Client: Auth)
  with per-version columns and unique All* count
- 7 unit tests for specVersions helpers
- Updated tier-audit skill docs with matrix format

* fix: fix console output template for tier audit skill

The template had an orphan table header (Check | Value | T2 | T1)
with no rows above the conformance matrix, causing an empty table
to render. The scorecard rows below the matrix also lacked their
own header.

Fix: two self-contained tables with clear labels — 'Conformance:'
for the per-version matrix, 'Scorecard:' for the check rows.

* fix: align skill console template with tier-check script output

Add asterisk footnote ('unique scenarios — a scenario may apply to
multiple spec versions') and rename 'Scorecard' to 'Repository Health'
to match the labels used by the tier-check CLI output.

* fix: add specVersions to CrossAppAccessCompleteFlowScenario

Added in 83c446d on main after this branch diverged.
This commit is contained in:
Felix Weinberger 2026-02-13 18:08:15 +00:00 • committed by GitHub
commit 3f9048a019
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 521 additions and 59 deletions

View file

@ -1,5 +1,5 @@
import type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls } from '../../../types';
import { ScenarioUrls, SpecVersion } from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -22,6 +22,7 @@ export const CIMD_CLIENT_METADATA_URL =
*/
export class AuthBasicCIMDScenario implements Scenario {
name = 'auth/basic-cimd';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests OAuth flow with Client ID Metadata Documents (SEP-991/URL-based client IDs). Server advertises client_id_metadata_document_supported=true and client should use URL as client_id instead of DCR.';
private authServer = new ServerLifecycle();

View file

@ -1,6 +1,11 @@
import * as jose from 'jose';
import type { CryptoKey } from 'jose';
import type { Scenario, ConformanceCheck, ScenarioUrls } from '../../../types';
import type {
Scenario,
ConformanceCheck,
ScenarioUrls,
SpecVersion
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -32,6 +37,7 @@ async function generateTestKeypair(): Promise<{
*/
export class ClientCredentialsJwtScenario implements Scenario {
name = 'auth/client-credentials-jwt';
specVersions: SpecVersion[] = ['extension'];
description =
'Tests OAuth client_credentials flow with private_key_jwt authentication (SEP-1046)';
@ -250,6 +256,7 @@ export class ClientCredentialsJwtScenario implements Scenario {
*/
export class ClientCredentialsBasicScenario implements Scenario {
name = 'auth/client-credentials-basic';
specVersions: SpecVersion[] = ['extension'];
description =
'Tests OAuth client_credentials flow with client_secret_basic authentication';

View file

@ -1,7 +1,12 @@
import * as jose from 'jose';
import type { CryptoKey } from 'jose';
import express, { type Request, type Response } from 'express';
import type { Scenario, ConformanceCheck, ScenarioUrls } from '../../../types';
import type {
Scenario,
ConformanceCheck,
ScenarioUrls,
SpecVersion
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { MockTokenVerifier } from './helpers/mockTokenVerifier';
@ -55,6 +60,7 @@ async function createIdpIdToken(
*/
export class CrossAppAccessCompleteFlowScenario implements Scenario {
name = 'auth/cross-app-access-complete-flow';
specVersions: SpecVersion[] = ['extension'];
description =
'Tests complete SEP-990 flow: token exchange + JWT bearer grant (Enterprise Managed OAuth)';

View file

@ -87,6 +87,7 @@ function createMetadataScenario(config: MetadataScenarioConfig): Scenario {
return {
name: `auth/${config.name}`,
specVersions: ['2025-11-25'],
description: `Tests Basic OAuth metadata discovery flow.
**PRM:** ${config.prmLocation}${config.inWwwAuth ? '' : ' (not in WWW-Authenticate)'}

View file

@ -1,5 +1,5 @@
import type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls } from '../../../types';
import { ScenarioUrls, SpecVersion } from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -8,6 +8,7 @@ import { SpecReferences } from './spec-references';
export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
name = 'auth/2025-03-26-oauth-metadata-backcompat';
specVersions: SpecVersion[] = ['2025-03-26'];
description =
'Tests 2025-03-26 spec OAuth flow: no PRM (Protected Resource Metadata), OAuth metadata at root location';
private server = new ServerLifecycle();
@ -68,6 +69,7 @@ export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
export class Auth20250326OEndpointFallbackScenario implements Scenario {
name = 'auth/2025-03-26-oauth-endpoint-fallback';
specVersions: SpecVersion[] = ['2025-03-26'];
description =
'Tests OAuth flow with no metadata endpoints, relying on fallback to standard OAuth endpoints at server root (2025-03-26 spec behavior)';
private server = new ServerLifecycle();

View file

@ -1,4 +1,9 @@
import type { Scenario, ConformanceCheck, ScenarioUrls } from '../../../types';
import type {
Scenario,
ConformanceCheck,
ScenarioUrls,
SpecVersion
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -19,6 +24,7 @@ const PRE_REGISTERED_CLIENT_SECRET = 'pre-registered-secret';
*/
export class PreRegistrationScenario implements Scenario {
name = 'auth/pre-registration';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests OAuth flow with pre-registered client credentials. Server does not support DCR.';

View file

@ -1,5 +1,5 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { ScenarioUrls, SpecVersion } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
@ -27,6 +27,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier.js';
*/
export class ResourceMismatchScenario implements Scenario {
name = 'auth/resource-mismatch';
specVersions: SpecVersion[] = ['draft'];
description =
'Tests that client rejects when PRM resource does not match server URL';
allowClientError = true;

View file

@ -1,5 +1,5 @@
import type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls } from '../../../types';
import { ScenarioUrls, SpecVersion } from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -15,6 +15,7 @@ import type { Request, Response, NextFunction } from 'express';
*/
export class ScopeFromWwwAuthenticateScenario implements Scenario {
name = 'auth/scope-from-www-authenticate';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client uses scope parameter from WWW-Authenticate header when provided';
private authServer = new ServerLifecycle();
@ -100,6 +101,7 @@ export class ScopeFromWwwAuthenticateScenario implements Scenario {
*/
export class ScopeFromScopesSupportedScenario implements Scenario {
name = 'auth/scope-from-scopes-supported';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client uses all scopes from scopes_supported when scope not in WWW-Authenticate header';
private authServer = new ServerLifecycle();
@ -195,6 +197,7 @@ export class ScopeFromScopesSupportedScenario implements Scenario {
*/
export class ScopeOmittedWhenUndefinedScenario implements Scenario {
name = 'auth/scope-omitted-when-undefined';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client omits scope parameter when scopes_supported is undefined';
private authServer = new ServerLifecycle();
@ -281,6 +284,7 @@ export class ScopeOmittedWhenUndefinedScenario implements Scenario {
*/
export class ScopeStepUpAuthScenario implements Scenario {
name = 'auth/scope-step-up';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client handles step-up authentication with different scope requirements per operation';
private authServer = new ServerLifecycle();
@ -477,6 +481,7 @@ export class ScopeStepUpAuthScenario implements Scenario {
*/
export class ScopeRetryLimitScenario implements Scenario {
name = 'auth/scope-retry-limit';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client implements retry limits to prevent infinite authorization loops on repeated 403 responses';
allowClientError = true;

View file

@ -1,5 +1,5 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { ScenarioUrls, SpecVersion } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
@ -45,6 +45,7 @@ const AUTH_METHOD_NAMES: Record<AuthMethod, string> = {
class TokenEndpointAuthScenario implements Scenario {
name: string;
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
description: string;
private expectedAuthMethod: AuthMethod;
private authServer = new ServerLifecycle();

View file

@ -11,7 +11,7 @@ import {
ListToolsRequestSchema,
ElicitResultSchema
} from '@modelcontextprotocol/sdk/types.js';
import type { Scenario, ConformanceCheck } from '../../types';
import type { Scenario, ConformanceCheck, SpecVersion } from '../../types';
import express, { Request, Response } from 'express';
import { ScenarioUrls } from '../../types';
import { createRequestLogger } from '../request-logger';
@ -474,6 +474,7 @@ function createServer(checks: ConformanceCheck[]): {
export class ElicitationClientDefaultsScenario implements Scenario {
name = 'elicitation-sep1034-client-defaults';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests client applies default values for omitted elicitation fields (SEP-1034)';
private app: express.Application | null = null;

View file

@ -1,9 +1,15 @@
import http from 'http';
import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types';
import {
Scenario,
ScenarioUrls,
ConformanceCheck,
SpecVersion
} from '../../types';
import { clientChecks } from '../../checks/index';
export class InitializeScenario implements Scenario {
name = 'initialize';
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
description = 'Tests MCP client initialization handshake';
private server: http.Server | null = null;

View file

@ -8,10 +8,16 @@
*/
import http from 'http';
import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js';
import {
Scenario,
ScenarioUrls,
ConformanceCheck,
SpecVersion
} from '../../types.js';
export class SSERetryScenario implements Scenario {
name = 'sse-retry';
specVersions: SpecVersion[] = ['2025-11-25'];
description =
'Tests that client respects SSE retry field timing and reconnects properly (SEP-1699)';

View file

@ -4,7 +4,7 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import type { Scenario, ConformanceCheck } from '../../types';
import type { Scenario, ConformanceCheck, SpecVersion } from '../../types';
import express, { Request, Response } from 'express';
import { ScenarioUrls } from '../../types';
import { createRequestLogger } from '../request-logger';
@ -115,6 +115,7 @@ function createServerApp(checks: ConformanceCheck[]): express.Application {
export class ToolsCallScenario implements Scenario {
name = 'tools_call';
specVersions: SpecVersion[] = ['2025-06-18', '2025-11-25'];
description = 'Tests calling tools with various parameter types';
private app: express.Application | null = null;
private httpServer: any = null;