Case Study · Independent Build

MCP Server: an AI interface over real commerce data, and an independent way to check its answers

How do you know an AI interface over business data is telling the truth? This is an MCP server that lets Claude answer business questions about a Rails and Spree store holding 99,441 real, anonymised Olist orders, and the independent harness I built to check what it says. It runs as a live, read-only demo deployment, not a business, and the results below are this project's engineering evaluation, not a benchmark.

99,441 real, anonymised Olist orders
5 → 35 of 35 questions right to the cent, before and after
9 correctness defects found and fixed
45 of 45 agent answers matching the oracle

At a glance

What it is
An MCP server over a Spree store in PostgreSQL, imported from the public Olist dataset, and an independent correctness harness that recomputes every answer from the raw data.
What runs today
A live, read-only demo deployment: OAuth with PKCE, exactly 9 read-only tools, writes disabled. Only the store admin can grant access, so visitors can inspect the discovery documents, not use the tools.
Hardest problems
  1. Tools that already looked right, and were not: 5 of 35 canonical questions right before the fixes.
  2. Deciding what each commerce metric means over reviews, sellers, split payments and cancelled orders.
  3. An oracle that shares no code with the server, so a shared bug cannot hide.
  4. A current Claude client exposed two problems the repository's own tests had not.
Stack
Ruby on Rails, Spree Commerce, PostgreSQL, Doorkeeper for OAuth, the MCP Ruby SDK, Kamal, and CI on every push.
My role
Sole engineer: I designed, built and operate it, using AI coding agents (Claude) under my direction and review.
More
The repository (MIT) · the build log, with a dated update

The demo in 88 seconds

MCP Server verification demo (AI voice: elevenlabs.io). Claude Code connected to the live, read-only demo over OAuth, two questions answered from the live data, then the independent harness run locally on the pinned dataset. Real commands and real results, recorded in one take.

The question

The first version, written up in August 2026, answered questions about the store fluently. Nothing showed whether its numbers were right. So the engineering question became concrete: how do you design MCP tools that give an AI correct answers over real, messy commerce data, and how do you show that they do?

Real orders do not line up with the easy join. 555 orders carry more than one review, 1,278 orders have more than one seller, payments can be split across methods, and cancelled and unavailable orders sit next to completed ones. Every tool returned a plausible number; none had a written definition of what it counted. Every metric now has one: its population, freight, multi-seller and multi-review treatment, grain, and whether it adds up across a breakdown, with five decisions I approved as the owner. Figures on this page are under those definitions.

The approach: two paths that share no code

  • A system under test. The Olist CSVs are imported into a Spree store in PostgreSQL. The MCP server exposes 9 read-only tools over OAuth with PKCE; the two write tools in the code are not exposed.
  • An independent oracle. Plain Ruby over the raw CSVs, with no importer, Spree model, SQL or tool code; a test asserts it issues no database queries. It shares only the input files and the written definitions, and compares answers field by field, to the cent.
  • A harness over everything the tools say. 35 canonical questions, 9 invariants over 150 seeded cases, 6 differential checks over 200 seeded random cases, a CSV-to-database reconciliation, and 12 known bugs it must catch.
  • An agent evaluation. Claude Code answers 15 questions with only the tools, graded deterministically against answers the oracle computed and committed before any run. No model judges the answers.
How the answers are checked. The raw Olist CSVs feed two paths that share no code: an independent oracle in plain Ruby, and the system under test (importer, PostgreSQL, MCP server with 9 read tools called over JSON-RPC). Expected and actual answers meet in a field-by-field comparator covering 35 questions, 9 invariants, 200 seeded differential cases and reconciliation. The same oracle also judges a regression corpus of 12 known-bad versions and an agent evaluation of Claude Code using the tools.
Two paths from the same raw data, sharing no code, compared field by field. Full diagram.

What was wrong

Run against the tools as they were, with the same harness and seed, the tools answered 5 of 35 canonical questions correctly. The import itself was sound: the CSV-to-database reconciliation passed before and after. The defects were in the tools.

CheckBeforeAfter
Canonical questions, to the cent5 / 3535 / 35
Invariants (150 seeded cases)4 / 99 / 9
Differential checks (200 seeded random cases)0 / 66 / 6
Reconciliation, CSV to database2 / 22 / 2
Known bugs caught3 / 3 historical12 / 12: 3 historical, 9 pre-fix
Before and after the fixes, same harness and seed: canonical questions 5 of 35 to 35 of 35; invariants 4 of 9 to 9 of 9; differential checks 0 of 6 to 6 of 6; reconciliation 2 of 2 both times; known bugs caught 3 of 3 to 12 of 12. 52 tests pass; result fingerprint f4841a48894b268d, identical locally and in CI.
The same harness and seed, before and after the fixes. Before the fixes only the 3 reconstructed historical bugs existed; the 9 pre-fix versions of the tools joined them after. Full diagram.

The nine defects

DefectWhat the harness showedThe rule now
Review fan-out555 orders have several reviews; search reported 100,000 matches for 99,441 orders; one customer's lifetime value doubled (R$46.85 shown as R$93.70)One review score per order
Review policyNo rule for which review countsAn order's most recent review
Category revenueTwo tools returned R$1,258,681 and R$1,445,137 for Health BeautyNamed metrics: item revenue R$1,255,695.13, gross R$1,437,665.78
Seller attributionIn 1,278 multi-seller orders, sellers were credited with other sellers' items; the top seller read R$253,122 in one tool, R$229,473 in anotherAllocate at the item grain
Cancelled as salesProduct, category and seller figures counted cancelled and unavailable ordersCompleted orders only
Overlapping totalsOverlapping buckets presented as if they added upEach breakdown says whether it adds up
Split paymentsCredit card overstated by R$185,429 (R$12,534,959 against R$12,349,530 collected); voucher about 1.4xPayment value, its own metric
Delivery populationSix cancelled orders in delivery metricsDelivered orders only
Customer lookupEmail matched as a substringExact, case-insensitive match

The MCP plumbing was not the hard part. The hard part was deciding what each commerce metric means over messy relational data, writing it down, and checking every tool against it. Each fix is kept as a regression the harness must go on catching.

What the harness found: nine correctness defects in five classes, each with what was found and the fix. Join fan-out; population errors; undefined metrics; revenue aggregation; customer lookup. Values are under the project's metric definitions.
Five classes of defect, with verified magnitudes only. Full diagram.

Reproducible evaluation

Every push to the repository rebuilds the evidence in CI: the pinned Olist dataset, checked file by file with SHA-256; a fresh PostgreSQL database and a full import; 52 tests (20 oracle self-tests, 21 HTTP/MCP boundary tests, 11 grader tests); then the whole harness. A run takes about 6 to 8 minutes.

The harness is deterministic. It ends with a fingerprint that hashes every check's id, status and differences and every regression verdict, leaving out timings, so two runs on the same data, code and seed agree exactly or visibly do not. Local runs and CI print the same one: f4841a48894b268d.

The 12 known bugs are the 9 pre-fix versions of the tools plus 3 reconstructed historical bugs. One reproduces the original write-up's inflated category revenue, R$52,545,084, exactly on the pre-fix counters; another (33 dropped payments) is a simulated effect.

Every push rebuilds the evidence: the pinned dataset, a SHA-256 check of each of the 8 files, a fresh build with a new PostgreSQL database and a full import of 99,441 orders, 52 tests and then the harness, and the result fingerprint f4841a48894b268d, identical locally and in CI. About 6 to 8 minutes.
Same data, same checks, same result, on every push. Full diagram.

Agent evaluation

Claude Code 2.1.284 with one model, claude-sonnet-5, answered 15 questions three times each with only the store's tools. 45 of 45 answers agreed with the pre-registered oracle answers, with an acceptable tool and acceptable arguments in all 45 runs, and 15 of 15 questions correct on every run.

14 of 15 gave an identical answer every time. The one variation was arithmetic: the overall late rate, which no tool reports, came back as 7.93 twice and 7.9 once against a correctly rounded 7.92, all inside the pre-registered 0.1-point tolerance. This is an engineering sample of one model and one client, not a benchmark and not a measure of model performance in general. A planned tool-description experiment was not run: the baseline was already at the ceiling.

Can Claude use the tools correctly? 15 questions, 3 runs each, 45 runs: 45 of 45 answers match the oracle, 45 of 45 correct tool chosen, 45 of 45 correct arguments, 15 of 15 questions correct on every run. Graded deterministically; no model judges another. An engineering evaluation of these tools, not a benchmark of the model.
Graded against oracle answers committed before any run. Full diagram.

Live verification

The live, read-only demo deployment at https://mcp-demo.railsfanatics.com/mcp requires OAuth with PKCE (S256), with discovery and dynamic client registration. Only the store admin can grant access, and only the mcp:read scope is issued, so a client sees the 9 read tools and no write tools.

Testing with a current Claude client found two problems the repository's own tests had not. Claude Code negotiated an MCP protocol revision the Ruby SDK claimed but did not implement: it saw zero tools and, in one probe, invented an answer (6 cancelled 2018 orders; the oracle says 334). Pinning the server to revision 2025-11-25 fixed it. The OAuth server also rejected Claude Code's localhost callback; plain HTTP is now allowed for loopback callbacks only, and every other callback still requires HTTPS.

After both fixes, Claude Code connected over OAuth, negotiated 2025-11-25 and saw exactly 9 read tools. Over the live endpoint, 334 orders placed in 2018 were cancelled, and November 2017 revenue was R$1,172,191.68 (gross revenue: items plus freight, completed orders). Both match the oracle.

Why testing with a real client mattered. The same question, how many orders placed in 2018 were cancelled, before and after a protocol fix. Before: Claude Code asks for MCP revision 2026-07-28, the server agrees but its SDK does not implement it, 0 tools are visible, and in one probe the answer is 6. After: the server negotiates 2025-11-25, 9 read tools are visible, and the answer is 334 over the live endpoint, matching the independent oracle's 334.
Before the protocol pin: zero tools, and one invented answer. After: 9 tools and 334, matching the oracle. Full diagram.

What is shown and what is not

AreaStatus, and what it means
Correctness of the toolsShown, within the checks run. 35/35 canonical questions to the cent, 9/9 invariants, 6/6 differential and 2/2 reconciliation checks, 12/12 known bugs caught. Not a claim about questions outside these checks.
DeploymentA live, read-only demo. Not a production system and not used by a business. Access is owner-consented through OAuth.
DataPublic and anonymised. The Olist orders are real but anonymised, and product names are synthesized by the importer.
Agent evaluationAn engineering sample. One model, one client, 15 questions, graded against pre-registered answers. Not a benchmark.
Write toolsNot exposed. Two write tools exist in the code; the deployment serves read tools only.
Open findingsTracked. 14 findings: 11 verified, 2 deferred, 1 out of scope. One deferred item is a defence-in-depth consideration: customer-written review text reaches the model without a "treat as data" label in structured results. The model declined it in 6 of 6 local probes; it is not claimed as solved.

How it was built

I designed, built and operate it as a solo engineer, from the first build in August 2026 to the verification work in September 2026. I used AI coding agents (Claude) for much of the implementation, under my direction and review. I set the architecture, the metric definitions and the acceptance bars, approved every production change and ran the releases.

What this demonstrates for client work

  • Define the metric before the tool, or every tool answers a different question.
  • Check AI-facing tools against a source that shares no code with them, so a shared bug cannot pass both.
  • Keep every fixed defect as a regression that the checks must go on catching.
  • Test with the real client, not only with the repository's own tests.
  • Say what is not shown, alongside what is.

Related writing

Data: Brazilian E-Commerce Public Dataset by Olist, CC BY-NC-SA 4.0, 99,441 orders from 2016 to 2018, anonymised. State as of 29 September 2026.

Facing a build like this?

← All case studies