UNPKG

@withpica/mcp-server

Version:

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

426 lines (389 loc) • 22.6 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * ADR-214 — workflow narrative for agents. * * Two markdown BUILDERS. ResourceRegistry serves buildAgentGuideLobby() when * config.lobbyMode || !pica, buildAgentGuideAuthenticated() otherwise. * * Nothing here is a literal, because a literal rots: this file claimed * "~232 tools" on 2026-08-24 while ToolRegistry.listTools() returned 329, and * the lobby half said "three tools are available" and listed all three by name * while the HTTP lobby exposes exactly ONE (the sign-in pair is * `audiences: ["stdio"]`). Counts AND names are parameters, supplied by the * transport that knows what its own caller can reach. Enforced by * src/__tests__/resources/agent-guide.test.ts. * * Tool sequences are verbatim from ADR-214 Decision section "Top * workflows" — audit-verified 2026-04-30 against the live ToolRegistry. * * Edits to this file are linted by scripts/lint-agent-guide.ts: every * `pica_*` token must resolve to a real tool in ToolRegistry. */ /** * The signed-in agent guide. * * @param toolCount MUST be **the number of catalogue tools THIS caller can * reach, excluding deprecated aliases**. The two transports compute that * differently and both are correct: * * - **stdio / any flat surface** — `ToolRegistry.listTools().length`, which * already drops aliases. This is the definition `MCP_TOOL_COUNT` in * `lib/constants/mcp-surface.ts` uses. * - **layered HTTP (`/api/mcp`, the connector default)** — the * scope-filtered allowed set, alias names removed * (`app/api/mcp/route.ts`, `catalogueToolCount`). `listTools()` is the * wrong source there: under layered discovery it returns only the * meta-tools, and the guide would announce that handful while the whole * catalogue is reachable by name and through `pica_execute`. * * Do NOT pass a literal, a frozen constant, or a raw * `allDefinitions().length` — the last one counts deprecated aliases, which * are hidden from every listing the caller can see, so the guide would * promise tools the agent cannot find. `route.test.ts` ("agent guide") * asserts the route passes a computed, alias-free count. */ export function buildAgentGuideAuthenticated(toolCount) { return `# pica — agent guide pica manages a music catalog: works (compositions), recordings (masters), people (writers, performers, publishers), credits, agreements, exports. multi-tenant per organisation; every write is org-scoped automatically. you are signed in. ${toolCount} tools are available. fourteen core workflows below cover most of the surface; for anything not listed, call \`pica_discover({ query: "..." })\` to search by intent, or \`pica_discover()\` with no arguments for the category map — then \`pica_tool_details\`. ## top workflows ### register a new work 1. \`pica_works_create\` — title required; iswc settable inline. 2. \`pica_credits_update\` — batch; attach writer/composer credits. writer_split_percentage values must sum to 100 across all writer rows. see \`pica://schemas/work-required\` and \`pica://schemas/work-credits-required\`. ### add a recording to an existing work - \`pica_recordings_create\` — title + artist_name + version_type required (version_type defaults to \`master\`). isrc and duration_ms are inline. - see \`pica://schemas/recording-required\`. ### add audio to a recording 1. \`pica_audio_presigned_upload\` — returns a signed url + token. 2. uploader (browser, curl, sdk) PUTs the file to that url. 3. \`pica_audio_complete_upload\` — registers the upload in the catalog. 4. (optional) \`pica_audio_analyze\` — async; extracts bpm, key, mood, and transcribes lyrics via whisper. 5. \`pica_audio_inspect\` (sections: ["analysis", "status"]) — poll for analysis results when you called step 4. - see \`pica://schemas/audio-upload-required\`. ### add recording credits - \`pica_recording_credits_update\` — batch. role enum is DISTINCT from work credits (MainArtist / FeaturedArtist / Producer / Engineer / etc.). - person_id is OPTIONAL — credits can carry just credited_name when the person isn't in the catalog yet. - see \`pica://schemas/recording-credits-required\`. ### enrich existing metadata 1. \`pica_resolve_work\` / \`pica_resolve_person\` / \`pica_resolve_recording\` — tier A identifier matches apply directly; tier B fuzzy matches queue as proposals. 2. \`pica_enrichment_proposals_list\` — view pending proposals. 3. \`pica_enrichment_proposal_apply\` — commit a proposal you've reviewed. \`pica_enrichment_proposal_reject\` — dismiss with content-hash suppression. - see \`pica://schemas/enrichment-resolve-required\`. ### create an agreement 1. \`pica_agreements_create\` — title + agreement_type + other_party_name required. prefer counterparty_org_id (FK) over free-text other_party_name. 2. \`pica_agreements_link_work\` — attach works (optional royalty_split per work). 3. \`pica_agreement_templates_render\` — template-driven alternative. - see \`pica://schemas/agreement-required\`. ### add multimedia - \`pica_multimedia_create\` — title + content_type required. - \`pica_multimedia_link_youtube\` — YouTube videos (auto-enriched). - \`pica_multimedia_import_url\` — external image / video URL (downloads into PICA storage). - \`pica_multimedia_link_work\` — attach to a work (relationship_type required). - see \`pica://schemas/multimedia-required\`. ### log a studio session 1. (optional) \`pica_sessions_types\` — pre-flight to discover valid \`session_type_id\` values for this org. Lookup table; org-scoped. 2. \`pica_sessions_create\` — title + start_time + end_time (ISO 8601). session_type_id, location, work_id, recording_id, participants are all optional inline fields. 3. \`pica_sessions_inspect\` / \`pica_sessions_query\` — read-back / browse. - the \`status\` enum (scheduled / in_progress / completed / cancelled) is documented but NOT enforced by a DB CHECK — values may evolve. - see \`pica://schemas/session-required\`. ### generate a split sheet (publishing royalties on a work) 1. **pre-condition:** writer credits on the work must already total 100% via \`pica_credits_update\`. \`pica_split_sheet_generate\` returns 400 otherwise. 2. \`pica_split_sheet_generate\` — work_id only; snapshots current splits. 3. \`pica_split_sheet_send\` — notify each credited writer for signature via in-app + Telegram + email. Non-PICA-user writers get a collaboration invite email instead. Per-writer soft-fails. 4. \`pica_split_sheet_get\` / \`pica_split_sheet_list\` — read-back. - see \`pica://schemas/split-sheet-required\`. ### assign master ownership splits (recording rights) - DISTINCT from publishing splits above. Recording splits live on \`recording_splits\`, not \`work_credits\`. Drives master royalty payouts + downstream credit gating. 1. \`pica_recording_splits_create\` — recording_id + split_type (master / mechanical / performance / sync) + percentage required. role / person_id / territory / start_date / end_date optional. Use \`dry_run: true\` first — it writes nothing and returns the shares already recorded plus \`likely_refusal\`. ADR-200 form-confirmation gate fires on connectors that support \`elicitation/url\`. 2. \`pica_recording_splits_verify\` — records that the split HOLDER agreed. Refuses agents (\`VERIFICATION_REQUIRES_HUMAN_SESSION\`): the holder confirms it signed in, via "confirm my share" under ownership splits at /inspect/recordings/<recording_id>#ownership-splits. 3. \`pica_recording_splits_list\` — list all splits on a recording. 4. a share is wrong → \`pica_recording_splits_update\` (recording_id + split_id + the fields to change: person_id to relink an "unknown" holder, percentage, role, split_type, territory, dates). The 100% total is checked after the change, this row's old value left out. Changing a term (who, how much, type, territory, dates — not role or notes) clears the holder's confirmation — the result says \`verification_cleared: true\`; tell the user the holder must confirm again. 5. a share should stop counting → \`pica_recording_splits_end\` (end_date defaults to today; the row is kept as history). Don't add a second share to work around a wrong one. - see \`pica://schemas/recording-splits-required\`. ### assign an industry identifier (iswc / isrc / isni / ipi / etc.) **there is no standalone identifier-assign tool.** every identifier is an inline field on the entity's update tool. don't search for a generic identifier-assign verb — set the field on \`pica_works_update\` / \`pica_recordings_update\` / \`pica_people_update\` directly. - **work identifiers** → \`pica_works_update\`: \`iswc\`, \`isrc\`, \`tunecode\`, \`society_registrations\`, \`spotify_track_uri\`. - **recording identifiers** → \`pica_recordings_update\`: \`isrc\`, \`isrc_source\`, \`musicbrainz_recording_id\`, \`mlc_recording_id\`, \`mlc_song_code\`, \`spotify_track_uri\`, \`spotify_track_id\`, \`youtube_video_id\`, \`youtube_url\`, \`apple_music_url\`, \`deezer_track_id\`, \`ppl_recording_id\`. - **person identifiers** → \`pica_people_update\`: \`cae_ipi_number\`, \`ipn_number\`, \`prs_number\`, \`ppl_number\`, \`mcps_member_number\`, \`pro_name\` + \`pro_member_number\`, plus the satellite-routed identifiers below. **satellite-routed person identifiers (set them inline anyway):** \`isni\`, \`musicbrainz_id\`, \`wikidata_id\`, \`discogs_artist_id\`, \`deezer_artist_id\`, \`imdb_id\`, \`viaf_id\` (and the biographical fields such as \`languages_spoken\`, \`date_of_death\`) are stored on the \`person_enrichment\` satellite table under the hood, NOT on \`people\`. **don't think about that.** set them as inline fields on \`pica_people_update\` exactly like the non-satellite identifiers above; the service fans out automatically. the surface is uniform — satellites are an implementation detail. **clearing wrong enrichment:** every field enrichment fills is editable and clearable on \`pica_people_update\` — send \`null\` (or \`[]\` for a list) to remove a misattributed value. the edit is recorded as the user's, and later enrichment runs will not put the old value back. field-name notes — these names are surface-precise; the route's allow-list rejects the abbreviated forms: - \`cae_ipi_number\` (NOT \`cae_ipi\`) - \`ipn_number\` (NOT \`ipn\`) - \`pro_member_number\` (the generic non-PRO/PPL/MCPS variant) is paired with \`pro_name\` to specify which PRO. **examples:** - \`pica_works_update { id: "<uuid>", iswc: "T-123.456.789-0" }\` - \`pica_people_update { id: "<uuid>", cae_ipi_number: "00000000001", isni: "0000000123456789" }\` ### export works - one of: \`pica_export_catalog_csv\` (per-work or per-batch — picks format pro / distributor / nro / sync-pdf, requires work_id or work_ids) / \`pica_export_song_registration\` (catalog-wide registration bundle for PRO/CMO — CSVs plus a CWR field-mapping guide, NOT a CWR file) / \`pica_export_industry_ready\` (catalog-wide publisher / label / sync package) / \`pica_export_catalog_asset_report\` (CAR — finance / insurance / diligence) / \`pica_export_ai_consent\` (AI-consent matrix). - see \`pica://schemas/export-required\` for which export matches which destination. ### claim a discovery 1. \`pica_discoveries_query\` — list pending; row carries \`type: 'credit' | 'custody' | 'artist'\`. 2. typed claim: \`pica_claim_credit\` / \`pica_claim_custody\` / \`pica_claim_artist\`. or \`pica_discoveries_review\` for generic accept/reject. - see \`pica://schemas/claim-required\`. ## reading the catalog write-flows above assume you already know what to write. these read- patterns help you find the right entity / gap / duplicate first. ### find duplicates and merge them 1. \`pica_find_duplicates\` — \`entity_type: "works" | "people" | "releases" | "recordings" | "audio_files"\`. returns groups + suggested winners. releases key on UPC, recordings on ISRC, and \`audio_files\` finds the SAME FILE uploaded twice (exact byte size + filename — audio rows carry no content hash). each audio item reports \`storage_bucket\`; a \`withpica-*\` bucket is storage pica pays for. 2. \`pica_merge_duplicates\` — a NARROWER set: \`"works" | "people" | "releases" | "recordings"\` only. \`entity_type\` accepts both forms; the canonical plural is preferred, legacy singular (\`"work" | "person"\`) still works for backwards compatibility. max 25 loser_ids per call. requires admin scope. 3. several groups confirmed? send them in ONE call as \`merges\` (up to 50 \`{entity_type, winner_id, loser_ids}\`). over a connector that is ONE approval email for the owner, not one per group. after approval, \`pica_approval_status\` gives each pair's outcome — merged, skipped (already merged or deleted) or failed with the reason. - ⚠️ \`audio_files\` can be FOUND but not MERGED. there is no merge semantics for a file — the outcome is a person deleting the redundant upload. passing it to \`pica_merge_duplicates\` is refused. - if you need to merge but don't have admin scope, ping a workspace owner — there is no agent-side workaround. ### two different people sharing one identity — split, never merge when two people who are NOT the same human share a global identity (a wrong ISNI or Wikidata match joined them; custody transfers to it land as "unknown"), merging is wrong. 1. \`pica_people_inspect\` \`{id, sections: ["global_identity"]}\` — who shares it, its identifiers, unattributed split rows and open custody legs naming it, and which of the person's identifiers equal it. 2. ask the user which identifiers and which rows are this person's. 3. \`pica_people_split_identity\` with only what they confirmed. nothing moves by default; never infer an identifier or a row. ### audit catalog completeness 1. \`pica_dashboard_briefing\` — high-level state: counts of needs_attention works, recent activity, unresolved discoveries, per-org \`agent_velocity\` block (workflow_outcomes events trailing 7d sliced by kind + top tools, ADR-247 PR b). start here, not at \`*_query\`. 2. drill into specifics from the briefing's pointers — usually a \`pica_*_query\` filtered by health, then \`pica_*_inspect\` per row. ### find gaps and fill them 1. \`pica_works_query { health_filter: "needs_attention" | "low_completeness" }\` — under-used. surfaces works missing credits / iswc / metadata. parallel filters exist on \`pica_recordings_query\` / \`pica_people_query\` (\`pica_people_query\` is full-text only — no typed health_filter yet). 2. \`pica_works_inspect { id, sections: ["health", "completeness", "enrichment"] }\` — request only the sections you need. sections=[] (omit) returns everything which is noisy. 3. if the gap is identifier-shaped (missing ISWC / ISRC / ISNI / MusicBrainz id) — \`pica_resolve_work\` / \`_recording\` / \`_person\` fans the cascade. tier B fuzzy matches queue as proposals — drain via \`pica_enrichment_proposals_list\` → \`_apply\` / \`_reject\`. ### look up a person by industry identifier - \`pica_people_query\` accepts a free-text \`query\` field that matches name, email, OR identifier (IPI / ISNI / etc.). there is NO typed filter (\`pica_people_query { isni: "..." }\` does not work) — use the \`query\` string. - if no match, fall back to \`pica_search_all\` for a cross-entity scan. - if still no match, the person isn't in the catalog yet — \`pica_people_create\` (set the identifier inline; see the identifier-assign section above) → \`pica_resolve_person\` to fan enrichment across ISNI + MusicBrainz. ## common gotchas - **duration units differ.** \`works.duration_seconds\` (integer seconds); \`recordings.duration_ms\` (integer milliseconds); \`audio_files.duration_seconds\` (seconds again). - **no standalone identifier-assign tool.** every identifier (ISWC, ISRC, ISNI, CAE/IPI, MusicBrainz id, etc.) is an inline field on the entity's update tool — see the "assign an industry identifier" section above for the surface-precise field names. - **two write surfaces for collaborators, and three verbs.** \`pica_credits_update\` is canonical for work splits and REPLACES the whole set; \`pica_recording_credits_update\` is the recording-side variant with a different role enum (MainArtist / FeaturedArtist / Producer / etc.). don't cross them. to touch ONE credit use the single-credit verbs instead: \`pica_credit_add\` (one new row), \`pica_credit_update\` (edit a row in place — a writer's share, an instrument, a name, or who it is for via \`person_id\` — keeping its id; a change of person sends a confirmed, disputed or declined credit back to pending; find the credit_id with \`pica_credits_list\`) and \`pica_credit_remove\`. changing one split does not require re-sending the others. a split edit reports the work's \`allocation.status\` (closed / open / incomplete) — if it reads open, ask the user how the remainder is shared; never redistribute it yourself. - **people identifiers — treat as one inline surface.** \`pica_people_update\` accepts every identifier as an inline field; some route to the \`person_enrichment\` satellite under the hood (see the identifier section above) — the surface stays uniform. - **audio analysis is async + explicit.** \`pica_audio_complete_upload\` does NOT auto-trigger analysis. call \`pica_audio_analyze\` and poll. - **lyrics from analysis auto-copy only into an EMPTY \`works.lyrics\`.** whisper transcripts land on the transcriptions row; when the linked work's \`lyrics\` is null the text + \`lyrics_language\` are copied across. existing lyrics are never overwritten. - **org scoping + duplicate prevention.** writes are org-scoped; iswc and isrc duplicates are blocked at write. ## schema resources (pre-flight before each workflow) - \`pica://schemas/work-required\` — backs \`pica_works_create\`. - \`pica://schemas/recording-required\` — backs \`pica_recordings_create\`. - \`pica://schemas/work-credits-required\` — backs \`pica_credits_update\`. - \`pica://schemas/recording-credits-required\` — backs \`pica_recording_credits_update\`. - \`pica://schemas/person-required\` — backs \`pica_people_create\`. - \`pica://schemas/audio-upload-required\` — 3-step audio chain. - \`pica://schemas/enrichment-resolve-required\` — resolve → proposals chain. - \`pica://schemas/agreement-required\` — backs \`pica_agreements_create\`. - \`pica://schemas/multimedia-required\` — backs \`pica_multimedia_create\`. - \`pica://schemas/export-required\` — five export destinations. - \`pica://schemas/claim-required\` — three discovery claim verbs. - \`pica://schemas/session-required\` — backs \`pica_sessions_create\`. - \`pica://schemas/split-sheet-required\` — backs \`pica_split_sheet_generate\` (publishing splits on works). - \`pica://schemas/recording-splits-required\` — backs \`pica_recording_splits_create\` (master ownership splits on recordings). each schema returns a json contract: required fields, recommended fields, CHECK enum values, companion-call orchestration, and an example payload. `; } /** * One line of explanation per lobby tool. Keyed by tool name so the guide can * render whatever subset a transport actually exposes; a name with no entry * still renders (bare, no blurb) rather than vanishing from the list, because * an unlisted callable tool is the failure mode this whole change is about. * * Every key must resolve to a real tool — `scripts/lint-agent-guide.ts` reads * the `pica_*` tokens out of this file and checks them against ToolRegistry. */ const LOBBY_TOOL_BLURBS = { pica_signup_start: "create a new organisation and get a magic link for first-time setup.", pica_sign_in: "request a magic link for an existing user.", pica_sign_out: "drop session.", }; /** "one tool is" / "two tools are" / "17 tools are" — no bare digit for 1-10. */ const SPELLED = [ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", ]; function countPhrase(n) { const word = n < SPELLED.length ? SPELLED[n] : String(n); return n === 1 ? `${word} tool is` : `${word} tools are`; } /** * The lobby guide, served when there is no authenticated client. * * @param lobbyToolNames The tools THIS caller can actually call right now, in * the order they should be read. Names, not a count: the two transports * expose different sets. stdio lists all three registered lobby tools; * the HTTP lobby (`app/api/mcp/route.ts`, `lobbyToolNames`) filters to * `audiences ∋ "lobby"` and only `pica_signup_start` declares it, so the * sign-in pair is not callable there. The guide previously hardcoded * "three tools" plus all three names, so two thirds of the HTTP lobby's * instructions named tools that would be refused. * @param unlockedToolCount The count of catalogue tools an AUTHENTICATED * client would reach — the sentence it lands in is "what unlocks after * sign-in", so it is deliberately NOT `lobbyToolNames.length`. */ export function buildAgentGuideLobby(lobbyToolNames, unlockedToolCount) { const toolLines = lobbyToolNames .map((name) => { const blurb = LOBBY_TOOL_BLURBS[name]; return blurb ? `- \`${name}\` — ${blurb}` : `- \`${name}\``; }) .join("\n"); // Name the tools the caller has, in the order given. With one tool there is // no "or". const callStep = lobbyToolNames.length ? lobbyToolNames.map((n) => `\`${n}\``).join(" or ") : "the lobby tool listed above"; return `# pica — sign in to begin you are in lobby mode. ${countPhrase(lobbyToolNames.length)} available; the rest unlock after sign-in. ## lobby tools ${toolLines} ## to sign in 1. ask the user for an email address. 2. call ${callStep}. 3. user clicks the magic link. on the next request, the catalog tool surface unlocks automatically — no config to refresh. ## what unlocks after sign-in ${unlockedToolCount} tools across catalog (works, recordings, people, credits, sessions, splits), enrichment, exports, agreements, multimedia, analytics, and audio. fourteen required-fields schema resources at \`pica://schemas/<workflow>-required\`. the full agent-guide replaces this lobby content. `; } //# sourceMappingURL=agent-guide.js.map