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