Skip to main content
Glama
harsha-moparthy

legacy-mcp

README.md
# Legacy MCP

**MCP-ifying a legacy system: governed, business-language tools over a SOAP + stored-procedure stand-in — with a semantic data dictionary, compensation for transactionless writes, and measured load protection.**

> Suggested GitHub repo name: `legacy-mcp`

---

## Why this project exists

Enterprises run on decades-old systems: SOAP services, stored-procedure databases, cryptic schemas nobody fully remembers. The current wave of forward-deployed work is making those systems usable by AI agents *without replacing them* — wrapping legacy interfaces in governed, modern tool layers. The craft is not the MCP part; it's the archaeology (what does `PROC_UPD_47` actually do?), the safe-write patterns on a backend with no cross-call transactions, and the duty of care toward infrastructure that dies if you hammer it.

This project builds the legacy system *and* the wrapper, so every claim is checkable end to end.

## The legacy stand-in (authentically awful, on purpose)

A wholesale order-management backend, reachable only through a SOAP-style XML endpoint fronting stored procedures:

- tables `T_CST`/`T_ITM`/`T_ORD_H`/`T_ORD_L`; columns like `C_STS CHAR(1)` with magic letters (`A`/`H`/`X`), money in integer cents, dates as `YYYYMMDD` ints
- positional parameters (`<P1>`, `<P2>`…), faults like `ERR-3007 CONSTRAINT VIOLATION SEGMENT 4`
- **no cross-call transactions**: creating an order is a three-procedure protocol (`HDR_INS` → `LN_INS` per line → `FIN`); die in the middle and an incomplete header is stranded forever (the seed data ships with orphan order 9005 as the exhibit)
- **undocumented side effects**: `PROC_UPD_47` "sets customer status" but also recalculates the credit-block flag from open order exposure — so releasing a hold does not necessarily unblock the customer
- **fragility**: above ~5 calls/s it faults `ERR-9999 SYSTEM BUSY`; under injected latency it just gets slower

The wrapper never imports the database — it speaks the XML wire only, like a real engagement.

## The deliverables

**1. The semantic data dictionary** (`docs/data_dictionary.md`) — every table, column, status letter, procedure contract, error code, and landmine, with how each was recovered (trial calls, audit-log diffing). `semantics.py` is its executable form; tests pin them to each other.

**2. Governed MCP tools** (official SDK) designed around business operations, not endpoints: `lookup_customer`, `get_customer_credit`, `check_item_availability`, `get_order_status`, `list_orders`, `place_order`, `cancel_order`, `release_hold`, `set_customer_status`, `cleanup_incomplete_orders`. Statuses are words, money is decimal, dates are ISO. Every failure — including a malformed argument from the agent — arrives as one actionable sentence: what happened, what to do, and the raw code for the humans. Verbatim:

> `ERR-3007` → *Blocked during order creation for customer 4711: the customer is on hold or over their credit limit. Use get_customer_credit to see exposure vs limit; release_hold clears a hold but will NOT clear an over-limit credit flag. [legacy: ERR-3007 CONSTRAINT VIOLATION SEGMENT 4]*

**3. Compensation for transactionless writes.** `place_order` drives the three-call protocol; on any mid-protocol failure it deletes the incomplete order and says so. The failed-order path is tested down to "no orphan rows, stock untouched."

**4. Load protection, measured live.** Every legacy call passes a token bucket (throttle by waiting, not shedding) and a circuit breaker (open on consecutive infra failures; half-open probe; business faults never count).

## Measured results

All produced by commands in this repo on 2026-07-31 (`results/` committed). Tests: **75/75 passed**.

### Before/after capability (`legacy-mcp capability`)

Same six tasks; a scripted *generic-competence* operator against the raw SOAP surface vs the same shell using the MCP tools. Judged only by end-state database checkers and exact numbers — no graders. (Methodology and the raw operator's generous assumptions are documented in `capability.py`; the knowledge it lacks — status letters, the three-call protocol, cents, fault semantics — is precisely what the wrapper packages.)

| task | raw interface | MCP tools | wrapped detail |
|---|---|---|---|
| place-simple-order | FAIL | PASS | pending order, correct total, stock decremented |
| blocked-order-explained | FAIL | PASS | diagnosed: hold AND over-limit; release won't fix |
| partial-failure-cleanup | FAIL | PASS | failure explained, no orphans, stock untouched |
| cancel-and-restock | FAIL | PASS | cancelled; restock observed (8 → 18) |
| janitor-incomplete-orders | FAIL | PASS | orphan 9005 identified and removed |
| credit-headroom | FAIL | PASS | 12,000.00 limit − 1,540.00 open = 10,460.00 |

**raw 0/6 — wrapped 6/6.** Representative raw failures: the order left stranded in `I` because nothing advertises that `OP_ORD_FIN` exists; headroom computed from cents and shipped orders; "stuck orders" invisible because `I` is just a letter.

### Protection under a degraded backend (`legacy-mcp protection-demo`, real sockets)

Slow backend (2s/call, wrapper timeout 0.5s, breaker threshold 3):

| call | outcome | wall (s) |
|---|---|---|
| 1–3 | timeout | ~0.50 each |
| 4–8 | fast-fail, circuit open | 0.000 |
| 9 (after recovery + cool-down) | ok — half-open probe closed the circuit | 0.003 |

The breaker held total backend calls to **4** across the whole episode (3 timed-out probes + 1 recovery probe); five agent calls were answered instantly with an actionable "backend degraded, retry in Ns" instead of hanging.

Busy backend (faults above 5 calls/s), 25 reads:

| caller | succeeded | SYSTEM BUSY faults | wall |
|---|---|---|---|
| unthrottled | 5/25 | 20 | 0.08s |
| wrapped (bucket 1 + 4/s) | **25/25** | **0** | 6.11s |

The wrapper spent 5.8s deliberately waiting — trading its own latency for the backend's health, which is the entire duty of care. Successes are reported next to faults on purpose: "zero busy faults" would also be true of a caller that fast-failed everything, so the table has to show that all 25 calls actually completed. The first sizing attempt (bucket 4 + 4/s) still produced 2 busy faults because burst + refill exceeded the backend's ceiling in the first second; the fix (capacity 1) is kept in the code comment as the lesson: *size the bucket to the backend's measured capacity, not to a round number.*

## Quickstart

```bash
uv sync --extra dev
cp .env.example .env

.venv/bin/pytest                     # 75 tests: stand-in, semantics, compensation, protection
.venv/bin/legacy-mcp capability      # the before/after table -> results/capability.md
.venv/bin/legacy-mcp protection-demo # live slow/busy scenarios -> results/protection.md
.venv/bin/legacy-mcp raw-peek        # feel the raw interface yourself

# run it for a real agent
.venv/bin/legacy-mcp serve-legacy &                    # the legacy stand-in (:8093)
.venv/bin/legacy-mcp serve-mcp                         # MCP over stdio, wired to it
# degrade the backend and watch the wrapper cope:
LEGACY_SLOW_MS=2000 .venv/bin/legacy-mcp serve-legacy
```

`protection-demo` stands up its own backends on `SOAP_PORT` and the two ports
above it, so stop a backgrounded `serve-legacy` first or point it elsewhere with
`SOAP_PORT=8200`. It says which port it could not bind rather than hanging.

## Repository layout

```
legacy-mcp/
├── docs/data_dictionary.md         # the archaeology deliverable
├── results/                        # committed capability + protection evidence
├── src/legacy_mcp/
│   ├── legacy/db.py                # the stand-in: procs, protocol, faults, fragility
│   ├── legacy/soap.py              # XML envelope endpoint (the only wall socket)
│   ├── legacy/client.py            # typed wire client; adds no meaning
│   ├── semantics.py                # recovered meaning: codes, units, error translation
│   ├── tools.py                    # business-operation tools + compensation
│   ├── protection.py               # token bucket + circuit breaker (Guard)
│   ├── protection_demo.py          # live measured scenarios
│   ├── capability.py               # before/after suite, end-state checkers
│   ├── server.py                   # MCP registration (official SDK)
│   └── cli.py                      # serve-legacy | serve-mcp | capability | protection-demo
└── tests/                          # 75 tests, incl. the agent-facing failure contract
```

## Honesty notes

- The "raw operator" in the capability suite is scripted, not an LLM; its generic competence and its ignorance are both explicit in code. The suite measures what the *interface* affords, and the strict test (`raw 0/6, wrapped 6/6`) fails in both directions if either side drifts.
- The legacy stand-in is self-built, so its awfulness is curated rather than accreted. Every quirk it has is one documented from real systems (transactionless multi-call writes, overloaded error codes, side-effecting status procs, cents/YYYYMMDD encodings, SYSTEM BUSY ceilings).
- The protection numbers come from real sockets and real timeouts, not mocks; the deterministic breaker/bucket unit tests use a simulated transport and say so.
- The failure contract is tested, not asserted in prose: `tests/test_error_contract.py` renders every fault template against every operation phrase the tools actually pass, and drives fifteen kinds of malformed argument through `place_order` to prove nothing escapes as a raw exception. Both suites exist because both properties were broken — the templates read "Customer order creation for customer 4711 is blocked" and bad quantities surfaced a `TypeError`.

TDQS

C2.9/5.0

Scored across 10 tools

Disambiguation5/5

Each tool has a clear, distinct purpose: customer lookup, credit, hold, status; order lifecycle (get, list, place, cancel); item availability; and cleanup. No overlapping functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (e.g., get_order_status, lookup_customer), making them predictable and easy to understand.

Tool Count5/5

10 tools cover the core operations of a legacy order/customer system without being excessive or too sparse.

Completeness4/5

Covers customer management, order lifecycle, inventory check, and cleanup. Missing update/modify order tool, but otherwise well-scoped for the domain.

Maintenance

ActivitySlowing
ResponsivenessNo issues