Anyone can ask a 2.5-million-row warehouse a business question in plain English and get the answer, the SQL behind it and a chart.
A read-only AI data analyst over a Kimball star schema in PostgreSQL. A semantic layer decides what every metric means, and an MCP server is the only door to the database.
- Self-directed build · V1 complete
- 2026
- Retail analytics
- AI agent, Data platform

The situation
The numbers exist. Access to them does not. The person who wants to know which region carried last quarter is rarely the person who can write a six-join query against a star schema, so the question joins a queue behind an analyst and the answer arrives after the decision has already been made. Every business of any size has this queue, and it is the reason so many decisions get made on the last dashboard anyone bothered to build.
Pointing a language model straight at the database is the obvious fix and the one nobody should ship. Ask two people in the same company what "revenue" means and you get two formulas; ask a model with no definition in front of it and you get a third, fluent and wrong. A model holding a live database connection can also, eventually, be talked into writing to it.
What we built
Three things, in this order.
A warehouse worth asking questions about. A Kimball-style star schema in PostgreSQL with one atomic fact grain — a single product line, in a single order, at a store, to a customer, at a timestamp — carrying 2.5 million rows, plus daily inventory snapshots and a returns fact. Around it sit conformed date, product, store, customer, promotion and payment dimensions, with the product hierarchy denormalized so an ordinary question doesn't pay for an extra join. Ten thousand products, a hundred thousand customers, three years of days, indexed for analytical scans.
A semantic layer, so meaning is never guessed. Net sales, average order value, profit margin, return rate and stockout rate each have exactly one definition, written down in YAML along with the words people actually use for them, the joins that are allowed, the time grains that make sense, and the columns nobody is allowed to see. The model does not decide what a metric is. It looks it up.
An MCP server as the only door to the database. The model never holds a connection. It asks for the schema, the semantic model, a table profile, SQL validation, read-only execution, an explanation and a chart recommendation — each one a named tool with its own limits and its own audit trail. The answer comes back in business language, with the SQL that produced it, the filters and assumptions stated out loud, and a chart type suggested for the shape of the result.
Because the boundary is MCP rather than something bespoke, the same server drops into Claude Desktop or Cursor unchanged. The web app on top of it adds the parts a team needs: accounts, a schema browser, and the history of every question anyone has asked.
What we deliberately left out
No write access, anywhere in the path. Queries run as a PostgreSQL role that holds SELECT on the warehouse and nothing at all on the accounts and audit tables, and the validator rejects anything that is not a single SELECT or WITH — no DDL, no DML, no comments, no stacked statements, no reading of the application's own schema. "Delete the low-profit products" returns a refusal, not a query.
No unbounded queries. Every statement runs under a timeout, and a row limit is applied when the SQL doesn't set one. A careless question cannot take the warehouse down for everyone else using it.
No free-form joins. The model may only join the tables the semantic layer says are joinable, on the keys it names. This costs a little flexibility and removes the failure that actually matters: a query that runs cleanly and answers the wrong question.
The result
A question in plain language comes back as an answer, the SQL that produced it, and a chart suggestion — against a warehouse at a scale a real retailer would recognise. Every question is stored with its generated SQL, the validation outcome, the row count, the latency and the person who asked, so a number that looks wrong is traced back to the query that produced it instead of argued about. Unsafe requests are refused, and the refusal is logged too.
Built with
- Next.js
- FastAPI
- PostgreSQL
- MCP
- Gemini
Have something like this?
Tell me what the problem is on a 30-minute call. If it's a fit, discovery starts with a written scope and a fixed number — not an estimate that moves.