@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
176 lines • 11.5 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatStructured, formatSuccess } from "@withpica/mcp-utils";
export class ShareSendTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_share_send",
scope: "write:catalog",
audiences: ["stdio", "http"],
tier: "write",
workflows: "infrastructure",
vernacular_kind: "creator-entry",
description: "Use when the user asks: 'send this song to my publisher', " +
"'share this work with sarah', 'email the report to my manager', " +
"'shoot this over to <name>', 'forward to <email>'. " +
"Sends a withPICA-branded email containing a scoped, expirable " +
"share-link to a named recipient. To send SEVERAL works — an " +
"album, a pitch of three songs — pass entity_ids: the recipient " +
"gets ONE email and ONE link with a track list, never one per " +
"song. The page the recipient opens shows each work's audio, " +
"identifiers (ISRC, ISWC, label), genre, lyrics, and credits " +
"(names and roles); split percentages are withheld unless " +
"include_splits is true. Recipients can be PICA users, " +
"existing org collaborators, or any external email address. " +
"First send to a NEW external email returns " +
"FIRST_EXTERNAL_SEND_CONFIRMATION_REQUIRED — re-call with " +
"confirm_first_external_send: true to proceed (deliberate friction " +
"against typo-driven phishing). Sender capped at 20 external " +
"sends / 24h. Recipient capped at 20 marketing+share emails / 30d " +
"via marketing-guard. Email From is always noreply@withpica.com; " +
"Reply-to is the sender's email; recipient can report unwanted " +
"contact via a footer link in the email. " +
"This sends the WORK itself, as a scoped, expirable share link the " +
"recipient can open. Use pica_send_message instead to invite " +
"someone or to ask them for a file — that is the Send Hub outbox, " +
"and it carries no link to an asset. Use pica_share_links_create " +
"when the user wants a link to copy and no email sent.",
inputSchema: {
type: "object",
properties: {
entity_type: {
type: "string",
enum: ["recording", "work", "audio_file", "document"],
description: "Asset class. v1: only 'work' is supported; other values return ENTITY_TYPE_NOT_SUPPORTED.",
},
entity_id: {
type: "string",
description: "UUID of the work being shared. Give this OR entity_ids.",
},
entity_ids: {
type: "array",
items: { type: "string", format: "uuid" },
description: "UUIDs of several works to send together as one link and one email, in the order the recipient should see them (max 50, one organisation). Give this OR entity_id.",
},
include_splits: {
type: "boolean",
description: "Default false. true shows each collaborator's split percentage to the recipient. Credits (names and roles) are always shown; a co-writer's share only goes out when the sender asks for it.",
},
recipient: {
type: "object",
description: "Who to send to. { kind: 'user_id' | 'collaborator_id' | 'email', value: <id-or-email> }. Internal users and existing org collaborators are detected automatically when kind='email'; otherwise the address is classified external and goes through the first-send confirmation + marketing-guard.",
properties: {
kind: {
type: "string",
enum: ["user_id", "collaborator_id", "email"],
description: "user_id = existing PICA user; collaborator_id = a person from the sender's org; email = arbitrary address (internal user / collaborator detected automatically; otherwise classified external).",
},
value: { type: "string" },
},
required: ["kind", "value"],
},
note: {
type: "string",
description: "Optional message from the sender (≤500 chars). Rendered in the email above the share-link CTA and on the page the link opens.",
},
expires_in_days: {
type: "number",
description: "Share-link TTL in days. Default 30, max 90.",
},
scope: {
type: "string",
enum: ["view", "view+download"],
description: "Default 'view'. 'view+download' allows the recipient to download attached files.",
},
confirm_first_external_send: {
type: "boolean",
description: "Required on the second call when the first attempt returned FIRST_EXTERNAL_SEND_CONFIRMATION_REQUIRED — confirms the recipient address is correct.",
},
included_file_ids: {
type: "array",
items: { type: "string", format: "uuid" },
description: "optional: audio_files ids to include in the share (across every work being sent); omit to share all of the works' files",
},
},
required: ["entity_type", "recipient"],
},
// `destructiveHint` was declared true and read false: `tier` drives
// that hint (ADR-230), and this tool is tier "write" / risk
// "mutating" — it creates a share link and sends a mail, destroying
// nothing. The friction it does need is already in the tool
// (FIRST_EXTERNAL_SEND_CONFIRMATION_REQUIRED on a new external
// address), not in the hint. `openWorldHint` stays true and is the
// honest warning here: the effect leaves PICA and cannot be recalled.
annotations: { destructiveHint: false, openWorldHint: true },
},
executor: this.shareSend.bind(this),
},
];
}
async shareSend(args) {
try {
const result = await this.pica.shareSend.send({
entity_type: args.entity_type,
entity_id: typeof args.entity_id === "string" ? args.entity_id : undefined,
entity_ids: Array.isArray(args.entity_ids)
? args.entity_ids
: undefined,
include_splits: args.include_splits === true ? true : undefined,
recipient: args.recipient,
note: args.note,
expires_in_days: args.expires_in_days,
scope: args.scope,
confirm_first_external_send: args.confirm_first_external_send === true ? true : undefined,
included_file_ids: Array.isArray(args.included_file_ids)
? args.included_file_ids
: undefined,
});
return formatSuccess(`shared via withPICA — ${result.recipient_resolution.classification}`, result);
}
catch (e) {
const errCode = e?.error_code ?? e?.code ?? "SHARE_SEND_ERROR";
const status = e?.status ?? e?.statusCode;
return formatStructured({
error: e?.message ?? String(e),
error_code: errCode,
...(typeof status === "number" ? { status } : {}),
suggestion: suggestionFor(errCode),
next_tool: "pica_share_send",
}, "pica_share_send — error");
}
}
}
function suggestionFor(code) {
switch (code) {
case "FIRST_EXTERNAL_SEND_CONFIRMATION_REQUIRED":
return "First time sending to this external address. Confirm the recipient is correct, then re-call with confirm_first_external_send: true.";
case "ROLE_ADDRESS_REJECTED":
return "The address looks like a role/system inbox (noreply@, postmaster@, abuse@, etc). Pick the recipient's personal email instead.";
case "RECIPIENT_VOLUME_EXCEEDED":
return "This person has been sent 20 shares through withPICA in the last 30 days. Wait for the window to clear, or contact them another way.";
case "SENDER_RATE_LIMIT_EXCEEDED":
return "You've sent 20+ external shares in the last 24h. Wait an hour and retry, or send to internal collaborators in the meantime.";
case "ENTITY_NOT_FOUND":
return "One or more works weren't found in your catalog (missing_ids names them; merged-away works count as missing). Verify with pica_works_get.";
case "ENTITY_ID_REQUIRED":
return "Pass entity_id (one work) or entity_ids (several works).";
case "TOO_MANY_ENTITIES":
return "One send carries at most 50 works. Split the set, or share the release with pica_share_links_create (release_id).";
case "ENTITIES_SPAN_ORGS":
return "All works in one send must belong to the same organisation. Send each organisation's works separately.";
case "ENTITY_TYPE_NOT_SUPPORTED":
return "v1 supports entity_type='work' only. Recording/audio_file/document share-send is on the roadmap.";
case "FORBIDDEN":
return "You're not a member of the asset's organisation. Switch context with pica_organisations_switch or share an asset you own.";
case "RECIPIENT_NOT_FOUND":
return "The recipient lookup failed. For collaborator_id, verify it's in your current org. For user_id, confirm the user exists.";
default:
return "Re-check the entity_id, recipient shape, and confirm_first_external_send flag against the tool description.";
}
}
//# sourceMappingURL=share-send.js.map