UNPKG

@withpica/mcp-server

Version:

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

114 lines • 4.68 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. export const CATALOGUE_ARG = "catalogue"; /** Scopes whose tools act on a catalogue's own records. */ export const CATALOGUE_SCOPES = new Set([ "read:catalog", "write:catalog", "read:people", "write:people", "read:agreements", "write:agreements", "read:files", "write:files", "read:analytics", ]); /** Tools in those scopes that must not act for a client, and why. */ export const CATALOGUE_EXCLUDED_TOOLS = new Set([ // Its route (/api/mcp/share-send) sends for the credential's own catalogue // and does not read the catalogue header. "pica_share_send", // They manage who looks after a catalogue: acting "as" a client through // them would let an organisation answer or widen its own relationship. "pica_relationships_list", "pica_relationships_answer", "pica_relationships_grant", "pica_relationships_revoke", "pica_relationships_activity", "pica_relationships_step_down", "pica_portfolio_list", "pica_portfolio_briefing", "pica_portfolio_invite", // Hand-overs (ADR-333 D5) act for the organisation that builds the // catalogue, or point the client at a screen; never "as" a client. "pica_handover_create", "pica_handover_send", "pica_handover_answer", ]); export const CATALOGUE_PROPERTY = { type: "string", format: "uuid", description: "Act for a client catalogue your organisation looks after: its id from pica_portfolio_list. " + "Leave out to act for your own catalogue. What you may do is set by that relationship; a call " + "outside it is refused with the reason.", }; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export function takesCatalogue(definition) { return (definition.scope !== undefined && CATALOGUE_SCOPES.has(definition.scope) && !CATALOGUE_EXCLUDED_TOOLS.has(definition.name)); } /** * Why a `catalogue` value cannot be used for this tool, in plain words, or * null when it can. A value is never silently ignored: a tool that cannot act * for a client refuses rather than acting for the caller's own catalogue. */ export function catalogueRefusal(definition, value) { if (value === undefined) return null; if (!takesCatalogue(definition)) { return `${definition.name} can't act for a client catalogue. Call it without "catalogue" to use your own.`; } if (typeof value !== "string" || !UUID.test(value)) { return `"catalogue" must be a catalogue id from pica_portfolio_list.`; } return null; } /** * A tool's answer for a call made in a client catalogue, checked against what * the app actually served (the SDK's CatalogueCallReport). * * Many tools fail their parts soft — an inspect tool turns each refused * section into a null or an error field and still answers "success". In a * client catalogue most routes are not opened (lib/auth/catalogue-routes.ts), * so such a tool could answer "success" with nothing in it. Here: * - every request refused and none answered → a plain refusal naming them; * - some refused → the answer stands, with a line naming what was not * available, so nothing is left out silently; * - an answer that is already an error, or a call that refused nothing, is * returned unchanged. * Pinned by mcp-server/src/__tests__/tools/catalogue-argument.test.ts. */ export function settleCatalogueAnswer(toolName, answer, report) { if (!answer || answer.isError || report.refused.length === 0) return answer; const paths = [...new Set(report.refused.map((r) => r.path))]; const reasons = [ ...new Set(report.refused.map((r) => r.reason).filter(Boolean)), ]; if (report.answered === 0) { return { content: [ { type: "text", text: JSON.stringify({ error: "RELATIONSHIP_REFUSED", message: `${toolName} could not be done in this client's catalogue: every part of it was refused (${paths.join(", ")}).`, reason_codes: reasons, }, null, 2), }, ], isError: true, }; } return { ...answer, content: [ ...(answer.content ?? []), { type: "text", text: `Not available in this client's catalogue, so left out: ${paths.join(", ")}${reasons.length ? ` (${reasons.join(", ")})` : ""}.`, }, ], }; } //# sourceMappingURL=catalogue-argument.js.map