UNPKG

@withpica/mcp-server

Version:

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

114 lines • 5.36 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * Per-tool argument aliasing for `pica_execute` (D1, 2026-09-03). * * Agents routinely pass a tool's OWN entity id under its natural name — * `audio_file_id` to `pica_audio_update`, `person_id` to * `pica_people_inspect`, `skill` to `pica_skill_get` — instead of the * schema's `id` (or `name`). The dispatcher's WRAPPED_ARGS_MISSING guard * (`discovery.ts`, `buildExecuteTool`) then rejected the call blaming * "client-side transport loss" — the wrong diagnosis every one of the 36 * times it happened since July; nothing was lost in transit, the caller just * used a different, entirely reasonable field name. * * `applyArgAliases` renames a tool's own alias to the schema's real required * field name, structurally: the domain is the first segment after `pica_` * in the tool name, and `id` may only be filled from that domain's entry in * `DOMAIN_ID_ALIASES`. Anything not shaped `<domain>_id` (e.g. * `pica_skill_get`'s `skill` / `skill_name` → `name`) is declared explicitly * in `TOOL_ARG_ALIASES`, per exact tool name. * * Renaming happens ONLY when the target field is absent from the args AND * exactly one alias candidate is present: * - a target already present is never overwritten (the caller's value * wins, even if a same-named alias is also present — see the discovery * dispatcher, which resolves aliases before this runs); * - two or more present candidates are ambiguous ("which one is THE id?") * and are left untouched rather than guessed; * - a foreign entity's id (`work_id` sent to a people tool) is never a * candidate for another tool's `id` — `DOMAIN_ID_ALIASES` is looked up * by the TOOL'S OWN domain, not by whatever key the caller happened to * send. * * Pure function: `args` is never mutated, a new object is returned. */ /** * Domain (first segment after `pica_` in the tool name) → candidate * spellings of "this entity's own id" an agent might send instead of the * schema's `id`. */ export const DOMAIN_ID_ALIASES = { audio: ["audio_file_id", "audio_id", "file_id"], people: ["person_id", "people_id"], works: ["work_id"], recordings: ["recording_id"], releases: ["release_id"], agreements: ["agreement_id"], sessions: ["session_id"], shows: ["show_id"], projects: ["project_id"], documents: ["document_id"], notes: ["note_id"], labels: ["label_id"], publishers: ["publisher_id"], }; /** * Explicit non-id aliases, keyed by exact tool name — for fields that don't * follow the `<domain>_id` shape `DOMAIN_ID_ALIASES` covers. Each entry maps * an alias spelling to the schema's real field name. */ export const TOOL_ARG_ALIASES = { pica_skill_get: { skill: "name", skill_name: "name" }, // `pica_catalog_health_plan` hands back a list of ACTIONS, each carrying an // `actionId`, and `fix` takes those ids in a field called `accept` — so an // agent that has just read the plan reaches for `actionIds`. ChatGPT did // exactly that on prod and drew WRAPPED_ARGS_MISSING (ops issue dbca9b4c, // 2026-09-08: "missing: [accept]. Unrecognised fields sent: [actionIds]"). // `action_ids` is here because the rest of this surface is snake_case, so // it is the other spelling an agent lands on. pica_catalog_health_fix: { actionIds: "accept", action_ids: "accept", }, // A deferred write answers with a `human_approval_required` envelope that // carries the id as `approval_reference` and names `pica_approval_status` // as its `poll_tool` (`buildAgentResponse`, lib/services/human-approval-gate.ts) // — but the poll tool's schema calls the same value `approval_id`. Agents // copy the key they were just handed, so PICA's own two names for one value // drew WRAPPED_ARGS_MISSING 10 times on prod, from Claude and ChatGPT alike // (ops issue da9ba396, 2026-07-05 → 2026-09-28). Pinned end to end, against // the real tool definition, by // `src/__tests__/tools/execute-approval-reference.test.ts`. pica_approval_status: { approval_reference: "approval_id", }, }; /** * Rename a tool's own id / explicit alias to the schema's real required * field name. See module doc above for the exact ambiguity and * no-overwrite rules. */ export function applyArgAliases(toolName, args, required) { const domain = toolName.replace(/^pica_/, "").split("_")[0] ?? ""; const toolAliases = TOOL_ARG_ALIASES[toolName]; const applied = []; const result = { ...args }; for (const field of required) { if (field in result) continue; // already present — never overwrite const candidates = field === "id" ? [...(DOMAIN_ID_ALIASES[domain] ?? [])] : Object.entries(toolAliases ?? {}) .filter(([, to]) => to === field) .map(([from]) => from); const present = candidates.filter((c) => c in result); if (present.length !== 1) continue; // absent, or ambiguous — leave alone const [from] = present; result[field] = result[from]; delete result[from]; applied.push({ from, to: field }); } return { args: result, applied }; } //# sourceMappingURL=arg-aliases.js.map