* feat: vendor spec schema types per-version
Copies schema/{version}/schema.ts from the modelcontextprotocol spec
repo into src/spec-types/{version}.ts so the conformance suite can type
against draft spec versions before any SDK ships them.
npm run sync-schema -- <ref> refreshes the copies and records the spec
commit in src/spec-types/SOURCE.
* feat: add Connection abstraction and RunContext
Connection encapsulates how the conformance suite talks to a
server-under-test for a given spec version:
- connectStateful: 2025-x lifecycle. Thin adapter over the SDK Client
(initialize handshake, session id, SSE handled by the SDK).
- connectStateless: 2026-x lifecycle (SEP-2575). Raw fetch with
per-request _meta + MCP-Protocol-Version header. Decoupled from the
SDK so the suite can test draft spec versions before the SDK
implements them.
connectFor(specVersion) picks the implementation. RunContext bundles
serverUrl, specVersion and a bound connect() for the runner to hand to
each scenario.
Nothing uses this yet; wiring follows in the next commit.
* refactor: thread RunContext through ClientScenario.run
ClientScenario.run(serverUrl) becomes run(ctx: RunContext). The runner
builds the context from --spec-version and the server URL; scenarios
destructure ctx.serverUrl and otherwise behave identically.
No scenario uses ctx.connect() yet, so behaviour is unchanged: 214/214
tests pass, all-scenarios.test.ts still drives the everything-server
fixture exactly as before.
Test files use a testContext(url) helper to construct a RunContext.
The authorization-server scenario list is retyped to
ClientScenarioForAuthorizationServer since those scenarios test an
OAuth server, not an MCP server, and keep run(serverUrl).
* refactor: migrate server scenarios to ctx.connect() + conn.request()
22 carry-forward and lifecycle scenarios now go through the Connection
abstraction instead of connectToServer + SDK Client:
- tools.ts (8): list, call x6, with-progress
- prompts.ts (5)
- resources.ts (7): list, read x3, subscribe x2, not-found
- utils.ts (3): completion, ping, set-level
- json-schema-2020-12.ts, caching.ts, http-standard-headers.ts
Result types come from spec-types/{introducedIn}. Same scenario code
now passes under both --spec-version 2025-11-25 (SDK-backed stateful)
and --spec-version draft (raw stateless).
ToolsCallSampling/Elicitation/WithLogging and elicitation-* keep
connectToServer (need setRequestHandler/setLoggingLevel SDK surface);
they are tagged removedIn: DRAFT in the next commit. stateless.ts and
input-required-result.ts keep their sendRpc helper; migrating those is
deferred to the DRAFT-scenario coherence pass.
* fix: normalize Connection error to JsonRpcError; clean up RunContext import
connectStateful now catches the SDK's McpError and rethrows as
JsonRpcError so scenarios always see the same error class regardless
of which Connection impl ran. ResourcesNotFoundError uses
instanceof JsonRpcError instead of duck-typing.
types.ts uses a normal top-level import for RunContext instead of an
inline import() type.
* fix(dns-rebinding): use version-appropriate probe body
The 'valid Host accepted' check was sending an initialize body, which
a 2026 server returns 404 for. Probe body is now picked from
ctx.specVersion: initialize for the stateful lifecycle, server/discover
with _meta for the stateless lifecycle. The Host/Origin rejection check
is unchanged since rejection happens before body parsing.
* feat(everything-server): route stateless carry-forward methods to McpServer
Adds an in-memory dispatch client connected to the same McpServer the
stateful path uses. Stateless requests for tools/call, resources/*,
prompts/get and completion/complete that fall through the MRTR-specific
handlers are routed to it, so the fixture serves the carry-forward
scenarios under --spec-version draft without duplicating ~500 lines of
tool/resource/prompt registrations.
tools/list now merges the McpServer's tool list with the MRTR-only
stubs so json-schema-2020-12 finds its tool.
draft suite against the fixture: 36/39 (was 13/39). Remaining 3 are
fixture-side SEP gaps (no SSE forwarding for progress, no SEP-2549
ttlMs, no SEP-2243 Mcp-Method validation in the stateless path).
* refactor(connection): drop unused RequestOptions; move sdk-client; add tests
Addresses self-review findings on the new connection module:
- RequestOptions.handlers/.meta and ServerRequestHandler removed: zero
callers (the scenarios that motivated them are deferred). They can be
reintroduced when something actually uses them.
- scenarios/server/client-helper.ts moved to connection/sdk-client.ts so
the connection module no longer depends on the scenarios module.
- connectStateless: handle CRLF SSE event separators; throw a useful
error for non-JSON/non-SSE responses instead of a JSON parse error.
- JSONRPCNotification consistently imported from spec-types/2025-11-25.
- New connection.test.ts (8 tests) covering connectFor selection,
_meta injection, error mapping, SSE LF/CRLF parsing, and the
server-request-on-stream rejection.
* fix: address bughunt findings (response.ok check; targetVersion naming)
- connectStateless: throw on non-2xx responses that lack a JSON-RPC
error envelope (e.g. gateway/framework error JSON), matching the
stateful path's behavior. Previously such a response would return
undefined as the result.
- json-schema-2020-12: rename negotiatedVersion to targetVersion and
reword the skip message and details field. The value is ctx.specVersion
(the run's --spec-version), not a negotiated version; the old wording
was misleading. Drop dead 'unknown' fallback (specVersion is required)
and the corresponding undefined test cases.
* fix(sse-multiple-streams): keep scenario in draft; version-aware request scaffolding
Concurrent POSTs each answered with JSON or their own SSE stream are
core transport behavior in the draft spec too, so removedIn was wrong
for this scenario. Only the request scaffolding is version-specific:
stateful runs keep the initialize/session-id setup, draft runs send
_meta + the MCP-Protocol-Version header on each request instead. The
two checks are unchanged.
* refactor: fold stateless-client helpers into connection/stateless
The two stateless request paths were ~90% duplicated:
- scenarios/server/stateless-client.ts (main #319): buildStandardHeaders,
withRequestMeta, readSseJsonRpcResponse, sendStatelessRequest -> raw
{status, body}; sends Mcp-Method/Mcp-Name (SEP-2243); has timeout
- connection/stateless.ts (this PR): connectStateless -> Connection.request
-> throws JsonRpcError; missing Mcp-Method/Mcp-Name; no timeout
connection/stateless.ts now exports both layers:
- low-level sendStatelessRequest() (and the building blocks) moved verbatim
from stateless-client.ts
- connectStateless() rebuilt as a thin wrapper over sendStatelessRequest():
classifies SSE events into the notification sink, surfaces server->client
requests on the response stream as a spec violation, throws JsonRpcError
on error responses
So connectStateless() now also picks up the SEP-2243 headers and the 10s
timeout that the previous impl lacked.
stateless-client.ts deleted; its 4 importers + test file repointed at
'../../connection'. Test file moved to connection/stateless.test.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(everything-server): make stateless resources reach the carry-forward dispatch
The merge of main left two competing handler sets for resources/read,
resources/list and resources/templates/list: the inline stateless handlers
(test://stateless-static-text + a catch-all -32602) and the carry-forward
dispatch to the McpServer-registered resources. The catch-all made the
carry-forward unreachable, so test://static-text, test://static-binary and
the templates errored under --spec-version draft.
Now resources/list and resources/templates/list dispatch to the McpServer and
merge with the inline entries (same pattern as tools/list and prompts/list),
and resources/read keeps the inline static-text response but falls through to
the carry-forward for unknown URIs (preserving SEP-2164 data.uri on errors).
Also wrap the tools/list and prompts/list dispatch in try/finally so
dispatch.close() always runs, matching tools/call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(scenarios): send SEP-2243 Mcp-Method header on hand-rolled stateless requests
sse-multiple-streams and dns-rebinding bypass sendStatelessRequest() because
they need raw fetch/undici for low-level inspection, but their hand-built
draft-path headers omitted Mcp-Method, so a strictly-conformant draft server
would 400 them before the behaviour under test runs. Build the headers via
buildStandardHeaders() instead, layering the scenario-specific overrides
(Accept ordering, Host/Origin) on top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(runner): infer draft spec version for draft-only scenarios
When --spec-version is omitted, the runner defaulted to LATEST_SPEC_VERSION,
so draft-only scenarios (caching, resources-not-found, ...) silently ran over
the stateful connection instead of the stateless draft transport they were
written for; all-scenarios.test.ts had the same problem via testContext().
Infer the spec version from scenario.source.introducedIn (as negative.test.ts
already does) when no explicit version is given.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(connection): only throw JsonRpcError for properly-shaped error objects
connectStateless() destructured response.body.error unconditionally, so a
proxy returning {"error": "upstream timeout"} produced a JsonRpcError with
undefined code/message and lost the HTTP status. Require an object with a
numeric code before treating it as a JSON-RPC error; otherwise surface the
HTTP status and raw body in the thrown Error. Adds a unit test for the
non-conformant 502 case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(connection): align connectStateful normalization with connectStateless
Two consistency nits from automated review:
- Collected notifications were missing the jsonrpc field (the SDK's Zod
parsing strips it), violating the JSONRPCNotification type contract.
- McpError messages kept the SDK's "MCP error <code>: " prefix, so
JsonRpcError.message differed between the stateful and stateless impls.
Also restore progressToken filtering in tools-call-with-progress, which was
lost when the scenario migrated from the SDK onprogress callback to
conn.notifications: the server must echo the request's token back, and
notifications for other tokens are now ignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(runner): skip inapplicable scenario/spec-version combos; add --force; plumb spec version to the wire
Running a draft-only scenario with an explicit dated --spec-version (e.g.
server-stateless --spec-version 2025-11-25) used to silently pass: the flag
only affected ctx.connect(), which hand-rolling scenarios never call, so
they sent draft requests regardless and the run looked green while testing
nothing the flag claimed.
Three changes:
- buildStandardHeaders / withRequestMeta / sendStatelessRequest /
connectStateless accept an optional specVersion (default: draft), and
hand-rolling scenarios pass ctx.specVersion through, so requests declare
the version the run was invoked with.
- The runner skips (exit 0) when an explicit --spec-version falls outside
the scenario's introducedIn/removedIn window, with a message instead of
a misleading green run.
- A new --force flag overrides the skip for deliberate mismatch probing;
combined with the plumbing, a forced server-stateless run at 2025-11-25
now genuinely sends that version (8/20 checks pass against the
everything-server instead of a vacuous 26/26).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
/**
|
|
* JSON Schema 2020-12 conformance test scenario (SEP-1613, SEP-2106)
|
|
*
|
|
* Validates that MCP servers correctly preserve JSON Schema 2020-12 keywords
|
|
* in tool definitions, ensuring implementations don't strip $schema, $defs,
|
|
* or additionalProperties fields (SEP-1613).
|
|
*
|
|
* SEP-2106 broadened inputSchema to permit the full JSON Schema 2020-12
|
|
* vocabulary alongside the required root `type: "object"`. This scenario also
|
|
* verifies that composition (allOf/anyOf), conditional (if/then/else), and
|
|
* reference ($anchor) keywords survive tools/list rather than being stripped.
|
|
*/
|
|
|
|
import {
|
|
CheckStatus,
|
|
ClientScenario,
|
|
ConformanceCheck,
|
|
DRAFT_PROTOCOL_VERSION
|
|
} from '../../types.js';
|
|
import type { RunContext } from '../../connection';
|
|
import type { ListToolsResult } from '../../spec-types/2025-11-25';
|
|
|
|
const EXPECTED_TOOL_NAME = 'json_schema_2020_12_tool';
|
|
const EXPECTED_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
|
|
|
|
/**
|
|
* Soft version gate for the SEP-2106 keyword-preservation checks.
|
|
*
|
|
* Preserving the broader JSON Schema 2020-12 vocabulary is good behavior at
|
|
* any protocol version, so a server that preserves the keywords gets SUCCESS
|
|
* regardless. Stripping them is only a conformance FAILURE when the run
|
|
* targets a protocol version that includes SEP-2106 (the draft); when
|
|
* targeting an earlier dated version the requirement does not apply, so the
|
|
* check is SKIPPED rather than failed.
|
|
*/
|
|
export function sep2106KeywordCheckStatus(
|
|
preserved: boolean,
|
|
targetProtocolVersion: string
|
|
): CheckStatus {
|
|
if (preserved) {
|
|
return 'SUCCESS';
|
|
}
|
|
return targetProtocolVersion === DRAFT_PROTOCOL_VERSION
|
|
? 'FAILURE'
|
|
: 'SKIPPED';
|
|
}
|
|
|
|
export class JsonSchema2020_12Scenario implements ClientScenario {
|
|
name = 'json-schema-2020-12';
|
|
readonly source = { introducedIn: '2025-11-25' } as const;
|
|
description = `Validates JSON Schema 2020-12 keyword preservation (SEP-1613, SEP-2106).
|
|
|
|
**Server Implementation Requirements:**
|
|
|
|
Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema 2020-12 features, including the broader vocabulary permitted by SEP-2106 (an \`$anchor\` inside \`$defs\`, composition keywords \`allOf\`/\`anyOf\`, and conditional keywords \`if\`/\`then\`/\`else\`):
|
|
|
|
\`\`\`json
|
|
{
|
|
"name": "${EXPECTED_TOOL_NAME}",
|
|
"description": "Tool with JSON Schema 2020-12 features",
|
|
"inputSchema": {
|
|
"$schema": "${EXPECTED_SCHEMA_DIALECT}",
|
|
"type": "object",
|
|
"$defs": {
|
|
"address": {
|
|
"$anchor": "addressDef",
|
|
"type": "object",
|
|
"properties": {
|
|
"street": { "type": "string" },
|
|
"city": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"address": { "$ref": "#/$defs/address" },
|
|
"contactMethod": { "type": "string", "enum": ["phone", "email"] },
|
|
"phone": { "type": "string" },
|
|
"email": { "type": "string" }
|
|
},
|
|
"allOf": [
|
|
{ "anyOf": [{ "required": ["phone"] }, { "required": ["email"] }] }
|
|
],
|
|
"if": {
|
|
"properties": { "contactMethod": { "const": "phone" } },
|
|
"required": ["contactMethod"]
|
|
},
|
|
"then": { "required": ["phone"] },
|
|
"else": { "required": ["email"] },
|
|
"additionalProperties": false
|
|
}
|
|
}
|
|
\`\`\`
|
|
|
|
**Verification**: The test verifies that \`$schema\`, \`$defs\`, and \`additionalProperties\` are preserved (SEP-1613), and that the composition (\`allOf\`/\`anyOf\`), conditional (\`if\`/\`then\`/\`else\`), and \`$anchor\` keywords are preserved (SEP-2106), in the tool listing response.`;
|
|
|
|
async run(ctx: RunContext): Promise<ConformanceCheck[]> {
|
|
const checks: ConformanceCheck[] = [];
|
|
const specReferences = [
|
|
{
|
|
id: 'SEP-1613',
|
|
url: 'https://github.com/modelcontextprotocol/specification/pull/655'
|
|
}
|
|
];
|
|
const sep2106References = [
|
|
{
|
|
id: 'SEP-2106',
|
|
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2106'
|
|
}
|
|
];
|
|
|
|
try {
|
|
const conn = await ctx.connect();
|
|
// Spec version this run is targeting; used to soft-gate the SEP-2106
|
|
// checks below.
|
|
const targetVersion = ctx.specVersion;
|
|
const result = await conn.request<ListToolsResult>('tools/list');
|
|
|
|
// 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 conn.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
|
|
}
|
|
});
|
|
|
|
// SEP-2106: the full JSON Schema 2020-12 vocabulary is permitted in
|
|
// inputSchema and must survive tools/list rather than being stripped to
|
|
// properties/required. These checks are soft-gated on the protocol
|
|
// version this run targets (see sep2106KeywordCheckStatus): stripping
|
|
// the keywords is only a FAILURE when targeting the draft version, and
|
|
// SKIPPED otherwise.
|
|
const skippedSuffix = ` (run targets protocol version ${targetVersion}; SEP-2106 applies from ${DRAFT_PROTOCOL_VERSION})`;
|
|
|
|
// Check 5: composition keywords (allOf / anyOf) preserved
|
|
const allOf = inputSchema['allOf'];
|
|
const hasAllOf = Array.isArray(allOf) && allOf.length > 0;
|
|
const hasNestedAnyOf =
|
|
Array.isArray(allOf) &&
|
|
allOf.some(
|
|
(sub) =>
|
|
typeof sub === 'object' &&
|
|
sub !== null &&
|
|
Array.isArray((sub as Record<string, unknown>)['anyOf'])
|
|
);
|
|
const compositionPreserved = hasAllOf && hasNestedAnyOf;
|
|
const compositionStatus = sep2106KeywordCheckStatus(
|
|
compositionPreserved,
|
|
targetVersion
|
|
);
|
|
|
|
checks.push({
|
|
id: 'sep-2106-composition-keywords-preserved',
|
|
name: 'JsonSchema2020_12CompositionKeywords',
|
|
description:
|
|
'inputSchema composition keywords (allOf/anyOf) preserved (SEP-2106)',
|
|
status: compositionStatus,
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: compositionPreserved
|
|
? undefined
|
|
: (!hasAllOf
|
|
? 'allOf keyword missing from inputSchema - composition keyword was likely stripped'
|
|
: 'nested anyOf missing from allOf - composition keyword was likely stripped') +
|
|
(compositionStatus === 'SKIPPED' ? skippedSuffix : ''),
|
|
specReferences: sep2106References,
|
|
details: {
|
|
hasAllOf,
|
|
hasNestedAnyOf,
|
|
targetProtocolVersion: targetVersion
|
|
}
|
|
});
|
|
|
|
// Check 6: conditional keywords (if / then / else) preserved
|
|
const hasIf = 'if' in inputSchema;
|
|
const hasThen = 'then' in inputSchema;
|
|
const hasElse = 'else' in inputSchema;
|
|
const conditionalPreserved = hasIf && hasThen && hasElse;
|
|
const conditionalStatus = sep2106KeywordCheckStatus(
|
|
conditionalPreserved,
|
|
targetVersion
|
|
);
|
|
|
|
checks.push({
|
|
id: 'sep-2106-conditional-keywords-preserved',
|
|
name: 'JsonSchema2020_12ConditionalKeywords',
|
|
description:
|
|
'inputSchema conditional keywords (if/then/else) preserved (SEP-2106)',
|
|
status: conditionalStatus,
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: conditionalPreserved
|
|
? undefined
|
|
: `Conditional keywords missing (if=${hasIf}, then=${hasThen}, else=${hasElse}) - likely stripped` +
|
|
(conditionalStatus === 'SKIPPED' ? skippedSuffix : ''),
|
|
specReferences: sep2106References,
|
|
details: {
|
|
hasIf,
|
|
hasThen,
|
|
hasElse,
|
|
targetProtocolVersion: targetVersion
|
|
}
|
|
});
|
|
|
|
// Check 7: reference keyword ($anchor) preserved within $defs.address
|
|
const addressDef = defsValue?.['address'] as
|
|
| Record<string, unknown>
|
|
| undefined;
|
|
const hasAnchor = !!addressDef && '$anchor' in addressDef;
|
|
const anchorStatus = sep2106KeywordCheckStatus(hasAnchor, targetVersion);
|
|
|
|
checks.push({
|
|
id: 'sep-2106-anchor-keyword-preserved',
|
|
name: 'JsonSchema2020_12AnchorKeyword',
|
|
description:
|
|
'inputSchema reference keyword ($anchor) preserved in $defs (SEP-2106)',
|
|
status: anchorStatus,
|
|
timestamp: new Date().toISOString(),
|
|
errorMessage: hasAnchor
|
|
? undefined
|
|
: '$anchor missing from $defs.address - reference keyword was likely stripped' +
|
|
(anchorStatus === 'SKIPPED' ? skippedSuffix : ''),
|
|
specReferences: sep2106References,
|
|
details: {
|
|
hasAnchor,
|
|
anchorValue: addressDef?.['$anchor']
|
|
}
|
|
});
|
|
|
|
await conn.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;
|
|
}
|
|
}
|