UNPKG

@withpica/mcp-server

Version:

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

226 lines • 12.1 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, formatStructuredList, pageEnvelope, } from "@withpica/mcp-utils"; // Server default when the caller omits limit — mirrors the route's // parsePaginationParams({ limit: 50 }) default on the sessions list route's // default branch, so pageEnvelope's `limit` reflects what the server // actually applied even when the agent didn't pass one. const DEFAULT_SESSIONS_QUERY_LIMIT = 50; export class SessionsTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ // Tier 2 (MCP tool-surface consolidation, ADR-149 query/inspect // pattern): pica_sessions_query/_inspect are the canonical read pair, // mirroring pica_releases_query/_inspect. The deprecated // pica_sessions_list / pica_sessions_get aliases were REMOVED in the // mcp-server 2.93.0 WS5 sweep (0 calls/30d); the listSessions / // getSession executors below stay — they back the canonical tools. { definition: { name: "pica_sessions_query", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "List studio sessions — recording, mixing, mastering, writing sessions. Shows who was there, what was recorded, and when. " + "For one session in full — participants, works, audio, documents — use pica_sessions_inspect.", workflows: ["session-required"], 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: { limit: { type: "number", minimum: 1, maximum: 200, description: "Max results (default 50)", }, offset: { type: "number", description: "Pagination offset — pass next_offset from the previous response to fetch the next page (default 0).", }, }, }, }, executor: this.listSessions.bind(this), }, { definition: { name: "pica_sessions_inspect", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Get details of a specific studio session — participants, works recorded, audio files, documents. " + "To list or scan sessions rather than open one by id, use pica_sessions_query.", workflows: ["session-required"], 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.", // id/session_id are interchangeable entry points — see // pica_recordings_inspect. required:[] + requiredOneOf lets // pica_execute still hard-reject empty/stripped args while // accepting either name. requiredOneOf: [["id", "session_id"]], inputSchema: { type: "object", properties: { id: { type: "string", description: "Session ID" }, session_id: { type: "string", description: "Session ID (alias for id — write tools use session_id).", }, }, required: [], }, }, executor: this.getSession.bind(this), }, { definition: { name: "pica_sessions_create", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Log a new studio session — who was there, what was worked on, where it happened. Session types: recording, mixing, mastering, writing, production, rehearsal. " + "For the valid session_type_id values, use pica_sessions_types first.", workflows: ["session-required"], 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: { title: { type: "string", description: "Session title (e.g. 'Vocal recording — Track 3')", }, description: { type: "string", description: "Freeform description of the session.", }, session_type_id: { type: "string", description: "Session type UUID — get valid IDs from pica_sessions_types.", }, start_time: { type: "string", description: "ISO 8601 datetime.", }, end_time: { type: "string", description: "ISO 8601 datetime.", }, timezone: { type: "string", description: "IANA timezone (e.g. 'Europe/London'). Defaults to Europe/London.", }, is_all_day: { type: "boolean", description: "Mark the session as all-day (skips start/end time-of-day semantics). Default false.", }, location: { type: "string", description: "Studio or location name.", }, location_url: { type: "string", description: "URL for virtual sessions (e.g. Zoom / Google Meet link).", }, work_id: { type: "string", description: "Work this session was for (optional).", }, recording_id: { type: "string", description: "Recording this session was for (optional).", }, status: { type: "string", enum: ["scheduled", "in_progress", "completed", "cancelled"], description: "Initial session status. Default 'scheduled'. Use PATCH actions to transition later.", }, notes: { type: "string", description: "Session notes — what happened, what was achieved.", }, metadata: { type: "object", description: "Freeform jsonb bag for session-specific extras.", additionalProperties: true, }, participants: { type: "array", description: "Participants to attach on create. Each element: { personId?, userId?, role? }.", items: { type: "object", additionalProperties: true }, }, }, required: ["title", "start_time", "end_time"], }, }, executor: this.createSession.bind(this), }, { definition: { name: "pica_sessions_types", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Get available session types (recording, mixing, mastering, etc.). " + "Returns the type list only — to log an actual session, use pica_sessions_create.", workflows: ["session-required"], 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: {} }, }, executor: this.getSessionTypes.bind(this), }, ]; } async listSessions(args) { // listPage carries the route's real total for the 7-day window (row-cap // T3: the route returned only the slice, so has_more was "this page came // back full" and an exactly-full last page pointed at an empty one). const result = await this.pica.sessions.listPage({ limit: args.limit, offset: args.offset, }); const sessions = Array.isArray(result?.data) ? result.data : []; const page = pageEnvelope({ limit: args.limit ?? DEFAULT_SESSIONS_QUERY_LIMIT, offset: args.offset ?? 0, returned: sessions.length, total: result.total, }); return formatStructuredList(sessions, "session", { total: result.total, has_more: page.has_more, page, }); } async getSession(args) { const _id = args.id ?? args.session_id; const session = await this.pica.sessions.get(_id); return formatSuccess(`Session: ${session?.title || _id}`, session); } async createSession(args) { // Derive start_time/end_time from date if not provided if (!args.start_time && !args.end_time && args.date) { args.start_time = `${args.date}T09:00:00`; args.end_time = `${args.date}T17:00:00`; delete args.date; } const session = await this.pica.sessions.create(args); return formatSuccess("Studio session logged successfully", session); } async getSessionTypes(_args) { // getTypes() returns an ARRAY of session types. formatAsText(array) set // structuredContent to that array, which the MCP SDK rejects with // "expected record, received array" (ops-issue 0fbfc9a2). Wrap it into a // record via formatStructuredList — the same helper listSessions uses. const result = await this.pica.sessions.getTypes(); const types = Array.isArray(result) ? result : result?.data || []; return formatStructuredList(types, "session type"); } } //# sourceMappingURL=sessions.js.map