mercury_update_invoice
Update an existing invoice by passing only the fields you want to change. Merge your changes with the current invoice to amend line items, due date, memo, or PO number before payment.
Instructions
Update an existing invoice. Pass only the fields you want to change.
USE WHEN: amending an outstanding invoice (line items, due date, memo, PO number) before the customer pays. The MCP fetches the current invoice and merges your changes before submitting — Mercury's update endpoint requires the full payload despite the API docs implying PATCH.
DO NOT USE: to cancel an invoice (use mercury_cancel_invoice). To change the customer or the destination account, cancel + recreate. Once an invoice is paid, updates are likely rejected by Mercury — fetch first to confirm status.
SIDE EFFECTS: overwrites the invoice on Mercury's side. The customer-facing payment URL stays the same. If the invoice was already emailed, the customer is NOT re-notified of the change — communicate the change out-of-band if needed.
RETURNS: { id, status, amount, ... } — the updated invoice.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| invoiceId | Yes | Invoice ID | |
| invoiceDate | No | Invoice date (YYYY-MM-DD) | |
| dueDate | No | Due date (YYYY-MM-DD) | |
| lineItems | No | ||
| ccEmails | No | ||
| payerMemo | No | ||
| internalNote | No | ||
| poNumber | No | ||
| invoiceNumber | No |
Implementation Reference
- src/tools/invoices.ts:167-190 (handler)The handler function for mercury_update_invoice. It fetches the current invoice, strips read-only fields (id, slug, status, amount, createdAt, updatedAt), merges user changes on top, and POSTs the merged payload to Mercury's update endpoint.
async ({ invoiceId, ...changes }) => { const current = await client.get<Record<string, unknown>>(`/ar/invoices/${invoiceId}`); // Strip read-only fields Mercury rejects in the update payload const { id: _index, slug: _s, status: _st, amount: _a, createdAt: _c, updatedAt: _u, ...editable } = current; const merged: Record<string, unknown> = { ...editable }; for (const [k, v] of Object.entries(changes)) { // Zod-validated args never carry undefined values (optional keys // are elided), so the false branch is a defensive guard rather // than a runtime path we can hit. /* v8 ignore next */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (v !== undefined) merged[k] = v; } const data = await client.post(`/ar/invoices/${invoiceId}`, merged); return textResult(data); }, - src/tools/invoices.ts:156-166 (schema)Input schema for mercury_update_invoice. Required: invoiceId (UUID). Optional: invoiceDate, dueDate, lineItems, ccEmails, payerMemo, internalNote, poNumber, invoiceNumber.
{ invoiceId: z.uuid().describe("Invoice ID"), invoiceDate: z.iso.date().optional().describe("Invoice date (YYYY-MM-DD)"), dueDate: z.iso.date().optional().describe("Due date (YYYY-MM-DD)"), lineItems: z.array(lineItemSchema).optional(), ccEmails: z.array(z.email()).optional(), payerMemo: z.string().optional(), internalNote: z.string().optional(), poNumber: z.string().optional(), invoiceNumber: z.string().optional(), }, - src/tools/invoices.ts:142-192 (registration)Registration of the mercury_update_invoice tool via defineTool (which calls server.registerTool) with name, description, schema, handler, and annotations including title='Update Invoice', destructiveHint=false, openWorldHint=true.
defineTool( server, "mercury_update_invoice", [ "Update an existing invoice. Pass only the fields you want to change.", "", "USE WHEN: amending an outstanding invoice (line items, due date, memo, PO number) before the customer pays. The MCP fetches the current invoice and merges your changes before submitting — Mercury's update endpoint requires the full payload despite the API docs implying PATCH.", "", "DO NOT USE: to cancel an invoice (use `mercury_cancel_invoice`). To change the customer or the destination account, cancel + recreate. Once an invoice is paid, updates are likely rejected by Mercury — fetch first to confirm status.", "", "SIDE EFFECTS: overwrites the invoice on Mercury's side. The customer-facing payment URL stays the same. If the invoice was already emailed, the customer is NOT re-notified of the change — communicate the change out-of-band if needed.", "", "RETURNS: `{ id, status, amount, ... }` — the updated invoice.", ].join("\n"), { invoiceId: z.uuid().describe("Invoice ID"), invoiceDate: z.iso.date().optional().describe("Invoice date (YYYY-MM-DD)"), dueDate: z.iso.date().optional().describe("Due date (YYYY-MM-DD)"), lineItems: z.array(lineItemSchema).optional(), ccEmails: z.array(z.email()).optional(), payerMemo: z.string().optional(), internalNote: z.string().optional(), poNumber: z.string().optional(), invoiceNumber: z.string().optional(), }, async ({ invoiceId, ...changes }) => { const current = await client.get<Record<string, unknown>>(`/ar/invoices/${invoiceId}`); // Strip read-only fields Mercury rejects in the update payload const { id: _index, slug: _s, status: _st, amount: _a, createdAt: _c, updatedAt: _u, ...editable } = current; const merged: Record<string, unknown> = { ...editable }; for (const [k, v] of Object.entries(changes)) { // Zod-validated args never carry undefined values (optional keys // are elided), so the false branch is a defensive guard rather // than a runtime path we can hit. /* v8 ignore next */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (v !== undefined) merged[k] = v; } const data = await client.post(`/ar/invoices/${invoiceId}`, merged); return textResult(data); }, { title: "Update Invoice", destructiveHint: false, openWorldHint: true }, ); - src/middleware.ts:46-55 (helper)Rate-limit bucket assignment: mercury_update_invoice is mapped to the 'invoices_write' bucket, which has daily=10 and monthly=200 limits as defined in DEFAULT_BUCKET_LIMITS (line 81). Shared with mercury_create_invoice.
const TOOL_BUCKET: Record<string, string> = { // Money out mercury_send_money: "payments", mercury_request_send_money: "payments", mercury_create_internal_transfer: "internal_transfer", // Invoicing mercury_create_invoice: "invoices_write", mercury_update_invoice: "invoices_write", mercury_cancel_invoice: "invoices_cancel", - src/tools/_shared.ts:28-57 (helper)The defineTool helper that wraps the handler with middleware (rate limiting, dry-run, audit) via wrapToolHandler, then registers it on the MCP server.
export function defineTool<S extends ZodRawShape>( server: McpServer, name: string, description: string, inputSchema: S, handler: (args: z.infer<z.ZodObject<S>>) => Promise<ToolResult>, annotations: ToolAnnotations, ): void { const wrapped = wrapToolHandler(name, handler); const strictSchema = z.object(inputSchema).strict(); // MCP behavioral annotations (readOnlyHint / destructiveHint / // idempotentHint / openWorldHint) — declared machine-readable so // hosts and rubrics (TDQS / Glama Behavior dimension) can detect // tool semantics without scraping the prose description. Required // (not optional) so every new tool ships with explicit semantics — // forgetting the annotation now fails typecheck instead of // silently shipping a tool with no hint set. // The MCP SDK overloads `registerTool` with shape narrowing the runtime // strict-schema and the wrapped callback can't satisfy through generics. // Both casts are runtime-safe — the signatures only diverge at the type // level. Asserted by the existing tool-registration tests. (server.registerTool as unknown as (...a: unknown[]) => unknown)( name, { description, inputSchema: strictSchema, annotations }, wrapped, ); } export { type ToolResult } from "../middleware.js"; export { type ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";