@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
109 lines • 7.36 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { PUBLIC_FORBIDDEN_KEYWORDS_FULL } from "./forbidden-keywords.js";
// ── Class B: include name, sanitized description ──
// The source-of-truth descriptions in the per-domain tool files name third-party
// services (MLC / ISNI / MusicBrainz / Spotify / YouTube / Discogs / ACRCloud /
// Librosa / Groq) or internal architecture (cascade engine / Tier A/B / identity
// graph / CAR scoring). The strings here are rewrites that preserve capability
// signal without leaking the algorithm or vendor binding.
const CLASS_B_SANITIZED = {
// Reclassified from Class C → B per 2026-04-27 sign-off
pica_graph_traverse: "Traverse identifier-based connections from any external identifier. Returns connected works, recordings, and people across the catalog.",
pica_run_work_cascade: "Re-run metadata enrichment for a work to fill gaps from external sources.",
pica_run_person_cascade: "Re-run metadata enrichment for a person to fill gaps from external identity sources.",
pica_cascade_health: "Snapshot of recent metadata-enrichment activity — outcome counts and recent failures.",
pica_export_catalog_asset_report: "Generate a catalog asset report — a structured summary of catalog completeness and provenance.",
pica_car_preview: "Preview a catalog asset report inline before generating the full export.",
// Original Class B
pica_resolve_work: "Resolve a work against external metadata sources. Returns structured proposals and applied updates.",
pica_resolve_person: "Resolve a person against external identity sources. Returns structured proposals and applied updates.",
pica_resolve_recording: "Resolve a recording against external metadata sources. Returns structured proposals and applied updates.",
pica_people_triangulate: "Check whether an external artist name already matches an existing person in the catalog. Returns a confidence score, the matched person (if any), and alternative candidates.",
pica_enrichment_candidates: "Find catalog entries that are good candidates for metadata enrichment.",
pica_enrichment_compare: "Compare what external sources say about a catalog entry, side by side.",
pica_registrations_compare: "Compare a work's registration status across society and platform records.",
pica_enrichment_propose: "Propose a metadata enrichment from agent research; goes to a review queue.",
pica_enrichment_proposals_list: "List pending metadata-enrichment proposals awaiting review.",
pica_enrichment_proposal_apply: "Apply a previously proposed metadata enrichment.",
pica_enrichment_proposal_reject: "Reject a previously proposed metadata enrichment.",
pica_audio_analyze: "Analyze an audio file for tempo, key, energy, and lyric transcription.",
pica_audio_identify: "Identify a track via audio fingerprinting. Returns matched metadata with confidence score.",
pica_documents_analyse: "Analyse a document with AI. Extracts structured fields from invoices, statements, contracts.",
// Tier 2 consolidation (ADR-149): pica_documents_query/_inspect are the
// canonical names that replaced pica_documents_list/_read (those two were
// removed in 2.99.0) — sanitized so the public discovery surface doesn't
// leak financial_documents_hidden / finance-admin access-control wording.
pica_documents_query: "List catalog documents: PICA's own split sheets first (kind 'split_sheet', the record; read with pica_split_sheet_get), then uploaded documents (registration exports, works lists, supporting split sheets) awaiting mapping.",
pica_documents_inspect: "Read an uploaded document's extracted text to map it into the catalog.",
pica_documents_link: "Link a source document to the catalog entity created from it (provenance).",
pica_documents_delete: "Delete an uploaded document and its stored file (e.g. to clear a failed-to-parse document and re-upload a clean copy).",
pica_import_streaming_link: "Import a work or recording from a public streaming-service link.",
pica_import_youtube_link: "Import a recording from a YouTube link.",
pica_multimedia_import_url: "Import an image or video from a public URL; optionally set it as a person's profile photo.",
pica_multimedia_link_youtube: "Link a YouTube video to a work.",
pica_ingest_statement: "Ingest a royalty statement document into the catalog.",
// ADR-289 Wave C2 — raw description names MusicBrainz explicitly.
pica_enrichment_clear: "Undo a metadata-enrichment match on a work — clears the ISWC and/or PRO IDs a prior automatic match wrote, but only the fields that actually came from that source. Cannot be undone.",
};
// ── Forbidden keywords (AC-4 in ADR-202) ──
// Source-of-truth list lives at `./forbidden-keywords.ts` and is shared
// with the `scripts/lint-mcp-tools.ts` description-hygiene check
// (ADR-180 Rule 9, 2026-05-02 amendment).
//
// The lobby surface uses the full union (PUBLIC ∪ AUTHENTICATED) — every
// vendor / data-source / infra / library / concept term that's banned for
// any audience is also banned at the unauthenticated boundary.
const FORBIDDEN_KEYWORD_FRAGMENTS = PUBLIC_FORBIDDEN_KEYWORDS_FULL;
/**
* Apply ADR-202 filtering to a list of ToolDefinitions.
*
* ADR-248: membership on the public surface is now a declared property of
* the tool — `audiences ∋ "public"` — instead of the former Class A allowlist
* / Class C exclude / meta-exclude lists. The description SANITISATION stays
* exactly as it was (ADR-202): a Class B tool ships its rewritten description,
* everything else ships its own. The `readOnlyOnly` Phase-1 risk gate stays.
*
* Order of operations:
* 1. Drop anything not declaring `audiences ∋ "public"` (default-exclude)
* 2. If readOnlyOnly: drop tools whose risk metadata is not "safe"
* 3. Replace description for CLASS_B_SANITIZED entries
* 4. Strip executor reference (return PublicToolDefinition only)
*/
export function filterToolsForPublicDiscovery(tools, options) {
const readOnlyOnly = options.readOnlyOnly ?? true;
const result = [];
for (const tool of tools) {
if (!tool.audiences?.includes("public"))
continue; // declared membership
if (readOnlyOnly) {
const meta = options.metadataLookup(tool.name);
if (meta?.risk !== "safe")
continue;
}
const isB = tool.name in CLASS_B_SANITIZED;
const description = isB ? CLASS_B_SANITIZED[tool.name] : tool.description;
result.push({
name: tool.name,
description,
inputSchema: tool.inputSchema,
annotations: tool.annotations,
});
}
return result;
}
/** Returns the first forbidden keyword found in a string, or null. */
export function findForbiddenKeyword(text) {
const lower = text.toLowerCase();
for (const fragment of FORBIDDEN_KEYWORD_FRAGMENTS) {
if (lower.includes(fragment.toLowerCase()))
return fragment;
}
return null;
}
// Internal exports for test introspection only — consumers use
// filterToolsForPublicDiscovery + findForbiddenKeyword.
export const _internal = {
CLASS_B_SANITIZED,
FORBIDDEN_KEYWORD_FRAGMENTS,
};
//# sourceMappingURL=public-filter.js.map