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