scripts/check-catalog-schema.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bun
/**
* Schema gate for a connector catalog.
*
* Takes a catalog path (a `ServerDetail` file or a directory of them)
* and reports every entry the runtime would silently drop: one that
* fails the upstream `ServerDetail` schema, duplicates a name already
* claimed by an earlier file, sits in a file that cannot be read or
* parsed, shares its server name with another entry (both are refused),
* is scrubbed at the directory boundary for an unsafe URL or a
* reserved OAuth param, or resolves to nothing installable — and every
* entry whose malformed `admin_tools` makes all its tools admin-only.
* Exit 1 if there is any, 0 if every entry reaches Browse as written.
*
* **Why this exists.** A catalog entry is validated at load, and an
* invalid one is dropped with a warn log while its siblings load fine
* — so the connector simply never appears in Browse, with no hard
* failure anywhere. The catalog is deployment-managed config, edited in
* a repo that does not compile against this schema, which means a
* malformed entry merges green and fails only in a running tenant. That
* exact failure has shipped four times, always on `description`, which
* upstream caps at 100 characters.
*
* It runs the real `validateCatalog` rather than a second
* validator, so the gate cannot drift from what production does — if
* this passes, the runtime keeps every entry.
*
* Offline and deterministic, unlike its sibling
* `check-catalog-dcr.ts`, which probes vendor OAuth endpoints over the
* network. This one is safe to run on every PR that touches a catalog.
*
* bun run scripts/check-catalog-schema.ts <catalog-file-or-dir>
* bun run check:catalog-schema # the in-repo example catalog
*/
import { validateCatalog } from "../src/connectors/catalog/read.ts";
function main(): void {
const path = process.argv[2];
if (!path) {
console.error("usage: bun run scripts/check-catalog-schema.ts <catalog-file-or-dir>");
process.exit(2);
}
const diagnostics = validateCatalog(path);
if (diagnostics.length === 0) {
console.log(`✓ ${path}: every entry reaches Browse (schema, safety, installable)`);
return;
}
console.error(
`✗ ${path}: ${diagnostics.length} problem(s) — these entries would be dropped or gated\n`,
);
// Print the diagnostic verbatim — it is already file-and-entry
// qualified. A source-stage line is worded exactly as the runtime logs
// it, so it is the string to grep for when confirming a fix landed in a
// running deployment; the directory-boundary lines name the entry and
// the reason, but the runtime logs those from a different site with its
// own registry-tagged wording.
for (const d of diagnostics) console.error(` ${d.message}`);
// Only entry-level problems have an entry to go fix. A missing path or
// an unparseable file has no `index`, and telling that operator about
// field rules would point them at the wrong thing entirely.
if (diagnostics.some((d) => d.index !== undefined)) {
console.error(
"\nA dropped entry does not fail at runtime — the connector just never appears.\n" +
"Field rules: `description` is capped at 100 characters by the upstream MCP\n" +
"registry schema (src/connectors/catalog/schemas/server.schema.json); icon, docs, and\n" +
"portal URLs must be http(s); OAuth params may not use reserved keys; and an\n" +
"entry needs `packages` or a `remotes` entry the installer supports; `admin_tools`\n" +
"must be a list of at most 64 tool names with no whitespace; and no two names may\n" +
"slugify to one server name (`a.b/c` and `a/b.c` are both `a-b-c`).",
);
}
process.exit(1);
}
// Only run when invoked directly, so the module can be imported without
// tripping the argv reads and the process.exit calls.
if (import.meta.main) {
main();
}