@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
466 lines • 26.6 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatAsText, formatList, formatSuccess } from "@withpica/mcp-utils";
import { withBillingGate } 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",
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: withBillingGate(this.pica, "sync placement creation", 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: withBillingGate(this.pica, "sync placement update", 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: withBillingGate(this.pica, "sync placement deletion", 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: withBillingGate(this.pica, "sync placement evidence", 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