@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
236 lines • 12.2 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess } 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: 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: 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: 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