UNPKG

@withpica/mcp-server

Version:

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

372 lines • 19.8 kB
// 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