feat: make the skills verification catalog budget configurable per server (#2294)

Adds a Skills section to Server Settings with Maximum Number of Skills and
Maximum Catalog Size fields, defaulting to SKILL_MAX_CATALOG_SKILLS (256) and
SKILL_MAX_CATALOG_BYTES (64 MiB). The values persist as skillCatalogMaxSkills /
skillCatalogMaxBytes on the server's mcp.json entry (omitted at the default),
are validated as positive integers by the /api/servers routes, and are read by
verifySkills through getServerSettings(), so the CLI's --verify and the TUI
Skills pane both honor them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6i1HBSdSS9kL4npZ2dDSe
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-13 23:54:29 -04:00
commit eb41185c37

View file

@ -65,6 +65,7 @@ import {
stripInspectorFields,
} from "../../serverList.js";
import { toRecord } from "../../../json/jsonUtils.js";
import { isSkillCatalogLimit } from "../../skills.js";
import { resolveImportSource } from "../../import/resolveSource.js";
import { RemoteSession } from "./remote-session.js";
import { createRemoteAuthProvider } from "./tokenAuthProvider.js";
@ -1681,6 +1682,19 @@ export function createRemoteApp(
);
delete valObj.maxFetchRequests;
}
// #2294 — positive integers only; `0` is not "unlimited" for these.
for (const key of [
"skillCatalogMaxSkills",
"skillCatalogMaxBytes",
] as const) {
if (key in valObj && !isSkillCatalogLimit(valObj[key])) {
logWarn(
{ route: "/api/servers", id, droppedKey: key },
`Dropping malformed \`${key}\` field — expected a positive integer.`,
);
delete valObj[key];
}
}
if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
logWarn(
{ route: "/api/servers", id, droppedKey: "oauth" },
@ -1953,6 +1967,18 @@ export function createRemoteApp(
error: "settings.maxFetchRequests must be a non-negative number",
};
}
// #2294 — optional on the wire; absent means the default budget.
for (const key of [
"skillCatalogMaxSkills",
"skillCatalogMaxBytes",
] as const) {
if (obj[key] !== undefined && !isSkillCatalogLimit(obj[key])) {
return {
ok: false,
error: `settings.${key} must be a positive integer`,
};
}
}
for (const optional of [
"oauthClientId",
"oauthClientSecret",
@ -2099,6 +2125,13 @@ export function createRemoteApp(
// writes no spurious `roots` field.
roots: isRootArray(obj.roots) ? obj.roots : [],
};
// Validated above; absent stays absent (the default budget).
if (isSkillCatalogLimit(obj.skillCatalogMaxSkills)) {
value.skillCatalogMaxSkills = obj.skillCatalogMaxSkills;
}
if (isSkillCatalogLimit(obj.skillCatalogMaxBytes)) {
value.skillCatalogMaxBytes = obj.skillCatalogMaxBytes;
}
if (typeof obj.oauthClientId === "string" && obj.oauthClientId !== "") {
value.oauthClientId = obj.oauthClientId;
}