UNPKG

@withpica/mcp-server

Version:

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

251 lines • 11.3 kB
// 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", minimum: 1, maximum: 200, 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); } // listPage carries the route's real total (list() returned only the // page, so "total" was the page length — row-cap T3). const page = await this.pica.bookings.listPage({ 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, }); return formatStructuredList(page.data, "booking_enquiry", { query: args.query, total: page.total, has_more: page.hasMore, }); } /** * `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