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
This commit is contained in:
parent
bca85e53ca
commit
7a3ff5d452
2 changed files with 2789 additions and 1 deletions
56
scripts/generate-schema.ts
Normal file
56
scripts/generate-schema.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env tsx
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const SCHEMA_TS = "schema.ts";
|
||||
const SCHEMA_JSON = "schema.json";
|
||||
|
||||
const CHECK_MODE = process.argv.includes("--check");
|
||||
|
||||
/**
|
||||
* Apply JSON Schema 2020-12 transformations to the generated schema string.
|
||||
*
|
||||
* `typescript-json-schema` emits draft-07 by default; mirror the same
|
||||
* post-processing the upstream MCP spec applies to its 2020-12 schemas.
|
||||
*/
|
||||
function applyJsonSchema202012Transformations(content: string): string {
|
||||
return content
|
||||
.replace(
|
||||
/http:\/\/json-schema\.org\/draft-07\/schema#/g,
|
||||
"https://json-schema.org/draft/2020-12/schema",
|
||||
)
|
||||
.replace(/"definitions":/g, '"$defs":')
|
||||
.replace(/#\/definitions\//g, "#/$defs/");
|
||||
}
|
||||
|
||||
async function generate(): Promise<string> {
|
||||
const { stdout } = await execAsync(
|
||||
`npx typescript-json-schema --defaultNumberType integer --required --skipLibCheck "${SCHEMA_TS}" "*"`,
|
||||
);
|
||||
return applyJsonSchema202012Transformations(stdout).trim() + "\n";
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
if (CHECK_MODE) {
|
||||
const existing = readFileSync(SCHEMA_JSON, "utf-8");
|
||||
const expected = await generate();
|
||||
if (existing.trim() !== expected.trim()) {
|
||||
console.error(`✗ ${SCHEMA_JSON} is out of date. Run: npm run generate`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ ${SCHEMA_JSON} is up to date`);
|
||||
} else {
|
||||
const content = await generate();
|
||||
writeFileSync(SCHEMA_JSON, content, "utf-8");
|
||||
console.log(`✓ Generated ${SCHEMA_JSON}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Schema generation failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
117
scripts/validate-examples.ts
Normal file
117
scripts/validate-examples.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#!/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();
|
||||
Loading…
Reference in a new issue