@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
256 lines • 13.3 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, SHARE_LINKS_WRITE_PROPERTIES, SHARE_LINKS_CREATE_REQUIRED, SHARE_LINKS_UPDATE_PROPERTIES, } from "@withpica/mcp-utils";
export class ShareLinksTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
// Tier 2 (MCP tool-surface consolidation, ADR-149 query/inspect
// pattern): pica_share_links_query is the canonical read tool,
// mirroring pica_releases_query/pica_projects_query. The deprecated
// pica_share_links_list alias was REMOVED in the mcp-server 2.93.0 WS5
// sweep (0 calls/30d); the listShareLinks executor stays — it backs
// pica_share_links_query.
{
definition: {
name: "pica_share_links_query",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "List existing share links, optionally filtered by work. " +
"Shows active links with view/download counts and expiry status. " +
"For one link in full, use pica_share_links_inspect.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: {
work_id: {
type: "string",
description: "Filter share links for a specific work (UUID)",
},
},
},
},
executor: this.listShareLinks.bind(this),
},
{
definition: {
name: "pica_share_links_create",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Create a share link for a work, a playlist (work_ids), a release, or a saved selection " +
"(selection_id — every work the selection matches, as a playlist snapshot). " +
"What the recipient sees is OFF unless you turn it on: allow_view_lyrics shows lyrics, " +
"allow_view_collaborators shows the writers and credits, allow_view_financials shows splits " +
"— all default false because anyone holding the link can open it. " +
"include_all_files gives the recipient every version of each song (master, instrumental, stems) " +
"to choose from; playlist links do this by default unless included_file_ids names a subset. " +
"Also supports password protection, view-count limits, expiry, access-type scoping " +
"(allowed_person_ids for known people, sent_to_emails for external recipients), " +
"notify-on-access, and audio watermarking. Returns share_url for the recipient. " +
"Permissions are fixed at create time — pica_share_links_update " +
"changes name, expiry, view cap, password, and can deactivate the link.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: SHARE_LINKS_WRITE_PROPERTIES,
required: [...SHARE_LINKS_CREATE_REQUIRED],
},
},
executor: this.createShareLink.bind(this),
},
{
definition: {
name: "pica_share_links_inspect",
scope: "read:catalog",
audiences: ["stdio", "http"],
tier: "read",
description: "Get full details for a single share link by id — name, description, " +
"expiry, view counts, access controls, password-protected flag, and " +
"the linked work's title + primary artist. Use after pica_share_links_query " +
"to drill into a specific link, or after pica_share_links_create to verify.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Share link ID (UUID).",
},
},
required: ["id"],
},
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
},
executor: this.inspectShareLink.bind(this),
},
{
definition: {
name: "pica_share_links_update",
scope: "write:files",
audiences: ["stdio", "http"],
tier: "write",
description: "Update a narrow set of share-link fields — name, description, " +
"personal_message, expires_at, max_views, is_active, password. " +
"Permission-bearing fields (access_type, allow_*, allowed_person_ids, " +
"watermark_audio, sent_to_emails) are NOT updatable post-create — " +
"revoke and recreate to change permissions. Set is_active=false to " +
"deactivate without deleting; set password=null to remove the password. " +
"To remove a link entirely, use pica_share_links_delete.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Share link ID (UUID).",
},
...SHARE_LINKS_UPDATE_PROPERTIES,
},
required: ["id"],
},
annotations: {
idempotentHint: true,
},
},
executor: this.updateShareLink.bind(this),
},
{
definition: {
name: "pica_share_links_delete",
scope: "write:files",
audiences: ["stdio"],
tier: "destructive",
description: "Hard-delete a share link by id. The link becomes inaccessible " +
"immediately. To deactivate without losing the audit trail, prefer " +
"pica_share_links_update with is_active=false.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Share link ID (UUID).",
},
},
required: ["id"],
},
annotations: {
destructiveHint: true,
idempotentHint: false,
},
},
executor: this.deleteShareLink.bind(this),
},
];
}
async listShareLinks(args) {
const data = await this.pica.shareLinks.list({
work_id: args.work_id,
});
const links = Array.isArray(data) ? data : [];
const hints = [];
if (!links.length && args.work_id) {
hints.push({
gap: "no_share_links",
suggestion: "Create a share link for this work (pica_share_links_create)",
severity: "nice_to_have",
});
}
return formatSuccess(`Found ${links.length} share link(s)`, links, hints);
}
async createShareLink(args) {
const targets = [
args.work_id ? "work_id" : null,
args.work_ids?.length ? "work_ids" : null,
args.release_id ? "release_id" : null,
args.selection_id ? "selection_id" : null,
].filter((t) => t !== null);
if (targets.length === 0) {
return formatSuccess("Please provide a work_id, work_ids (array), release_id or selection_id to create a share link", null);
}
// The route refuses this too; saying so here spares a round trip and
// names the conflict in the agent's own terms.
if (args.selection_id && targets.length > 1) {
return {
content: [
{
type: "text",
text: `selection_id cannot be combined with ${targets
.filter((t) => t !== "selection_id")
.join(", ")} — pass exactly one target`,
},
],
isError: true,
};
}
// Pass through every property the shared schema advertises. The admin
// route's allow-list (lib/services/share-links-write-constants.ts —
// ADR-174 Phase 2 item 20 slice 1) rejects anything outside the
// contract; column defaults apply when a field is omitted.
const payload = {};
for (const key of Object.keys(args)) {
if (args[key] !== undefined)
payload[key] = args[key];
}
if (payload.allow_download === undefined)
payload.allow_download = false;
const data = await this.pica.shareLinks.create(payload);
// ⚠️ This hint said "send the URL to the recipient" while the response
// carried only an opaque `token` and no origin to build one from, so the
// agent had to send the user to the dashboard to find the link by hand
// (ops issue 3c24918d, 2026-09-10). The route now returns `share_url`
// beside `token` — name it, so the hint points at a field that exists.
const hints = [];
if (data?.token) {
hints.push({
gap: "share_link_created",
suggestion: data?.share_url
? `Share link created — give the recipient ${data.share_url}`
: "Share link created, but the response carried no share_url — report it with pica_report_issue rather than guessing a URL from the token.",
severity: "nice_to_have",
});
}
return formatSuccess("Share link created", data, hints);
}
async inspectShareLink(args) {
const data = await this.pica.shareLinks.get(args.id);
return formatSuccess("Share link details", data);
}
async updateShareLink(args) {
const { id, ...updates } = args;
// Forward only the keys explicitly passed — preserves the route's
// "narrow allow-list" semantics (any unknown key 400s with a
// helpful redirect message). Empty payload also 400s server-side
// so agents get a clear "nothing to update" response instead of a
// silent no-op.
const payload = {};
for (const key of Object.keys(updates)) {
if (updates[key] !== undefined)
payload[key] = updates[key];
}
const data = await this.pica.shareLinks.update(id, payload);
return formatSuccess("Share link updated", data);
}
async deleteShareLink(args) {
await this.pica.shareLinks.delete(args.id);
return formatSuccess(`Share link ${args.id} deleted`);
}
}
//# sourceMappingURL=share-links.js.map