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