We Built an AI Data Analyst, Then Deleted the AI A company that built an in-house AI data analyst scrapped the AI layer before shipping and instead exposed its warehouse to Claude through an MCP-over-HTTP server, answering 1,436 questions across 781 tables in nine countries in its first twelve weeks without giving anyone a warehouse login. The server enforces read-only, single-source SELECTs parsed with Postgres's own parser, rewrites every SELECT, CTE and subquery to add the tenant filter, and returns model-readable refusals that tell Claude how to fix a query; 87% of failed queries were followed by a successful one within ten minutes, and Claude searched the attached knowledge bases 454 times. The team said it stopped its original classifier-search-prompt-fixer pipeline because it was "rebuilding Claude, and not as well. Most companies have the same problem with their data. Someone needs a number, it’s in the warehouse, and only a few people know the warehouse well enough to get it out. So the question becomes a request, and it waits until an analyst or a developer has time. We wanted people to answer those questions themselves, without handing everyone warehouse credentials and without an AI that guesses what our metrics mean. Our first attempt was an AI analyst built into our own app. We stopped before it shipped. What we shipped instead is simpler: Claude does the thinking, and our product gives it safe access to our data and the knowledge of what that data means. Twelve weeks in, it has answered 1,436 questions across 781 tables in nine countries. Teams across the company rely on it, and nobody had to be given a warehouse login. The first version looked like most AI analyst projects: a classifier to spot data questions, a search step to find relevant tables, a long prompt to write the SQL, and a fixer to retry when it failed, plus the usual plumbing for model providers, token budgets and streaming. As it came together, we realised we were rebuilding Claude, and not as well. Claude already plans, recovers from bad queries and keeps track of a conversation, and it gets better at all of that with every release. What it can’t do is see our warehouse, know what an “active listing” means here, or know who is allowed to see which table. So that’s what we built, and we turned our app into something Claude could use. There are three parts now. Claude is the brain: it reads the question, writes the SQL and explains the answer. Our server holds the data and the knowledge. It’s a fairly ordinary web app that speaks MCP over HTTP, so Claude, or any other MCP client, can connect to it. And a skill, a set of written instructions Claude loads for data questions, teaches it how we want those questions handled. The server’s job is to make the right thing easy for Claude and the wrong thing impossible. Claude browses a catalog of schemas, tables and columns, each with a short description written by a person. It’s paged, after our first version dumped more than a thousand schemas into a single response. Claude can run one read-only SELECT at a time, against one data source. The server parses it into a syntax tree with Postgres's own parser rather than pattern-matching the text, checks every table it touches against what the person is allowed to see, and adds the tenant filter by rewriting that tree, in every SELECT, CTE and subquery. Many of our tables hold several markets side by side, so this matters. Claude just says which market it wants. It never writes the filter, so it can't forget it or widen it: -- what Claude wroteSELECT c.city, COUNT FROM shop.orders oJOIN shop.customers c ON c.id = o.customer idGROUP BY c.city; -- what actually runsSELECT c.city, COUNT FROM shop.orders oJOIN shop.customers c ON c.id = o.customer idWHERE o.tenant id = 7 AND c.tenant id = 7GROUP BY c.city; If you build something similar, put that condition in the ON clause on the optional side of a LEFT JOIN, or the join quietly becomes an inner join. Refusals are written for the model, and they say how to fix the query: the aggregate FILTER WHERE … clause is not supported on this warehouse redshift ;rewrite it with CASE inside the aggregate, e.g. SUM CASE WHEN