ext-server-card/scripts/validate-examples.ts
Tadas Antanavicius 7a3ff5d452 Bootstrap repo with TypeScript source-of-truth and generated JSON Schema
Tracks SEP-2127 (MCP Server Cards) as an experimental extension while the SEP
itself is under review. The schema.ts is structured to be lifted directly into
schema/draft/schema.ts of the main spec when Server Cards graduate -- only the
inlined MetaObject and Icon definitions need to be removed during migration.

- schema.ts: ServerCard, Server, Repository, Remote, Package, Argument, Input,
  KeyValueInput, etc. plus inlined MetaObject and Icon
- scripts/generate-schema.ts: typescript-json-schema with 2020-12 transforms
- scripts/validate-examples.ts: ajv validator over examples/{ServerCard,Server}/
- examples/: 2 valid + 4 invalid Server Card cases, 1 valid Server case
- README: status, layout, schema URL conventions, graduation plan
- CI workflow: check + validate + format on every PR
2026-04-27 19:47:01 +00:00

117 lines
3.1 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Validate the example documents under `examples/` against the generated
* `schema.json`. Each example is run against either `#/$defs/ServerCard` or
* `#/$defs/Server` based on the directory it lives under, and against the
* schema's expected acceptance — `valid/` examples must validate cleanly,
* `invalid/` examples must fail at least one constraint.
*/
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { readdirSync, readFileSync, statSync } from "fs";
import { join } from "path";
const SCHEMA_JSON = "schema.json";
const EXAMPLES_DIR = "examples";
type Outcome = {
name: string;
expected: "valid" | "invalid";
passed: boolean;
message: string;
};
function loadSchema() {
const schema = JSON.parse(readFileSync(SCHEMA_JSON, "utf-8"));
const ajv = new Ajv2020({ allErrors: true, strict: false });
addFormats.default(ajv);
ajv.addSchema(schema, "schema.json");
return ajv;
}
function listJsonFiles(dir: string): string[] {
try {
return readdirSync(dir)
.filter((f) => f.endsWith(".json"))
.map((f) => join(dir, f));
} catch {
return [];
}
}
function runDirectory(
ajv: Ajv2020,
defName: "ServerCard" | "Server",
): Outcome[] {
const root = join(EXAMPLES_DIR, defName);
const outcomes: Outcome[] = [];
if (!safeIsDir(root)) return outcomes;
const validate = ajv.getSchema(`schema.json#/$defs/${defName}`);
if (!validate) {
throw new Error(`Could not resolve ${defName} schema in ${SCHEMA_JSON}`);
}
for (const expected of ["valid", "invalid"] as const) {
const dir = join(root, expected);
for (const file of listJsonFiles(dir)) {
const data = JSON.parse(readFileSync(file, "utf-8"));
const ok = validate(data);
const passed = expected === "valid" ? !!ok : !ok;
const errors = validate.errors ?? [];
outcomes.push({
name: `${defName}/${expected}/${file.split("/").pop()}`,
expected,
passed,
message: passed
? expected === "valid"
? "validated cleanly"
: `rejected (${errors.length} error(s))`
: expected === "valid"
? `unexpectedly invalid: ${ajv.errorsText(errors)}`
: `unexpectedly valid (no errors raised)`,
});
}
}
return outcomes;
}
function safeIsDir(p: string): boolean {
try {
return statSync(p).isDirectory();
} catch {
return false;
}
}
function main(): void {
const ajv = loadSchema();
const outcomes = [
...runDirectory(ajv, "ServerCard"),
...runDirectory(ajv, "Server"),
];
if (outcomes.length === 0) {
console.log(`No examples found under ${EXAMPLES_DIR}/. Skipping.`);
return;
}
let failed = 0;
for (const o of outcomes) {
const status = o.passed ? "✓" : "✗";
console.log(`${status} ${o.name} — ${o.message}`);
if (!o.passed) failed++;
}
console.log();
if (failed > 0) {
console.error(`${failed} of ${outcomes.length} example(s) failed.`);
process.exit(1);
}
console.log(`All ${outcomes.length} example(s) passed.`);
}
main();