UNPKG

@withpica/mcp-server

Version:

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

132 lines • 6.11 kB
// 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