* 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.
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
/**
|
|
* JSON Schema 2020-12 conformance test scenario (SEP-1613)
|
|
*
|
|
* Validates that MCP servers correctly preserve JSON Schema 2020-12 keywords
|
|
* in tool definitions, ensuring implementations don't strip $schema, $defs,
|
|
* or additionalProperties fields.
|
|
*/
|
|
|
|
import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types.js';
|
|
import { connectToServer } from './client-helper.js';
|
|
|
|
const EXPECTED_TOOL_NAME = 'json_schema_2020_12_tool';
|
|
const EXPECTED_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
|
|
|
|
export class JsonSchema2020_12Scenario implements ClientScenario {
|
|
name = 'json-schema-2020-12';
|
|
specVersions: SpecVersion[] = ['2025-11-25'];
|
|
description = `Validates JSON Schema 2020-12 keyword preservation (SEP-1613).
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema 2020-12 features:
|
|
|
|
\`\`\`json
|
|
{
|
|
"name": "${EXPECTED_TOOL_NAME}",
|
|
"description": "Tool with JSON Schema 2020-12 features",
|
|
"inputSchema": {
|
|
"$schema": "${EXPECTED_SCHEMA_DIALECT}",
|
|
"type": "object",
|
|
"$defs": {
|
|
"address": {
|
|
"type": "object",
|
|
"properties": {
|
|
"street": { "type": "string" },
|
|
"city": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"address": { "$ref": "#/$defs/address" }
|
|
},
|
|
"additionalProperties": false
|
|
}
|
|
}
|
|
\`\`\`
|
|
|
|
**Verification**: The test verifies that \`$schema\`, \`$defs\`, and \`additionalProperties\` are preserved in the tool listing response.`;
|
|
|
|
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
const specReferences = [
|
|
{
|
|
id: 'SEP-1613',
|
|
url: 'https://github.com/modelcontextprotocol/specification/pull/655'
|
|
}
|
|
];
|
|
|
|
try {
|
|
const connection = await connectToServer(serverUrl);
|
|
const result = await connection.client.listTools();
|
|
|
|
// Find the test tool
|
|
const tool = result.tools?.find((t) => t.name === EXPECTED_TOOL_NAME);
|
|
|
|
// Check 1: Tool exists
|
|
checks.push({
|
|
id: 'json-schema-2020-12-tool-found',
|
|
name: 'JsonSchema2020_12ToolFound',
|
|
description: `Server advertises tool '${EXPECTED_TOOL_NAME}'`,
|
|
status: tool ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: tool
|
|
? undefined
|
|
: `Tool '${EXPECTED_TOOL_NAME}' not found. Available tools: ${result.tools?.map((t) => t.name).join(', ') || 'none'}`,
|
|
specReferences,
|
|
details: {
|
|
toolFound: !!tool,
|
|
availableTools: result.tools?.map((t) => t.name) || []
|
|
}
|
|
});
|
|
|
|
if (!tool) {
|
|
await connection.close();
|
|
return checks;
|
|
}
|
|
|
|
const inputSchema = tool.inputSchema as Record<string, unknown>;
|
|
|
|
// Check 2: $schema field preserved
|
|
const hasSchema = '$schema' in inputSchema;
|
|
const schemaValue = inputSchema['$schema'];
|
|
const schemaCorrect = schemaValue === EXPECTED_SCHEMA_DIALECT;
|
|
|
|
checks.push({
|
|
id: 'json-schema-2020-12-$schema',
|
|
name: 'JsonSchema2020_12$Schema',
|
|
description: `inputSchema.$schema field preserved with value '${EXPECTED_SCHEMA_DIALECT}'`,
|
|
status: hasSchema && schemaCorrect ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: !hasSchema
|
|
? '$schema field missing from inputSchema - field was likely stripped'
|
|
: !schemaCorrect
|
|
? `$schema has unexpected value: ${JSON.stringify(schemaValue)}`
|
|
: undefined,
|
|
specReferences,
|
|
details: {
|
|
hasSchema,
|
|
schemaValue,
|
|
expected: EXPECTED_SCHEMA_DIALECT
|
|
}
|
|
});
|
|
|
|
// Check 3: $defs field preserved
|
|
const hasDefs = '$defs' in inputSchema;
|
|
const defsValue = inputSchema['$defs'] as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
const defsHasAddress = defsValue && 'address' in defsValue;
|
|
|
|
checks.push({
|
|
id: 'json-schema-2020-12-$defs',
|
|
name: 'JsonSchema2020_12$Defs',
|
|
description:
|
|
'inputSchema.$defs field preserved with expected structure',
|
|
status: hasDefs && defsHasAddress ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: !hasDefs
|
|
? '$defs field missing from inputSchema - field was likely stripped'
|
|
: !defsHasAddress
|
|
? '$defs exists but missing expected "address" definition'
|
|
: undefined,
|
|
specReferences,
|
|
details: {
|
|
hasDefs,
|
|
defsKeys: defsValue ? Object.keys(defsValue) : [],
|
|
defsValue
|
|
}
|
|
});
|
|
|
|
// Check 4: additionalProperties field preserved
|
|
const hasAdditionalProps = 'additionalProperties' in inputSchema;
|
|
const additionalPropsValue = inputSchema['additionalProperties'];
|
|
const additionalPropsCorrect = additionalPropsValue === false;
|
|
|
|
checks.push({
|
|
id: 'json-schema-2020-12-additionalProperties',
|
|
name: 'JsonSchema2020_12AdditionalProperties',
|
|
description: 'inputSchema.additionalProperties field preserved',
|
|
status:
|
|
hasAdditionalProps && additionalPropsCorrect ? 'SUCCESS' : 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: !hasAdditionalProps
|
|
? 'additionalProperties field missing from inputSchema - field was likely stripped'
|
|
: !additionalPropsCorrect
|
|
? `additionalProperties has unexpected value: ${JSON.stringify(additionalPropsValue)}, expected: false`
|
|
: undefined,
|
|
specReferences,
|
|
details: {
|
|
hasAdditionalProps,
|
|
additionalPropsValue,
|
|
expected: false
|
|
}
|
|
});
|
|
|
|
await connection.close();
|
|
} catch (error) {
|
|
checks.push({
|
|
id: 'json-schema-2020-12-error',
|
|
name: 'JsonSchema2020_12Error',
|
|
description: 'JSON Schema 2020-12 conformance test',
|
|
status: 'FAILURE',
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
specReferences
|
|
});
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
}
|