UNPKG

@withpica/mcp-server

Version:

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

236 lines • 12.4 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, withBillingGate } from "@withpica/mcp-utils"; export class RecordingSamplesTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_recording_samples_list", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Use when the user asks: 'what samples are in this recording?', 'are the samples " + "cleared?', 'where did this loop come from?'. Lists the marketplace samples logged " + "against a recording (Splice, Tracklib, etc.) — source, name, link, and clearance status. " + "To log a sample rather than read the ones already logged, use pica_recording_samples_add.", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { recording_id: { type: "string", description: "UUID of the recording.", }, }, required: ["recording_id"], }, annotations: { title: "list recording samples", readOnlyHint: true, idempotentHint: true, }, }, executor: this.list.bind(this), }, { definition: { name: "pica_recording_samples_add", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Use when the user asks: 'add a sample to this recording', 'log the Splice loop I used', " + "'record where this sample came from'. Logs a marketplace sample used in a recording. " + "source is the provider (e.g. 'splice', 'tracklib'); sample_name is a human label. " + "clearance_status defaults to 'unknown' — set it to 'not_required' for royalty-free " + "sources, 'pending'/'cleared'/'disputed' otherwise. Idempotent on (recording, source, sample_name). " + "To change the clearance on a sample already logged, use pica_recording_samples_set_status.", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { recording_id: { type: "string", description: "UUID of the recording.", }, source: { type: "string", description: "Provider/marketplace, e.g. 'splice', 'tracklib', 'loopmasters'.", }, sample_name: { type: "string", description: "Human label for the sample.", }, source_url: { type: "string", description: "Optional link to verify the sample.", }, clearance_status: { type: "string", enum: [ "unknown", "not_required", "pending", "cleared", "disputed", ], description: "Rights posture. Defaults to 'unknown'.", }, notes: { type: "string", description: "Optional freetext (e.g. 'main loop in chorus').", }, }, required: ["recording_id", "source", "sample_name"], }, annotations: { title: "add recording sample", destructiveHint: false, idempotentHint: true, }, }, executor: withBillingGate(this.pica, "recording samples add", this.add.bind(this)), }, { definition: { name: "pica_recording_samples_set_status", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Use when the user asks: 'mark this sample as cleared', 'this sample is disputed', " + "'update the clearance on the loop'. Updates the clearance status (and optionally notes) " + "of a logged sample. States: unknown, not_required, pending, cleared, disputed. " + "If the entry should not exist at all, use pica_recording_samples_remove.", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { recording_id: { type: "string", description: "UUID of the recording.", }, sample_id: { type: "string", description: "UUID of the sample row.", }, clearance_status: { type: "string", enum: [ "unknown", "not_required", "pending", "cleared", "disputed", ], description: "New clearance status.", }, notes: { type: "string", description: "Optional updated notes.", }, }, required: ["recording_id", "sample_id", "clearance_status"], }, annotations: { title: "set recording sample status", destructiveHint: false, idempotentHint: true, }, }, executor: withBillingGate(this.pica, "recording samples set status", this.setStatus.bind(this)), }, { definition: { name: "pica_recording_samples_remove", scope: "write:catalog", audiences: ["stdio", "http", "public"], // "write" is DELIBERATE (ADR-259 design, 2026-06-15: "low-stakes, // re-addable → no two-step confirm token"). TOOL_METADATA carried // risk: "destructive" for this tool — a drifted label, since the // declared tier is the ADR-230 source of truth — and was aligned // down to "mutating" on 2026-08-24. Escalating the tier instead // would have added the confirm token AND an elevated destructive:* // scope, refusing every existing write:catalog-only integration. tier: "write", description: "Use when the user asks: 'remove this sample from the recording', 'I logged that sample " + "by mistake', 'delete the sample entry'. Removes a sample entry from a recording (e.g. " + "logged in error). Deletes only that sample entry; the recording is untouched. " + "To mark a sample disputed rather than delete the entry, use pica_recording_samples_set_status.", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { recording_id: { type: "string", description: "UUID of the recording.", }, sample_id: { type: "string", description: "UUID of the sample row to remove.", }, }, required: ["recording_id", "sample_id"], }, annotations: { title: "remove recording sample", // The last surviving piece of the tier/risk mismatch settled on // 2026-08-24 (commits db043e0cd → 416fc4b6f): tier stays "write" // per the ADR-259 design, TOOL_METADATA.risk was aligned down to // "mutating", and this declaration was still claiming destructive. // It was never published — `tier` drives the hint (ADR-230) — so // the only thing it did was tell the next reader something untrue. destructiveHint: false, // Not declared idempotent: since 2be5a992 a retry after a completed // removal answers NOT_FOUND (recovery hint → pica_recording_samples_list), // so the published hint follows TOOL_METADATA.retry_safe (false). }, }, executor: withBillingGate(this.pica, "recording samples remove", this.remove.bind(this)), }, ]; } async list(args) { const data = await this.pica.recordingSamples.list({ recording_id: args.recording_id, }); const rows = data?.data ?? data ?? []; return formatSuccess(rows.length === 0 ? "No samples logged on this recording yet." : `${rows.length} sample(s) logged on this recording.`, data); } async add(args) { const data = await this.pica.recordingSamples.add({ recording_id: args.recording_id, source: args.source, sample_name: args.sample_name, source_url: args.source_url, clearance_status: args.clearance_status, notes: args.notes, }); return formatSuccess(`Sample "${args.sample_name}" (${args.source}) logged. Review at https://withpica.com/inspect/recordings/${args.recording_id}`, data); } async setStatus(args) { const data = await this.pica.recordingSamples.setStatus({ recording_id: args.recording_id, sample_id: args.sample_id, clearance_status: args.clearance_status, notes: args.notes, }); return formatSuccess(`Sample ${args.sample_id} clearance status set to '${args.clearance_status}'. Review at https://withpica.com/inspect/recordings/${args.recording_id}`, data); } async remove(args) { const data = await this.pica.recordingSamples.remove({ recording_id: args.recording_id, sample_id: args.sample_id, }); return formatSuccess(`Sample ${args.sample_id} removed from the recording. Review at https://withpica.com/inspect/recordings/${args.recording_id}`, data); } } //# sourceMappingURL=recording-samples.js.map