@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
372 lines • 19.8 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess } from "@withpica/mcp-utils";
export class CustodyTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_custody_claim",
scope: "admin",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Request custody of a work's composition or master chain. " +
"If another org currently holds custody, creates a pending claim; the current " +
"custodian has 7 days to accept, decline, or dispute before the claim auto-approves. " +
"If you already hold custody, this errors (CUSTODY_ALREADY_HELD) — a song you entered " +
"yourself is already in your custody, so there is nothing to claim. To record who owns " +
"a recording's master (e.g. the artist at 100%), use pica_recording_custody_assign. " +
"Admin/owner access required. " +
"Works that name your publisher but that no PICA organisation holds " +
"cannot be claimed yet; list them with pica_custody_resolve_pending.",
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: "UUID of the work to claim.",
},
chain: {
type: "string",
enum: ["composition", "master"],
description: "'composition' = publishing rights; 'master' = recording rights.",
},
reason: {
type: "string",
description: "Optional freetext shown to the current custodian in the notification.",
},
},
required: ["work_id", "chain"],
},
annotations: {
title: "claim custody",
destructiveHint: false,
idempotentHint: false,
},
},
executor: this.claim.bind(this),
},
{
definition: {
name: "pica_custody_respond",
scope: "admin",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Respond to an incoming custody claim. Actions: 'accept' transfers custody " +
"to the claimant; 'decline' rejects the claim (custody unchanged); 'dispute' " +
"freezes the claim for human review. reason is required for dispute. Only the " +
"current custodian can accept or decline; the custodian OR any credited party " +
"can dispute. Admin/owner access required. Use pica_custody_list " +
"to find the claim_id of an incoming claim first.",
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: {
claim_id: {
type: "string",
description: "UUID of the custody claim to respond to.",
},
action: {
type: "string",
enum: ["accept", "decline", "dispute"],
description: "How to respond to the claim",
},
reason: {
type: "string",
description: "Required when action='dispute'. Optional for decline.",
},
},
required: ["claim_id", "action"],
},
annotations: {
title: "respond to a custody claim",
destructiveHint: false,
idempotentHint: false,
},
},
executor: this.respond.bind(this),
},
{
definition: {
name: "pica_custody_list",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Use when the user asks: 'who has custody of X right now?', 'who owns the asset?'. " +
"List custody claims for your catalog — both incoming " +
"(claims others filed against your custody) and outgoing (claims you filed). " +
"Defaults to pending claims only. Available to any authenticated org member. " +
"For the record of past custody changes on one work, use " +
"pica_custody_history; this one shows open claims.",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
direction: {
type: "string",
enum: ["incoming", "outgoing", "both"],
description: "Default: both.",
},
status: {
// ⚠️ COPIED, NOT IMPORTED — and nothing detects a drift.
// The seven live values are the `ClaimStatus` union in
// lib/services/custody-claim.ts (ADR-158 Plan B migrations
// 02/03); `all` is this filter's own escape hatch, not a
// stored status. mcp-server cannot import that module: it sits
// outside `rootDir: ./src`, and it reaches the DB client, the
// Telegram integration and the Slack types through `@/`
// aliases this package does not map. So the list is restated
// here, and `filter-param-enums.test.ts` pins only that THIS
// schema still declares these eight strings — it compares a
// literal to a literal and cannot see a change made on the
// service side. Before A10 (2026-08-24) the same values sat in
// the description as prose, equally unchecked; the enum at
// least makes an invalid value a schema error at the tool
// surface. If ClaimStatus gains a state, change it here too.
type: "string",
enum: [
"pending",
"accepted",
"declined",
"disputed",
"auto_approved",
"withdrawn",
"expired",
"all",
],
description: "Filter by claim status, or `all` for every status. Default: pending.",
},
limit: {
type: "number",
minimum: 1,
maximum: 200,
description: "1..200 per direction. Default 50.",
},
offset: {
type: "number",
minimum: 0,
description: "Skip this many claims in each direction (default 0). Page while has_more.incoming or has_more.outgoing is true.",
},
},
},
annotations: {
title: "list custody claims",
readOnlyHint: true,
idempotentHint: true,
},
},
executor: this.list.bind(this),
},
{
definition: {
name: "pica_custody_history",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Return the custody events in a work's provenance history. Every entry " +
"includes an attestation_hash so the history is cryptographically anchored. " +
"Use this to audit who changed custody when. Available to any authenticated org member. " +
"For claims still awaiting a decision, use pica_custody_list; " +
"this one is the settled record.",
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: "UUID of the work whose history to fetch.",
},
change_type: {
type: "string",
description: "Filter to one change_type (custody_claimed|custody_transferred|custody_released|custody_expired|custody_transfer_auto) or 'all'. Default 'all'.",
},
limit: {
type: "number",
minimum: 1,
maximum: 500,
description: "1..500. Default 100.",
},
},
required: ["work_id"],
},
annotations: {
title: "custody history",
readOnlyHint: true,
idempotentHint: true,
},
},
executor: this.history.bind(this),
},
{
definition: {
name: "pica_custody_resolve_pending",
scope: "admin",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Preview works where your organisation's own publisher identity " +
"(its global_publisher_id) is named as canonical custodian but no PICA " +
"organisation holds the custody yet. The 'Warner Chappell signs up and finds " +
"38 works waiting' view. Read-only. Claiming them is not available yet; " +
"to count them, use pica_custody_reconcile. Admin/owner access required.",
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: {
global_publisher_id: {
type: "string",
description: "Optional. Your organisation's own global_publisher_id (the default); any other id is refused.",
},
},
},
annotations: {
title: "resolve pending custody",
readOnlyHint: true,
idempotentHint: true,
},
},
executor: this.resolvePending.bind(this),
},
{
definition: {
name: "pica_custody_reconcile",
scope: "admin",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Count the works where your organisation's own publisher identity is named " +
"as canonical custodian but no PICA organisation holds the custody yet. " +
"Claiming them is not available yet: with confirm=true it is refused, " +
"naming how many are waiting. Admin/owner access required. List them " +
"with pica_custody_resolve_pending.",
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: {
global_publisher_id: {
type: "string",
description: "Optional. Your organisation's own global_publisher_id (the default); any other id is refused.",
},
confirm: {
type: "boolean",
description: "Asks to claim. Claiming is not available yet, so confirm=true is refused while any work is waiting.",
},
},
},
annotations: {
title: "reconcile custody",
destructiveHint: false,
idempotentHint: false,
},
},
executor: this.reconcile.bind(this),
},
];
}
async claim(args) {
const data = await this.pica.custody.claim({
work_id: args.work_id,
chain: args.chain,
reason: args.reason,
});
return formatSuccess(`Custody claim created (${args.chain} chain). The current custodian has 7 days to respond before the claim auto-approves.`, data);
}
async respond(args) {
const data = await this.pica.custody.respond({
claim_id: args.claim_id,
action: args.action,
reason: args.reason,
});
const verb = args.action === "accept"
? "accepted"
: args.action === "decline"
? "declined"
: "disputed";
return formatSuccess(`Custody claim ${verb}.`, data);
}
async list(args) {
const data = await this.pica.custody.list({
direction: args.direction,
status: args.status,
limit: args.limit,
offset: args.offset,
});
const incoming = data?.incoming ?? [];
const outgoing = data?.outgoing ?? [];
// The route's real per-direction totals (row-cap T3: only the slices
// came back, so the counts below were page lengths). An older server
// sends no totals; fall back to what this page holds.
const incomingTotal = data?.totals?.incoming ?? incoming.length;
const outgoingTotal = data?.totals?.outgoing ?? outgoing.length;
const hasMore = data?.has_more?.incoming === true || data?.has_more?.outgoing === true;
const hints = [];
if (incomingTotal > 0) {
hints.push({
gap: "custody_claims_incoming",
suggestion: `You have ${incomingTotal} incoming custody claim(s) — respond via pica_custody_respond before the 7-day window closes.`,
count: incomingTotal,
severity: "important",
});
}
const nextOffset = (data?.offset ?? args.offset ?? 0) + (data?.limit ?? args.limit ?? 50);
return formatSuccess(`Found ${incomingTotal} incoming + ${outgoingTotal} outgoing custody claim(s).` +
(hasMore
? ` This page shows ${incoming.length} + ${outgoing.length}; pass offset=${nextOffset} for more.`
: ""), data, hints);
}
async history(args) {
const data = await this.pica.custody.history({
work_id: args.work_id,
change_type: args.change_type,
limit: args.limit,
});
const count = data?.count ?? 0;
return formatSuccess(count > 0
? `Returned ${count} custody history row(s) for the work.`
: "No custody history rows for this work.", data);
}
async resolvePending(args) {
const data = await this.pica.custody.resolvePending({
global_publisher_id: args.global_publisher_id,
});
const count = data?.count ?? 0;
const hints = [];
if (count > 0) {
hints.push({
gap: "custody_inheritable",
suggestion: `${count} work(s) name your publisher identity as custodian but no PICA organisation holds them. Claiming them is not available yet — contact support if they are yours.`,
count,
severity: "important",
});
}
return formatSuccess(count > 0
? `${count} inheritable work(s) found.`
: "No inheritable works found.", data, hints);
}
async reconcile(args) {
const data = await this.pica.custody.reconcile({
global_publisher_id: args.global_publisher_id,
confirm: args.confirm === true,
});
// The route claims nothing (claiming custody no PICA organisation holds
// is not available); with confirm=true and works waiting it refuses, and
// the SDK throws that refusal as the tool's error.
return formatSuccess((data?.waiting ?? 0) > 0
? `${data.waiting} work(s) waiting for your publisher identity. Claiming them is not available yet.`
: "No custody is waiting for your publisher identity.", data);
}
}
//# sourceMappingURL=custody.js.map