Writing ·
Why the safest SQL prompt is the one that does not exist
Nobody sets out to hand an agent an unguarded SQL prompt. The design always arrives with guards attached — a validator, an allowlist, a read-only account, a second model checking the first. The guards are sincere, and each of them does something real. Against the problem that actually matters they are still about as useful as a chocolate teapot — and if I were a cynic (narrator: he is a cynic…) I’d argue their main value is using them to explain to management, the media, elected officials and possibly the police that you took ‘all reasonable precautions’ before “The Incident”.
The root cause is that SQL is the wrong abstraction layer. But before we explain why, let’s look at the rationalisations that people engage in when they give a SQL prompt to an MCP server.
“We only allow SELECT”
This is a string check, and SQL is not a string. In Oracle a SELECT can call a PL/SQL function,
and that function can indirectly write — an autonomous transaction inside a function called from a
WHERE clause is perfectly ordinary, and a text filter cannot see it. SELECT ... FOR UPDATE takes
locks, which remain in place until COMMIT, ROLLBACK or the session is cleaned up.
And that’s before we even consider the chaos and misery well-formed SQL that isn’t calling functions can get up to. In one of his previous jobs the author managed a DBA team that was responsible for 24/7 support for major phone companies. Roughly 25% of all incidents were caused by bored/nervous operators issuing “SELECT COUNT(*)” queries in the early hours of the morning ‘to see how things were going’. In a static database this is not a problem. In a highly volatile one it’s a disaster waiting to happen, as Oracle has to remember what every single affected row looked like the moment the query was issued, and the data underneath is changing all the time, which will sooner or later lead to ORA-01555. This is in addition to the real risk of SQL that is syntactically correct but functionally wrong.
“We parse it properly”
Better, and now you own a SQL parser that has to agree with Oracle’s — in every version you run, forever. Any disagreement between your parser and the database is a bypass by definition, and you will not find out about it from your own test suite.
It is worse than syntax. Whether a statement is safe depends on what the objects it names actually do, and those definitions move underneath you — a view redefined, a synonym repointed. The statement your parser approved on Tuesday means something else on Thursday, and nothing in the parse tree told you.
“We use a read-only account”
This makes sense, but only as part of a bigger plan, as the business case may allow it and management will understand it. It is belt and braces, but if it were a belt it’d be a Cat 5 cable tied in a knot around your waist.
It stops writes. It does not stop the 3am COUNT(*) above, or a read of a column nobody should
ever have seen — “read-only” is not “only these rows and columns”. And it does nothing about the
failure that costs most: a query that runs, returns a number, and the number is wrong.
“We ask another model to check it”
A non-deterministic guard on a non-deterministic system. You have added a second thing that is usually right, in front of a first thing that is usually right, and called the product safe.
What all four have in common
Each is a filter over an infinite input space. A SQL prompt accepts an unbounded set of strings, and every guard is an attempt to decide a property of each one in advance. You do not get to test that. You get to test the examples you thought of.
The alternative is not a better filter. It is not having an infinite input space in the first place. What that looks like in practice, on one ordinary question asked both ways, is the side-by-side.
Abstract to the procedure and statement level, not a single SQL prompt
Give the agent tools instead. The PL/SQL your team already wrote and tested. The specific
queries you curated, that you know are correct, and that the agent runs and nothing else. Each one
takes bind values, and a bind value is data no matter how it is phrased — there is no spelling of a
customer ID that becomes a DROP.
The input space is now something you can read off a list, review when it changes, and hand to an auditor. That is the whole trick, and there is nothing clever in it.
What you give up
Improvisation, and it is a real cost — we have written about that trade on its own terms.
What is worth adding here is the thing you give up that nobody mourns: the belief that you had measured the risk. Four guards on a SQL prompt feel like four controls. They are four filters over an input space none of them can enumerate, and a filter you cannot test is not a control. It is a hope with a configuration file.
How do I get MCP DB Wizard?
It’s available from our GitHub repo as a Docker image, and the quickstart will get you a running server against your own schema.