UNPKG

@withpica/mcp-server

Version:

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

350 lines • 18.5 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, withBillingGate, } 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. " + "Prefer pica_custody_reconcile to claim many works at once where " + "your catalog is already the named custodian.", 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: withBillingGate(this.pica, "custody claim", 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: withBillingGate(this.pica, "custody response", 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", description: "1..200. Default 50.", }, }, }, 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", 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 this org (or a specific global_publisher_id) is named " + "as canonical custodian via work_custodians.custodian_global_publisher_id but " + "has not yet been claimed via a PICA org membership. The 'Warner Chappell signs " + "up and finds 38 works waiting' view. Read-only — use pica_custody_reconcile " + "to actually claim them. 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. If omitted, uses the caller org's own global_publisher_id bridge.", }, }, }, annotations: { title: "resolve pending custody", readOnlyHint: true, idempotentHint: true, }, }, executor: withBillingGate(this.pica, "custody resolve", this.resolvePending.bind(this)), }, { definition: { name: "pica_custody_reconcile", scope: "admin", audiences: ["stdio", "http", "public"], tier: "write", description: "Bulk-claim every work where your catalog (or a specific global_publisher_id) " + "is named as canonical custodian but hasn't been locally claimed yet. Files " + "one claim per matching work. Default is dry-run — pass confirm=true " + "to execute. Admin/owner access required. Preview the same set " + "read-only with pica_custody_resolve_pending 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: { global_publisher_id: { type: "string", description: "UUID of the global publisher to reconcile claims for. If omitted, reconciles for all publishers linked to your catalog.", }, confirm: { type: "boolean", description: "Required to actually write claims. Default false = dry-run.", }, }, }, annotations: { title: "reconcile custody", destructiveHint: false, idempotentHint: false, }, }, executor: withBillingGate(this.pica, "custody reconcile", 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, }); const incoming = data?.incoming ?? []; const outgoing = data?.outgoing ?? []; const hints = []; if (incoming.length > 0) { hints.push({ gap: "custody_claims_incoming", suggestion: `You have ${incoming.length} incoming custody claim(s) — respond via pica_custody_respond before the 7-day window closes.`, count: incoming.length, severity: "important", }); } return formatSuccess(`Found ${incoming.length} incoming + ${outgoing.length} outgoing custody claim(s).`, 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) reference your publisher identity but haven't been claimed — run pica_custody_reconcile with confirm=true to claim them in bulk.`, 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, }); if (data?.dry_run) { return formatSuccess(`Dry run — ${data.would_claim ?? 0} claim(s) would be created. Pass confirm=true to execute.`, data); } return formatSuccess(`Reconciliation complete — ${data?.claimed ?? 0} claimed, ${data?.failed ?? 0} failed.`, data); } } //# sourceMappingURL=custody.js.map