MCP Scoping and Permissions: Why "Three Read-Only Tools" Isn't Actually Read-Only
If you've built an MCP server for your database, you've probably done the obvious thing: expose a handful of specific tools — getOrderStatus, listInventory, getCustomerHistory — instead of a raw SQL passthrough. It feels safer. It isn't as safe as it feels.
The tool boundary is not the security boundary. An LLM agent doesn't call your three tools in a vacuum — it constructs the arguments that go into them, often from untrusted input: a user's chat message, a scraped webpage, a document it was asked to summarize. If any of those three "read-only" tools builds a query by concatenating a string instead of using bound parameters, you don't have three narrow read tools — you have one general-purpose SQL injection point with a friendly name. The tool count was never the thing protecting you; the query construction was.
This is the exact scenario a recent r/mcp thread walked through: someone shipped a warehouse MCP server with three read queries, then got the right question from a teammate — "can the agent do anything outside those three?" — and the answer, on reflection, was murkier than "no." The thread converged on the real fixes, and they're worth stating plainly because they're not exotic.
The hardening that actually matters
A dedicated, minimally-privileged DB user for the connector — not your app's normal service account. If the connector's credential can't run DROP TABLE at the database layer, it doesn't matter what your application code intends to allow; the floor is the actual grant. Parameterize everything, no string-building queries ever, even for values that feel internal and safe. And treat row data as untrusted text once it comes back — if the agent re-injects retrieved data into a later prompt or tool call, that data can carry instructions. Don't assume your own database rows are safe just because you wrote them.
Those are good, standard fixes — and they all live at the database or application layer, which means they depend on someone remembering to configure them correctly and keep them correct as the schema evolves and new handlers get added six months from now. That's the gap worth naming: DB grants are a policy, not an enforcement mechanism, and policies drift.
Enforce it somewhere that doesn't drift
The layer that doesn't drift is the connector itself refusing to pass through anything that isn't a read. That's the design decision behind Bufflehead: it inspects query shape before a query ever reaches your Postgres, MySQL, or BigQuery credential, and rejects write-shaped statements at that layer — regardless of what the underlying DB grant would technically allow. It's not a replacement for DB-level scoping or IAM roles; it's a second, independent check that doesn't depend on the first one having been configured correctly. Bufflehead today supports local files, S3, direct Postgres and MySQL, Postgres/MySQL over an AWS SSM tunnel using your existing IAM roles, and BigQuery — all read-only by design. (No MongoDB support yet, worth saying plainly rather than letting anyone assume otherwise.)
The takeaway for anyone scoping an MCP server to a database: count your tools less, and audit your query construction and enforcement layers more. "Three read-only tools" is a UX decision. Read-only is an enforcement decision, and it should be made in at least two places that don't trust each other.