@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
132 lines • 6.11 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess } from "@withpica/mcp-utils";
export class SharingTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_share_trace",
scope: "read:catalog",
audiences: ["stdio", "http"],
tier: "read",
description: "Use when the user asks: 'why does Tony have access to this song?', 'who can see this work?', 'how did this person see my recording?'. " +
"Returns the chain of reasons a viewer (user or org) can see an entity. " +
"Composes four substrates — org membership, custody chain, share links, cross-org grants — and ranks reasons by directness. " +
"Two-axis access: 403 INSUFFICIENT_SCOPE if you don't own the entity; has_access:false with reasons:[] when you DO own it but the viewer can't see. " +
"→ then: pica_share_links_update is_active=false or pica_share_links_delete (revoke a share_link reason), pica_works_inspect / pica_recordings_inspect (verify state)",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
inputSchema: {
type: "object",
properties: {
entity_type: {
type: "string",
enum: ["work", "recording", "person"],
description: "What kind of entity to trace access to. Persons only support the org_membership substrate (no work-scoped reasons apply).",
},
entity_id: {
type: "string",
description: "UUID of the work, recording, or person.",
},
viewer_user_id: {
type: "string",
description: "UUID of the user whose access we're tracing. Supply EITHER viewer_user_id OR viewer_org_id, not both.",
},
viewer_org_id: {
type: "string",
description: "UUID of the org whose access we're tracing. Supply EITHER viewer_user_id OR viewer_org_id, not both.",
},
},
required: ["entity_type", "entity_id"],
},
nextSteps: [
{
tool: "pica_share_links_query",
reason: "When the trace surfaces a share_link reason, list links for the work to find the right one to revoke.",
when: "on_success",
},
{
tool: "pica_works_inspect",
reason: "Inspect the work to see related metadata when investigating sharing.",
when: "on_success",
},
],
},
executor: this.shareTrace.bind(this),
},
];
}
async shareTrace(args) {
const entityType = args.entity_type;
const entityId = args.entity_id;
const viewerUserId = args.viewer_user_id;
const viewerOrgId = args.viewer_org_id;
if (!entityType || !["work", "recording", "person"].includes(entityType)) {
return {
content: [
{
type: "text",
text: "entity_type must be 'work', 'recording', or 'person'.",
},
],
isError: true,
};
}
if (!entityId) {
return {
content: [{ type: "text", text: "entity_id is required." }],
isError: true,
};
}
if (Boolean(viewerUserId) === Boolean(viewerOrgId)) {
return {
content: [
{
type: "text",
text: "supply exactly one of viewer_user_id or viewer_org_id.",
},
],
isError: true,
};
}
try {
const result = await this.pica.shareTrace.trace({
entity_type: entityType,
entity_id: entityId,
viewer_user_id: viewerUserId,
viewer_org_id: viewerOrgId,
});
const verdict = result.has_access
? `Access via ${result.reasons.length} reason${result.reasons.length === 1 ? "" : "s"} (most-direct: ${result.reasons[0]?.kind})`
: "No access — viewer cannot see this entity";
return formatSuccess(verdict, result);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
// 403 INSUFFICIENT_SCOPE bubbles up as a fetch error from the
// SDK — surface it without leaking entity existence.
if (/INSUFFICIENT_SCOPE|insufficient_scope|403/i.test(errorMessage)) {
return {
content: [
{
type: "text",
text: `entity is not in scope for this organisation — only the entity's owning org can trace its sharing graph.`,
},
],
isError: true,
};
}
return {
content: [
{ type: "text", text: `Failed to trace sharing: ${errorMessage}` },
],
isError: true,
};
}
}
}
//# sourceMappingURL=sharing.js.map