Implementation Guide

    Giving an AI Agent a Read-Only Postgres Role Through the Postgres MCP Server

    Create a dedicated Postgres login role with no inherited privileges, grant it only CONNECT, schema USAGE and SELECT on the objects the agent needs, set read-only and timeout defaults on the role, and point the MCP server at that role. Treat the role as the authoritative boundary and any MCP server read-only mode as a secondary safeguard. Then prove the boundary by attempting writes and DDL as that role and confirming the database rejects them. A role does not control what query results contain or which tool calls the agent makes, so those need runtime controls.

    Verify that the database layer rejects writes

    • Inspect the role in pg_roles: superuser, createdb, createrole and replication flags should all be false, and pg_auth_members should show no memberships.
    • Use has_table_privilege and has_schema_privilege to confirm SELECT is true and INSERT, UPDATE, DELETE and TRUNCATE are false on each granted table.
    • As agent_ro, attempt INSERT, UPDATE and DELETE against a granted table. Each should fail with a permission error from Postgres.
    • As agent_ro, attempt CREATE TABLE, ALTER TABLE and DROP TABLE. Each should fail at the database.
    • Attempt to read a schema or column you did not grant, and confirm access is denied. Review which functions are executable by PUBLIC, since functions with side effects may need EXECUTE revoked.
    • Repeat the write and DDL attempts through the MCP server itself, then create a new table as the owner role and confirm agent_ro can read it only if you intended that.

    Where enforcement actually happens

    When an agent reaches Postgres through an MCP server, three components can each claim to be read-only: the database role, the connection session, and the MCP server itself. They are not equivalent. A restriction that lives in the MCP server is enforced by application code. If the code has a defect, or the agent finds a query shape the filter did not anticipate, the database will execute whatever the connecting role is allowed to execute.

    That is why this guide treats the database role as the authoritative boundary. Postgres checks privileges on every statement regardless of which client sent it. Any read-only mode offered by the MCP server is worth enabling, but only as a second layer. Before you rely on it, confirm in the documentation for your chosen server and version whether the mode is enforced in the application or by the database session.

    The same reasoning follows general least-privilege practice: a role should hold the minimum permissions needed for its task, and those privileges should be reviewed on a recurring basis. Restricting a role to read-only also limits the damage if its credentials or behavior are abused.

    Create the role and grant only what the agent needs

    Run these statements in a non-production database first. Replace the database, schema and owner names with your own. The sequence matters: start from a role with no privileges, remove broad defaults, then add narrow grants.

    • Create the identity: CREATE ROLE agent_ro LOGIN PASSWORD '<from secret manager>' NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION CONNECTION LIMIT 5; Do not add the role to any other role, since membership is how privileges are inherited.
    • Remove broad defaults: REVOKE ALL ON DATABASE appdb FROM PUBLIC; REVOKE CREATE ON SCHEMA public FROM PUBLIC; Depending on your Postgres version, the public schema may allow object creation by default, so state the revocation explicitly.
    • Allow connection: GRANT CONNECT ON DATABASE appdb TO agent_ro;
    • Allow schema access: GRANT USAGE ON SCHEMA reporting TO agent_ro; Grant USAGE only on schemas the agent needs. Do not grant it on schemas holding credentials, personal data or internal tooling.
    • Grant reads: GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO agent_ro; For sensitive tables, use column-level grants instead, for example GRANT SELECT (id, status, created_at) ON reporting.orders TO agent_ro;
    • Cover future tables: ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA reporting GRANT SELECT ON TABLES TO agent_ro; Default privileges apply only to objects created by the named owner role, so repeat this for each role that creates tables.

    Set connection-level defaults and configure the MCP server

    Two role-level settings reduce risk further. Run ALTER ROLE agent_ro SET default_transaction_read_only = on; and ALTER ROLE agent_ro SET statement_timeout = '15s'; An idle_in_transaction_session_timeout setting is a sensible addition so abandoned sessions do not hold locks or connections.

    Be precise about what these settings do. default_transaction_read_only sets a default, and a session can change it. It is a useful guard against accidental writes, but it is not the boundary. The boundary is the absence of INSERT, UPDATE, DELETE, TRUNCATE and CREATE privileges. The statement timeout limits how long a single query can run, which bounds the cost of an expensive query but does not stop one from being issued.

    For the MCP server, supply the agent_ro credentials in the connection string or environment configuration, and source the password from a secret manager rather than a config file in a repository. If a read replica is available, point the server at it so heavy reads do not compete with production writes. Set an application name on the connection so agent sessions are identifiable in database activity views. If the server offers a restricted or read-only access mode, enable it, and keep the role restrictions identical either way.

    What a read-only role cannot control

    A correctly scoped role answers one question: what can this database identity do? It does not answer what the agent sees or why it asks.

    Result content is the first gap. Any row the role may select can be returned to the agent, and from there into a model context, a chat transcript or a downstream tool. If a granted table contains personal or confidential data, a permitted read is also a disclosure. Column-level grants and views that omit or mask fields narrow this, but they are design-time decisions and cannot respond to a particular request.

    Tool-call behavior is the second gap. The agent decides which MCP tools to call, with which arguments, and how often. Instructions embedded in data the agent reads could steer it toward queries that gather more than the task requires, and the database will see only valid SELECT statements. Statement timeouts bound each query, but they do not cap volume, row counts or the frequency of calls. Large result sets and repeated queries remain possible within the privileges you granted.

    The third gap is evidence. Database logs record statements, but reconstructing which tool call, which agent and which policy decision led to them requires a record made at the point where the call is decided. Protect that record from modification, as you would any audit information.

    Adding runtime policy alongside the role

    Trussed addresses the gaps above without replacing the role. It enforces policy in the execution path across MCP server tool calls, so each call can be allowed, redacted, blocked, rate limited or escalated for human approval at runtime, not reviewed after the fact. Enforcement produces audit-ready evidence of what the agent was permitted to do and what it did. Trussed supports self-managed deployment in the customer cloud, with configurable retention and data residency.

    In practice, the database role limits what is possible, and runtime policy governs what is actually done with that access. A regulated enterprise needs both: the role remains the control that holds even if the policy layer is misconfigured, and the policy layer covers result content and call behavior that the role cannot see.

    Whichever tooling you choose, review the role's grants on a schedule, re-run the verification tests after schema changes, and treat any new table, function or schema as a prompt to revisit what the agent can reach.

    Layers between an agent and production data

    Database role

    Authoritative boundary: grants decide what the agent identity can read, write or alter.

    Connection settings

    Read-only default and statement timeout limit session behavior and query cost.

    MCP server access mode

    Secondary safeguard that may be enforced only in the application layer.

    Runtime policy

    Controls tool calls and result content, which the role cannot see.

    Pair database least privilege with runtime control

    See how policy is enforced on MCP server tool calls in the execution path, and how enforcement produces audit-ready evidence.

    Request a Demo
    Next step

    How does your AI governance program compare?

    See where your program has gaps in less than 2 minutes.