DEV Community

Martin Pisacreta
Martin Pisacreta

Posted on Fully Autonomous

An MCP Assistant That Checks Before Repeating a Payment Reminder

An unpaid invoice does not tell an assistant whether someone has already followed up. If the assistant sorts only by due date, it can recommend contacting the same customer again while another overdue invoice has received no reminder.

A recorded Claude Code session on November 3 demonstrates how reminder history changes that recommendation. The client connected to the PayPal sandbox MCP server, read two fictional invoices, retrieved a local reminder history, and requested a deterministic priority explanation. The older invoice moved behind the newer one because the supplied snapshot assigned it one previous reminder.

The session produced a recommendation. The model did not approve or send a reminder.

What the invoice tool returned

list_unpaid_invoices returned two invoices with status UNPAID, each showing an outstanding amount of 10.00 USD. One was due October 24; the other was due October 28. I will call them the older and newer invoice, respectively, without reproducing recipient details.

Both included the note: “Fictional sandbox validation invoice. No real goods or payment required.” These were demonstration records, not evidence of real customers owing money.

The response reported scannedCount: 3, returnedCount: 2, and pagination scoped to single_page, with hasMore: false. That describes this response; it does not establish a general guarantee about complete account history.

The list supplied dates, amounts, and invoice statuses. It did not supply a verified count of previous reminders. That context came from a separate history lookup and the snapshot subsequently passed to the priority tool.

Reading history without authorizing another action

get_reminder_history accepted a proposal ID. Its response explicitly identified its source: “Local SQLite snapshot only; no PayPal API request.”

For the older invoice, it returned four recorded workflow stages: proposal, approval, execution attempt, and result. All were dated October 25. The approval recorded approved_via as local-terminal; the execution attempt recorded state as succeeded.

The result explained exactly what that success meant: “The local executor recorded a successful sender return. Recipient delivery remains unverified.”

That distinction matters. A successful local sender return is evidence about execution, not proof that a person received or read a message. The response also warned that current PayPal invoice status, payment status, and remote reminder history were not checked by this lookup.

Its effects reported no persistence, approval, execution, or API call. Reading the history did not repeat the historical operation. The reconciliation section also returned resendAllowed: false.

There is another useful detail: the stored proposal still contained status: pending. The history response explicitly warned that this draft field did not override the execution record. Looking at the proposal alone would therefore miss relevant workflow context.

Reading, approving, and sending are separate steps

The recorded history makes those boundaries visible. A proposal captures a proposed reminder. An approval is a separate record. An execution attempt has its own start, completion, and state. The result describes what the executor recorded.

The current AI session inspected those records rather than performing those steps again. Its connected PayPal tool list contained invoice reading, dispute reading, reminder-history reading, and priority explanation. It did not expose an approval or sending tool to this client.

The source reinforces the explanation boundary. In src/mcp-tools.ts, the description of explain_invoice_priority says: “Does not call APIs, persist proposals, approve or send reminders.”

The explanation response included a reminder draft with requiresHumanApproval: true, while its effects were persisted: false, approved: false, and sent: false. Generating wording did not create authorization. The history response likewise stated: “A historical approval does not establish current permission to execute.”

Why the order changed

The client called explain_invoice_priority for each invoice using the same two-invoice snapshot. It supplied one previous reminder for the older invoice and zero for the newer one, equal outstanding amounts, and openDispute: false for both.

The comparison used an explicit now value of 2026-11-03T12:00:00Z. That is the supplied evaluation time, not a claim about the actual moment of execution. Both invoices were classified as overdue under that input.

The older invoice's response contained this reason: “Previous reminders: 1. Within an urgency category, fewer prior reminders rank first.”

Its contextual position was 2, compared with position 1 under date-only ordering. The newer invoice received contextual position 1. Because both shared the urgency category, reminder count decided their relative order before outstanding amount or due date could break a tie.

The relevant comparator is in src/priorities.ts:

  const contextual = [...invoices].sort((a, b) => {
    const reviewOrder = Number(needsReview(a)) - Number(needsReview(b));
    if (reviewOrder) return reviewOrder;
    const urgencyOrder = rank[classification.get(a.id)!.priority] - rank[classification.get(b.id)!.priority];
    if (urgencyOrder) return urgencyOrder;
    const reminderOrder = a.previousReminders - b.previousReminders;
    if (reminderOrder) return reminderOrder;
    const aAmount = amounts.get(a.id)!;
    const bAmount = amounts.get(b.id)!;
    if (aAmount !== bAmount) return aAmount > bAmount ? -1 : 1;
    return explanationDateOrder(a, b);
  });
Enter fullscreen mode Exit fullscreen mode

The subtraction of reminder counts puts zero before one. It runs after manual-review ordering and urgency, so fewer reminders do not universally override every other concern. The policy also routes invoices with an open dispute or missing deadline into manual review after the other invoices.

The response describes this as a deterministic heuristic. The calling AI client writes the final explanation and reminder wording; the comparator supplies the declared ordering.

What this demonstration does not prove

The priority response labels its source: “Caller-supplied snapshot; not verified against PayPal.” It does not fetch reminder history itself or independently validate the client's counts.

The retrieved local history supports a recorded prior execution for the older invoice. The zero assigned to the newer invoice is an input in the transcript, not a separately demonstrated exhaustive history search. Likewise, the supplied dispute flags should not be presented as independently verified facts.

This rule changes which invoice comes first. It does not permanently exclude previously contacted invoices or implement a blanket duplicate-send prohibition. The older invoice remained second in the queue. Any actual follow-up would still require a separate decision and authorization.

The demonstrated outcome is narrower and useful: with explicit reminder context, an assistant can recommend reviewing the untouched invoice first instead of repeatedly prioritizing the oldest debt. In this session, no new reminder was approved or sent, and recipient delivery of the historical reminder remains unverified.

Source code: paypal-ops-agent. A sanitized transcript of this session is in the repository: evidence/ai-client-session-20261103/TRANSCRIPT.md.

If you want an MCP server that lets an AI assistant use your own API, I offer a fixed-scope service on Upwork: https://www.upwork.com/services/product/2113736906027325062 .

Top comments (0)