UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

256 lines • 13.3 kB
// 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