@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
118 lines • 6.64 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatStructured } from "@withpica/mcp-utils";
const MAX_LIMIT = 100;
export class SyncFitTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_sync_fit_query",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Where the user's songs sit against film and TV productions still " +
"in progress: the same list as the sync page's 'fit' section. Use " +
"when the user asks: 'which of my songs could fit a film?', 'what " +
"productions suit this song?', 'which songs fit this show?'. Each " +
"row is one song and one production, with a band (strong or " +
"possible: how much they have in common, NOT a likelihood of a " +
"placement) and the reasons PICA's matcher found (shared genre, " +
"language and territory, production stage). Each production " +
"carries its genres, themes, countries, language, release date, " +
"stage, companies and networks as TMDB and Wikidata hold them. " +
"PICA holds no synopsis or tone for a production, and themes are " +
"rarely filled. With song_facts: true each row also carries the " +
"song's own raw facts: its genres and lyric language, and a " +
"`measured` entry per analysed master with tempo_bpm, key, mode " +
"(major/minor) and duration_seconds, measured from the audio by " +
"PICA's analyser, the same names the other PICA tools use. " +
"Energy is left out until its measurement is fixed, and lyric " +
"themes are not stored yet. Use the facts to judge the fit " +
"yourself and explain your " +
"reasoning to the user. Your judgement belongs in your answer: " +
"never write it back to PICA as data (no notes, tags or fields " +
"recording a fit). Nothing here is a confirmed opportunity. " +
"Without work_id or screen_project_id the list covers the whole " +
"catalogue, spread so each song's best three pairs come before " +
"any song's fourth. Pages with limit (up to 100, default 40) and " +
"offset; total says how many pairs there are in all. " +
"→ first: pica_works_query (find a work_id) " +
"→ then: pica_sync_placements_query (placements already made)",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
work_id: {
type: "string",
description: "One song's productions: a work id from pica_works_query.",
},
screen_project_id: {
type: "string",
description: "One production's songs: a screen_project_id from an earlier result.",
},
song_facts: {
type: "boolean",
description: "Also return each song's raw facts (genres, lyric language, and measured tempo_bpm, key, mode, duration_seconds per analysed master) so you can judge the fit yourself.",
},
limit: {
type: "number",
minimum: 1,
maximum: MAX_LIMIT,
description: "Pairs per page (default 40, at most 100).",
},
offset: {
type: "number",
minimum: 0,
description: "Pairs to skip, for the next page.",
},
},
},
},
executor: this.query.bind(this),
},
];
}
async query(args) {
const page = await this.pica.syncFit.query({
...(typeof args.work_id === "string" ? { work_id: args.work_id } : {}),
...(typeof args.screen_project_id === "string"
? { screen_project_id: args.screen_project_id }
: {}),
...(args.song_facts === true ? { song_facts: true } : {}),
...(typeof args.limit === "number"
? { limit: Math.min(Math.max(1, Math.floor(args.limit)), MAX_LIMIT) }
: {}),
...(typeof args.offset === "number"
? { offset: Math.max(0, Math.floor(args.offset)) }
: {}),
});
const n = page.rows.length;
const first = n === 0 ? 0 : page.offset + 1;
const last = page.offset + n;
const more = last < page.total;
const summary = n === 0 && page.total > 0
? `No pairs at offset ${page.offset}: there are ${page.total} in all. Start again at offset 0.`
: page.total === 0
? "No song–production pairs strong enough to show. Matching needs a genre or an audio analysis on a song, and an open production it shares something with."
: `Showing pairs ${first}–${last} of ${page.total} (${page.songs} ${page.songs === 1 ? "song" : "songs"}).` +
(more
? ` More remain: call again with offset ${last}. Do not treat this page as everything.`
: "") +
" Bands are how much a song and a production have in common, not how likely a placement is.";
return formatStructured({
count: n,
total: page.total,
songs: page.songs,
offset: page.offset,
limit: page.limit,
has_more: more,
items: page.rows,
}, summary);
}
}
//# sourceMappingURL=sync-fit.js.map