@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
515 lines • 24.6 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatAsText, formatList, formatSuccess } from "@withpica/mcp-utils";
/** An enquiry's budget for display; NULL means none was given (2026-09-25). */
function budgetLabel(e) {
if (e.proposed_budget_amount == null)
return "no budget given";
return `${e.proposed_budget_currency || "GBP"} ${e.proposed_budget_amount}`;
}
export class LicensingTools {
pica;
constructor(pica) {
this.pica = pica;
}
/**
* Get all licensing tools
*/
getTools() {
return [
{
definition: {
name: "pica_search_for_sync",
scope: "read:analytics",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Search for licensable tracks by audio characteristics. " +
"This is a powerful sync licensing search that finds tracks based on " +
"mood, genre, BPM, musical key, energy level, and other audio features. " +
"Use this when a music supervisor or client is looking for specific types of music.",
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: {
mood: {
type: "string",
description: "Comma-separated mood tags to search for (e.g., 'uplifting,energetic' or 'melancholic,introspective')",
},
genre: {
type: "string",
description: "Genre to filter by (e.g., 'electronic', 'acoustic', 'orchestral')",
},
min_bpm: {
type: "number",
description: "Minimum BPM (beats per minute)",
},
max_bpm: {
type: "number",
description: "Maximum BPM (beats per minute)",
},
key: {
type: "string",
description: "Musical key to filter by (e.g., 'C', 'Am', 'F#', 'Bb')",
},
min_energy: {
type: "number",
description: "Minimum energy level (0.0 to 1.0, where 1.0 is highest energy)",
},
max_energy: {
type: "number",
description: "Maximum energy level (0.0 to 1.0)",
},
min_valence: {
type: "number",
description: "Minimum valence/positivity (0.0 to 1.0, where 1.0 is most positive/happy)",
},
max_valence: {
type: "number",
description: "Maximum valence/positivity (0.0 to 1.0)",
},
min_duration: {
type: "number",
description: "Minimum duration in seconds",
},
max_duration: {
type: "number",
description: "Maximum duration in seconds",
},
instrumental_only: {
type: "boolean",
description: "Only return instrumental tracks (no vocals)",
},
available_for_sync: {
type: "boolean",
description: "Only return tracks marked as available for sync licensing",
},
limit: {
type: "number",
description: "Maximum number of results (default: 20, max: 100)",
},
},
},
},
executor: this.searchForSync.bind(this),
},
{
definition: {
name: "pica_submit_license_enquiry",
scope: "admin",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Record a licensing enquiry against one of your own works, e.g. one " +
"that arrived by email or phone. Captures who asked, the project, " +
"territory, term and (optionally) a proposed budget. Sends no email: " +
"it only records the enquiry so it can be tracked and answered.",
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: "ID of the work to license",
},
track_title: {
type: "string",
description: "Title of the track (for reference)",
},
contact_name: {
type: "string",
description: "Name of the person making the enquiry",
},
contact_email: {
type: "string",
description: "Email address for contact",
},
company_name: {
type: "string",
description: "Company or organization name (optional)",
},
project_type: {
type: "string",
description: "Type of project",
enum: [
"tv_ad",
"film",
"social_media",
"game",
"podcast",
"corporate",
"other",
],
},
project_description: {
type: "string",
description: "Description of how the music will be used in the project",
},
territory: {
type: "string",
description: "Geographic territory for the license (e.g., 'uk', 'eu', 'worldwide', 'north_america')",
},
duration: {
type: "string",
description: "License duration (e.g., '1_year', '3_years', 'perpetual')",
},
distribution: {
type: "array",
description: "Distribution channels (e.g., ['streaming', 'broadcast', 'cinema', 'social', 'web'])",
items: {
type: "string",
},
},
proposed_budget_amount: {
type: "number",
description: "Proposed license fee amount (optional; omit if no budget was given)",
},
proposed_budget_currency: {
type: "string",
description: "Currency for the budget (e.g., 'GBP', 'USD', 'EUR')",
},
budget_notes: {
type: "string",
description: "Additional notes about the budget or negotiation flexibility",
},
},
required: [
"work_id",
"track_title",
"contact_name",
"contact_email",
"project_type",
"project_description",
"territory",
"duration",
],
},
},
executor: this.submitLicenseEnquiry.bind(this),
},
{
definition: {
name: "pica_list_license_enquiries",
scope: "read:analytics",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "List license enquiries with optional filters. View pending, reviewed, " +
"quoted, accepted, or declined enquiries.",
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: "Filter by work ID",
},
status: {
type: "string",
description: "Filter by status",
enum: [
"pending",
"reviewing",
"quoted",
"accepted",
"declined",
"expired",
],
},
project_type: {
type: "string",
description: "Filter by project type",
enum: [
"tv_ad",
"film",
"social_media",
"game",
"podcast",
"corporate",
"other",
],
},
territory: {
type: "string",
description: "Filter by territory",
},
limit: {
type: "number",
description: "Maximum number of results (default: 50)",
},
offset: {
type: "number",
description: "Pagination offset (default: 0)",
},
},
},
},
executor: this.listLicenseEnquiries.bind(this),
},
{
definition: {
name: "pica_get_license_enquiry",
scope: "read:analytics",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Get details of a specific license enquiry by ID. For a filtered " +
"list, use pica_list_license_enquiries; to reply with a status or " +
"counter offer, use pica_update_license_enquiry_status.",
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: {
id: {
type: "string",
description: "License enquiry ID",
},
},
required: ["id"],
},
},
executor: this.getLicenseEnquiry.bind(this),
},
{
definition: {
name: "pica_update_license_enquiry_status",
// Rescoped admin → write:agreements (ADR-289 Wave C1 review):
// ADMIN_SEGMENT_WRITE_FLOOR["license-enquiries"] = "write:agreements",
// and admin is excluded from GRANT_EDITABLE_SCOPES, so the Wave B1
// counter-offer fields were unreachable by the in-app assistant grant.
scope: "write:agreements",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Update the status of a license enquiry, or set a counter " +
"offer. Use status to move enquiries through the workflow " +
"(reviewing, quoted, accepted, declined). Pass " +
"counter_offer_amount to propose a different price than the " +
"requester asked for — this also moves the enquiry to status " +
"'quoted' automatically, so you don't need to pass status too " +
"when setting a counter offer. Find the enquiry first with " +
"pica_list_license_enquiries or pica_get_license_enquiry; this " +
"one only writes the reply.",
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.",
// status and counter_offer_amount are two independent write modes —
// at least one is required (mirrors the id/work_id alias pattern
// on pica_works_inspect, requiredOneOf § index.ts).
requiredOneOf: [["status", "counter_offer_amount"]],
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "License enquiry ID",
},
status: {
type: "string",
description: "New status",
enum: [
"pending",
"reviewing",
"quoted",
"accepted",
"declined",
"expired",
],
},
notes: {
type: "string",
description: "Internal notes about the status change. Ignored when counter_offer_amount is set — use counter_offer_notes instead.",
},
counter_offer_amount: {
type: "number",
description: "Counter-offer amount to propose in place of the requested price. Setting this moves the enquiry to status 'quoted' — no need to also pass status.",
},
counter_offer_notes: {
type: "string",
description: "Internal notes recorded alongside the counter offer.",
},
},
required: ["id"],
},
},
executor: this.updateLicenseEnquiryStatus.bind(this),
},
];
}
/**
* Search for sync-licensable tracks
*/
async searchForSync(args) {
const result = await this.pica.licensing.searchForSync({
mood: args.mood,
genre: args.genre,
min_bpm: args.min_bpm,
max_bpm: args.max_bpm,
key: args.key,
min_energy: args.min_energy,
max_energy: args.max_energy,
min_valence: args.min_valence,
max_valence: args.max_valence,
min_duration: args.min_duration,
max_duration: args.max_duration,
instrumental_only: args.instrumental_only,
available_for_sync: args.available_for_sync,
limit: args.limit,
});
// Format tracks for display
const formattedTracks = result.tracks.map((track) => ({
id: track.id,
title: track.title,
work: track.work?.title || "No linked work",
artist: track.work?.primary_artist || "Unknown",
bpm: track.detected_bpm
? `${Math.round(track.detected_bpm)} BPM`
: "Unknown",
key: track.detected_key || "Unknown",
energy: track.detected_energy
? `${Math.round(track.detected_energy * 100)}%`
: "Unknown",
mood: track.detected_mood?.join(", ") || "Not analyzed",
duration: track.duration_seconds
? `${Math.floor(track.duration_seconds / 60)}:${String(Math.floor(track.duration_seconds % 60)).padStart(2, "0")}`
: "Unknown",
available: track.published ? "Yes" : "No",
}));
return formatList(formattedTracks, {
title: "Sync Licensing Search Results",
total: result.count,
filters: result.filters,
});
}
/**
* Submit a license enquiry
*/
async submitLicenseEnquiry(args) {
const enquiry = await this.pica.licensing.submitEnquiry({
work_id: args.work_id,
track_title: args.track_title,
contact_name: args.contact_name,
contact_email: args.contact_email,
company_name: args.company_name,
project_type: args.project_type,
project_description: args.project_description,
territory: args.territory,
duration: args.duration,
distribution: args.distribution,
proposed_budget_amount: args.proposed_budget_amount,
proposed_budget_currency: args.proposed_budget_currency,
budget_notes: args.budget_notes,
});
return formatSuccess("License enquiry submitted successfully", {
id: enquiry.id,
track: enquiry.track_title,
project_type: enquiry.project_type,
territory: enquiry.territory,
proposed_budget: budgetLabel(enquiry),
status: enquiry.status,
});
}
/**
* List license enquiries
*/
async listLicenseEnquiries(args) {
const enquiries = await this.pica.licensing.listEnquiries({
work_id: args.work_id,
status: args.status,
project_type: args.project_type,
territory: args.territory,
limit: args.limit || 50,
offset: args.offset || 0,
});
// Format enquiries for display
const formattedEnquiries = enquiries.map((enquiry) => ({
id: enquiry.id,
track: enquiry.track_title,
contact: `${enquiry.contact_name} (${enquiry.contact_email})`,
company: enquiry.company_name || "Individual",
project: enquiry.project_type,
territory: enquiry.territory,
budget: budgetLabel(enquiry),
status: enquiry.status,
created: new Date(enquiry.created_at).toLocaleDateString(),
}));
return formatList(formattedEnquiries, {
title: "License Enquiries",
total: enquiries.length,
});
}
/**
* Get license enquiry by ID
*/
async getLicenseEnquiry(args) {
const enquiry = await this.pica.licensing.getEnquiry(args.id);
const formatted = {
id: enquiry.id,
track: enquiry.track_title,
work_id: enquiry.work_id,
contact: {
name: enquiry.contact_name,
email: enquiry.contact_email,
company: enquiry.company_name,
},
project: {
type: enquiry.project_type,
description: enquiry.project_description,
territory: enquiry.territory,
duration: enquiry.duration,
distribution: enquiry.distribution,
},
budget: {
proposed: budgetLabel(enquiry),
counter_offer: enquiry.your_counter_offer
? `${enquiry.proposed_budget_currency} ${enquiry.your_counter_offer}`
: null,
notes: enquiry.budget_notes,
},
status: enquiry.status,
internal_notes: enquiry.your_notes,
responded_at: enquiry.responded_at,
created_at: enquiry.created_at,
};
return formatAsText(formatted);
}
/**
* Update license enquiry status, or set a counter offer
* (ADR-289 Wave B1 item 6 — ports the bespoke assistant's
* `set_license_counter_offer`).
*/
async updateLicenseEnquiryStatus(args) {
if (args.counter_offer_amount !== undefined &&
args.counter_offer_amount !== null) {
const enquiry = await this.pica.licensing.setLicenseEnquiryCounterOffer(args.id, args.counter_offer_amount, args.counter_offer_notes);
return formatSuccess("License enquiry counter offer set", {
id: enquiry.id,
track: enquiry.track_title,
status: enquiry.status,
counter_offer_amount: enquiry.your_counter_offer,
notes: enquiry.your_notes,
});
}
if (!args.status) {
return {
content: [
{
type: "text",
text: "status or counter_offer_amount is required.",
},
],
isError: true,
};
}
const enquiry = await this.pica.licensing.updateEnquiryStatus(args.id, args.status, args.notes);
return formatSuccess("License enquiry status updated", {
id: enquiry.id,
track: enquiry.track_title,
status: enquiry.status,
notes: enquiry.your_notes,
});
}
}
//# sourceMappingURL=licensing.js.map