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