AI Employees, Safely · For engineers
MCP server security: read-only belongs in the database
Three September bulletins for database MCP servers share one lesson: a server's read-only mode is a safeguard, and the database role is the boundary.
Short answer An MCP server can do anything its database credential allows, regardless of its own read-only setting. After read-only bypasses in AWS's database MCP servers in September 2026, AWS told users to connect with a minimal database role. Give each server its own narrow role, transport and tools.
In the first ten days of September 2026, AWS published three security bulletins for its open-source database MCP servers. Two were for awslabs.postgres-mcp-server: crafted SQL could modify data beyond its read-only scopeSource 1, and, where the server's database role was a superuser or a member of pg_execute_server_program, reach operating system commands on a self-managed database hostSource 2. The third, for the MySQL server, put the lesson in one sentence: the read-only mode "is a best-effort safeguard and is not a substitute for correctly scoped database permissions"Source 3.
All three are fixed, but the lesson applies to any database MCP server: it can do what its credential allows, not what its settings promise. Here is how to give each server the smallest credential, transport and tool list that does its job, with a reader role we tested on Postgres 17.
Why agents make this urgent
In both Postgres bulletins, the harmful SQL did not have to come from the user. It could arrive in content submitted or processed while an authenticated user worked with the MCP serverSource 1Source 2. An agent reads a support ticket, a web page or a document, and text inside it can steer the next tool call.
OWASP's Top 10 for LLM applications calls the result Excessive Agency: damaging actions taken in response to unexpected, ambiguous or manipulated output from a model, regardless of the causeSource 4. Its root causes are excessive functionality, excessive permissions and excessive autonomySource 4. You cannot make a model immune to the text it reads. You can decide what its tools are able to do.
The credential is the boundary
A local MCP server that talks over stdio should not use MCP's OAuth flow; the specification says it should retrieve credentials from the environment insteadSource 5. In practice that is the connection string in your client config, and what that string can do, the agent can do.
AWS's workaround is the rule to follow for every database MCP server: connect with a dedicated role that has only the privileges it needs, "so that the database itself enforces the boundary regardless of what SQL reaches it"Source 1. Never connect as a superuser, rds_superuser or the cluster master userSource 1. For read-only use, grant only CONNECT, USAGE and SELECT on the schemas the agent needs, and force read-only transactions at the role levelSource 1. OWASP gives the same advice with a product database: read access to the products table only, enforced by the database permissions of the identity the extension connects withSource 4.
A read-only Postgres role for an MCP server
For an agent that answers questions about products and orders in a database called shop:
CREATE ROLE mcp_reader LOGIN PASSWORD 'change-me';GRANT CONNECT ON DATABASE shop TO mcp_reader;GRANT USAGE ON SCHEMA public TO mcp_reader;GRANT SELECT ON products, orders TO mcp_reader;ALTER ROLE mcp_reader SET default_transaction_read_only = on;
We ran these on Postgres 17.11 against a database with a third table, customers, and asked Postgres what the role may do with has_table_privilege:
| Table | SELECT | INSERT | UPDATE | DELETE |
|---|---|---|---|---|
| products | yes | no | no | no |
| orders | yes | no | no | no |
| customers | no | no | no | no |
Connected as the role, SELECT on products returned rows, SELECT on customers failed with "permission denied for table customers", and an INSERT failed inside the read-only transaction. Four notes. By default, Postgres gives every role CONNECT and TEMPORARY on every databaseSource 6, so the role can already connect to every database and statement 2 adds nothing by itself: run REVOKE CONNECT, TEMPORARY ON DATABASE shop FROM PUBLIC;, grant CONNECT back to the other roles that use shop, and do the same on the cluster's other databases. On Postgres 14 or older, and in databases upgraded from it, every role can also create objects in the public schemaSource 7: run REVOKE CREATE ON SCHEMA public FROM PUBLIC; there too. The last statement sets a default: Postgres describes default_transaction_read_only as the default read-only status of each new transactionSource 8, and a session can change it for itself, so treat it as a seatbelt and the grants as the boundary. Because the grant names tables, a table added later stays closed until you grant it, which is what you want for an agent role. And put the role's password in the MCP client's environment, not in a file the agent can read.
Four more places to cut privileges
- Tools. OWASP lists, as its first example, an agent with access to functions it does not need for the intended operationSource 4. If the job is "look up an order's status", a tool that does that is safer than a tool that runs any SQL.
- Transport. Servers meant to run locally should use stdio, or require an authorization token when they use HTTPSource 9. A community MySQL MCP server, designcomputer/mysql_mcp_server before 0.4.2, bound to 0.0.0.0 with no authentication on any route when switched to its SSE transport; its default stdio mode was not affectedSource 10. The fix turned on Origin and Host checks and recommends 127.0.0.1 as the bind addressSource 10.
- Scopes. For HTTP servers that use OAuth, the specification recommends a minimal initial scope set with only low-risk discovery and read operations, raised only when a privileged operation is first attemptedSource 9. Servers must reject tokens that were not issued for themSource 5.
- Process. Run servers in a container or another sandboxSource 9, with no access to your home directory or other credentials, for the same reason our GitSpawn post keeps untrusted code away from them.
None of this makes prompt injection go away. It changes what a successful one can do: read two of your tables instead of all of them.
- Each MCP server has its own database role Never the owner, a superuser or the master user.
- The role can read only the tables the task needs Check with has_table_privilege; on MySQL, no FILE privilege unless needed.
- Local servers use stdio; HTTP servers require auth
- Tools are as narrow as the job
- Servers run sandboxed and are patched awslabs.postgres-mcp-server 1.1.7, awslabs.mysql-mcp-server 1.0.23, designcomputer/mysql_mcp_server 0.4.2.
Next step
Get new posts as they come out
Every new post, in plain English and in depth. Follow the RSS feed.
Sources
- CVE-2026-85787: awslabs postgres-mcp-server (AWS Security Bulletin 2026-101-AWS, Sep 4, 2026). Retrieved . ↩ ↩ ↩ ↩ ↩
- CVE-2026-87911: awslabs postgres-mcp-server (AWS Security Bulletin 2026-104-AWS, Sep 9, 2026). Retrieved . ↩ ↩
- CVE-2026-85788: awslabs mysql-mcp-server (AWS Security Bulletin 2026-103-AWS, Sep 9, 2026). Retrieved . ↩
- LLM06:2025 Excessive Agency (OWASP Top 10 for LLM Applications). Retrieved . ↩ ↩ ↩ ↩
- Authorization (Model Context Protocol specification, 2026-07-28). Retrieved . ↩ ↩
- Privileges (PostgreSQL 17 documentation). Retrieved . ↩
- Schemas (PostgreSQL 17 documentation). Retrieved . ↩
- Client Connection Defaults (PostgreSQL 17 documentation). Retrieved . ↩
- Security Best Practices (Model Context Protocol specification, 2026-07-28). Retrieved . ↩ ↩ ↩
- MySQL MCP Server: missing Origin/Host validation in SSE transport (GHSA-rqfv-2mw9-78g2, CVE-2026-59971). Retrieved . ↩ ↩