@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
114 lines • 5.36 kB
JavaScript
// 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