OpsDesk
by katareayush
README.md
# OpsDesk
A remote MCP server that lets a commerce operations agent investigate and resolve stuck orders — without escalating to engineering, and without being handed a database.
- **Hosted MCP endpoint:** `https://opsdesk-mcp.vercel.app/api/mcp`
- **Health check:** `https://opsdesk-mcp.vercel.app/api/health`
- **Source:** https://github.com/katareayush/opdesk-mcp
- **Demo keys:** `demo-agent-key` (ops agent), `demo-manager-key` (ops manager)
All data is synthetic. No real customers, no real payment data, no production credentials.
Reviewing this: [the workflow](#the-workflow) · [connect in one command](#connect-to-it) · [demo orders worth trying](#demo-orders) · [MCP design decisions](#mcp-design-decisions) · [safety model](#safety-model) · [scope and exclusions](#scope) · [limitations](#limitations--next-steps) · [AI worklog](./AI_WORKLOG.md)
---
## The problem
When a customer writes *"I paid three days ago and nothing has shipped"*, the answer lives in four systems: the order system knows what was bought, the payment processor knows whether money actually moved, the warehouse knows whether anyone was ever asked to pick it, and the carrier knows whether it left the building. No single one of them can tell you what went wrong.
So the ops agent escalates, and an engineer spends twenty minutes joining records by hand.
OpsDesk does that join server-side, names the failure, and offers only the fixes that are actually legal for that specific order — behind a confirmation step that a human has to pass.
## The workflow
```
find_stuck_orders → investigate_order → propose_remediation → [human says yes] → execute_remediation
↓
escalate_to_engineering
```
1. **`find_stuck_orders`** — ranked queue of what's actually broken, with money at risk and how long each customer has waited. Healthy orders are excluded by construction.
2. **`investigate_order`** — one merged chronological timeline across all four systems, a diagnosed failure class with the evidence behind it, and an availability check on every remediation *including why the unavailable ones are unavailable*.
3. **`propose_remediation`** — computes exactly what a fix would do (money, stock, resulting status, draft customer message) and issues a signed confirmation token. **Changes nothing.**
4. **`execute_remediation`** — the only tool that mutates anything. Requires the token plus an idempotency key.
5. **`escalate_to_engineering`** — packages the evidence bundle when nothing is legal or the diagnosis is `UNKNOWN_STALL`. Escalating well is a success path, not a failure.
Supporting: **`check_inventory`** (oversells and who's competing for the stock), **`get_audit_log`** (what was done, by whom, and what was refused).
Plus an MCP **resource** (`opsdesk://runbook` — the failure-class runbook and role limits) and a **prompt** (`morning_triage`).
---
## Connect to it
### Claude Code
```bash
claude mcp add --transport http opsdesk https://opsdesk-mcp.vercel.app/api/mcp \
--header "Authorization: Bearer demo-agent-key"
```
Then try: *"What orders are stuck right now?"* → *"What's wrong with ORD-1001?"* → *"Refund it."*
To see the policy escalation path, reconnect with `demo-manager-key` and compare what each role is allowed to do to `ORD-1013`.
### MCP Inspector (no project setup)
```bash
npx @modelcontextprotocol/inspector
```
Transport `Streamable HTTP`, URL `https://opsdesk-mcp.vercel.app/api/mcp`, and add header `Authorization: Bearer demo-agent-key`. Gives you a click-through view of every tool, its schema, and the runbook resource.
### Raw HTTP
```bash
curl -s -X POST https://opsdesk-mcp.vercel.app/api/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer demo-agent-key' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"investigate_order","arguments":{"order_id":"ORD-1001"}}}'
```
### Locally
```bash
npm install
npm test # 54 tests
npm run typecheck
npm run dev # vercel dev
```
Auth is header-only by design. I did not add a `?key=` query-param fallback even though it would be marginally easier to paste around — keys in query strings end up in access logs and referrers, and a demo that models bad credential handling teaches the wrong thing.
---
## Demo orders
Stable IDs, one per failure class, so the demo and the tests reference the same cases.
| Order | Diagnosis | Why it's interesting |
|---|---|---|
| `ORD-1001` | `PAYMENT_CAPTURED_NO_FULFILLMENT` | The hero case: paid 74h ago, warehouse never got it |
| `ORD-1003` | `DUPLICATE_CHARGE` | Charged twice; server computes the refund, caller can't override it |
| `ORD-1004` | `OVERSOLD_INVENTORY` | Retry is **refused** — stock is still zero, it would fail identically |
| `ORD-1016` | `OVERSOLD_INVENTORY` | Same rejection, but stock arrived since, so retry **is** allowed |
| `ORD-1007` | `ADDRESS_VALIDATION_FAILED` | Label failed as undeliverable → routed to the address class, not the carrier class |
| `ORD-1009` | `PAYMENT_AUTHORIZED_NOT_CAPTURED` | Authorization expired; capture refused, release allowed |
| `ORD-1011` | `UNKNOWN_STALL` | Doesn't match any rule — the system says so instead of guessing |
| `ORD-1013` | `PAYMENT_CAPTURED_NO_FULFILLMENT` | $939 → agent gets `requires_approval`, manager gets `ready` |
| `ORD-1014` | `PAYMENT_CAPTURED_NO_FULFILLMENT` | $3,490 → blocked for *every* ops role; that's a finance decision |
| `ORD-1015` | `HEALTHY` | Delivered. Must never appear in the stuck queue |
---
## MCP design decisions
**The server holds the judgement, not the model.** Diagnosis is a pure function over the order bundle — a priority-ordered rule set, not a prompt. The model orchestrates and explains; it never decides what class of failure something is, and it cannot invent a remediation. If diagnosis were left to the model it would be non-deterministic, unreviewable, and untestable, and the class gate below would be built on sand.
**A failure class is a gate, not a label.** Each class declares which remediations are valid from it. `retry_carrier_label` is simply not reachable from `ADDRESS_VALIDATION_FAILED`, because retrying the label would fail in exactly the same way.
**Legality is separate from policy.** *Is this action coherent for this order?* and *is this actor allowed to do it at this amount?* are different questions with different answers. Keeping them apart is why a refund can be perfectly valid and still correctly refused for a junior agent — which is how a real back office works.
**Every response carries the workflow forward.** Tool results include a `next_steps` field. Refusals are structured data with reasons written for the ops person, not exceptions — so the model relays them instead of trying a different tool to get around them.
**Unavailable options stay visible.** `investigate_order` returns every remediation with `available: true|false` and, when false, `unavailable_because`. Hiding them would make the model retry blindly; showing the reason lets it explain the situation to the human.
**Tool descriptions are written for the AI consumer.** They say when to use the tool, when *not* to, and what the caller is expected to do next. The server also ships `instructions` on initialize that state the approval requirement explicitly.
---
## Safety model
Assume the model in front of this server will sometimes be confused, and that a confused model with a refund tool is a financial incident.
| Rail | What it stops |
|---|---|
| **Two-phase write** | Nothing that moves money or stock is reachable in one call. `execute_remediation` requires a token only the server can mint, and only after legality + policy pass. |
| **HMAC-signed tokens** | A token can't be constructed or edited. Tampering with the order id inside it fails the signature check. |
| **Precondition fingerprint** | The token is bound to the state the preview was computed against. If anything material changed in between, execution refuses rather than applying a stale plan. Irrelevant churn (an added note) deliberately does *not* invalidate it. |
| **Single-use + 15m TTL** | A token leaked into a transcript can't be replayed later, and a token can't be used twice. |
| **Actor binding** | A token issued to one user can't be executed by another. The approver has to get their own preview. |
| **Idempotency keys** | Retry after a network error is safe; the original result replays and nothing is applied twice. Reusing a key for a *different* action is refused outright. |
| **Re-validation at execute** | Legality and policy are re-run against live state at execution, not trusted from proposal time. |
| **Role limits** | Per-action and per-day refund caps by role, evaluated against the audit log so caps can't be evaded by splitting one refund into many. |
| **Transactional apply** | A failure mid-apply rolls back the whole change. "Money moved, stock didn't" is unreachable. |
| **Append-only audit** | Refusals are recorded alongside successes — the record shows what was *attempted*, not just what worked. |
**Deliberately not automated:** OpsDesk never contacts a customer. It drafts messages; a human sends them. It also never picks between competing orders for oversold stock — it surfaces the options and says that's a business decision.
---
## Scope
**In:** one workflow (stuck-order triage → resolution), seven tools, nine remediations, ten failure classes, the full safety model above, hosted and tested.
**Out, deliberately:**
- **Frontend** — the MCP client *is* the interface.
- **Real auth** — bearer key → actor+role. It provides attribution and limits, which the safety model needs; SSO would change one file.
- **Real payment/carrier/WMS integrations** — mocked as internal services with realistic failure codes. Wiring Stripe would prove integration skill, not product judgement, and the brief says use synthetic data.
- **A complete commerce backend** — no cart, no checkout, no returns, no tax.
- **Multi-currency** — USD only. Every money value is integer cents; the string fields are display-only.
**Assumptions:** one warehouse; refunds go to the original payment method; a 24h fulfillment SLA and a 7-day lost-parcel threshold (both configurable constants in `diagnose.ts`); ops staff are trustworthy but time-pressured — the rails are there to catch mistakes, not malice.
---
## Limitations & next steps
**State is in-memory and per-instance.** This is the significant one. The dataset is seeded deterministically at cold start, so every instance begins identical, but writes are local to the instance that served them and are lost on cold start. Practical consequences:
- A propose→execute pair that lands on two different instances returns `unknown_proposal` rather than doing something wrong — it fails safe, but it *can* fail. In practice consecutive calls hit the same warm instance; the deployed demo above works end to end.
- Single-use token enforcement and idempotency are per-instance, not global.
Everything durable goes through the `Database` class, so the fix is one adapter (Postgres, with the proposal/idempotency/audit tables as the schema) rather than a refactor. I chose the demo being trivially runnable over the durability story, and I'd reverse that for anything real.
**Other known gaps:**
- Refund caps are per-actor-per-day but not per-order — an actor could refund many different orders up to their daily cap. Intentional; a per-order cap belongs to a returns policy that doesn't exist here.
- `UNKNOWN_STALL` is a catch-all. In production the rule set would grow from real incidents, and the escalation records are exactly the raw material for that.
- The mock carrier is deterministic. Real transient failures would need retry/backoff semantics that this doesn't model.
- No rate limiting on the endpoint, no pagination beyond `limit` caps.
---
## Tests
```
npm test # 54 tests, ~0.5s
```
- **`diagnose.test.ts`** — every planted scenario pinned to its expected class, plus the judgement calls (undeliverable label → address class, duplicate charge outranks the stall hiding behind it, at-risk money is the duplicate only, `UNKNOWN_STALL` admits low confidence, delivered orders never flagged).
- **`workflow.test.ts`** — end to end over the **real MCP protocol** via an in-memory transport, so tool registration, schema validation, and the response envelope are all exercised. Also asserts the parts of the surface a client sees before any tool call: that `execute_remediation` is the only tool flagged destructive, that the server instructions state the approval rule, and that the runbook resource and triage prompt resolve. Includes the money invariant (refunded never exceeds captured) and that a second refund is refused.
- **`safety.test.ts`** — the adversarial set: forged token, edited payload, expired token, replayed token, token handed between users, stale precondition, idempotent retry, key reuse across different actions, the three policy verdicts, and rollback when a downstream system refuses.
Runtime verification against the deployed instance is in `scripts/remote-smoke.sh`.
One test failure was a real bug, not a bad expectation: a $939 refund hit the *agent's* daily cap and returned `blocked` when it should have routed to a manager. The agent was never going to execute it — the approver's limits are the ones that matter. Fixed in `policy.ts`.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues