@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
168 lines • 7.28 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { SkillsRegistry, SkillNotFoundError, } from "../skills/index.js";
export class SkillsTools {
registry;
constructor(extraSkills = []) {
this.registry = new SkillsRegistry(extraSkills);
}
getTools() {
return [
{
definition: {
name: "pica_skill_list",
scope: "always",
audiences: ["stdio", "http"],
tier: "read",
description: "Use when the user asks: 'what catalog workflows are available?', 'what can you help me do with my songs?', 'show me my workflow options'. " +
"Returns the list of downloadable PICA skill methodologies — name, one-line description, and trigger phrases. " +
"Cheap to call. Use pica_skill_get(name) to load a specific skill's full methodology body when you're ready to follow it.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
annotations: {
readOnlyHint: true,
},
},
executor: this.listSkills.bind(this),
},
{
definition: {
name: "pica_skill_get",
scope: "always",
audiences: ["stdio", "http"],
tier: "read",
description: "Use when the user asks: 'audit my catalog', 'register my songs', 'prepare for sync', 'set up my workspace', 'close the loop on this song'. " +
"Returns the full methodology body for one skill — markdown with step-by-step instructions, tool chains, and offer scripts. " +
"Call pica_skill_list first to see available skill names, then pica_skill_get to load one. Read once at the start of the workflow; do not re-fetch on every step.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Kebab-case skill name from pica_skill_list (e.g. 'catalog-audit', 'register-my-works', 'prepare-for-sync').",
},
},
required: ["name"],
additionalProperties: false,
},
annotations: {
readOnlyHint: true,
},
},
executor: this.getSkill.bind(this),
},
];
}
async listSkills(_args) {
const skills = this.registry.listSkills();
return {
content: [
{
type: "text",
text: skills.length
? `Available skills (${skills.length}):\n\n` +
skills
.map((s) => `- ${s.name} — ${s.description}${s.origin === "org" ? " (your org's skill)" : ""}\n triggers: ${s.triggers.join(", ")}\n audience: ${s.audience}`)
.join("\n\n") +
"\n\nCall pica_skill_get(name) to load a skill's full methodology."
: "No skills available.",
},
],
structuredContent: { skills },
};
}
async getSkill(args) {
const name = String(args?.name ?? "").trim();
if (!name) {
return {
isError: true,
content: [
{
type: "text",
text: "Missing required argument: name. Call pica_skill_list to see available skill names.",
},
],
};
}
let skill;
try {
skill = this.registry.getSkill(name);
}
catch (err) {
if (err instanceof SkillNotFoundError) {
return {
isError: true,
content: [
{
type: "text",
text: `Skill not found: ${name}. Call pica_skill_list to see available skill names.`,
},
],
};
}
throw err;
}
const origin = this.registry.originOf(name) ?? "builtin";
const provenance = origin === "user"
? "> This is a skill you authored — not PICA-provided guidance.\n\n"
: origin === "org"
? "> This is a skill your organisation authored — not PICA-provided guidance.\n\n"
: "";
const frontmatterSummary = [
`# ${skill.name}`,
"",
`**Description:** ${skill.description}`,
`**Audience:** ${skill.audience}`,
`**Triggers:** ${skill.triggers.join(", ")}`,
`**Tools used:** ${skill.tools_required.join(", ")}`,
`**Output:** ${skill.output}`,
"",
"---",
"",
].join("\n");
return {
content: [
{
type: "text",
text: provenance + frontmatterSummary + skill.body,
},
{
type: "resource_link",
uri: `skill://withpica/${skill.name}/SKILL.md`,
name: `Skill — ${skill.name}`,
description: skill.description,
mimeType: "text/markdown",
},
],
structuredContent: {
name: skill.name,
description: skill.description,
// The methodology itself. Clients that read structuredContent and
// ignore the text blocks previously got the frontmatter and no steps.
// Raw body — no provenance banner, no frontmatter summary; both of
// those are prose we add for the text channel. Pinned by
// "structuredContent.body is the registry body verbatim" in
// src/__tests__/skills/skills-tools.test.ts.
body: skill.body,
triggers: skill.triggers,
audience: skill.audience,
tools_required: skill.tools_required,
output: skill.output,
origin,
...(skill.definition_of_done !== undefined
? { definition_of_done: skill.definition_of_done }
: {}),
...(skill.when_not_to_use !== undefined
? { when_not_to_use: skill.when_not_to_use }
: {}),
},
};
}
}
//# sourceMappingURL=skills.js.map