UNPKG

@withpica/mcp-server

Version:

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

176 lines • 11.5 kB
// 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