mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(plugins): capabilities.mcpTools in the manifest
Tools are declared in the signed manifest, not only reported at load. plugin-host-entry composes its loaded payload AFTER onLoad runs, so a signed, sha256-pinned artifact could otherwise emit different tool text on every restart with no version bump and no re-consent. The host will advertise the intersection of the two. It also gives the admin something to read. What mcp:tools will put in front of every user is a manifest field now, so it can be shown on the consent screen before the grant is given. Rejects rather than drops, unlike most capability parsing: a malformed tool should fail the install visibly, not become a tool that silently never appears once the plugin is running. Caps and sanitising come from mcp-tool-schema.ts, so the install path and the advertise path cannot disagree about them. Declaring tools without mcp:tools is an error on both sides. Otherwise the consent screen lists tools that can never run. No migration: install/discovery.ts already persists capabilities as JSON. The SDK validator mirrors the checks it can make without the server's normaliser, so `trek-plugin validate` catches a bad declaration before publish rather than at install.
This commit is contained in:
@@ -207,6 +207,7 @@ export function validateManifest(raw: unknown): ValidationResult {
|
||||
tripPage?: { replaces?: unknown; position?: unknown };
|
||||
notificationChannel?: { title?: unknown; events?: unknown };
|
||||
routeProfiles?: unknown;
|
||||
mcpTools?: unknown;
|
||||
provides?: unknown;
|
||||
emits?: unknown;
|
||||
settingsUi?: unknown;
|
||||
@@ -284,6 +285,46 @@ export function validateManifest(raw: unknown): ValidationResult {
|
||||
}
|
||||
}
|
||||
}
|
||||
// MCP tools go into every user's assistant context, so the declaration is
|
||||
// checked here too rather than only at install: an author should hear about a
|
||||
// malformed one from `trek-plugin validate`, not from a tool that never shows up.
|
||||
const mcpTools = capabilities?.mcpTools;
|
||||
if (mcpTools !== undefined) {
|
||||
if (!permissions.includes('mcp:tools')) {
|
||||
errors.push('capabilities.mcpTools requires the "mcp:tools" permission');
|
||||
}
|
||||
if (!Array.isArray(mcpTools)) errors.push('capabilities.mcpTools must be an array');
|
||||
else {
|
||||
if (mcpTools.length > 8) errors.push('capabilities.mcpTools: at most 8 tools');
|
||||
const seenTools = new Set<string>();
|
||||
for (const v of mcpTools) {
|
||||
if (!v || typeof v !== 'object' || Array.isArray(v)) {
|
||||
errors.push('capabilities.mcpTools entries must be objects');
|
||||
continue;
|
||||
}
|
||||
const t = v as Record<string, unknown>;
|
||||
const name = typeof t.name === 'string' ? t.name : '';
|
||||
if (!/^[a-z0-9_]{1,48}$/.test(name)) {
|
||||
errors.push('capabilities.mcpTools: name must be lowercase [a-z0-9_], max 48 chars');
|
||||
continue;
|
||||
}
|
||||
if (seenTools.has(name)) errors.push(`capabilities.mcpTools: duplicate name "${name}"`);
|
||||
seenTools.add(name);
|
||||
if (typeof t.description !== 'string' || !t.description.trim()) {
|
||||
errors.push(`capabilities.mcpTools["${name}"]: description is required`);
|
||||
}
|
||||
if (t.inputSchema !== undefined) {
|
||||
const schema = t.inputSchema as Record<string, unknown> | null;
|
||||
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||
errors.push(`capabilities.mcpTools["${name}"]: inputSchema must be an object`);
|
||||
} else if (schema.type !== undefined && schema.type !== 'object') {
|
||||
errors.push(`capabilities.mcpTools["${name}"]: inputSchema root type must be "object"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings keys become JSON object keys in the plugin's stored config, so they are
|
||||
// constrained (mirrors the server's SETTING_KEY_RE). `__proto__`/`constructor` would
|
||||
// resolve off Object.prototype on read and make a required field look configured for
|
||||
|
||||
Reference in New Issue
Block a user