@withpica/mcp-server
Version:
MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog
252 lines • 13.8 kB
JavaScript
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
/**
* Registration Integrity Floor MCP Tool — spec 2026-07-09, Task 10.
*
* Tool: pica_integrity_waive
*
* Flagged spec deviation (documented in the plan's Global Constraints and
* the PR body): the design proposed NO new MCP tool for the floor — Tasks
* 3-9 only enrich existing write-tool responses with integrity pressure
* (see integrity-hints.ts). Waiving a gap has no existing tool vehicle, so
* this is the one new surface. The connector-visible discover/execute
* surface (pica_discover / pica_tool_details / pica_execute) is otherwise
* unchanged — this tool is just one more entry in the taxonomy.
*
* Mirrors mcp-server/src/tools/custody.ts for file structure (small,
* single-tool file, PicaClient constructor injection, withBillingGate) AND
* for the confirm-gate precedent — pica_custody_reconcile's `confirm`
* boolean (executes only when confirm === true). Here the gate is a
* refusal rather than a dry-run: refuse client-side, before any SDK call,
* telling the agent to ask the user first, when confirm !== true.
*/
import { ApiError } from "@withpica/mcp-sdk";
import { formatSuccess, withBillingGate } from "@withpica/mcp-utils";
/** Mirrors lib/services/integrity-floor.ts IntegrityGapCode (mcp<->lib isolation — duplicated by hand). */
const GAP_CODES = [
"WORK_NO_RECORDING",
"RECORDING_NO_WORK",
"RECORDING_NO_RELEASE",
"WORK_NO_WRITER",
"RECORDING_NO_OWNER",
"COLLABORATOR_NOT_INVITED",
"COLLABORATOR_NOT_ATTESTED",
"WORK_NO_MASTER",
"WORK_SPLITS_ABSENT",
"WORK_SPLITS_INVALID",
];
/**
* Mirrors lib/services/platform-waiver-codes.ts PLATFORM_WAIVER_CODES: a work
* that is deliberately not on a platform. The ONLY completeness fields that
* may be waived — identifiers and registrations never are (the DB CHECK on
* integrity_gap_waivers.gap_code, mig 20260929163712, refuses them too).
*/
const PLATFORM_CODES = [
"PLATFORM_SPOTIFY",
"PLATFORM_YOUTUBE",
"PLATFORM_APPLE_MUSIC",
];
const PLATFORM_KINDS = ["not_applicable", "restricted"];
const PLATFORM_CODE_SET = new Set(PLATFORM_CODES);
const PLATFORM_KIND_SET = new Set(PLATFORM_KINDS);
const ALL_CODES = [...GAP_CODES, ...PLATFORM_CODES];
const GAP_CODE_SET = new Set(ALL_CODES);
const NEVER_WAIVABLE = "identifiers and registrations (ISWC, ISRC, IPI, society registrations) can never be waived";
/** Mirrors lib/services/integrity-floor.ts IntegrityEntityType. */
const ENTITY_TYPES = ["work", "recording", "release", "person"];
const ENTITY_TYPE_SET = new Set(ENTITY_TYPES);
function refusal(text) {
return { content: [{ type: "text", text }], isError: true };
}
export class IntegrityTools {
pica;
constructor(pica) {
this.pica = pica;
}
getTools() {
return [
{
definition: {
name: "pica_integrity_waive",
scope: "write:catalog",
audiences: ["stdio", "http", "public"],
tier: "write",
description: "waive a registration-integrity gap the user has confirmed is intentional or unresolvable " +
"(cover writer, deceased collaborator, unreleased demo), OR mark a work not applicable / " +
"restricted on a platform it is deliberately not on (PLATFORM_SPOTIFY, PLATFORM_YOUTUBE, " +
"PLATFORM_APPLE_MUSIC with kind + reason — e.g. no YouTube by design, a licence rules out " +
"Content ID); the platform then drops out of the work's completeness instead of counting as " +
"missing. Platform waivers need an org owner or admin. Identifiers and registrations (ISWC, " +
"ISRC, IPI, society registrations) can NEVER be waived. REQUIRES explicit " +
"user confirmation in conversation — set confirm: true only after the user has agreed. " +
"Waivers are reversible (pass revoke_waiver_id to undo).",
workflows: "infrastructure",
vernacular_kind: "specialist",
vernacular_reason: "Escape-hatch invoked by exact name only after explicit in-conversation user confirmation — " +
"not a creator-vernacular entry point.",
inputSchema: {
type: "object",
properties: {
gap_code: {
type: "string",
enum: [...ALL_CODES],
description: "The gap code to waive, from an EntityAssessment's gaps[].code (e.g. the integrity block " +
"on a pica_works_create/update response), or a PLATFORM_* code for a platform the work " +
"is deliberately not on (entity_type work, kind required). Identifiers can never be " +
"waived. Ignored when revoke_waiver_id is set.",
},
entity_type: {
type: "string",
enum: [...ENTITY_TYPES],
description: "The gap's entity type. Ignored when revoke_waiver_id is set.",
},
entity_id: {
type: "string",
description: "UUID of the entity carrying the gap. Ignored when revoke_waiver_id is set.",
},
reason: {
type: "string",
description: "Why this gap is intentional or unresolvable (e.g. 'cover recording — no original writer " +
"credit applies here'). Required to waive; must be non-empty. Ignored when revoke_waiver_id is set.",
},
kind: {
type: "string",
enum: [...PLATFORM_KINDS],
description: "Platform codes only, and required for them: not_applicable (the work is not meant to " +
"be on that platform) or restricted (a licence or agreement rules it out). Ignored " +
"for integrity gap codes and when revoke_waiver_id is set.",
},
confirm: {
type: "boolean",
description: "Required. Set true only after the user has explicitly agreed, in conversation, to waive " +
"(or revoke) this integrity gap.",
},
revoke_waiver_id: {
type: "string",
description: "When set, revokes this existing waiver instead of creating a new one — the gap becomes " +
"active again on the next assessment.",
},
},
required: ["confirm"],
},
annotations: {
title: "waive an integrity gap",
destructiveHint: false,
idempotentHint: false,
},
},
executor: withBillingGate(this.pica, "integrity waive", this.waive.bind(this)),
},
];
}
async waive(args) {
// Confirm-gate first, before any validation or SDK call — mirrors
// pica_custody_reconcile's confirm gate (custody.ts): no partial
// validation work happens ahead of the human-in-the-loop check.
if (args.confirm !== true) {
return refusal("confirmation required — ask the user to explicitly agree, in conversation, before waiving " +
"(or revoking) this integrity gap, then re-call with confirm: true.");
}
const revokeId = args.revoke_waiver_id;
if (typeof revokeId === "string" && revokeId.length > 0) {
const revoked = await this.pica.integrity.revokeWaiver(revokeId);
// A platform waiver answers with the work's re-scored completeness.
const score = revoked?.completeness?.score;
if (revoked?.completeness) {
return formatSuccess(`Waiver ${revokeId} revoked — the platform counts again in the work's completeness` +
`${typeof score === "number" ? ` (score now ${score})` : ""}.`, {
revoked: true,
waiver_id: revokeId,
completeness: revoked.completeness,
});
}
return formatSuccess(`Waiver ${revokeId} revoked — the gap is active again and will reappear on the next assessment.`, { revoked: true, waiver_id: revokeId });
}
if (typeof args.gap_code !== "string" || !GAP_CODE_SET.has(args.gap_code)) {
return refusal(`gap_code must be one of: ${ALL_CODES.join(", ")}. ${NEVER_WAIVABLE}.`);
}
const isPlatform = PLATFORM_CODE_SET.has(args.gap_code);
if (isPlatform && args.entity_type !== "work") {
return refusal("a platform waiver applies to a work: entity_type must be work.");
}
if (isPlatform &&
(typeof args.kind !== "string" || !PLATFORM_KIND_SET.has(args.kind))) {
return refusal(`kind is required for a platform code: ${PLATFORM_KINDS.join(" or ")}.`);
}
if (typeof args.entity_type !== "string" ||
!ENTITY_TYPE_SET.has(args.entity_type)) {
return refusal(`entity_type must be one of: ${ENTITY_TYPES.join(", ")}.`);
}
if (typeof args.entity_id !== "string" || args.entity_id.length === 0) {
return refusal("entity_id is required.");
}
if (typeof args.reason !== "string" || args.reason.trim().length === 0) {
return refusal("reason is required — explain why this gap is intentional or unresolvable.");
}
try {
const data = await this.pica.integrity.waive({
gap_code: args.gap_code,
entity_type: args.entity_type,
entity_id: args.entity_id,
reason: args.reason,
...(isPlatform ? { kind: args.kind } : {}),
});
if (isPlatform) {
const score = data.completeness?.score;
return formatSuccess(`Marked ${args.gap_code} ${args.kind} on this work — it no longer counts against ` +
`completeness${typeof score === "number" ? ` (score now ${score})` : ""}. Reversible — pass ` +
`revoke_waiver_id: "${data.id}" to undo.`, data);
}
return formatSuccess(`Waived ${args.gap_code} on this ${args.entity_type}. Reversible — pass ` +
`revoke_waiver_id: "${data.id}" to undo.`, data);
}
catch (err) {
// Duplicate-active-waiver surfaces as a plain ApiError with status
// 409 (not a typed exception on the SDK side) — treat it as a
// helpful outcome rather than an opaque failure: the gap is already
// waived, which is what the caller wanted. The route's 409 body
// (embedded in ApiError.message — the SDK throws
// "API request failed: 409 <body>", duplicates.ts precedent) carries
// error.details.existing_waiver_id so the advice is actionable, not
// a dead end: the agent could not otherwise obtain the id to revoke.
if (err instanceof ApiError && err.status === 409) {
const existingWaiverId = extractExistingWaiverId(err.message);
return formatSuccess(existingWaiverId
? `This gap is already waived (waiver ${existingWaiverId}) — nothing to do. Pass ` +
`revoke_waiver_id: "${existingWaiverId}" to undo it if you want the gap active again.`
: `This gap is already waived — nothing to do. To undo it, find the waiver via the ` +
`integrity block on the entity's next write response, then pass its id as revoke_waiver_id.`, {
already_waived: true,
gap_code: args.gap_code,
entity_type: args.entity_type,
entity_id: args.entity_id,
...(existingWaiverId
? { existing_waiver_id: existingWaiverId }
: {}),
});
}
throw err;
}
}
}
/**
* Pull `error.details.existing_waiver_id` out of the JSON body the SDK
* embeds in ApiError.message ("API request failed: 409 {...}"). The body
* is PICA's errorResponse shape: { success: false, error: { code, message,
* details } }. Returns null when the body is missing, unparseable, or has
* no id — the caller falls back to softer copy rather than failing.
*/
function extractExistingWaiverId(message) {
const first = message.indexOf("{");
const last = message.lastIndexOf("}");
if (first === -1 || last <= first)
return null;
try {
const body = JSON.parse(message.slice(first, last + 1));
const id = body?.error?.details?.existing_waiver_id;
return typeof id === "string" && id.length > 0 ? id : null;
}
catch {
return null;
}
}
//# sourceMappingURL=integrity.js.map