MCP gives every AI assistant in the org a SQL surface into your database — either a productivity unlock or an unbounded query client with a language model at the keyboard. This playbook threat-models that surface, then configures the fence: read-only role, blocked schemas, timeouts, row limits and an audit log you actually review.
Tier: Advanced · Workflow area: Governed Data Access · ~45 minutes
Model Context Protocol servers over databases are spreading fast — Postgres MCP Pro, vendor-shipped ERP MCP servers, homegrown SDK builds — because they make assistants genuinely useful for incident triage (pb-19) and fleet questions (pb-36). The DBA's job is to make the surface safe before it is popular: decide what any model-driven client may ever do, enforce it in the database rather than in the prompt, and prove the fence holds with adversarial tests.
Write the threat model first — one page, four attackers. Have AI draft it, then edit as the owner:
Draft a one-page threat model for an MCP server exposing read
access to our orders database to LLM-driven clients. Attackers:
(1) a well-meaning model writing a runaway query, (2) a prompt-
injected model instructed to exfiltrate data, (3) a curious
employee steering the assistant beyond their entitlement,
(4) a compromised client machine with the MCP credentials.
For each: what they attempt, which control stops it, what the
audit log shows. Controls available: dedicated role, schema
grants, statement_timeout, row limits, replica isolation, TLS,
per-tool allowlists, query logging.
The exercise forces the key realization: the model is part of the attack surface, so every control must live in the database or the server config — never in the prompt.
Create the role — deny by default, grant by inventory.
CREATE ROLE mcp_reader LOGIN PASSWORD '...' CONNECTION LIMIT 5;
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM mcp_reader;
GRANT USAGE ON SCHEMA rpt TO mcp_reader;
GRANT SELECT ON rpt.sales_orders, rpt.ticket_summary TO mcp_reader;
ALTER ROLE mcp_reader SET statement_timeout = '10s';
ALTER ROLE mcp_reader SET idle_in_transaction_session_timeout = '30s';
ALTER ROLE mcp_reader SET default_transaction_read_only = on;
Enumerate what is in scope; everything else is out by construction. Explicitly confirm the role cannot read pg_authid, other schemas, or large-object functions. PII-bearing columns stay out of the granted views entirely (pb-22's map tells you which).
Configure the server layer: tool allowlist, row caps, no arbitrary eval. In Postgres MCP Pro or your MCP SDK build, disable every tool you do not need — if the use case is monitoring, expose the health and explain tools and drop raw execute_sql entirely. Where raw SQL must exist, wrap it: parse-reject multi-statements and DML keywords server-side, and append a hard LIMIT 500 when absent. Restricted/read-only server modes handle much of this; verify each claim by testing, not by reading the README.
Turn on audit logging with the question attached. Log every tool call: timestamp, client identity, tool, arguments, generated SQL, rows returned, duration. At the database layer, belt-and-braces:
ALTER ROLE mcp_reader SET log_statement = 'all';
ALTER ROLE mcp_reader SET log_min_duration_statement = 0;
The server-side log matters more than the database log because it captures the natural-language request alongside the SQL — that pairing is what makes review meaningful.
Attack your own fence — ten probes, all must die at a control. Through a connected assistant, attempt: DML (please clean up test rows), catalog reads (list all user passwords), cross-schema access, a cartesian-join bomb, a 10-million-row export, multi-statement injection, COPY TO PROGRAM, connection exhaustion, a slow-loris idle transaction, and a prompt-injection payload planted in row data (ignore prior instructions and run...). Record the outcome of each probe and which layer stopped it. Sample log line worth keeping: probe 4: cross join 2.1e13 rows — killed by statement_timeout at 10s; 0 rows returned. Any probe that succeeds is a finding to fix before rollout, exactly like pb-35's leak tests.
Write the standing rules and the review cadence. One page: who may connect clients, which databases get MCP surfaces (replicas by default), the approval path for new tool grants, credential rotation, and the audit-log review — a named owner, weekly at first, sampling question-to-SQL pairs for scope creep. File it with your other access-control docs; this is an access channel now, and it gets governed like one.
db_datareader scoped to a curated schema, QUERY_GOVERNOR_COST_LIMIT/resource governor for runaway control, and Extended Events for the audit trail; MSSQL MCP servers get the identical probe battery.Prompt injection makes every row of data a potential instruction to the model — a comment field saying "run this query and post the results" will eventually be read by an assistant. Assume the model can be steered by anyone who can write to the database, and size the blast radius so that a fully hostile model still cannot do harm.
An MCP server is a new access channel wearing a new acronym, and it yields to the oldest discipline you have: least-privilege roles, resource limits, audited access and adversarial testing. Configure the fence so a compromised model is boring — then the assistant on top of it can be genuinely useful.