UNPKG

@withpica/mcp-server

Version:

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

169 lines • 8.65 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess } from "@withpica/mcp-utils"; // Mirror of COUNT_SOURCES in lib/services/count-explain.ts. The lint // check at scripts/lint-mcp-tools.ts asserts this stays in sync with // the service-layer registry. Adding a value here without a registry // entry trips the lint at build time. const COUNT_SOURCES = [ "dashboard.registered_pro", "dashboard.registered_nro", "briefing.audio_pending", "briefing.unconnected_collaborators", "briefing.unenriched_writers", // @deprecated alias of briefing.unenriched_writers — removal 2.74 "briefing.unverified_writers", "briefing.works_without_credits", ]; export class ExplainabilityTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_count_explain", scope: "read:catalog", audiences: ["stdio", "http"], tier: "read", description: "Use when the user asks: 'why does the dashboard say 80 but I counted 78?', 'where are the missing works?', 'explain this number', 'what issues do I have?'. " + "Two modes: " + "(1) Omit count_source to get a machine-readable routing table of all actionable issue classes with per-class counts and drill_down_skill names — this is the coherence surface that ties pica_dashboard_briefing.critical_issues_total to the per-class breakdown. " + "(2) Supply count_source for the legacy single-source explainer: returns the canonical query, the computed value, and (when observed_value supplied) a delta_breakdown that sums to the absolute delta. " + "→ then: pica_works_query (drill into the specific rows), pica_dashboard_briefing (re-read the briefing now that you understand the count), pica_dashboard_attention class=<class> (get the specific rows to act on)", workflows: "infrastructure", vernacular_kind: "creator-entry", inputSchema: { type: "object", properties: { count_source: { type: "string", enum: [...COUNT_SOURCES], description: "Which legacy count to explain. Omit to get the full actionable-issue taxonomy (sources[] with drill_down_skill). " + "dashboard.registered_pro: works registered with any PRO (PRS, ASCAP, BMI, SACEM, GEMA, JASRAC, etc.). dashboard.registered_nro: works registered with any NRO (PPL, SoundExchange, GVL, etc.). briefing.audio_pending: audio_files where analysis_status='pending'. briefing.unconnected_collaborators: distinct credited people without a linked PICA user. briefing.unenriched_writers: writer credits where attestation_status != 'attested' (canonical; briefing.unverified_writers is its deprecated alias, removal 2.74). briefing.works_without_credits: works with zero credits (no work_collaborators rows).", }, observed_value: { type: "number", description: "Optional. The number the user is seeing somewhere else. When supplied, the response includes delta + delta_breakdown. Only applies when count_source is also supplied.", }, }, }, nextSteps: [ { tool: "pica_dashboard_attention", reason: "Get the specific rows to act on for a class from the routing table.", when: "on_success", }, { tool: "pica_works_query", reason: "Drill into the specific works the count covers when investigating a discrepancy.", when: "on_success", }, { tool: "pica_dashboard_briefing", reason: "Re-read the briefing now that the count is explained.", when: "on_success", }, ], }, executor: this.countExplain.bind(this), }, ]; } async countExplain(args) { const countSource = args.count_source; const observedValueRaw = args.observed_value; // ADR-242 mode: no count_source → return the full actionable taxonomy // as sources[] with drill_down_skill. The route returns this shape when // count_source is absent from the query string. if (!countSource) { try { const result = await this.pica.countExplain.taxonomy(); const sources = result?.sources ?? []; const total = result?.critical_issues_total ?? 0; const activeSources = sources.filter((s) => s.count > 0); const summary = total === 0 ? "no actionable issues found" : `${total} total actionable issues across ${activeSources.length} class${activeSources.length === 1 ? "" : "es"}`; return formatSuccess(summary, { sources, critical_issues_total: total, }); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); return { content: [ { type: "text", text: `Failed to load actionable taxonomy: ${errorMessage}`, }, ], isError: true, }; } } if (!COUNT_SOURCES.includes(countSource)) { return { content: [ { type: "text", text: `Invalid count_source '${countSource}'. Must be one of: ${COUNT_SOURCES.join(", ")}. Omit count_source to get the full actionable taxonomy.`, }, ], isError: true, }; } let observedValue; if (observedValueRaw !== undefined && observedValueRaw !== null) { const parsed = Number(observedValueRaw); if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) { return { content: [ { type: "text", text: "observed_value must be a non-negative integer.", }, ], isError: true, }; } observedValue = parsed; } try { const result = await this.pica.countExplain.explain({ count_source: countSource, observed_value: observedValue, }); const verdict = result.delta == null ? `${result.source.name} = ${result.computed_value}` : result.delta === 0 ? `${result.source.name} = ${result.computed_value} (matches your observed value)` : `${result.source.name} = ${result.computed_value} (differs from observed by ${result.delta})`; return formatSuccess(verdict, result); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); // INVALID_INPUT bubbles up from the API — surface message as-is. if (/INVALID_INPUT|invalid_input|400/i.test(errorMessage)) { return { content: [ { type: "text", text: `count_source not recognised. Allowed values: ${COUNT_SOURCES.join(", ")}.`, }, ], isError: true, }; } return { content: [ { type: "text", text: `Failed to explain count: ${errorMessage}` }, ], isError: true, }; } } } //# sourceMappingURL=explainability.js.map