@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
172 lines • 6.48 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
// Remix family first so "Féz Remix Instrumental" resolves as a remix.
const SUFFIX_RULES = [
{
tokens: ["remix", "rmx", "flip", "bootleg", "vip"],
kind: "remix",
versionType: "remix",
},
{ tokens: ["instrumental"], kind: "recording", versionType: "instrumental" },
{
tokens: ["acoustic", "unplugged"],
kind: "recording",
versionType: "acoustic",
},
{
tokens: ["live performance", "live"],
kind: "recording",
versionType: "live_performance",
},
{
tokens: ["backing track", "karaoke"],
kind: "recording",
versionType: "backing_track",
},
{
tokens: [
"radio edit",
"single edit",
"short edit",
"short version",
"edit",
"clean",
],
kind: "recording",
versionType: "edit",
},
{ tokens: ["demo"], kind: "recording", versionType: "demo" },
{
tokens: ["slowed + reverb", "slowed", "sped up", "reverb", "nightcore"],
kind: "recording",
versionType: "alternate",
},
{ tokens: ["cover"], kind: "recording", versionType: "cover" },
];
// ---------------------------------------------------------------------------
// Detection (mirrors detectVersionOfExistingWork from version-detection.ts)
// ---------------------------------------------------------------------------
function normalize(s) {
return s
.toLowerCase()
.replace(/[._]/g, " ")
.replace(/[^\p{L}\p{N}\s+]/gu, "") // keep letters (incl. accented), digits, whitespace, '+'
.replace(/\s+/g, " ")
.trim();
}
function suffixHasToken(suffixNorm, token) {
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(^|\\s)${escaped}(\\s|$)`).test(suffixNorm);
}
function matchRule(suffixNorm) {
for (const rule of SUFFIX_RULES) {
if (rule.tokens.some((t) => suffixHasToken(suffixNorm, t)))
return rule;
}
return null;
}
function parseTitle(title) {
const t = title.trim();
const patterns = [
/^(.*\S)\s*[([{]([^)\]}]+)[)\]}]\s*$/,
/^(.*\S)\s*[-–—:]\s*(\S.*)$/,
];
for (const re of patterns) {
const m = t.match(re);
if (!m)
continue;
const stem = m[1].trim();
if (!stem)
continue;
const rule = matchRule(normalize(m[2]));
if (rule)
return { stem, marker: m[2].trim(), rule };
}
return null;
}
/**
* Parse a candidate title and return the stem + version metadata, or null if
* the title does not match any known version suffix. Pure function, no I/O.
* Used by the MCP create-guard to search by stem instead of list-and-scan,
* avoiding the /admin/works?limit=100 cap on large catalogs (ADR-280 fix).
*/
export function parseVersionTitle(title) {
const p = parseTitle(title);
if (!p)
return null;
return {
stem: p.stem,
suffix: p.rule.tokens[0],
marker: p.marker,
suggestedVersionType: p.rule.versionType,
kind: p.rule.kind,
};
}
/**
* Detect whether a candidate work title is a version-suffix of an existing
* work by the same artist. Returns the match or null.
*
* Same-artist scoping is best-effort: when either side lacks an artistId,
* title match alone is sufficient (advisory guard, never blocks a create).
*/
export function detectVersionOfExistingWork(candidate, existingWorks) {
const parsed = parseTitle(candidate.title);
if (!parsed)
return null;
const stemNorm = normalize(parsed.stem);
if (!stemNorm)
return null;
const parent = existingWorks.find((w) => {
if (normalize(w.title) !== stemNorm)
return false;
if (candidate.artistId && w.artistId)
return candidate.artistId === w.artistId;
return true;
});
if (!parent)
return null;
return {
parentWork: { id: parent.id, title: parent.title, iswc: parent.iswc },
suffix: parsed.rule.tokens[0],
marker: parsed.marker,
suggestedVersionType: parsed.rule.versionType,
kind: parsed.rule.kind,
};
}
// ---------------------------------------------------------------------------
// Warning builder (mirrors buildVersionWarning from version-detection-lookup.ts)
// ---------------------------------------------------------------------------
/**
* Format advisory response fields for a detected version match.
* Always returns a value; never throws.
*/
export function buildVersionWarning(match, candidateWorkId) {
const { parentWork, kind, suggestedVersionType } = match;
if (kind === "recording") {
return {
warning: `this looks like a version of "${parentWork.title}" (work ${parentWork.id}). ` +
`versions are usually modelled as recordings under the parent work, not separate works.`,
suggestion: "create as a recording under the parent work instead",
next_tool: "pica_recordings_create",
next_args: { work_id: parentWork.id, version_type: suggestedVersionType },
};
}
// remix (ADR-324): by default a recording of the same song. Only a remix
// that adds writers is its own work — then it stays, linked to the parent.
return {
warning: `this looks like a remix of "${parentWork.title}" (work ${parentWork.id}). ` +
`a remix is usually a new recording of the same song: same work, same writers, its own ISRC, ` +
`with the remixer credited on the recording (role Remixer), not as a writer.`,
suggestion: "create it as a recording under the original work with version_type remix — unless the remix added new writers (new lyrics, melody or a new section)",
next_tool: "pica_recordings_create",
next_args: { work_id: parentWork.id, version_type: suggestedVersionType },
if_new_writers: {
suggestion: "if the remix added writers it is a modified version: keep it as its own work, add its writers, and link it to the original",
next_tool: "pica_works_update",
next_args: {
...(candidateWorkId ? { id: candidateWorkId } : {}),
original_work_id: parentWork.id,
},
},
};
}
//# sourceMappingURL=version-guard.js.map