@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
253 lines • 14.5 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatList, formatDeletion, } from "@withpica/mcp-utils";
export class DocumentTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_documents_analyse",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Trigger AI analysis on an uploaded document. Extracts metadata such as document type, " +
"agreement terms, parties, dates, and key fields. Uses AI when available, " +
"falls back to rule-based extraction otherwise. Upload documents first with pica_upload_file + pica_upload_complete. " +
"For the extracted text itself rather than field extraction, use pica_documents_inspect.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Document ID to analyse",
},
},
required: ["id"],
},
},
executor: this.analyseDocument.bind(this),
},
// Tier 2 (MCP tool-surface consolidation, ADR-149 query/inspect
// pattern): pica_documents_query/_inspect are the canonical read pair,
// mirroring pica_releases_query/_inspect. The old pica_documents_list
// and pica_documents_read names were removed in mcp-server 2.99.0.
{
definition: {
name: "pica_documents_query",
scope: "read:files",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "List catalog documents — PRO/NRO records, works lists, split sheets, lyric sheets, and (for finance/admin callers) financial documents like statements, invoices and receipts. " +
"PICA's own split sheets come first: kind 'split_sheet'. split_sheet_role 'record' is the song's live split of record; 'superseded' is an archived sheet kept for history, not the record. Read either with pica_split_sheet_get (work_id, split_sheet_id = this entry's id). " +
"Uploaded or forwarded split sheets are kind 'upload' with split_sheet_role 'supporting': evidence beside the record, never a replacement for it. " +
"Returns id, title, kind, document_type, parsing_status. Read an upload with pica_documents_inspect, then map it with the " +
"works/people/recordings tools and link the source with pica_documents_link. " +
"Financial documents require finance/admin access; a caller without it gets metadata.financial_documents_hidden (a count of withheld financial documents) instead of the rows — tell the user they exist and that admin access is needed to view them.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; document bridge tools address catalog ingestion workflows outside the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
inputSchema: {
type: "object",
properties: {
parsed_status: {
type: "string",
enum: ["pending", "completed", "all"],
description: "filter by parse state",
},
limit: {
type: "number",
minimum: 1,
maximum: 50,
description: "max rows (default 25, max 50)",
},
offset: { type: "number", description: "pagination offset" },
},
},
},
executor: this.listDocuments.bind(this),
},
{
definition: {
name: "pica_documents_inspect",
scope: "read:files",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Read an uploaded document's extracted text so you can map it into the catalog. Returns content (or tabular rows), " +
"bounded for context — page text with offset when has_more is true, and page parsed spreadsheet rows with row_offset when has_more_rows is true (follow next_row_offset until it disappears). If parsing_status is 'processing', retry shortly. " +
"If parsing_status is 'failed', STOP retrying — read parse_error and tell the user the reason (e.g. a legacy .xls that must be re-saved as .xlsx). " +
"After mapping, call pica_documents_link to attach this document to the entities you created. " +
"If the document is a royalty/streaming statement, import its lines with pica_statement_ingest_document.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
// id/document_id are interchangeable entry points — see
// pica_recordings_inspect. required:[] + requiredOneOf lets
// pica_execute still hard-reject empty/stripped args while
// accepting either name.
requiredOneOf: [["id", "document_id"]],
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Document ID (alias for document_id)",
},
document_id: {
type: "string",
description: "document id from pica_documents_query",
},
offset: {
type: "number",
description: "char offset for paging long content",
},
max_chars: {
type: "number",
description: "max chars to return (default/cap 12000)",
},
row_offset: {
type: "number",
description: "row offset for paging parsed spreadsheet rows — use next_row_offset from the previous response",
},
row_limit: {
type: "number",
description: "max parsed rows per page (default/cap 200)",
},
},
required: [],
},
},
executor: this.readDocument.bind(this),
},
{
definition: {
name: "pica_documents_link",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Link a source document to a catalog entity you created from it (provenance). Call after mapping a document so the " +
"work, recording, or person points back at the file it came from. " +
"Find the document id first with pica_documents_query (an entry with kind 'upload').",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
annotations: {
title: "link a document",
readOnlyHint: false,
},
inputSchema: {
type: "object",
properties: {
document_id: {
type: "string",
description: "Document ID to link",
},
entity_type: {
type: "string",
enum: ["work", "recording", "person"],
description: "Type of entity to link the document to",
},
entity_id: {
type: "string",
description: "ID of the entity to link the document to",
},
},
required: ["document_id", "entity_type", "entity_id"],
},
},
executor: this.linkDocument.bind(this),
},
{
definition: {
name: "pica_documents_delete",
scope: "write:files",
audiences: ["stdio", "http", "public"],
tier: "destructive",
description: "Delete an uploaded document. Removes the document record AND its " +
"underlying storage object (no orphan is left behind). Use this to " +
"clear out a document that failed to parse (parsing_status:'failed') " +
"or is superseded, so a clean copy can be re-uploaded. This does not " +
"delete any catalog entity the document was linked to — only the file. " +
"Check what it holds with pica_documents_inspect first — the file cannot be recovered.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Document ID to delete (from pica_documents_query, an entry with kind 'upload')",
},
},
required: ["id"],
},
},
executor: this.deleteDocument.bind(this),
},
];
}
async readDocument(args) {
const _id = args.id ?? args.document_id;
const res = await this.pica.documents.read(_id, {
offset: args.offset,
max_chars: args.max_chars,
row_offset: args.row_offset,
row_limit: args.row_limit,
});
return formatSuccess("Document content", res);
}
async listDocuments(args) {
const res = await this.pica.documents.listCatalog({
parsed_status: args.parsed_status,
limit: args.limit,
offset: args.offset,
});
// financial_documents_hidden: for a caller WITHOUT finance access, the count
// of money-category documents (statements, invoices, receipts) that exist but
// are withheld — surfaced so the agent can tell the user they exist and that
// admin access is needed to see them, instead of silently omitting them. It is
// 0 (or absent) for a finance/admin caller, who receives the rows themselves.
const hidden = res.financial_documents_hidden;
return formatList(res.documents, {
total: res.total,
has_more: res.has_more,
...(typeof hidden === "number" && hidden > 0
? { financial_documents_hidden: hidden }
: {}),
});
}
async analyseDocument(args) {
const result = await this.pica.documents.analyse(args.id);
return formatSuccess(`Document ${args.id} analysed successfully.`, result);
}
async linkDocument(args) {
const res = await this.pica.documents.link(args.document_id, args.entity_type, args.entity_id);
return formatSuccess("Document linked", res);
}
async deleteDocument(args) {
// DocumentsResource.delete is added in this change's SDK update. In an
// isolated worktree, mcp-server resolves @withpica/mcp-sdk via the parent
// repo's node_modules (a known worktree gotcha), so the freshly-built local
// dist isn't seen by tsc here. Cast to the real signature — CI builds the
// SDK fresh and typechecks this without the cast. (Not @ts-expect-error,
// which would become an unused-directive error once the dist is current.)
const documents = this.pica.documents;
const outcome = await documents.delete(args.id);
return formatDeletion("document", args.id, outcome);
}
}
//# sourceMappingURL=documents.js.map