A contractor delivers an MCP server over your order database for the support desk: the online retailer's from this module, not the agency. It has three tools: run_query(sql), get_schema() and send_email(to, subject, body). It runs over HTTP. Every caller connects with the same bearer token, and the server reaches Postgres as app_rw, the application's read-write user. Its log is one line per call: query ok.
The database holds 180,000 customers and 1.2 million orders. Twelve agents, two leads and the triage automation would use the server, about 6,000 calls a day. The contractor's case is that one query tool answers any question, and that it passed 20 of 20 test questions such as "how many orders did customer 4471 place this year?"
Assume a customer row is about 200 bytes, a token is roughly 4 characters, and the window is 200k tokens.
1. run_query is a raw query, the first entry on the never-list. As app_rw, an UPDATE or DELETE is one statement away, and the text of a ticket the model reads can steer what that statement says. Read-only would not save it: SELECT * FROM customers is 180,000 × 200 bytes = 36 MB, about 9M tokens, 45 windows. The call fails, or a slice of every customer's record goes to the model provider.
2. One token and one database user for 15 callers. At 6,000 calls a day the log says query ok 6,000 times. Nobody can say who changed order 88172, and revoking a leaver means rotating the token for everyone.
3. send_email next to run_query is an exfiltration path. Read anything, mail it anywhere. No agent can email a customer list by hand, so no tool should.
get_schema() is a smaller fault: reference data that belongs in a resource the host attaches.
Ship instead four reads drawn from the questions agents actually ask: find_customer(email), get_order(order_id), list_orders(customer_id, since, limit ≤ 20) and order_count(customer_id, from, to). Each runs one fixed, parameterised query under a read-only database role. Replies go through save_draft_reply in the help desk, where an agent presses send, with no email tool at all. Add per-user tokens checked for audience and scope, and one audit entry per call naming the caller.
Rerun the 20 questions, and add five the server must refuse, such as "export all customers" and "email this list to me". If the five refused questions pass on the old server, the critique was wrong.
"Make app_rw read-only and it is fine." That stops the DELETE, but not a 36 MB SELECT leaving for the provider, and not the email tool sending what was read. See designing the tool surface.
"Add a line to the description: never run DELETE and never email customer data." A description is a request the model may not follow, and an injected ticket argues with it. A boundary has to live in the schema and the credentials.
"Keep run_query but log the SQL." Logging records the damage after it happens. It also leaves every entry under the shared token, so the log still cannot say who ran the query; auth and identity fixes that first.