reflect-mcp
by jeffma8888
README.md
# reflect-mcp
> An MCP server for LLM tasks that verify themselves: **Generator → second-model Critic → human-in-the-loop gate**. Never a silent wrong answer.




---
## 1. TL;DR
`reflect-mcp` is a small multi-agent pipeline that **checks its own work before surfacing it**, exposed as an [MCP](https://modelcontextprotocol.io) server so it plugs into Claude Desktop, Cursor, or any MCP client. A **Generator** extracts a typed object from messy text; a **second-model Critic** scores every field and the whole extraction; a **confidence-thresholded gate** either returns the result or, if it isn't confident enough, returns a `needs_review` payload naming the exact fields a human should confirm.
```
┌───────────┐ ┌──────────────┐ ┌────────┐ ┌──────────────┐
messy text ──▶ │ Generator │ ───▶ │ heuristics + │ ───▶ │ Gate │ ───▶ │ ok / │
│ (model A) │ │ Critic (B) │ │ (HITL) │ │ needs_review│
└───────────┘ └──────────────┘ └────────┘ └──────────────┘
typed JSON per-field + overall threshold flagged fields
confidence decision + rationale
```
## 2. Why — the trust problem
LLMs are **confidently wrong**. The failure mode that actually hurts in production isn't a model that says "I don't know" — it's a model that returns a clean, plausible, wrong value with no signal that anything is off. `reflect-mcp` makes verification a **first-class step**: a *different* model critiques the output, deterministic structural checks run alongside it, and anything below a confidence threshold is escalated instead of emitted.
The pattern is well established: naive extraction pipelines look great in a demo and then quietly degrade on real inputs, and adding a multi-model verification + human-review gate is a proven way to claw accuracy back. The point `reflect-mcp` makes concrete: *the verification discipline is the product*, not the extraction call.
## 3. Demo
Messy input → typed JSON with per-field confidence → one low-confidence field flagged for review:
```jsonc
// input
"Hi team, bill from Globex Ltd. Ref GLX/2026/778, dated 2026-02-28.
540.00 before tax, tax 43.20, amount due 683.20. Pay in GBP."
// output (the model hallucinated the total; the critic + arithmetic check caught it)
{
"status": "needs_review",
"data": { "vendor_name": "Globex Ltd", "invoice_number": "GLX/2026/778",
"invoice_date": "2026-02-28", "subtotal": 540.0, "tax": 43.2,
"total": 683.2, "currency": "GBP" },
"field_confidence": { "total": 0.12, "subtotal": 0.95, "tax": 0.95, "...": 0.95 },
"overall_confidence": 0.70,
"flagged_fields": [
{ "field": "total", "value": 683.2, "confidence": 0.12,
"reason": "683.20 does not reconcile with 540.00 + 43.20; not found in source" }
],
"notes": "1 field(s) below the 70% confidence threshold require human review before use."
}
```
*(A ~20s GIF of this in Claude Desktop goes here in the published repo.)*
## 4. Use it in Claude Desktop
Install and point Claude Desktop at the server. Add this to your `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"reflect-mcp": {
"command": "/ABSOLUTE/PATH/reflect-mcp/.venv/bin/python",
"args": ["-m", "reflect_mcp.server"],
"env": {
"REFLECT_PROVIDER": "anthropic",
"ANTHROPIC_API_KEY": "sk-ant-...",
"REFLECT_GENERATOR_MODEL": "claude-3-5-sonnet-latest",
"REFLECT_CRITIC_MODEL": "claude-3-5-haiku-latest"
}
}
}
}
```
Then restart Claude Desktop and ask it to *"extract the invoice fields from this text …"* — it will call the `extract` tool.
**Tool exposed**
| Tool | Args | Returns |
| --- | --- | --- |
| `extract` | `text: str`, `field_threshold=0.70`, `overall_threshold=0.75` | `{ status, data, field_confidence, overall_confidence, flagged_fields, trace_id, notes }` |
When `status == "needs_review"`, do **not** use `data` blindly — confirm the fields in `flagged_fields` first.
## 5. Architecture
Three roles, one shared trace id, clean layering (`resolver`-free by design — the pipeline has no infra dependency):
- **Generator** (`generator.py`) — one LLM call → validated `InvoiceData` (Pydantic). Absent fields come back `null`; the model is instructed never to invent a value.
- **Critic** (`critic.py`) — a **different model** (`REFLECT_CRITIC_MODEL`) is shown the source *and* the candidate extraction and must ground each field, returning per-field + overall confidence. A different model matters: self-critique with the same model tends to rubber-stamp its own errors.
- **Deterministic heuristics** (`heuristics.py`) — a free, non-LLM signal: grounding (is a committed value actually present in the source?), arithmetic reconciliation (`subtotal + tax == total`), and ISO-date validation. These *cap* the Critic — a structurally impossible value can't be waved through on the Critic's word alone.
- **Gate** (`gate.py`) — combines the two signals multiplicatively (`combined = critic_confidence × heuristic_factor`), flags any non-null field below `field_threshold`, and escalates the whole result if any field is flagged **or** overall confidence is below `overall_threshold`.
The `extract` tool (`server.py`) is the only module that imports the `mcp` SDK, and `providers.py` is the only module that imports a provider SDK — so the core pipeline and the entire test suite have **zero** hard dependency on either.
## 6. Evaluation
A tiny golden-file eval treats LLM quality as **measurable, not vibes**. `eval/golden/dataset.jsonl` holds labeled messy-text → expected-fields examples; `eval/run_eval.py` runs the full pipeline over them and reports field accuracy, exact-match accuracy, needs-review rate, and *exactly where it fails*.
```bash
make eval # OFFLINE: deterministic, uses recorded model outputs (no API keys)
make eval-live # LIVE: runs against REFLECT_PROVIDER (needs credentials)
```
Offline run (recorded fixtures, one example carries an injected hallucination to prove the harness catches misses):
```
============================================================
reflect-mcp eval | mode: OFFLINE (replay fixtures)
============================================================
examples : 5
field accuracy : 97.1% (34/35)
exact-match acc : 80.0% (4/5)
needs-review rate : 20.0% (1/5)
------------------------------------------------------------
field-level failures (where it breaks):
[inv-004] total: expected 583.2 but got 683.2
============================================================
PASS: field accuracy 97.1% >= required 90.0%
```
`make eval` exits non-zero below `--min-accuracy` (default 0.90), so it works as an **offline eval-CI gate** with no secrets.
## 7. Testing
Deterministic tests around a stochastic core. The single LLM boundary (`LLMClient`) is mocked with a `ScriptedLLMClient` that returns canned JSON and raises if it's ever asked for more than expected — so `pytest` runs **fully offline, with no API keys, and can't leak a real call**.
```bash
make install # .venv + package + dev/server deps
make test # deterministic offline suite
make cov # with coverage
```
The flagship test is `tests/test_critic_catches_error.py`: the Generator is fed a deliberately wrong value (`vendor_name = "Invoice"` — a word that *does* appear in the source, so the deterministic grounding check can't catch it), and the test asserts the **second-model Critic** catches it and the gate routes to `needs_review`. It also flips only the Critic's verdict and shows the decision flip to `ok` — proving the verification step is doing real work, not the plumbing.
## 8. Design decisions
- **Second-model Critic (multi-model reflection), not self-critique.** The Critic defaults to a *different, cheaper* model than the Generator (`sonnet` generates, `haiku` critiques). Cross-model disagreement is a real signal; same-model self-review is mostly agreement theater.
- **Provider switching via env var.** `REFLECT_PROVIDER` selects `anthropic` | `openai` | `bedrock`, so a reviewer runs it against whatever key they have. The **Bedrock (Claude)** path is there for AWS users; the public-API paths (Anthropic/OpenAI) let anyone try it without AWS. Provider SDKs are optional extras (`pip install 'reflect-mcp[anthropic]'`) and imported lazily.
- **Confidence is multiplicative.** `critic_confidence × heuristic_factor` — evidence can only *lower* confidence. A structurally impossible value (bad arithmetic, ungrounded amount) is capped regardless of how sure the Critic sounded.
- **Nulls are honest.** A field absent from the source returns `null` and is never flagged — abstention is correct behavior, not a low-confidence guess.
- **Prompt versioning + injection awareness.** Prompts live in versioned files (`src/reflect_mcp/prompts/`). Both instruct the model to treat document content as **data, not commands** — the source text is untrusted input, a basic prompt-injection guard.
- **Observability.** Every stage emits a structured JSON log line carrying a shared `trace_id`, so one extraction is greppable end to end.
## 9. What I'd productionize next
- **Batching + caching** of Generator/Critic calls; per-request cost/latency budgets.
- A real **human-review queue** (persist `needs_review` payloads, route to a reviewer UI, feed corrections back as new golden examples).
- **Offline eval in CI** on every PR (the `make eval` gate is already CI-shaped) plus periodic **live** eval to catch model drift.
- **Confidence calibration** — replace hand-set thresholds with thresholds tuned on the labeled set to hit a target precision.
- Streaming + partial results, and a richer schema library (invoices, receipts, resumes, log lines) behind the same verify-then-gate loop.
---
## Repository layout
```
reflect-mcp/
├── src/reflect_mcp/
│ ├── schema.py # Pydantic extraction + verdict/decision models
│ ├── providers.py # provider switching (Anthropic/OpenAI/Bedrock), lazy SDKs
│ ├── scripted.py # ScriptedLLMClient — the mock at the LLM boundary
│ ├── generator.py # Generator stage
│ ├── critic.py # second-model Critic stage
│ ├── heuristics.py # deterministic structural checks
│ ├── gate.py # confidence gate / HITL decision
│ ├── pipeline.py # orchestration (generate → heuristics → critique → decide)
│ ├── logging_config.py # structured JSON-line logging
│ ├── parsing.py # robust JSON extraction + prompt loader
│ ├── prompts/ # versioned generator.txt / critic.txt
│ └── server.py # FastMCP server exposing the `extract` tool
├── eval/
│ ├── golden/dataset.jsonl # labeled golden examples
│ ├── fixtures/replay.json # recorded model outputs for offline eval
│ └── run_eval.py # eval harness
├── tests/ # deterministic, fully offline
├── Makefile # install / test / cov / eval targets
├── pyproject.toml
└── LICENSE
```
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues