@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
247 lines • 11.1 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatStructuredList } from "@withpica/mcp-utils";
const BOOKING_STATUSES = [
"new",
"reviewed",
"responded",
"in_discussion",
"booked",
"declined",
"archived",
"consultation_requested",
];
const BOOKING_PRIORITIES = ["low", "normal", "high", "urgent"];
export class BookingsTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_bookings_query",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
annotations: { title: "query booking enquiries", readOnlyHint: true },
description: "Search or list internal-team booking enquiries (session/production requests — " +
"mixing, mastering, production, drum tracking, session musician, composition, " +
"licensing, collaboration, other). Distinct from pica_list_license_enquiries, " +
"which covers sync-license requests for a specific work.\n\n" +
"Pass id to fold in the full detail for one enquiry instead of a list. " +
"Omit id to search/list with filters.",
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: "Booking enquiry ID. When given, returns full detail for that one enquiry and every other filter below is ignored.",
},
status: {
type: "string",
enum: BOOKING_STATUSES,
description: "Filter by status.",
},
priority: {
type: "string",
enum: BOOKING_PRIORITIES,
description: "Filter by priority.",
},
enquiry_type: {
type: "string",
description: "Filter by enquiry type (e.g. 'mixing', 'mastering', 'production').",
},
follow_up_required: {
type: "boolean",
description: "Filter to enquiries flagged as needing follow-up.",
},
query: {
type: "string",
description: "Free-text search over the enquiry name.",
},
limit: {
type: "number",
description: "Maximum number of results (default 100).",
},
offset: {
type: "number",
description: "Pagination offset (default 0).",
},
},
},
},
executor: this.queryBookings.bind(this),
},
{
definition: {
name: "pica_bookings_update",
// ADMIN_SEGMENT_WRITE_FLOOR["bookings"] = "write:catalog" — admin
// over-scoped the route AND was uncallable by the in-app assistant
// grant (GRANT_EDITABLE_SCOPES excludes admin by policy).
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Update a booking enquiry. Three independent update modes, any combination of " +
"which can be set in one call — each is applied as its own write, mirroring the " +
"backing route which only ever processes one mode per request:\n" +
"1. status — move the enquiry through its lifecycle.\n" +
"2. response_content + responded_by (both required together) — record that a " +
"response was sent, and by whom ('pica' or 'manual').\n" +
"3. follow_up_date — schedule a follow-up.\n\n" +
"At least one mode must be provided.",
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.",
requiredOneOf: [["status", "response_content", "follow_up_date"]],
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Booking enquiry ID.",
},
status: {
type: "string",
enum: BOOKING_STATUSES,
description: "New status.",
},
response_content: {
type: "string",
description: "Response text to record. Requires responded_by to also be set.",
},
responded_by: {
type: "string",
enum: ["pica", "manual"],
description: "Who sent the response. Requires response_content to also be set.",
},
follow_up_date: {
type: "string",
description: "Follow-up date (ISO 8601). Sets follow_up_required too.",
},
},
required: ["id"],
},
},
executor: this.updateBooking.bind(this),
},
];
}
async queryBookings(args) {
if (args.id) {
const booking = await this.pica.bookings.get(args.id);
return formatSuccess(`Booking enquiry ${args.id}`, booking);
}
const result = await this.pica.bookings.list({
status: args.status,
priority: args.priority,
query: args.query,
enquiry_type: args.enquiry_type,
follow_up_required: args.follow_up_required,
limit: args.limit,
offset: args.offset,
});
const items = Array.isArray(result) ? result : [];
return formatStructuredList(items, "booking_enquiry", {
query: args.query,
total: items.length,
});
}
/**
* `PATCH /admin/bookings/[id]` only processes one update mode per call
* (status, OR responseContent+respondedBy, OR followUp). A caller
* setting multiple modes at once gets one write per mode, sequentially,
* so a failure in one mode doesn't lose the others (mirrors the
* bespoke assistant's three independent handler methods).
*/
async updateBooking(args) {
if (!args.id) {
return {
content: [{ type: "text", text: "Error: id is required" }],
isError: true,
};
}
const hasResponseContent = args.response_content !== undefined;
const hasRespondedBy = args.responded_by !== undefined;
if (hasResponseContent !== hasRespondedBy) {
return {
content: [
{
type: "text",
text: "Error: response_content and responded_by must be set together.",
},
],
isError: true,
};
}
const hasStatus = args.status !== undefined;
const hasResponse = hasResponseContent && hasRespondedBy;
const hasFollowUp = args.follow_up_date !== undefined;
if (!hasStatus && !hasResponse && !hasFollowUp) {
return {
content: [
{
type: "text",
text: "Error: at least one of status, response_content+responded_by, or follow_up_date is required.",
},
],
isError: true,
};
}
const applied = [];
const failed = [];
let latest;
if (hasStatus) {
try {
latest = await this.pica.bookings.update(args.id, {
status: args.status,
});
applied.push("status");
}
catch (err) {
failed.push({ mode: "status", error: err?.message ?? String(err) });
}
}
if (hasResponse) {
try {
latest = await this.pica.bookings.update(args.id, {
responseContent: args.response_content,
respondedBy: args.responded_by,
});
applied.push("response");
}
catch (err) {
failed.push({ mode: "response", error: err?.message ?? String(err) });
}
}
if (hasFollowUp) {
try {
latest = await this.pica.bookings.update(args.id, {
followUp: true,
followUpDate: args.follow_up_date,
});
applied.push("follow_up");
}
catch (err) {
failed.push({ mode: "follow_up", error: err?.message ?? String(err) });
}
}
const summary = `Booking enquiry ${args.id}: applied ${applied.join(", ") || "none"}` +
(failed.length
? ` — failed ${failed.map((f) => `${f.mode} (${f.error})`).join(", ")}`
: "");
if (failed.length > 0 && applied.length === 0) {
return { content: [{ type: "text", text: summary }], isError: true };
}
return formatSuccess(summary, {
id: args.id,
applied,
failed,
booking: latest,
});
}
}
//# sourceMappingURL=bookings.js.map