UNPKG

@withpica/mcp-server

Version:

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

172 lines • 6.48 kB
// 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