Stuck Order
БесплатноНе проверенMCP server for investigating payment/webhook drift, classifying order status mismatches, detecting duplicate charges, and escalating findings for human review.
Описание
MCP server for investigating payment/webhook drift, classifying order status mismatches, detecting duplicate charges, and escalating findings for human review. It is read-only for payment state and does not automatically retry or correct transactions.
README
A remotely hosted MCP server that lets an ops engineer investigate, classify, and triage drift between an internal order/payment record and a payment provider's ground truth — and escalate confirmed drift for human review. It does not retry, void, capture, or refund anything automatically; every correction is applied by a person, outside this MCP.
Why this exists
Commerce backends keep two records of the same payment: the internal order/payment state, and the provider's ground truth, delivered via webhooks. These drift apart in practice — webhooks arrive late, get missed, or arrive out of order — so an order can end up marked unpaid when the provider actually captured funds, or paid when no capture ever completed. Today an ops engineer manually cross-references the internal DB, the provider dashboard, and raw webhook logs to spot this. This MCP encapsulates that investigation and classification logic directly, rather than exposing plain CRUD over the tables.
Scope
In: single-order investigation, fleet-wide drift triage (including cross-order duplicate-charge detection), an idempotent human-review escalation queue, a bearer-token gate, remote hosting on Vercel + Neon Postgres, synthetic seeded data.
Out (by design, not oversight): no frontend, no real payment-provider integration (provider data is simulated via seed data), no multi-tenant support, no accounts/login system (bearer token only — see Assumptions), and no tool that mutates payment/order state. This is the direction confirmed by the team: diagnosis and evidence gathering are in scope, automatic correction is not.
Architecture
- Next.js 16 App Router, single route:
app/api/mcp/route.ts. mcp-handlerv2 (createMcpHandler) +experimental_withMcpAuthfor a bearer-token gate (Authorization: Bearer <MCP_BEARER_TOKEN>).- Neon Postgres via
@neondatabase/serverless(HTTP-based driver, avoids connection-pool exhaustion in serverless functions). - The diagnostic decisions live in Postgres, not in application code.
order_diagnoses(a view defined inschema.sql) computes the 5-way drift classification (clean,missed_capture,missed_refund,false_capture,false_refund) directly via aCASEexpression overorders,webhook_events, andprovider_charges, and pre-shapes the JSON bothget_order_snapshotandscan_fleet_for_driftreturn. Both toolsSELECTfrom this one view rather than each re-deriving the classification, and rather than pulling raw rows into the app to classify in TypeScript. - Duplicate-charge detection is the same story:
lib/queries.tsholds the raw, parameterized SQL (DUPLICATE_CANDIDATES_FOR_ORDER_SQLfor the single-order case,DUPLICATE_PAIRS_SINCE_SQLfor the fleet-wide self-join) as the one place the 10-minute rule is enforced. Every row either query returns is already a confirmed match — same customer/amount/currency, distinct orders, withinDUPLICATE_CHARGE_WINDOW_MS— so there's no re-verification step left in application code, only presentation (toDuplicateEvidence, inlib/reconciliation.ts) and pair-level dedup. - Because the diagnostic and duplicate-matching decisions now live in SQL, they can't be unit-tested as pure functions anymore — verifying them requires a real Postgres. See "Automated tests" below for how that's done without depending on the live deployed database.
lib/reconciliation.tskeeps only what's still genuinely pure: shared types, theDUPLICATE_CHARGE_WINDOW_MSconstant (imported by both the app and, transitively, the SQL call sites, so there's exactly one number rather than independently hardcoded copies), evidence formatting (toDuplicateEvidence), and idempotency-key construction (buildIdempotencyKey) — none of which is a diagnostic decision, so these stay unit-tested.lib/queries.tsexists specifically so the duplicate-charge SQL text is identical between production (executed via the Neon driver'ssql.query(text, params)) and the integration test suite (executed viapgagainst a local Postgres) — one string, two drivers, rather than the test suite re-typing an equivalent query that could silently drift from what actually runs in production.
Data model
Five tables (schema.sql):
orders— internal order/payment record (status:unpaid | paid | refunded).webhook_events— what our system actually recorded from provider webhooks. Can be missing an event even when the provider-side fact exists.provider_charges— the provider's ground truth, independent of whether a webhook for it was ever processed.reconciliation_actions— the human-review escalation queue, holding only the current state of each escalation (one row peridempotency_key, overwritten in place on re-escalation). Nothing here executes a payment operation.idempotency_keyis unique, so re-scanning and re-escalating the same drift updates the existing row instead of duplicating it.reconciliation_action_history— append-only, never updated or deleted. Beforeescalate_for_reviewreopens a previously-acknowledged escalation (overwritingstatus/acknowledged_at/evidenceon the row above), it archives that acknowledge+reopen cycle here first, so the fact that an escalation was acknowledged before — when, on what evidence, and when it was reopened — is never lost to a later re-diagnosis. Seeget_escalation_history.
These two tables are the only ones the MCP ever writes to — nothing in either executes a payment operation; both are pure record-keeping around the human-review process.
seed.sql seeds 7 orders covering 5 distinct drift types plus one clean
control case (see comments in the file for the exact scenario each order
represents): missed_capture, missed_refund, false_capture,
false_refund, and a duplicate_charge pair (two orders, same
customer/amount/currency, provider charges 5 minutes apart).
schema.sql also defines order_diagnoses, a view over these four tables
that computes the drift diagnosis and pre-shapes the JSON the MCP tools
return — see Architecture above.
MCP tools
| Tool | Type | Description |
|---|---|---|
get_order_snapshot |
read-only | Investigate one order: internal record, webhook events, provider charges, and a computed diagnosis (clean, missed_capture, missed_refund, false_capture, or false_refund). Does not check for duplicate charges — use check_duplicate_charge for that. |
check_duplicate_charge |
read-only | Given one order_id, checks whether its successful charge matches another order's charge (same customer, amount, currency, within 10 minutes) and reports whether a duplicate was found. Built for the "customer says they were charged twice" support workflow, where the answer needs to be a direct yes/no for one order rather than a full investigation. |
scan_fleet_for_drift |
read-only | Triage orders from the last window_hours (default 24) for drift, and cross-reference provider charges for possible duplicate successful charges (same customer, amount, and currency within 10 minutes). Duplicate-charge detection always covers the full window regardless of limit, so a real pair can never be split across a row-count cutoff. |
escalate_for_review |
write (idempotent) | Upserts a human-review record for a diagnosed drift. Never touches orders or provider_charges. |
list_open_escalations |
read-only | Shows the current human-review queue, most recent first, capped by an optional limit (default 100). |
acknowledge_escalation |
write (idempotent) | Marks an open escalation acknowledged once a human has resolved it out-of-band, stamping acknowledged_at. Re-acknowledging an already-acknowledged escalation is a no-op — it returns the existing record instead of moving acknowledged_at again. Never touches orders or provider_charges. |
get_escalation_history |
read-only | Shows every past acknowledge+reopen cycle for an escalation from reconciliation_action_history — when it was acknowledged, on what evidence, and when it was reopened by a later re-diagnosis. |
Setup
- Create a Neon Postgres database and copy its connection string.
- Copy
.env.exampleto.env.localand fill inDATABASE_URLandMCP_BEARER_TOKEN(any long random string you control). - Apply the schema and seed data:
psql "$DATABASE_URL" -f schema.sql psql "$DATABASE_URL" -f seed.sql npm installnpm run dev(local) or deploy to Vercel with the same two environment variables set in the project settings.- For the automated test suite, Docker is required (
npm testbrings up its own throwaway Postgres — see Automated tests below). No Neon credential is needed to run tests.
Connecting from Claude Code
Register the deployed server as a remote HTTP MCP server with a bearer-token header:
claude mcp add --transport http stuck-order-mcp \
"https://stuck-order-mcp.vercel.app/api/mcp" \
--header "Authorization: Bearer $MCP_BEARER_TOKEN"
(swap the URL for http://localhost:3000/api/mcp to point at a local
npm run dev instance instead).
--scope local(the default) keeps the server private to you in this project; use--scope projectto check it into.mcp.jsonfor collaborators, or--scope userto make it available across all projects.- Verify the connection with
claude mcp get stuck-order-mcp— it should reportStatus: ✔ Connected. - MCP servers are loaded when a Claude Code session starts, so if you added it mid-session, start a new session (or restart) before its tools show up.
- To remove it:
claude mcp remove stuck-order-mcp -s local.
Once connected, ask Claude Code to investigate an order (e.g. "check order ord_missed_capture for drift") or triage the fleet ("scan for drift in the last 24 hours") — see MCP tools below for the full list and Verifying the workflow end to end for the exact scenario each seeded order represents.
Verifying the workflow end to end
Against the deployed URL (or http://localhost:3000/api/mcp locally), using
any MCP-compatible client, or directly with curl against the Streamable
HTTP endpoint:
curl -X POST "$MCP_URL/api/mcp" \
-H "Authorization: Bearer $MCP_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_order_snapshot","arguments":{"order_id":"ord_missed_capture"}}}'
Expected walkthrough:
get_order_snapshotonord_missed_capturereturns diagnosismissed_capture.scan_fleet_for_drift(defaultwindow_hours: 24) returns findings forord_missed_capture,ord_missed_refund,ord_false_capture,ord_false_refund, and theord_dup_1/ord_dup_2duplicate-charge pair — but nothing forord_clean. All seeded orders are timestamped within the last 20 hours specifically so they're visible under the default window without needing a widerwindow_hoursvalue.check_duplicate_chargeonord_dup_1returnsduplicate_found: trueagainstord_dup_2— the "customer says they were charged twice" workflow, answered directly for one order without a full fleet scan. It fetches candidate charges pre-filtered to the same customer/amount/ currency within a wide window and checks the 10-minute rule directly against them, so it doesn't need the fleet-wide scan either.get_order_snapshoton the same order returns its diagnosis only — duplicate-charge checking is deliberately not duplicated into that tool, sincecheck_duplicate_chargealready exists for it.escalate_for_reviewwith that evidence creates an open record; calling it again with the samedrift_type+order_idsupdates the same row rather than creating a second one (checklist_open_escalationsbefore and after — the count doesn't change on the second call).list_open_escalationsshows the escalation, closing the loop: diagnose → escalate → visible to ops.acknowledge_escalationwith that escalation'sidmarks itacknowledgedand stampsacknowledged_at; it then disappears fromlist_open_escalations(which only showsopenrecords). Callingacknowledge_escalationagain with the same id returns the existing record (already_acknowledged: true) instead of erroring or movingacknowledged_ata second time.- If the same drift is diagnosed again later, calling
escalate_for_reviewwith the samedrift_type+order_idsreopens the acknowledged record (back toopen,acknowledged_atcleared) rather than leaving it invisible inlist_open_escalations— and archives the acknowledge+reopen cycle being overwritten intoreconciliation_action_historyfirst, soget_escalation_historyon that escalation'sidstill shows when it was acknowledged, on what evidence, and when it was reopened, even thoughreconciliation_actionsitself has already moved on to its new state.
Automated tests
npm test
This brings up a throwaway Postgres via docker-compose.test.yml, runs the
integration suite against it, runs the unit suite, then tears the container
down — self-contained, no live/deployed database credential required. (Docker
must be available; npm run test:db:up / npm run test:db:down manage the
container directly if you want it to stay up between runs, and
npm run test:integration / npm run test:unit run either suite alone.)
Integration tests (lib/reconciliation.integration.test.ts, run against
real Postgres via pg) are now the primary coverage, because the
diagnostic and duplicate-matching decisions live in SQL (see Architecture)
and can't be exercised as pure functions anymore. They apply schema.sql +
seed.sql plus a few extra boundary fixtures, then assert: all 5 drift types
classify correctly via order_diagnoses (plus the clean case), evidence
JSON references the right underlying charge, the duplicate-charge queries
find the seeded pair, a pair exactly 10 minutes apart still matches
(confirming the boundary is inclusive), a pair 10 minutes and 1 second apart
does not, and a currency mismatch never matches regardless of timing. Because
lib/queries.ts is imported directly rather than re-typed, these tests are
verifying the literal SQL text that runs in production, not an equivalent
reimplementation of it.
Unit tests (lib/reconciliation.test.ts) now only cover what's still
pure: toDuplicateEvidence's formatting, and that buildIdempotencyKey is
order-independent, which is what guarantees the database-level upsert
actually dedupes repeated escalations.
Key assumptions
- The provider is simulated via seeded data rather than a real Stripe test account, to keep this self-contained and synthetic.
- No accounts/user-management system: access is gated by a single shared bearer token, not per-user login. There is one actor (the ops engineer), so a single permission level is sufficient.
reconciliation_actionsrecords an escalation, never a correction — applying the actual fix (issuing a refund, re-triggering a capture, etc.) happens outside this system, by a human, using the provider's own tools.- Duplicate-charge detection matches on customer + amount + currency within
a 10-minute window.
scan_fleet_for_driftfinds it fleet-wide via a single indexed self-join directly in Postgres (idx_provider_charges_dup_lookup), independently of thelimit-bounded per-order diagnosis pass — the independence matters because a row-count cutoff, unlike a time-based one, can silently split a real duplicate pair across the boundary. Only rows that already match come back from the database; charges with no match are never pulled into app memory at all.check_duplicate_chargefinds it for one order the same way, by fetching only the charges that could plausibly match it, rather than requiring a fleet-wide scan —get_order_snapshotdeliberately doesn't duplicate that check, since a dedicated tool for it already exists. Both queries are a complete, exact implementation of the rule (not a wide prefilter re-verified in code) — see Architecture. - The automated test suite never runs against the live/deployed Neon
database. It uses a disposable local Postgres via
docker-compose.test.ymlinstead, so running tests never requires — or risks — a shared production credential.
Remaining risks / explicitly out of scope
- No pagination on
scan_fleet_for_drift's per-order diagnosis pass or onlist_open_escalationsbeyond a simplelimit/window_hours— fine at this data volume, would need it at real scale. - Putting the classification decision in a SQL view trades away pure-function
unit testability for it (see Architecture) — that trade was made
deliberately, but it does mean the
order_diagnosesCASE logic can now only be verified with a real Postgres connection (the integration suite), not with a fast, dependency-free unit test. acknowledge_escalationonly has two states (open→acknowledged); there is no "reopen" transition and no notion of who acknowledged it (no per-user auth exists to attribute it to — see Key assumptions). Fine for a single shared ops actor; would need an identity if this grew multi-user.
Установка Stuck Order
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/SafalBhandari12/stuck-order-mcpFAQ
Stuck Order MCP бесплатный?
Да, Stuck Order MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Stuck Order?
Нет, Stuck Order работает без API-ключей и переменных окружения.
Stuck Order — hosted или self-hosted?
Доступен hosted-вариант: Unyly запускает сервер в облаке, локальная установка не обязательна.
Как установить Stuck Order в Claude Desktop, Claude Code или Cursor?
Открой Stuck Order на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
Stripe
Payments, customers, subscriptions
автор: Stripemalamutemayhem/unclick-agent-native-endpoints
110+ tools for AI agents spanning social media, finance, gaming, music, AU-specific services, and utilities. Zero-config local tools plus platform connectors. n
автор: malamutemayhemwhiteknightonhorse/APIbase
Unified API hub for AI agents with 56+ tools across travel (Amadeus, Sabre), prediction markets (Polymarket), crypto, and weather. Pay-per-call via x402 micropa
автор: whiteknightonhorsetrackerfitness729-jpg/sitelauncher-mcp-server
Deploy live HTTPS websites in seconds. Instant subdomains ($1 USDC) or custom .xyz domains ($10 USDC) on Base chain. Templates for crypto tokens and AI agent pr
Compare Stuck Order with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории finance
