From cec46082a2889254950f05041d80df8030974fdd Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Sat, 25 Apr 2026 02:33:23 +0800 Subject: [PATCH] fix: validate category refs in CI + fix 4 servers with unknown categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four server definitions referenced categories not present in \`categories.json\` (\`web-scraping\`, \`business\`, \`crm\`, \`search-web\`). These silently failed to sync in the bundler because server_categories has a FK to categories.id — the upsert threw and dropped the entire server, so any update to these four files never reached production. - Fix the four offenders to use valid categories: ai.suprsonic-mcp-npx web-scraping -> dropped (kept search, ai-ml, communication) app.businys-mcp-npx business, crm -> productivity, communication dev.agentdeals-mcp-http business -> productivity io.global-chat-mcp-npx search-web -> search - Extend scripts/validate.js to load categories.json and reject any server that references an ID not listed there. Reports the full list of valid IDs on failure so contributors can pick the right one. - Broaden the Validate PR workflow so changes to categories.json, schemas, scripts, or tests all trigger validation. Added a "Run full test suite" step so cross-file checks (consistency, categories, deprecated icon field, logo URL format) run on every PR. - Fix the one pre-existing \`type: "password"\` test fixture, which was always invalid under the input schema but went unnoticed because CI didn't run the test suite. Verified \`pnpm validate servers/\` now prints a clear error listing every valid category ID. Signed-off-by: Mohammod Al Amin Ashik --- .github/workflows/validate-pr.yml | 16 ++++++++++- scripts/validate.js | 43 ++++++++++++++++++++++++++-- servers/ai.suprsonic-mcp-npx.json | 1 - servers/app.businys-mcp-npx.json | 3 +- servers/dev.agentdeals-mcp-http.json | 2 +- servers/io.global-chat-mcp-npx.json | 2 +- tests/schema-validation.test.js | 2 +- 7 files changed, 60 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 92bf24c..8d6696c 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -4,6 +4,10 @@ on: pull_request: paths: - "servers/**" + - "categories.json" + - "schemas/**" + - "scripts/**" + - "tests/**" jobs: validate: @@ -23,13 +27,17 @@ jobs: - name: Install dependencies run: npm ci - - name: Get changed files + - name: Get changed server files id: changed uses: tj-actions/changed-files@v44 with: files: "servers/*.json" - name: Validate changed server files + # Runs schema validation AND category-reference checks via scripts/validate.js. + # A category ID not present in categories.json fails here, since the + # bundler's server_categories table has a FK to categories.id and a + # typo would silently break sync for that server. run: | if [ -n "${{ steps.changed.outputs.all_changed_files }}" ]; then npm run validate -- ${{ steps.changed.outputs.all_changed_files }} @@ -40,3 +48,9 @@ jobs: - name: Check for conflicts run: npm run check-conflicts + + - name: Run full test suite + # Covers cross-file consistency checks (categories.json, bundle build, + # deprecated `icon` field, logo URL format) that per-file validation + # can't express. + run: npm test diff --git a/scripts/validate.js b/scripts/validate.js index b6ef476..33c2dd1 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -18,10 +18,28 @@ const { glob } = require("glob"); const ROOT = path.resolve(__dirname, ".."); const SCHEMA_PATH = path.join(ROOT, "schemas", "server-definition.schema.json"); const SERVERS_DIR = path.join(ROOT, "servers"); +const CATEGORIES_PATH = path.join(ROOT, "categories.json"); // Fields that contributors must not set -- they are platform-managed const PLATFORM_FIELDS = ["badges", "stats", "sponsored", "featured"]; +/** + * Load the set of valid category IDs from categories.json. The bundler's + * server_categories table has a foreign key to categories.id, so any server + * that references an ID not in categories.json will silently fail to sync. + * We catch those at validate time so they never reach main. + */ +function loadValidCategoryIds() { + try { + const raw = fs.readFileSync(CATEGORIES_PATH, "utf-8"); + const categories = JSON.parse(raw); + return new Set(categories.map((c) => c.id)); + } catch (err) { + console.error(`Failed to load categories.json: ${err.message}`); + return null; + } +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -75,6 +93,8 @@ async function validateFiles(files) { addFormats(ajv); const validate = ajv.compile(schema); + const validCategoryIds = loadValidCategoryIds(); + let hasErrors = false; for (const filePath of files) { @@ -96,10 +116,29 @@ async function validateFiles(files) { } const valid = validate(data); + const fileErrors = []; if (!valid) { - console.error(`FAIL ${relative}`); for (const err of validate.errors) { - console.error(` - ${err.instancePath || "/"}: ${err.message}`); + fileErrors.push(`${err.instancePath || "/"}: ${err.message}`); + } + } + + // Enforce that every category reference exists in categories.json. + // The bundler's server_categories table has a FK constraint — a typo + // here silently breaks sync for this server. + if (validCategoryIds && Array.isArray(data.categories)) { + const unknown = data.categories.filter((id) => !validCategoryIds.has(id)); + for (const id of unknown) { + fileErrors.push( + `/categories: unknown category "${id}" (not in categories.json; pick one of: ${Array.from(validCategoryIds).sort().join(", ")})` + ); + } + } + + if (fileErrors.length > 0) { + console.error(`FAIL ${relative}`); + for (const msg of fileErrors) { + console.error(` - ${msg}`); } hasErrors = true; } else { diff --git a/servers/ai.suprsonic-mcp-npx.json b/servers/ai.suprsonic-mcp-npx.json index 66bcc5b..bde4d8b 100644 --- a/servers/ai.suprsonic-mcp-npx.json +++ b/servers/ai.suprsonic-mcp-npx.json @@ -8,7 +8,6 @@ "schema_version": "2.1", "categories": [ "search", - "web-scraping", "ai-ml", "communication" ], diff --git a/servers/app.businys-mcp-npx.json b/servers/app.businys-mcp-npx.json index d370ccb..e51277b 100644 --- a/servers/app.businys-mcp-npx.json +++ b/servers/app.businys-mcp-npx.json @@ -7,9 +7,8 @@ "logo": "https://businys.app/icon.png", "schema_version": "2.1", "categories": [ - "business", "productivity", - "crm" + "communication" ], "tags": [ "smb", diff --git a/servers/dev.agentdeals-mcp-http.json b/servers/dev.agentdeals-mcp-http.json index f38565c..296e94e 100644 --- a/servers/dev.agentdeals-mcp-http.json +++ b/servers/dev.agentdeals-mcp-http.json @@ -8,7 +8,7 @@ "schema_version": "2.1", "categories": [ "developer-tools", - "business" + "productivity" ], "tags": [ "free-tiers", diff --git a/servers/io.global-chat-mcp-npx.json b/servers/io.global-chat-mcp-npx.json index eb49edf..8f4a5bd 100644 --- a/servers/io.global-chat-mcp-npx.json +++ b/servers/io.global-chat-mcp-npx.json @@ -8,7 +8,7 @@ "schema_version": "2.1", "categories": [ "developer-tools", - "search-web" + "search" ], "tags": [ "ai-agents", diff --git a/tests/schema-validation.test.js b/tests/schema-validation.test.js index d954891..5e6df88 100644 --- a/tests/schema-validation.test.js +++ b/tests/schema-validation.test.js @@ -58,7 +58,7 @@ describe("Server Definition Schema", () => { metadata: { inputs: [ { id: "LOG_LEVEL", label: "Log Level", type: "text", required: false, default: "info" }, - { id: "API_KEY", label: "API Key", type: "password", required: true, secret: true }, + { id: "API_KEY", label: "API Key", type: "text", required: true, secret: true }, ], }, },