@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
167 lines • 11 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatValidationError } from "@withpica/mcp-utils";
const ENTITY_TYPES = ["work", "recording", "person", "release"];
const ACTIONS = ["view", "share_link_preview", "claim", "grant"];
const RECIPIENT_TYPES = ["user", "anonymous", "share_link"];
export class AccessSimulateTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_access_simulate",
scope: "admin",
audiences: ["stdio"],
tier: "read",
description: "Simulate who would see what — for any work, recording, person, or " +
"release, given a hypothetical action (view, share-link preview, " +
"claim, grant) and a recipient configuration, returns the visible " +
"and hidden fields plus the chain of rules that produced the answer. " +
"Read-only — never mutates state. Use BEFORE creating a share link " +
"to verify the recipient sees what you intend, or BEFORE claiming " +
"custody to see what changes. v1 ships real share_link_preview " +
"logic for works; other paths return a stable scaffolded shape.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Specialist read-only simulator — agents invoke this BEFORE a share link / claim / grant action to preview what the recipient would see. Not a natural-language entry surface; invoked operationally as a safety check, not from a 'Use when the user asks' question.",
inputSchema: {
type: "object",
properties: {
entity_type: {
type: "string",
enum: [...ENTITY_TYPES],
description: "Entity to simulate access for. v1 implements 'work' fully; the others return a not_yet_supported scaffold with the same shape so agents can iterate against the API.",
},
entity_id: {
type: "string",
description: "UUID of the entity. Must belong to the caller's organisation — cross-org IDs return entity_not_found rather than leaking existence.",
},
action: {
type: "string",
enum: [...ACTIONS],
description: "Access action to simulate. 'share_link_preview' is the v1 real path: pass recipient.share_config and get back the field-level visibility matrix the share link would expose. The other actions return scaffolded responses.",
},
recipient: {
type: "object",
description: "Hypothetical recipient. For share_link_preview pass type='share_link' and a share_config; for view/claim/grant pass type='user'/'org' as appropriate.",
properties: {
type: {
type: "string",
enum: [...RECIPIENT_TYPES],
description: "Recipient type — 'user' (authenticated user_id), 'anonymous' (no auth), 'share_link' (hypothetical share-link config).",
},
user_id: {
type: "string",
description: "Recipient user UUID, when type='user'. Optional otherwise.",
},
org_id: {
type: "string",
description: "Recipient organisation UUID, when simulating an action that targets an org (e.g. 'grant'). Optional otherwise.",
},
share_config: {
type: "object",
description: "Hypothetical share-link configuration to simulate. Fields mirror the share-links write surface — access_type, allow_*, allowed_person_ids, sent_to_emails, watermark_audio, expires_at, max_views.",
properties: {
access_type: {
type: "string",
enum: ["private", "limited", "public"],
description: "Recipient scope — 'private' (allowed_person_ids only), 'limited' (sent_to_emails only), 'public' (anyone with link).",
},
allow_download: {
type: "boolean",
description: "When true, the recipient can download original audio files. Pair with watermark_audio=true for pre-release pitches.",
},
allow_streaming: {
type: "boolean",
description: "When true, the recipient can stream audio inline.",
},
allow_comments: {
type: "boolean",
description: "When true, the recipient sees the discussion thread and can post comments.",
},
allow_view_collaborators: {
type: "boolean",
description: "When true, the recipient sees the credit lines, splits, and attestation status.",
},
allow_view_financials: {
type: "boolean",
description: "When true, the recipient sees earnings, advances, and royalty statements. The most common reason a recipient sees less than expected.",
},
watermark_audio: {
type: "boolean",
description: "When true, audio surfaces are mixed with an inaudible watermark identifier.",
},
notify_on_access: {
type: "boolean",
description: "When true, the link's creator gets notified each time the recipient accesses it.",
},
allowed_person_ids: {
type: "array",
items: { type: "string" },
description: "Person UUIDs allowed to access when access_type='private'.",
},
sent_to_emails: {
type: "array",
items: { type: "string" },
description: "Email addresses the link was sent to when access_type='limited'.",
},
max_views: {
type: "number",
description: "Cap on total views; null/omitted for unlimited. <=0 makes the link unusable.",
},
expires_at: {
type: "string",
description: "ISO timestamp; recipients see an expired page after this time.",
},
},
required: ["access_type"],
},
},
required: ["type"],
},
},
required: ["entity_type", "entity_id", "action", "recipient"],
},
annotations: {
readOnlyHint: true,
idempotentHint: true,
},
},
executor: this.simulate.bind(this),
},
];
}
async simulate(args) {
if (!args.entity_type || !ENTITY_TYPES.includes(args.entity_type)) {
return formatValidationError({
entity_type: `must be one of: ${ENTITY_TYPES.join(", ")}`,
});
}
if (!args.entity_id || typeof args.entity_id !== "string") {
return formatValidationError({ entity_id: "is required" });
}
if (!args.action || !ACTIONS.includes(args.action)) {
return formatValidationError({
action: `must be one of: ${ACTIONS.join(", ")}`,
});
}
if (!args.recipient ||
typeof args.recipient !== "object" ||
!RECIPIENT_TYPES.includes(args.recipient.type)) {
return formatValidationError({
recipient: `recipient.type must be one of: ${RECIPIENT_TYPES.join(", ")}`,
});
}
const result = await this.pica.accessSimulate.simulate(args);
const summary = result.not_yet_supported
? `Access simulation scaffolded (v1 stub) — ${result.reasons[0]?.detail ?? "see reasons"}`
: result.visible
? `Access simulated — ${result.visible_fields.length} field(s) visible, ${result.hidden_fields.length} hidden, ${result.warnings.length} warning(s)`
: `Access denied — ${result.reasons[0]?.detail ?? "see reasons"}`;
return formatSuccess(summary, result);
}
}
//# sourceMappingURL=access-simulate.js.map