UNPKG

@withpica/mcp-server

Version:

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

467 lines • 26.4 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatAsText, formatList, formatSuccess } from "@withpica/mcp-utils"; export class SyncPlacementsTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_sync_placements_query", scope: "read:agreements", audiences: ["stdio", "http"], tier: "read", description: "Use when the user asks: 'any sync placements?', 'what jingles are on?', 'list my syncs'. " + "List sync placements for the org. Filters: work_id, status, verification_status, brand, production_company, person_id, recording_id, source_license_enquiry_id, min_fee, currency. Returns up to `limit` (default 50) with cursor support via `offset`. " + "For one placement in full — recordings, evidence, agreement — use pica_sync_placements_inspect.", workflows: ["sync-placement-required"], vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { work_id: { type: "string", description: "Filter by work id", }, status: { type: "string", enum: ["licensed", "aired", "expired", "renewed", "terminated"], description: "Filter by placement lifecycle status", }, verification_status: { type: "string", enum: [ "unverified", "self_attested", "evidence_attached", "operator_verified", "disputed", ], description: "Filter by evidence verification state", }, brand: { type: "string", description: "Filter by advertiser/brand name", }, production_company: { type: "string", description: "Filter by production company name", }, person_id: { type: "string", description: "Filter to placements where this person is a contact (music supervisor, etc.)", }, recording_id: { type: "string", description: "Filter to placements that licensed this recording", }, source_license_enquiry_id: { type: "string", description: "Filter to placements that originated from this license enquiry", }, min_fee: { type: "number", description: "Minimum fee_amount (pre-pagination SQL filter)", }, currency: { type: "string", description: "ISO-4217 currency code (e.g. USD, GBP)", }, limit: { type: "number", minimum: 1, maximum: 200, description: "Max results (default 50)", }, offset: { type: "number", description: "Cursor offset", }, }, }, }, executor: this.query.bind(this), }, { definition: { name: "pica_sync_placements_inspect", scope: "read:agreements", audiences: ["stdio", "http"], tier: "read", description: "Get one sync placement by id, including all linked recordings, evidence sources, the underlying agreement, and the parties on it. " + "To filter or list placements rather than open one by id, use pica_sync_placements_query.", workflows: ["sync-placement-required"], 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: { id: { type: "string", description: "Sync placement id", }, }, required: ["id"], }, }, executor: this.inspect.bind(this), }, { definition: { name: "pica_sync_placements_create", scope: "write:agreements", audiences: ["stdio", "http"], tier: "write", description: "Use when the user asks: 'log a new sync placement', 'log a jingle', 'add a sync'. " + "Create a sync placement (atomic — placement + draft sync_license agreement + at least one party + recordings + evidence sources, all in one transaction). Requires at least one entry in `contacts[]`. Empty contacts rejects with structured MISSING_CONTACT (next_tool: pica_people_query). New people can be auto-created inline by passing `person_name` instead of `person_id`. " + "To change a placement that already exists, use pica_sync_placements_update.", workflows: ["sync-placement-required"], vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { work_id: { type: "string", description: "Work being licensed for the placement", }, production_title: { type: "string", description: "The film/series/ad/game name the work is licensed into", }, production_type: { type: "string", description: "Film, TV, advertising, trailer, video game, etc.", }, brand: { type: "string", description: "Advertiser brand (for ad placements)", }, production_company: { type: "string", description: "Production company name", }, status: { type: "string", enum: ["licensed", "aired", "expired", "renewed", "terminated"], description: "Initial status (default 'licensed')", }, fee_amount: { type: "number", description: "Total fee in minor units of fee_currency", }, fee_currency: { type: "string", description: "ISO-4217 currency code", }, use_start_date: { type: "string", description: "ISO-8601 date — first day of licensed use", }, use_end_date: { type: "string", description: "ISO-8601 date — last day of licensed use", }, territory: { type: "array", items: { type: "string" }, description: "ISO-3166 country codes; ['WW'] = worldwide", }, media_channels: { type: "array", items: { type: "string" }, description: "tv, online, theatrical, social, in-game, in-store, etc.", }, notes: { type: "string", description: "Free-text notes", }, confidentiality_level: { type: "string", enum: ["public", "redacted_in_public_car", "private"], description: "Public-CAR redaction policy (default 'redacted_in_public_car')", }, contacts: { type: "array", description: "At least one contact required. Pass `person_id` for an existing person OR `person_name` (+ optional email/company/position) to auto-create.", items: { type: "object", properties: { person_id: { type: "string" }, person_name: { type: "string" }, role: { type: "string", description: "music_supervisor | brand_marketing_lead | sync_agent | clearance_attorney | film_producer | director | …", }, email: { type: "string" }, company: { type: "string" }, position: { type: "string" }, split_percentage: { type: "number", description: "Optional commission split (0–100)", }, }, required: ["role"], }, minItems: 1, }, recordings: { type: "array", description: "Specific recordings being licensed under the placement (optional but typical for sync — one master per recording).", items: { type: "object", properties: { recording_id: { type: "string" }, role: { type: "string", description: "main_master | re_record | instrumental | edit | trailer_cut | …", }, master_fee: { type: "number" }, master_share_licensed_pct: { type: "number" }, notes: { type: "string" }, }, required: ["recording_id", "role"], }, }, sources: { type: "array", description: "Evidence sources backing the placement. Auto-advances verification_status: any 'cue_sheet'/'invoice'/'agreement_pdf'/'email'/'screenshot'/'web'/'spreadsheet_import' → evidence_attached; 'user_attestation' only → self_attested; none → unverified.", items: { type: "object", properties: { url: { type: "string" }, kind: { type: "string", enum: [ "user_attestation", "agreement_pdf", "cue_sheet", "invoice", "email", "screenshot", "web", "spreadsheet_import", ], }, agent_id: { type: "string" }, field_coverage: { type: "array", items: { type: "string" }, }, notes: { type: "string" }, }, required: ["url", "kind"], }, }, }, required: ["work_id", "production_title", "contacts"], }, }, executor: this.create.bind(this), }, { definition: { name: "pica_sync_placements_update", scope: "write:agreements", audiences: ["stdio", "http"], tier: "write", description: "Patch a sync placement. Most fields are unconditional; `verification_status` (when set to 'operator_verified' or 'disputed') and `confidentiality_level` are operator-gated and will throw OPERATOR_REQUIRED for customer agents — use a soft transition like status:'terminated' for self-service retirement instead. Status transitions follow the placement state machine; illegal transitions throw INVALID_TRANSITION (inspect first to see the current state). " + "To attach evidence and let verification_status advance on its own, use pica_sync_placements_cite.", workflows: ["sync-placement-required"], 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: { id: { type: "string", description: "Sync placement id", }, status: { type: "string", enum: ["licensed", "aired", "expired", "renewed", "terminated"], description: "Placement lifecycle status — transitions follow the sync_placement_status state machine; setting status:'terminated' is the customer-facing soft-delete path.", }, verification_status: { type: "string", enum: [ "unverified", "self_attested", "evidence_attached", "operator_verified", "disputed", ], description: "Evidence verification state. Operator-only when set to 'operator_verified' or 'disputed'; other values throw OPERATOR_REQUIRED for customer agents.", }, confidentiality_level: { type: "string", enum: ["public", "redacted_in_public_car", "private"], description: "Public-CAR redaction policy. Operator-only — changing this throws OPERATOR_REQUIRED for customer agents.", }, production_title: { type: "string", description: "The film/series/ad/game name the work is licensed into", }, production_type: { type: "string", description: "Film, TV, advertising, trailer, video game, etc.", }, brand: { type: "string", description: "Advertiser brand (for ad placements)", }, production_company: { type: "string", description: "Production company name", }, fee_amount: { type: "number", description: "Total fee in minor units of fee_currency", }, fee_currency: { type: "string", description: "ISO-4217 currency code", }, use_start_date: { type: "string", description: "ISO-8601 date — first day of licensed use", }, use_end_date: { type: "string", description: "ISO-8601 date — last day of licensed use", }, territory: { type: "array", items: { type: "string" }, description: "ISO-3166 country codes; ['WW'] = worldwide", }, media_channels: { type: "array", items: { type: "string" }, description: "tv, online, theatrical, social, in-game, in-store, etc.", }, notes: { type: "string", description: "Free-text notes", }, }, required: ["id"], }, }, executor: this.update.bind(this), }, { definition: { name: "pica_sync_placements_delete", scope: "write:agreements", audiences: ["stdio", "http"], tier: "destructive", description: "Hard-delete a sync placement. Operator-only — customer agents will receive structured OPERATOR_REQUIRED. For self-service retirement, use pica_sync_placements_update with status:'terminated' (soft-delete). Two-step confirmation pattern: first call returns a confirmation_token; second call with the token executes the delete.", workflows: ["sync-placement-required"], vernacular_kind: "specialist", vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Sync placement id", }, confirmation_token: { type: "string", description: "Token from a prior call; required to actually execute the delete", }, }, required: ["id"], }, }, executor: this.delete.bind(this), }, { definition: { name: "pica_sync_placements_cite", scope: "write:agreements", audiences: ["stdio", "http"], tier: "write", description: "Attach an evidence source to a sync placement. Auto-recomputes verification_status — any non-attestation kind ('cue_sheet'/'invoice'/'agreement_pdf'/'email'/'screenshot'/'web'/'spreadsheet_import') advances unverified/self_attested → evidence_attached. operator_verified is sticky (only an explicit operator action exits it). " + "To change the placement's own fields rather than attach evidence, use pica_sync_placements_update.", workflows: ["sync-placement-required"], 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: { id: { type: "string", description: "Sync placement id", }, url: { type: "string", description: "Evidence URL", }, kind: { type: "string", enum: [ "user_attestation", "agreement_pdf", "cue_sheet", "invoice", "email", "screenshot", "web", "spreadsheet_import", ], description: "Source kind. Any non-attestation kind advances verification_status to evidence_attached on first attach.", }, agent_id: { type: "string", description: "Optional agent identity that supplied the source", }, field_coverage: { type: "array", items: { type: "string" }, description: "Which placement fields this source attests to (used for evidence attribution in exports)", }, notes: { type: "string", description: "Free-text notes", }, }, required: ["id", "url", "kind"], }, }, executor: this.cite.bind(this), }, ]; } // ── executors ────────────────────────────────────────────────────────── async query(args) { const result = await this.pica.syncPlacements.list(args); return formatList(result.data, { total: result.total }); } async inspect(args) { const placement = await this.pica.syncPlacements.get(args.id); return formatAsText(placement); } async create(args) { const placement = await this.pica.syncPlacements.create(args); return formatSuccess("Sync placement created", placement); } async update(args) { const { id, ...patch } = args; const placement = await this.pica.syncPlacements.update(id, patch); return formatSuccess("Sync placement updated", placement); } async delete(args) { const { id, confirmation_token } = args; await this.pica.syncPlacements.delete(id, { confirmation_token }); return formatSuccess(`Sync placement ${id} deleted`); } async cite(args) { const { id, ...source } = args; const result = await this.pica.syncPlacements.addSource(id, source); return formatSuccess("Evidence attached to sync placement", result); } } //# sourceMappingURL=sync-placements.js.map