UNPKG

@withpica/mcp-server

Version:

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

140 lines • 7.96 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * Approvals Tools — the read-only poll for an out-of-band human approval. * * When a delegated agent's destructive write is deferred for human approval, * the dispatcher returns a `human_approval_required` envelope carrying an * `approval_reference`. `pica_approval_status` is the tool that reference * points the agent at: it polls the approval row by id and reports whether the * owner has decided. Read-only; ownership is enforced server-side by * `/api/approvals/{id}/status` (org match for keys, grant match for grant * tokens) — a foreign approval comes back as `not_found`, never a leak. */ import { EntityNotFoundError } from "@withpica/mcp-sdk"; import { formatSuccess } from "@withpica/mcp-utils"; import { applyArgAliases } from "./arg-aliases.js"; export class ApprovalsTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ { definition: { name: "pica_approval_status", scope: "always", audiences: ["stdio", "http"], tier: "read", description: "Check whether a pending human approval has been decided. Returns " + "the current status of the approval by id — pending, approved, " + "denied, expired, executed, or failed — plus when and how it was " + "decided. Use this only when an earlier write returned a " + "human_approval_required envelope; pass the approval_reference from " + "that envelope as approval_id. While status is pending, nothing has " + "executed yet; poll again shortly. For a batched pica_merge_duplicates " + "approval that has run, the result also carries summary + items: one " + "outcome per pair (merged / skipped / failed, with the reason) — status " + "is failed when any pair failed, even though the others ran.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Polled by an agent after a write was deferred for out-of-band human approval; not a creator-vernacular entry point.", inputSchema: { type: "object", properties: { approval_id: { type: "string", description: "The approval reference returned in the human_approval_required envelope (its approval_reference field).", }, }, required: ["approval_id"], }, outputSchema: { type: "object", properties: { status: { type: "string", description: "pending | approved | denied | expired | executed | failed | not_found", }, decided_at: { type: "string", description: "ISO timestamp the owner decided; null while still pending.", }, decided_via: { type: "string", description: "Channel the decision arrived through (email | telegram); null while pending.", }, expires_at: { type: "string", description: "ISO timestamp the pending request expires.", }, summary: { type: "object", description: "Batched merge approvals only, once run: counts of merged / skipped / failed pairs.", }, items: { type: "array", description: "Batched merge approvals only, once run: each pair's outcome (merged | skipped | failed) with the reason for a skip or failure.", }, }, }, }, executor: this.getApprovalStatus.bind(this), }, ]; } /** * Poll a single approval's status. A not-found / foreign approval surfaces * as a clean `{ status: "not_found" }` (the route returns 404 → the SDK * throws EntityNotFoundError) rather than a hard error, so an agent can poll * an unknown reference without crashing its loop. */ async getApprovalStatus(args) { // The deferral envelope names this value `approval_reference`, so accept // that spelling on a DIRECT call too. `pica_execute` applies the same // alias before dispatch; resolving it here through the one alias table // (`TOOL_ARG_ALIASES.pica_approval_status`) keeps direct and wrapped calls // agreeing. Without it a direct `{approval_reference}` fell into the // branch below and was answered `status: "not_found"` for an approval // that exists. Pinned by `execute-approval-reference.test.ts`. const { args: resolved } = applyArgAliases("pica_approval_status", args, [ "approval_id", ]); const approvalId = typeof resolved.approval_id === "string" ? resolved.approval_id.trim() : ""; if (!approvalId) { // ⚠️ An agent lands here when a person points at the "N awaiting // approval" figure on /inspect and asks it to clear them. There is no // list tool by DESIGN — `/api/admin/agent-approvals` calls // `assertNotGrantAuth()`, so an agent grant cannot enumerate the // approvals it is itself waiting on. That separation is the point of an // out-of-band human gate, not an oversight. // // "approval_id is required" left the agent nowhere to go, so it searched // pica_discover, tried the admin-gated audit tools, and finally told the // user it could not explain the number (ops issue 400fddc3, // 2026-09-10). Explaining the gate costs nothing and does not open it. return formatSuccess("approval_id is required — this tool checks ONE approval you were already handed a reference for.", { status: "not_found", why: "Approvals are decided by a person out of band. An agent grant deliberately cannot list them, so there is no way for you to enumerate or clear the dashboard's 'N awaiting approval' count.", what_the_dashboard_count_means: "Writes an agent attempted that were deferred for a human decision. Each was returned to the agent that raised it as a human_approval_required envelope carrying an approval_reference.", tell_the_user: "Only they can action these, signed in, from /inspect. The count excludes expired requests — an approval past its deadline is no longer actionable.", retry_eligible: false, }); } try { const result = await this.pica.approvals.getStatus(approvalId); return formatSuccess(`Approval status: ${result.status}`, result); } catch (err) { if (err instanceof EntityNotFoundError) { return formatSuccess("no approval found with that reference", { status: "not_found", }); } throw err; } } } //# sourceMappingURL=approvals.js.map