@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
101 lines • 3.69 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
/**
* Skills Registry — ADR-140 Phase 2b
*
* Exposes PICA's named methodologies as MCP-loadable skills. Each skill is
* reachable as a `skill://` Resource, through the SEP-2640 skills extension
* (skills/list, skills/get — ./sep2640.ts, ADR-328), and as MCP tools
* (pica_skill_list + pica_skill_get).
*
* Bodies are bundled at build time by `scripts/build-skills.ts`. The registry
* is intentionally thin — list returns a metadata-only summary (tiny handshake
* cost), get returns the full body on demand (lazy slicing, mirrors the
* _inspect sections pattern from ADR-199).
*/
import { SKILLS, SKILL_NAMES } from "./skills.generated.js";
export class SkillsRegistry {
extra = new Map();
/**
* @param extraSkills Customer-owned skills to merge with built-ins.
* Built-in WINS on name collision; first entry wins on duplicate extras.
* Default is empty — keeps the published package byte-identical for
* stdio/external callers that pass no extras (hard invariant, ADR-271).
* Pass `origin: "user"` on an entry to tag it as user-authored.
*/
constructor(extraSkills = []) {
for (const s of extraSkills) {
if (!s?.name)
continue;
if (Object.hasOwn(SKILLS, s.name))
continue; // built-in wins
if (this.extra.has(s.name))
continue; // first wins on dup
this.extra.set(s.name, s);
}
}
/** Stable list of available skill names (built-ins first, then extras). */
listNames() {
return [...SKILL_NAMES, ...this.extra.keys()];
}
/**
* Lightweight list for `pica_skill_list` — no body bytes.
* Total size scales with skill count, not body size.
*/
listSkills() {
const builtins = SKILL_NAMES.map((name) => {
const s = SKILLS[name];
return {
name: s.name,
description: s.description,
triggers: s.triggers,
audience: s.audience,
origin: "builtin",
};
});
const extras = [...this.extra.values()].map((s) => ({
name: s.name,
description: s.description,
triggers: s.triggers,
audience: s.audience,
origin: (s.origin ?? "org"),
}));
return [...builtins, ...extras];
}
/**
* Full skill — bodies are loaded only when an agent invokes get.
* Resolves built-ins first, then extras.
* Throws when name is unknown so callers can return a RecoveryHint.
*/
getSkill(name) {
if (Object.hasOwn(SKILLS, name))
return SKILLS[name];
const org = this.extra.get(name);
if (org)
return org;
throw new SkillNotFoundError(name);
}
/** "builtin" | "org" | "user" | null (null = not registered). */
originOf(name) {
if (Object.hasOwn(SKILLS, name))
return "builtin";
const extra = this.extra.get(name);
if (extra)
return extra.origin ?? "org";
return null;
}
has(name) {
return Object.hasOwn(SKILLS, name) || this.extra.has(name);
}
}
export class SkillNotFoundError extends Error {
skillName;
constructor(skillName) {
super(`Skill not found: ${skillName}`);
this.skillName = skillName;
this.name = "SkillNotFoundError";
}
}
export { SKILLS, SKILL_NAMES };
// ADR-328 — SEP-2640 skills extension (skills/list, skills/get, served SKILL.md).
export * from "./sep2640.js";
//# sourceMappingURL=index.js.map