@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
275 lines • 14.6 kB
JavaScript
// 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