@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
840 lines • 47 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { formatSuccess, formatDeletion, withBillingGate, } from "@withpica/mcp-utils";
import { buildIntegrityPressure } from "./integrity-hints.js";
/**
* Canonical write-schema for `releases`. Every property below must
* correspond to a real column on `public.releases`. Keep this list
* tight — reintroducing a phantom field is the exact regression
* ADR-174 Phase 2 exists to prevent.
*/
const RELEASE_WRITE_PROPERTIES = {
title: {
type: "string",
description: "Release title",
},
release_date: {
type: "string",
description: "Release date (YYYY-MM-DD)",
},
release_type: {
type: "string",
description: "Release type",
enum: ["album", "ep", "single", "compilation"],
},
pre_release_date: {
type: "string",
description: "Pre-release / announcement date (YYYY-MM-DD)",
},
upc: {
type: "string",
description: "Universal Product Code (UPC/EAN barcode), as a string. Stored in one canonical form per GTIN: a 13-digit EAN that starts with 0 is stored as its 12-digit UPC-A (0198002851000 → 198002851000 — the same barcode, not a lost digit); any other 13-digit EAN is kept as given. pica_releases_query `upc` accepts either form.",
},
catalog_number: {
type: "string",
description: "Label catalog number",
},
label_organization_id: {
type: "string",
description: "organisations.id of the label (must be an org with org_type='label'). " +
"Resolve a label name to an ID via pica_labels_query first — never pass a free-text name.",
},
parent_label: {
type: "string",
description: "Free-text parent/umbrella label name (e.g. 'Integrated Music Rights (UK)'). " +
"Use label_organization_id when the label exists as an org; parent_label is the " +
"plain-text fallback for a label you are not modelling as an organisation.",
},
distributor_organization_id: {
type: "string",
description: "organisations.id of the distributor. Resolve a distributor name via pica_search_all first.",
},
artwork_url: {
type: "string",
description: "Album artwork URL",
},
spotify_album_id: {
type: "string",
description: "Canonical Spotify album id (bare id, e.g. 6QaVfG1pHYl1z15ZxkvVDW). " +
"The SINGLE stored Spotify column — preferred over spotify_album_uri/" +
"spotify_url; when only the uri/url is supplied the server derives " +
"this id automatically. The UPC backfill fires only on a WRITE to this " +
"field: a release that already carries a spotify_album_id but no UPC " +
"is backfilled by passing the same id again through pica_releases_update.",
},
spotify_album_uri: {
type: "string",
description: "Spotify album URI (e.g. spotify:album:…). Legacy input NAME — the " +
"server extracts the album id and stores ONLY the canonical " +
"spotify_album_id.",
},
spotify_url: {
type: "string",
description: "Spotify album web URL. Legacy input NAME — extracted to the " +
"canonical spotify_album_id, never stored.",
},
apple_music_url: {
type: "string",
description: "Apple Music album URL",
},
other_urls: {
type: "object",
description: "DSP URLs keyed by service name (e.g. { tidal: '…', deezer: '…' })",
additionalProperties: { type: "string" },
},
notes: {
type: "string",
description: "Internal notes",
},
availability: {
type: "string",
description: "Release availability: 'available' (live) or 'withdrawn' (was out, now taken down). " +
"Setting 'withdrawn' makes the work read as 'previously released' in /inspect; " +
"restoring to 'available' clears the withdrawal.",
enum: ["available", "withdrawn"],
},
};
export class ReleasesTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
// Tier 1 (MCP tool-surface consolidation, ADR-149 query/inspect
// pattern): pica_releases_query/_inspect are the canonical read pair,
// mirroring pica_recordings_query/_inspect. The old pica_releases_list
// and pica_releases_get names were silent aliases of this pair until
// mcp-server 2.99.0, which removed them.
{
definition: {
name: "pica_releases_query",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Search/list releases — albums, EPs, and singles with release dates, labels, distributors, and UPCs. " +
"Filter to locate a specific release instead of paging the whole catalogue: `query` (title substring), `upc` (exact), or `spotify_album_id` (exact — answers 'does a release for this Spotify album already exist?'). Omit all filters to list everything. " +
"→ then: pica_releases_inspect (details for a specific release)",
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: {
query: {
type: "string",
description: "Case-insensitive substring match on release title (e.g. 'paradise').",
},
upc: {
type: "string",
description: "UPC match (the release dedup key). Either form finds the release — a 12-digit UPC-A and its 13-digit 0-prefixed EAN are the same barcode.",
},
spotify_album_id: {
type: "string",
description: "Exact spotify_album_id match — check whether a release for a given Spotify album already exists before importing.",
},
limit: {
type: "number",
description: "Max results to return (default 50)",
},
offset: {
type: "integer",
description: "Pagination offset — number of releases to skip (default 0). Pass the next_offset value from the previous response to fetch the next page.",
},
release_date_from: {
type: "string",
description: "Only releases with release_date on/after this ISO date (YYYY-MM-DD). For 'released in 2013' pass release_date_from=2013-01-01 & release_date_to=2013-12-31.",
},
release_date_to: {
type: "string",
description: "Only releases with release_date on/before this ISO date (YYYY-MM-DD).",
},
},
},
},
executor: this.listReleases.bind(this),
},
{
definition: {
name: "pica_releases_inspect",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Get release details including tracks. " +
"Prefer pica_releases_query when finding a release by title, UPC, or Spotify album id rather than reading one by ID.",
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.",
// id/release_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", "release_id"]],
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Release ID" },
release_id: {
type: "string",
description: "Release ID (alias for id — write tools use release_id).",
},
},
required: [],
},
},
executor: this.getRelease.bind(this),
},
{
definition: {
name: "pica_releases_create",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Create a new release — set title, dates, type, catalog number, " +
"label/distributor organisation IDs, artwork, DSP URLs, and notes. " +
"For label_organization_id use pica_labels_query first to resolve a name to an ID " +
"(freetext label names are not accepted — the label must be an organisation that already exists). " +
"After several mutations, surface a pica_dashboard_briefing to the user. " +
"Prefer pica_releases_create_with_tracks when the tracklist is already known; this one creates the release only.",
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: RELEASE_WRITE_PROPERTIES,
required: ["title"],
},
},
executor: this.createRelease.bind(this),
},
{
definition: {
name: "pica_releases_update",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Update release details — any field in the release write schema. " +
"For label_organization_id use pica_labels_query first to resolve a name to an ID. " +
"Prefer pica_releases_create when no release exists yet; this one edits an existing release.",
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: "Release ID" },
...RELEASE_WRITE_PROPERTIES,
},
required: ["id"],
},
},
executor: this.updateRelease.bind(this),
},
// ADR-173: release-track primitives + compound workflow
{
definition: {
name: "pica_releases_attach_track",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Attach track(s) to a release. Three ways to call it: (1) single track by " +
"recording_id and/or work_id, with track_number; (2) single track by external " +
"identifier — identifier_type + value (isrc/spotify_url/spotify_track_uri/" +
"youtube_video_id/musicbrainz_recording_id/mlc_recording_id), resolved to an " +
"EXISTING internal recording id already in your catalog, with track_number. This " +
"identifier form is NOT for importing from a Spotify/Deezer URL or extracting " +
"metadata/ISRCs — for that use pica_import_streaming_link; (3) batch — items[], each " +
"{ recording_id?, work_id?, track_number, disc_number? }, attached in one call. " +
"At least one of recording_id or work_id must be provided for the single-track " +
"forms. Idempotent on (release_id, disc_number, track_number) — a second call at " +
"the same position updates the existing row rather than creating a duplicate. " +
"→ then: pica_releases_list_tracks (verify tracklist), pica_releases_reorder_tracks (change order)",
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: {
release_id: { type: "string", description: "Release ID" },
recording_id: {
type: "string",
description: "Recording ID (optional — at least one of recording_id or work_id must be provided)",
},
work_id: {
type: "string",
description: "Work ID (optional — omit for licensed-masters releases where no composition record exists)",
},
track_number: {
type: "number",
description: "Position on disc (1-based). Required for the single-track forms; ignored when items[] is passed.",
},
disc_number: {
type: "number",
description: "Disc number (default 1)",
},
identifier_type: {
type: "string",
enum: [
"isrc",
"spotify_url",
"spotify_track_uri",
"youtube_video_id",
"musicbrainz_recording_id",
"mlc_recording_id",
],
description: "Type of external identifier to resolve",
},
value: {
type: "string",
description: "The external identifier value (used with identifier_type).",
},
items: {
type: "array",
description: "Batch mode: attach many tracks in one call. Each item: " +
"{ recording_id?, work_id?, track_number, disc_number? }.",
items: {
type: "object",
properties: {
recording_id: { type: "string" },
work_id: { type: "string" },
track_number: { type: "number" },
disc_number: { type: "number" },
},
required: ["track_number"],
},
},
dry_run: {
type: "boolean",
description: "Preview a batch (items[]) attach without writing. When true, " +
"returns { dry_run: true, would_affect, target_count } instead " +
"of attaching any tracks. Only applies to items[] batch mode.",
},
},
required: ["release_id"],
},
previewMode: "dry_run",
},
executor: this.attachTrack.bind(this),
},
{
definition: {
name: "pica_releases_list_tracks",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "List tracks on a release in (disc_number, track_number) order with " +
"inlined title, artist, ISRC, and duration from the linked recording and work. " +
"→ then: pica_releases_reorder_tracks (change order), pica_recordings_inspect (deep-dive a track)",
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: {
release_id: { type: "string", description: "Release ID" },
},
required: ["release_id"],
},
},
executor: this.listTracks.bind(this),
},
{
definition: {
name: "pica_releases_detach_track",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Remove a track from a release by position. Soft-confirmation: call without " +
"confirm:true to see a preview of the row that would be removed, then call " +
"again with confirm:true to execute. " +
"To SWAP a track's position with another rather than remove it, use " +
"pica_releases_move_track — its destination must already be occupied.",
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: {
release_id: { type: "string", description: "Release ID" },
track_number: {
type: "number",
description: "Position on disc (1-based)",
},
disc_number: {
type: "number",
description: "Disc number (default 1)",
},
confirm: {
type: "boolean",
description: "Set to true to execute the detach after reviewing the preview",
},
},
required: ["release_id", "track_number"],
},
},
executor: this.detachTrack.bind(this),
},
{
definition: {
name: "pica_releases_reorder_tracks",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Reorder tracks on a release atomically. Pass the full new order as an array " +
"of { track_id, track_number, disc_number } — the entire reorder commits or " +
"rolls back as one transaction. Duplicate (disc_number, track_number) pairs " +
"in the payload are rejected before any write. " +
"Prefer pica_releases_move_track when swapping just two positions; this one rewrites the whole order.",
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: {
release_id: { type: "string", description: "Release ID" },
new_order: {
type: "array",
description: "New ordering of tracks. Every existing track should appear here.",
items: {
type: "object",
properties: {
track_id: {
type: "string",
description: "release_tracks.id",
},
track_number: { type: "number" },
disc_number: {
type: "number",
description: "Default 1",
},
},
required: ["track_id", "track_number"],
},
},
},
required: ["release_id", "new_order"],
},
},
executor: this.reorderTracks.bind(this),
},
{
definition: {
name: "pica_releases_attach_recording_with_work",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Attach a recording to a release at a specific position, automatically " +
"pulling the recording's linked work (if any) onto the same track row. " +
"Saves agents the recordings.work_id lookup that pica_releases_attach_track " +
"would otherwise require. Idempotent on (release_id, disc_number, track_number).",
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: {
release_id: { type: "string", description: "Release ID" },
recording_id: { type: "string", description: "Recording ID" },
track_number: {
type: "number",
description: "Position on disc (1-based)",
},
disc_number: {
type: "number",
description: "Disc number (default 1)",
},
},
required: ["release_id", "recording_id", "track_number"],
},
},
executor: this.attachRecordingWithWork.bind(this),
},
// ADR-175: release workflow tools — compound operations on the primitives
{
definition: {
name: "pica_releases_move_track",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Swap two track positions on a release atomically. Internally reads " +
"both rows and writes them with exchanged (disc_number, track_number) " +
"in one transaction. For the rare full re-sequence, use " +
"pica_releases_reorder_tracks instead.",
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: {
release_id: { type: "string", description: "Release ID" },
from: {
type: "object",
description: "Source position",
properties: {
track_number: { type: "number" },
disc_number: { type: "number", description: "Default 1" },
},
required: ["track_number"],
},
to: {
type: "object",
description: "Destination position (must be occupied — this is a swap, not a slide)",
properties: {
track_number: { type: "number" },
disc_number: { type: "number", description: "Default 1" },
},
required: ["track_number"],
},
},
required: ["release_id", "from", "to"],
},
},
executor: this.moveTrack.bind(this),
},
{
definition: {
name: "pica_releases_create_with_tracks",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "Create a release and its tracks in a single atomic transaction. " +
"Any failure — an unknown recording or work, an identifier lookup " +
"miss, an invalid track entry — rolls the entire release back. No " +
"partial releases. Each " +
"track entry can pass recording_id/work_id directly OR pass an " +
"identifier_lookup object which the tool resolves server-side " +
"(same semantics as pica_releases_attach_track with " +
"identifier_type + identifier_value). " +
"Use this when importing a new album — use pica_releases_attach_track " +
"with items[] when the release already exists.",
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: {
release: {
type: "object",
description: "Release fields (same shape as pica_releases_create). " +
"title is required; release_type defaults to 'single'.",
properties: {
title: { type: "string", description: "Release title" },
release_type: {
type: "string",
enum: ["album", "ep", "single", "compilation"],
description: "Release type (default 'single')",
},
release_date: {
type: "string",
description: "Release date (YYYY-MM-DD)",
},
pre_release_date: {
type: "string",
description: "Pre-release / announcement date (YYYY-MM-DD)",
},
upc: {
type: "string",
description: "UPC / EAN barcode. A 13-digit EAN starting with 0 is stored as its 12-digit UPC-A (same barcode).",
},
catalog_number: {
type: "string",
description: "Label catalog number",
},
label_organization_id: {
type: "string",
description: "organisations.id of the label — resolve via pica_labels_query first.",
},
parent_label: {
type: "string",
description: "Free-text parent/umbrella label name (fallback to the label_organization_id FK).",
},
distributor_organization_id: {
type: "string",
description: "organisations.id of the distributor.",
},
artwork_url: {
type: "string",
description: "Album artwork URL",
},
spotify_album_id: {
type: "string",
description: "Canonical Spotify album id (derived from the uri/url when omitted)",
},
spotify_album_uri: { type: "string" },
spotify_url: { type: "string" },
apple_music_url: { type: "string" },
other_urls: {
type: "object",
additionalProperties: { type: "string" },
},
notes: { type: "string" },
},
required: ["title"],
},
tracks: {
type: "array",
description: "Track entries. Each needs track_number plus one of: " +
"recording_id, work_id, or identifier_lookup.",
items: {
type: "object",
properties: {
recording_id: { type: "string" },
work_id: { type: "string" },
identifier_lookup: {
type: "object",
description: "Resolve a recording by external identifier in " +
"the caller's org before inserting the track. " +
"Misses and collisions abort the whole create.",
properties: {
identifier_type: {
type: "string",
enum: [
"isrc",
"spotify_url",
"spotify_track_uri",
"youtube_video_id",
"musicbrainz_recording_id",
"mlc_recording_id",
],
},
value: { type: "string" },
},
required: ["identifier_type", "value"],
},
track_number: {
type: "number",
description: "Position on disc (1-based)",
},
disc_number: {
type: "number",
description: "Disc number (default 1)",
},
},
required: ["track_number"],
},
},
},
required: ["release", "tracks"],
},
},
executor: this.createWithTracks.bind(this),
},
{
definition: {
name: "pica_releases_completeness_check",
scope: "read:catalog",
audiences: ["stdio", "http", "public"],
tier: "read",
description: "Run the release-scoped completeness diligence report. Pass a " +
"release_id for a single-release report, or omit for a scan of " +
"every release in your catalog. Returns a list of " +
"issues per release with severity (critical/warning/suggestion) " +
"and a recovery hint pointing at the right tool. Issues also " +
"surface in pica_catalog_diligence; this tool gives release-" +
"level granularity. " +
"For the tracklist itself rather than what is missing from it, use pica_releases_list_tracks.",
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: {
release_id: {
type: "string",
description: "Optional — a single release to check. Omit to scan " +
"every release in your catalog.",
},
},
},
},
executor: this.completenessCheck.bind(this),
},
{
definition: {
name: "pica_releases_delete",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "destructive",
description: "Delete a release. If the release still has tracks attached, or " +
"has active share links, the delete is REFUSED and the error lists " +
"exactly what to clear first (releases cannot be archived — there " +
"is no soft-delete). Detach the tracks (pica_releases_detach_track) " +
"and revoke the share links, then retry.",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Release ID",
},
},
required: ["id"],
},
},
executor: withBillingGate(this.pica, "release deletion", this.deleteRelease.bind(this)),
},
];
}
async deleteRelease(args) {
const outcome = await this.pica.releases.delete(args.id);
return formatDeletion("release", args.id, outcome);
}
async attachTrack(args) {
// Superset dispatch — subsumed pica_releases_bulk_attach_tracks and
// pica_releases_attach_track_by_identifier (both names removed in
// mcp-server 2.99.0; their executors went with them).
if (Array.isArray(args.items)) {
if (args.dry_run === true) {
return formatSuccess(`Dry run — would attach ${args.items.length} track(s) to release ${args.release_id}`, {
dry_run: true,
would_affect: { release_id: args.release_id, tracks: args.items },
target_count: args.items.length,
});
}
const tracks = await this.pica.releases.bulkAttachTracks(args.release_id, args.items);
return formatSuccess(`Bulk attached ${args.items.length} tracks`, tracks);
}
if (args.identifier_type && args.value) {
const result = await this.pica.releases.attachTrackByIdentifier(args.release_id, {
identifier_type: args.identifier_type,
value: args.value,
track_number: args.track_number,
disc_number: args.disc_number,
});
return formatSuccess("Track attached by identifier", result);
}
if (typeof args.track_number !== "number") {
throw new Error("attach_track requires track_number for a single track, or items[] for a batch");
}
const track = await this.pica.releases.attachTrack(args.release_id, {
recording_id: args.recording_id ?? null,
work_id: args.work_id ?? null,
track_number: args.track_number,
disc_number: args.disc_number,
});
return formatSuccess("Track attached to release", track);
}
async listTracks(args) {
const tracks = await this.pica.releases.listTracks(args.release_id);
return formatSuccess("Release tracks retrieved", tracks);
}
async detachTrack(args) {
const result = await this.pica.releases.detachTrack(args.release_id, {
track_number: args.track_number,
disc_number: args.disc_number,
confirm: args.confirm,
});
// The SDK's request() already unwraps the route's {success, data}
// envelope, so this tool receives {confirmed, preview|removed} directly.
// The old `result?.data?.confirmed` check was ALWAYS undefined — the tool
// rendered the "call again with confirm:true" preview instruction even
// after the delete had executed (trust bug on a destructive op;
// ops_issue 0137b1b8, user_feedback e6320d08).
const confirmed = result?.confirmed ===
true;
const message = confirmed
? "Track detached from release"
: "Preview of track to be detached — call again with confirm:true to execute";
return formatSuccess(message, result);
}
async reorderTracks(args) {
const tracks = await this.pica.releases.reorderTracks(args.release_id, args.new_order);
return formatSuccess("Release tracks reordered", tracks);
}
async attachRecordingWithWork(args) {
const track = await this.pica.releases.attachRecordingWithWork(args.release_id, {
recording_id: args.recording_id,
track_number: args.track_number,
disc_number: args.disc_number,
});
return formatSuccess("Recording attached to release with work", track);
}
async listReleases(args) {
const limit = args.limit;
const offset = typeof args.offset === "number" ? args.offset : 0;
const result = await this.pica.releases.list({
limit,
offset,
query: args.query,
upc: args.upc,
spotify_album_id: args.spotify_album_id,
release_date_from: args.release_date_from,
release_date_to: args.release_date_to,
});
const releases = Array.isArray(result) ? result : [];
// B2 (ADR-264-WS1b): truthful pagination envelope — every release is
// reachable without truncation dead-ends.
//
// has_more heuristic: page count == limit ⇒ "possibly more pages".
// The follow-up request may return an empty list — that is not an error,
// it simply means you have reached the end of the catalog.
const count = releases.length;
const hasMore = limit != null && count === limit;
const nextOffset = hasMore ? offset + count : null;
const from = offset + 1;
const to = offset + count;
const showingMsg = count === 0
? "No releases found"
: `showing ${from}–${to}${hasMore ? `; pass offset=${nextOffset} for more (if the next page is empty, you've reached the end)` : ""}`;
const envelope = {
summary: showingMsg,
count,
offset,
has_more: hasMore,
next_offset: nextOffset,
releases,
};
const payload = JSON.stringify(envelope, null, 2);
return {
content: [{ type: "text", text: payload }],
structuredContent: envelope,
};
}
async getRelease(args) {
const releaseId = args.id ?? args.release_id;
const release = await this.pica.releases.get(releaseId);
return formatSuccess("Release retrieved", release);
}
async createRelease(args) {
const { id: _id, ...data } = args;
const release = await this.pica.releases.create(data);
// Registration integrity floor (spec 2026-07-09, Task 9) — assess the
// just-created release and attach both the rendered hints and the raw
// gap block. Best-effort: never blocks or fails the create.
// NOTE (Task 12): release-grain assessment is a documented v1 no-op —
// the block renders empty by design (no release-grain gap code exists
// yet; see assessEntity's release branch in lib/services/integrity-floor.ts).
const pressure = await buildIntegrityPressure(this.pica, "release", release.id);
return formatSuccess("Release created", { ...release, integrity: pressure.integrity }, pressure.hints);
}
async updateRelease(args) {
const { id, ...updates } = args;
const release = await this.pica.releases.update(id, updates);
// Registration integrity floor (Task 9) — same pressure rendering as
// createRelease.
// NOTE (Task 12): release-grain assessment is a documented v1 no-op —
// the block renders empty by design (see createRelease's note above).
const pressure = await buildIntegrityPressure(this.pica, "release", release.id);
return formatSuccess("Release updated", { ...release, integrity: pressure.integrity }, pressure.hints);
}
// ADR-175 workflow executors
async moveTrack(args) {
const tracks = await this.pica.releases.moveTrack(args.release_id, args.from, args.to);
return formatSuccess("Tracks swapped", tracks);
}
async createWithTracks(args) {
const result = await this.pica.releases.createWithTracks({
release: args.release,
tracks: args.tracks,
});
const count = Array.isArray(args.tracks) ? args.tracks.length : 0;
return formatSuccess(`Release created with ${count} tracks`, result);
}
async completenessCheck(args) {
const report = await this.pica.releases.completenessCheck(args.release_id ? { release_id: args.release_id } : undefined);
return formatSuccess("Release completeness report", report);
}
}
//# sourceMappingURL=releases.js.map