Sk. Azraf SamiBackend & Applied AI Engineer

There is no write path to find.

2026

MCP Servers Monorepo

Self-contained MCP servers, including a database-agnostic read-only SQL server that turns plain English into guarded queries.

Private · write-up

Write paths exposed
0

read-only by construction, not by instruction

Engine coupling
none

database-agnostic query surface

The problem

Giving a language model direct database access is the fastest way to a destructive query written with complete confidence. The useful version of "ask your database a question" needs the guard rails to live outside the model, where they can be reasoned about and tested.

Architecture

  • Read-only by construction — the server exposes no write path at all, so a prompt injection has nothing to reach for rather than being talked out of using it.
  • Database-agnostic, so the same guarded surface works across engines instead of one bespoke integration per database.
  • Packaged as a monorepo of independent MCP servers, each self-contained, so one server's dependencies never constrain another's.

What I chose, and what I rejected

The premise most natural-language-to-SQL tools get wrong is that safety is a prompting problem. It is not.

A model told “never write to the database” complies right up until something in its context suggests otherwise — a retrieved document containing instructions, a user message engineered to look like a system message, or simply a long conversation where the original constraint has fallen out of attention. The failure is silent until it isn’t, and the blast radius is whatever credentials you handed it.

So the guard here is structural rather than instructional. The server has no write path. There is no DELETE to be tricked into and no UPDATE behind a flag, because neither is implemented. The worst outcome of a completely successful prompt injection is an expensive SELECT.

Why “read-only” is necessary but not sufficient

This is the part I would want to be asked about, because “read-only” sounds like a finished answer and isn’t.

A read-only surface still has two real failure modes. The first is cost and availability: a sufficiently broad query — an unbounded join across large tables, or a cartesian product from a missing predicate — is its own denial-of-service. It does not need to be malicious; a model asked a vague question will happily write it.

The second is disclosure. Read-only says nothing about which rows and columns. A query that joins the wrong way, or selects a table the caller was never meant to see, leaks data without writing a byte. In a database-agnostic tool this is sharper than usual, because the server cannot assume the engine has row-level security configured.

That makes “guarded” a question about the query surface, not about the prompt. The lever is what the server is willing to construct and execute at all — bounded result sets, an explicit allowlist of what is reachable, enforced limits that do not depend on the model asking nicely. The model proposes; the server decides what is even expressible.

Database-agnostic on purpose

Building this against one engine would have been faster and would have made the guarantee worth much less.

The moment you add an engine-specific escape hatch — raw SQL passthrough for “just this one case”, a vendor extension that is easier than the portable equivalent — that hatch becomes the thing everyone uses, and the guarantee becomes “read-only except when it isn’t”. Portability here is not about supporting many databases. It is a discipline that keeps the safe path the only path, because there is nowhere else to go.

The cost is real: you give up engine-specific query optimisation and some expressiveness. For a tool whose entire value proposition is that you can hand it to a language model without auditing every call, that is a good trade.

Why a monorepo of independent servers

Model Context Protocol servers are small, single-purpose things. The temptation is one server that does several jobs, because it is fewer processes to run.

That coupling shows up as dependency conflicts first: one server needs a database driver, another needs a filesystem library, and now the SQL server’s security surface includes whatever the filesystem server dragged in. Keeping them genuinely self-contained means each has the smallest dependency tree it can, and a vulnerability in one does not become a vulnerability in the tool that talks to your production database.

The monorepo gives shared tooling, shared types and one place to review changes, without the runtime coupling. It is the arrangement that makes “this server can only read” a claim you can verify by reading one small package rather than auditing a platform.

What this is really an argument about

Most of the LLM tooling ecosystem is built on instructions: system prompts, guardrail prompts, models judging other models’ outputs. All of that is probabilistic, and probabilistic safety is fine for quality and wrong for authority.

Anything that touches a database, a filesystem or a payment system needs its limits enforced somewhere the model cannot reach. That is not a novel idea — it is how we already build every other system that accepts untrusted input. It just gets forgotten when the untrusted input is arriving from something that sounds cooperative.