<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mcp on EXPLAIN ANALYZE</title><link>https://explainanalyze.com/tags/mcp/</link><description>Recent content in Mcp on EXPLAIN ANALYZE</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 15 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://explainanalyze.com/tags/mcp/index.xml" rel="self" type="application/rss+xml"/><item><title>Exposing Data to an Agent: MCP vs API</title><link>https://explainanalyze.com/p/exposing-data-to-an-agent-mcp-vs-api/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://explainanalyze.com/p/exposing-data-to-an-agent-mcp-vs-api/</guid><description>&lt;img src="https://explainanalyze.com/" alt="Featured image of post Exposing Data to an Agent: MCP vs API" /&gt;&lt;div class="tldr-box"&gt;
 &lt;strong&gt;TL;DR&lt;/strong&gt;
 &lt;div&gt;MCP is a wire protocol; what sits behind it decides the blast radius. In non-prod, pointing it at the database tends to be fine, because unbounded exploration is worth more than the occasional mistake. In prod, the shape that holds up is having the MCP server&amp;rsquo;s tools call an agent-specific API that enforces allowlisted operations, row caps, column masking, and per-prompt audit, rather than the database directly. The version that points at the database tends to surface later as a privacy incident.&lt;/div&gt;
&lt;/div&gt;

&lt;div class="note-box"&gt;
 &lt;strong&gt;Note&lt;/strong&gt;
 &lt;div&gt;This is about the third-party database MCP servers from public registries (Postgres, MySQL, MongoDB, Redis, Elasticsearch), whose load-bearing tool is &lt;code&gt;query(sql_string)&lt;/code&gt; against whatever connection they were configured with. A custom MCP server you wrote to wrap your own API is a different shape and isn&amp;rsquo;t the argument here.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;A revenue dashboard agent runs against production through the MCP server the analytics team stood up last quarter. Marketing asks for enterprise signups in Q1 with their account contacts. The agent generates &lt;code&gt;SELECT id, email, phone, last_login_at, plan, mrr FROM users JOIN subscriptions ... WHERE created_at &amp;gt;= '2026-01-01' AND plan = 'enterprise'&lt;/code&gt;, and 2.3M rows come back. The agent truncates the chat-side display to the first fifty. The full result set leaves the database, crosses the MCP server, and lands in the conversation history the model provider keeps for thirty days. The connection that ran the SELECT held a slot on the read replica for fourteen minutes before the proxy reaped it, and p99 read latency for the customer-facing dashboard tripled over that window. The audit log records one MCP call from &lt;code&gt;mcp-readonly@analytics&lt;/code&gt;. No prompt, no agent identity, no user attribution. The post-mortem has six unanswered questions.&lt;/p&gt;
&lt;h2 id="read-only-doesnt-bound-any-of-this"&gt;Read-only doesn&amp;rsquo;t bound any of this
&lt;/h2&gt;&lt;p&gt;The patch the post-mortem will land on in fifteen minutes is &amp;ldquo;make the MCP connection read-only.&amp;rdquo; The connection already was. Read-only restricts the verb set, and every failure above happened on &lt;code&gt;SELECT&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A read-only SELECT against a 50M-row table is still a SELECT, with the same cost on the replica. Read access on &lt;code&gt;users&lt;/code&gt; is read access on &lt;code&gt;users.password_hash&lt;/code&gt; and &lt;code&gt;users.api_token&lt;/code&gt;. The corruption floor that &lt;a class="link" href="https://explainanalyze.com/p/if-your-guardrail-is-a-prompt-you-dont-have-a-guardrail/" &gt;If Your Guardrail Is a Prompt&lt;/a&gt; describes eventually emits a query against a table the agent had no business touching, and read-only lets it through. And every row the agent reads becomes part of the context window the model provider keeps for thirty days, regardless of what your privacy policy says.&lt;/p&gt;
&lt;p&gt;The verb was never the surface. The catalog is.&lt;/p&gt;
&lt;h2 id="mcp-is-the-wire-the-endpoint-is-the-policy"&gt;MCP is the wire, the endpoint is the policy
&lt;/h2&gt;&lt;p&gt;MCP is a tool-surface protocol. The standard database MCP server exposes &lt;code&gt;query(sql_string)&lt;/code&gt;: the model writes SQL, the server forwards it to whatever connection it was configured with. That makes the MCP server a conduit between the model and the catalog. The agent&amp;rsquo;s effective permissions are the connection&amp;rsquo;s, the agent&amp;rsquo;s query surface is every SQL statement the connection can run, and the audit trail is one row per call from one identity with the SQL as the only payload, which &lt;a class="link" href="https://explainanalyze.com/p/if-your-guardrail-is-a-prompt-you-dont-have-a-guardrail/" &gt;a pre-AI audit log treated as sufficient and an AI-era audit log doesn&amp;rsquo;t&lt;/a&gt;.&lt;/p&gt;
&lt;div class="note-box"&gt;
 &lt;strong&gt;Note&lt;/strong&gt;
 &lt;div&gt;The protocol isn&amp;rsquo;t the problem. MCP solves a real coordination problem: how a model discovers and calls tools across hosts, harnesses, and vendors. What you put on the other end is the part that decides whether you&amp;rsquo;ve exposed a database or an API.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;A SQL conduit also makes the silent-failure shapes from &lt;a class="link" href="https://explainanalyze.com/p/what-ai-gets-wrong-about-your-database/" &gt;What AI Gets Wrong About Your Database&lt;/a&gt; reachable from a chat window: JOIN paths against tables the model inferred from names, &lt;code&gt;status = 1&lt;/code&gt; filters where &lt;code&gt;1&lt;/code&gt; means &amp;ldquo;pending&amp;rdquo; not &amp;ldquo;active&amp;rdquo;, unconstrained bridge tables that multiply rows. None of it requires write access, and all of it lands in the model provider&amp;rsquo;s trace.&lt;/p&gt;
&lt;p&gt;The thing you want on the other end of MCP is an API. Not your customer-facing API. An API written for the agent: a list of operations it can call, with parameters, shaped responses, per-operation entitlements, row caps, timeouts, column masking, and an audit trail that records the agent identity and the prompt that produced the call. The agent never composes SQL. It calls &lt;code&gt;get_enterprise_signups(quarter, plan)&lt;/code&gt; and gets back an aggregated result.&lt;/p&gt;
&lt;h2 id="what-the-agent-api-looks-like"&gt;What the agent API looks like
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Named operations, not raw SQL.&lt;/strong&gt; &lt;code&gt;get_revenue_by_segment(quarter, segment)&lt;/code&gt;, &lt;code&gt;list_active_enterprise_accounts(limit, cursor)&lt;/code&gt;, &lt;code&gt;get_customer_summary(customer_id)&lt;/code&gt;. The agent picks from a menu the platform team curated. Operations get added when an analysis pattern proves useful enough to commit to a stable interface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Responses shaped for the agent, not for the application.&lt;/strong&gt; A revenue-by-segment call returns aggregated totals, not the 2.3M rows behind them. The shape is token-budget aware: a top-N list with totals beats a paged row dump.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Column-level masking inside the API.&lt;/strong&gt; Email becomes a domain plus a hash. Account IDs are opaque tokens the API resolves on the next call, not database primary keys. Sensitive columns are gated by per-operation entitlements granted explicitly to the agent identity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Row caps and statement timeouts the API enforces.&lt;/strong&gt; Every operation has a hard cap on rows and database time. Caps live in code the API team owns, not in the prompt. If an operation needs higher caps, the cap is raised for that operation, not the connection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-call audit with prompt provenance.&lt;/strong&gt; Every call records the agent identity, upstream user, operation, parameters, response shape, row counts, latency, and the prompt that produced the call. Six months later, &amp;ldquo;who ran the query that leaked the enterprise customer list&amp;rdquo; is two &lt;code&gt;SELECT&lt;/code&gt;s away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-agent rate limits.&lt;/strong&gt; Agents loop. Agents retry. The API budgets calls per identity, per operation, and per database time. The budget is a backstop on cost, on the replica, and on the model provider&amp;rsquo;s trace volume.&lt;/p&gt;
&lt;div class="warning-box"&gt;
 &lt;strong&gt;Warning&lt;/strong&gt;
 &lt;div&gt;Don&amp;rsquo;t reuse your customer-facing API for this. Your customer API is shaped for an authenticated user reading their own data. The agent API is shaped for a service account reading across users, returning aggregates rather than rows, masking PII by default, and logging every call against a prompt. Two consumers, two contracts. One API that tries to serve both ends up either too permissive for customers or too restrictive for agents.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The MCP server&amp;rsquo;s tools then become thin wrappers over the API. Each MCP tool corresponds to one API operation. The agent sees &lt;code&gt;get_revenue_by_segment&lt;/code&gt; as a tool; under the hood it&amp;rsquo;s an HTTP call to a service that talks to the database with its own pool, its own identity, and its own rules. The model never speaks SQL to anything.&lt;/p&gt;
&lt;h2 id="what-you-get-for-the-work"&gt;What you get for the work
&lt;/h2&gt;&lt;p&gt;Control over what&amp;rsquo;s exposed, including the catalog. The API is the curated surface; what isn&amp;rsquo;t on the surface isn&amp;rsquo;t reachable. PII is masked or omitted by default, sensitive tables don&amp;rsquo;t have an operation, and the system catalog (&lt;code&gt;information_schema&lt;/code&gt;, &lt;code&gt;pg_catalog&lt;/code&gt;, MongoDB&amp;rsquo;s &lt;code&gt;listCollections&lt;/code&gt;) never reaches the agent. Hide the catalog and you hide the menu of mistakes the model can make. The same surface-narrowing pays a partial dividend on prompt injection: an instruction smuggled into a document the agent reads has no &lt;code&gt;query(sql)&lt;/code&gt; tool to hijack, only the operations on the menu.&lt;/p&gt;
&lt;p&gt;Observability. Who called, when, with what parameters, against what prompt, returning what row counts. You can see which agents are over-fetching, which operations are getting hammered, which prompts produce weird call patterns. Patterns drive the next iteration: the operation called twenty times an hour gets cached, the one that always returns a million rows gets a tighter cap.&lt;/p&gt;
&lt;p&gt;Throttling in a layer the database doesn&amp;rsquo;t reach. Per-agent, per-operation, per-minute, with hard backpressure during a customer-facing incident. This matters most when the agent is pointed at a primary: it shares a connection pool and CPU budget with the customer-facing write path, and a runaway loop or deep aggregation can move primary CPU enough to slow checkout. Statement timeouts on the database alone don&amp;rsquo;t help, because most of the damage lands in the first ten seconds. The API can apply the throttle at the call boundary, before the SQL reaches the connection: per-agent QPS caps, per-operation concurrency limits, a circuit breaker on customer-facing latency.&lt;/p&gt;
&lt;h2 id="where-mcp-direct-still-earns-its-keep"&gt;Where MCP-direct still earns its keep
&lt;/h2&gt;&lt;p&gt;Local development against a seeded test database. Nightly-refreshed sanitized snapshots of production with PII stripped. CI integration tests against ephemeral databases built from fixtures. Single-operator setups where the agent&amp;rsquo;s permissions are explicitly the operator&amp;rsquo;s. In all four, the cost of a mistake is bounded, and the loop of asking any question and throwing the answer away is the point of the environment. Patterns that prove useful in dev or snapshots get promoted to operations on the prod API; the rest stay in dev.&lt;/p&gt;
&lt;p&gt;The dividing line is who pays the cost of a mistake. If it&amp;rsquo;s the same person running the agent, MCP-direct is fine. If it&amp;rsquo;s a customer whose contact list just got absorbed into a model provider&amp;rsquo;s training-eligible context buffer, MCP through the API. A two-engineer team with one agent and one use case can defer the API, but they&amp;rsquo;ll feel the cost the first time a second agent shows up or the first time a privacy review asks where customer data has been read from.&lt;/p&gt;
&lt;h2 id="if-mcp-direct-harden-the-database-side"&gt;If MCP-direct, harden the database side
&lt;/h2&gt;&lt;p&gt;When the team picks MCP-direct in prod anyway, the database layer has knobs worth turning on. None substitute for an API. All are cheap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A dedicated database user for the MCP connection.&lt;/strong&gt; Not the analytics role, not an existing service account, not anything with grants accumulated over years. The agent&amp;rsquo;s user gets its own grants and an audit-log identity that names a single purpose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-schema and per-table grants.&lt;/strong&gt; PostgreSQL&amp;rsquo;s &lt;code&gt;REVOKE ALL ON SCHEMA ... FROM PUBLIC&lt;/code&gt; is the underused default. The agent&amp;rsquo;s role gets read on a small set of schemas (often a dedicated &lt;code&gt;analytics&lt;/code&gt; schema of shaped views), with explicit denies on schemas holding credentials, secrets, audit logs, and the system catalog.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Column-level masking via views or row-level security.&lt;/strong&gt; A view over &lt;code&gt;users&lt;/code&gt; that hashes email and omits &lt;code&gt;password_hash&lt;/code&gt;, &lt;code&gt;api_token&lt;/code&gt;, and &lt;code&gt;phone&lt;/code&gt; closes most PII exfiltration in five minutes. RLS policies on tenant-scoped tables enforce a single-tenant read by default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Aggressive statement timeouts and connection caps.&lt;/strong&gt; &lt;code&gt;statement_timeout&lt;/code&gt; and &lt;code&gt;idle_in_transaction_session_timeout&lt;/code&gt; set per role at five or ten seconds kill runaway aggregations before they touch replica CPU. Connection caps via PgBouncer prevent the agent from monopolizing the pool during a retry storm.&lt;/p&gt;
&lt;h2 id="the-bigger-picture"&gt;The bigger picture
&lt;/h2&gt;&lt;p&gt;The pattern is the one every public-facing system already settled into a decade ago: you don&amp;rsquo;t expose the database to the internet, you put an API in front. The agent is a new principal that deserves the same treatment. MCP is the transport, the way HTTP is the transport for your frontend. Transports don&amp;rsquo;t make policy. Pointing MCP at a database makes the database the endpoint, and the database has no concept of an agent identity, a prompt, or a column-level mask for a non-human caller.&lt;/p&gt;
&lt;p&gt;Building the agent API is the ideal case of an &lt;a class="link" href="https://explainanalyze.com/p/the-10x-is-real-on-internal-tools-youd-otherwise-never-ship/" &gt;internal tool an AI agent can write quickly&lt;/a&gt;: greenfield code, one team owning the contract, low blast radius, replaceable v1, sandbox available for the first cut. A day or two with a coding agent rather than the quarter-long platform initiative it would have been in 2022. It&amp;rsquo;s testable, observable, and the thing that lets you point MCP at production without filing a privacy incident the following Tuesday.&lt;/p&gt;</description></item></channel></rss>