Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
paths:
- "servers/**"
- "categories.json"
- "schemas/**"
- "scripts/**"
- "tests/**"

jobs:
validate:
Expand All @@ -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 }}
Expand All @@ -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
43 changes: 41 additions & 2 deletions scripts/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion servers/ai.suprsonic-mcp-npx.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"schema_version": "2.1",
"categories": [
"search",
"web-scraping",
"ai-ml",
"communication"
],
Expand Down
3 changes: 1 addition & 2 deletions servers/app.businys-mcp-npx.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
"logo": "https://businys.app/icon.png",
"schema_version": "2.1",
"categories": [
"business",
"productivity",
"crm"
"communication"
],
"tags": [
"smb",
Expand Down
2 changes: 1 addition & 1 deletion servers/dev.agentdeals-mcp-http.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"schema_version": "2.1",
"categories": [
"developer-tools",
"business"
"productivity"
],
"tags": [
"free-tiers",
Expand Down
2 changes: 1 addition & 1 deletion servers/io.global-chat-mcp-npx.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"schema_version": "2.1",
"categories": [
"developer-tools",
"search-web"
"search"
],
"tags": [
"ai-agents",
Expand Down
2 changes: 1 addition & 1 deletion tests/schema-validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
],
},
},
Expand Down
Loading