UNPKG

@withpica/mcp-server

Version:

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

253 lines • 14.6 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, formatList, formatDeletion, withBillingGate, } 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: { title: "list documents", readOnlyHint: true, idempotentHint: true, }, inputSchema: { type: "object", properties: { parsed_status: { type: "string", enum: ["pending", "completed", "all"], description: "filter by parse state", }, limit: { type: "number", 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: { title: "read a document", 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: withBillingGate(this.pica, "document deletion", 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