UNPKG

@withpica/mcp-server

Version:

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

252 lines • 13.7 kB
// 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) 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 } 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: 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