UNPKG

@withpica/mcp-server

Version:

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

275 lines • 14.6 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, formatList } from "@withpica/mcp-utils"; export class SendTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_send_message", // write:files matches BOTH gates on the backing route (POST // /api/admin/send): ADMIN_SEGMENT_WRITE_FLOOR["send"] = // "write:files" AND the route's own requiredScope: "write:files" // (the route declared write:catalog until 2026-07-04, which 403'd // write:files-only callers after they'd passed the floor). // Originally rescoped from admin (ADR-289 whole-branch review) — // admin over-shot and made the tool uncallable by the in-app // assistant grant (admin is excluded from GRANT_EDITABLE_SCOPES). scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Send a message through PICA's Send Hub (the unified outbox for invites, file requests, " + "and audio shares — one queryable log for everything sent to a person). Supports portal invites, team invites, " + "project invites, and file requests. Each send_type requires different fields. " + "For file_request: title + (person_id or recipient_email) are required; " + "the upload constraints are optional and documented per field. " + "This sends an INVITE or a request for something. Use pica_share_send instead to " + "send a work itself — it emails a scoped, expirable share link to the recipient. " + "Prefer pica_send_resend when the recipient just needs the same item again.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { send_type: { type: "string", description: "Type of message to send", enum: [ "portal_invite", "team_invite", "project_invite", "file_request", ], }, // Portal invite fields person_id: { type: "string", description: "Person ID to send to (required for portal_invite, optional for file_request)", }, email: { type: "string", description: "Email address (required for portal_invite, team_invite)", }, message: { type: "string", description: "Optional personal message to include", }, // Team invite fields name: { type: "string", description: "Recipient name (for team_invite)", }, permissions: { type: "string", description: "Permission level for team_invite (e.g. 'admin', 'editor', 'viewer')", }, // Project invite fields project_id: { type: "string", description: "Project ID (required for project_invite)", }, participant_ids: { type: "array", items: { type: "string" }, description: "List of person IDs to invite to the project", }, // File request fields title: { type: "string", description: "Title for file_request", }, recipient_email: { type: "string", description: "Recipient email for file_request", }, recipient_name: { type: "string", description: "Recipient name for file_request", }, file_type_restrictions: { type: "array", items: { type: "string" }, description: "file_request only: allowed file extensions/types the recipient may upload (e.g. ['mp3', 'wav']). Omit to allow any type.", }, max_file_size_mb: { type: "number", description: "file_request only: maximum size per uploaded file, in MB.", }, max_file_count: { type: "number", description: "file_request only: maximum number of files the recipient may upload.", }, related_agreement_id: { type: "string", description: "file_request only: an agreement this request is collecting files for (e.g. a signed split sheet).", }, expires_at: { type: "string", description: "file_request only: ISO 8601 timestamp after which the request stops accepting responses. Omit for no expiry.", }, }, required: ["send_type"], }, }, executor: this.sendMessage.bind(this), }, { definition: { name: "pica_send_query", scope: "read:analytics", audiences: ["stdio", "http", "public"], tier: "read", description: "Query Send Hub (the unified outbox for invites, file requests, and audio shares) with " + "optional filters. Returns sent messages, invites, file requests, and audio shares. " + "Use status='pending' to see items awaiting response. This only " + "reads the outbox — use pica_send_message to send something new.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { status: { type: "string", description: "Filter by status: 'all' for everything, 'sent' for delivered, 'pending' for awaiting response (default: all)", enum: ["all", "sent", "pending"], }, type: { type: "string", description: "Filter by type", enum: [ "portal_invite", "team_invite", "project_invite", "file_request", "audio_share", ], }, person_id: { type: "string", description: "Filter by recipient person ID", }, work_id: { type: "string", description: "Filter by related work ID", }, limit: { type: "number", description: "Max results (default: 50)", }, }, }, }, executor: this.querySent.bind(this), }, { definition: { name: "pica_send_resend", // write:files matches BOTH gates on the backing route (POST // /api/admin/send/[id]/resend): the "send" floor AND the route's // own requiredScope: "write:files" — same history as // pica_send_message above. scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Resend a Send Hub item — re-send an invite, file request, or audio share. Send Hub is " + "PICA's unified outbox for everything sent to a person; pica_send_query lists items to resend.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Send Hub item ID to resend", }, }, required: ["id"], }, }, executor: this.resendItem.bind(this), }, { definition: { name: "pica_send_cancel", // ADMIN_SEGMENT_WRITE_FLOOR["send"] = "write:files" — the backing // DELETE /admin/send/[id] route declares no requiredScope of its // own, so the floor is the real gate. write:files is the least // scope that satisfies it (and is GRANT_EDITABLE_SCOPES-eligible). // pica_send_message/pica_send_resend above have since been // rescoped to match (ADR-289 whole-branch review). scope: "write:files", audiences: ["stdio", "http", "public"], tier: "write", description: "Cancel a pending Send Hub item (PICA's unified outbox for invites, file requests, and " + "audio shares) before it's " + "actioned by the recipient. Sets status to 'cancelled' — the item stays visible in " + "pica_send_query history but can no longer be responded to. Use pica_send_query with " + "status='pending' to find the id first.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Send Hub item ID to cancel", }, }, required: ["id"], }, }, executor: this.cancelItem.bind(this), }, ]; } async sendMessage(args) { // Remap person_id → recipient_person_id for file_request if (args.send_type === "file_request" && args.person_id && !args.recipient_person_id) { args.recipient_person_id = args.person_id; } const result = await this.pica.send.send(args); return formatSuccess(`${args.send_type} sent successfully.`, result); } async querySent(args) { const status = args.status || "all"; // "pending" delegates to the dedicated pending endpoint if (status === "pending") { const items = await this.pica.send.listPending(); return formatList(items, { status: "pending" }); } // "all" or "sent" use the general list endpoint const result = await this.pica.send.list({ type: args.type, status: status === "all" ? args.status : status, personId: args.person_id, workId: args.work_id, limit: args.limit, }); return formatList(result.items, { total: result.total, type: args.type, status: args.status, }); } async resendItem(args) { const result = await this.pica.send.resend(args.id); return formatSuccess(`Item ${args.id} resent.`, result); } async cancelItem(args) { await this.pica.send.cancel(args.id); return formatSuccess(`Item ${args.id} cancelled.`, { id: args.id, cancelled: true, }); } } //# sourceMappingURL=send.js.map