Comparison
PgBeam vs a DIY Postgres MCP server
The reference Postgres MCP server runs on whatever your connection string grants, usually full privileges, and the guardrails are whatever you code and maintain. PgBeam is a hosted, policy-enforced MCP endpoint with read-only, masking, budgets, and audit built in.
a DIY Postgres MCP server: A DIY Postgres MCP server is a self-hosted MCP process (often the reference server) that you point at a Postgres connection string so an agent can run SQL through MCP tools.
Running the reference Postgres MCP server is the fastest way to give an agent SQL tools, and for a local toy database it is fine. In production the problem is the connection string: the server can do whatever that string grants, which is usually everything, and any limit beyond that is code you write and then maintain forever. PgBeam is the same MCP surface, hosted, with the policy already enforced on the wire.
PgBeam vs a DIY Postgres MCP server, side by side
| Capability | PgBeam | a DIY Postgres MCP server |
|---|---|---|
| Read-only enforcement | Built in, at the wire | Whatever the DSN grants |
| Table allowlists | Per credential | DIY |
| PII masking | Yes | DIY |
| Query budgets / max rows | Yes | DIY |
| Per-statement audit trail | Yes | DIY |
| Instant revoke / kill-switch | Yes | DIY |
| OAuth-protected endpoint | Yes | DIY |
| Hosting + patching | Managed | You run it |
| Also a guarded connection string | Yes (same policy) | No |
The connection string is the problem
A DIY MCP server is only as constrained as the credential you hand it. Point it at your app's connection string and the agent inherits full privileges; the only guardrails are the ones you coded into the server, and they are now yours to maintain, test, and keep current as the schema changes.
PgBeam issues a scoped agent credential and enforces the policy in the Postgres wire protocol, not in the MCP layer. Read-only, allowlists, masking, budgets, and the kill-switch apply to every tool call, and a blocked call returns an LLM-readable reason the agent can act on.
Hosted, with one policy for both front doors
There is no server process to deploy, patch, or scale. The endpoint exposes ten tools (briefing, query, validate_sql, list_tables, describe_table, explain, schema_catalog, my_permissions, search_docs, and read_doc), runs on a globally distributed proxy near your database, and is OAuth-protected so MCP clients can discover and connect to it.
The same policy engine also backs a guarded connection string, so an agent that speaks raw SQL and one that speaks MCP are governed by the exact same rules. With a DIY server you would build and maintain that twice.
When a DIY Postgres MCP server is the better fit
A self-hosted server is open source, free, runs locally in minutes, and keeps your data path entirely inside infrastructure you control, which for some teams is the requirement that settles the question before any feature list is opened. For a throwaway or local database where you already control the blast radius, it is the right answer and a hosted proxy would be overhead. It is also auditable in a way a hosted service is not: you can read every line of what runs. A maintained option exists and is good, so if you self-host, take crystaldba/postgres-mcp in restricted mode rather than the archived reference server. PgBeam is for production agent access where the guardrails have to be enforced rather than coded, and where more than one agent needs different answers.
Move from a self-hosted server to a policy-enforced endpoint
The MCP client config is the last step, not the first. What changes is which credential is in play and what decides on the SQL inside a tool call, so do the credential first.
Rotate the credential the old server held
Swapping servers does not invalidate the connection string the old one used, and that string has been readable by every process the agent ran. If the server you are leaving is the archived reference one, read the advisory first: its read-only transaction can be escaped with a semicolon, so treat the credential as having been fully privileged whatever you configured.
psql, as an adminALTER ROLE old_agent_role WITH PASSWORD 'a-value-you-will-not-reuse';Write the policy as rules rather than as server code
This is the substitution. Every line here is a guardrail you would otherwise implement, test, and keep current against the schema in a server process you now own.
Terminalpgbeam policies create \ --name agent-mcp \ --mode read_only \ --allow public.orders --allow public.order_items \ --mask customers.email=redact \ --max-rows 500 \ --budget-queries-per-hour 300 \ --statement-timeout-ms 8000Point the MCP client at the hosted endpoint
One credential, two front doors. The same bearer token addresses the MCP endpoint, and the same policy applies to a connection string when the agent stops speaking MCP and opens psql or an ORM.
.mcp.json{ "mcpServers": { "pgbeam": { "type": "http", "url": "https://<project>.proxy.pgbeam.app/mcp", "headers": { "Authorization": "Bearer pba_..." } } } }Verify with the statements you were afraid of
Run them through the policy engine before an agent does, then watch the refusals arrive as the agent works.
Terminalpgbeam policies dry-eval --policy pol_xxx --sql "DROP TABLE public.orders" pgbeam policies dry-eval --policy pol_xxx --sql "SELECT email FROM public.customers" pgbeam audit list --event blocked --limit 10
You can run both for a while. Leave the self-hosted server pointed at a staging database while the hosted endpoint carries production, and compare the audit trail against what your own server logged.
The same tool call, on each side
A self-hosted server exposes a query tool. What the model puts in the sql argument is a string, and whether that string is examined is the entire difference. These verdicts and rule names are what the policy engine emits.
1 SELECT id, total FROM public.orders LIMIT 10
2 SELECT email FROM public.customers WHERE id = 42
3 DROP TABLE public.orders
4 SELECT o.id FROM public.orders o JOIN internal.payouts p ON p.order_id = o.id
5 COPY public.orders TO '/tmp/orders.csv'1 runs Verdict: allow Rule: ok
2 runs Verdict: mask Rule: masked_column Masked: customers.email (redact)
3 refused Verdict: block Rule: destructive_ddl
4 runs Verdict: block Rule: table_not_allowed
Reason: table "internal.payouts" is not in this credential's allowlist
5 refused Verdict: block Rule: copy_blockedLines 3 and 5 are the ones a read-only DSN already covers, and they are the ones people picture when they say the server is read-only. Point the same server at your application's connection string, which is the common case, and even those two run.
Line 4 is the case a server-shaped control cannot reach without becoming a SQL parser itself. The statement names an allowed table first, so a check on the tool name passes it and so does a check on the first relation. Deciding over every relation in the parse tree is what catches the second one.
Line 2 is a rewrite rather than a refusal, and it is the shape of control a self-hosted server would have to implement in the result path: intercepting the row description and the data rows, which means speaking the wire protocol rather than calling a driver.
Every line on the right is a policy sentence rather than a code path. The distinction that matters is maintenance: line 4 keeps working when somebody adds a new schema, and a hand-written allowlist in a server keeps working until someone forgets to update it.
What you are signing up to maintain
Self-hosting is a real option and these are its real ongoing costs, alongside where PgBeam does not help.
The server we run turns out to have a vulnerability
This is not hypothetical for this category. The archived reference server can be escaped out of its read-only transaction with a leading COMMIT, and it was still downloaded 95,430 times in the week ending 2026-09-06. Running a server means owning its patch cadence.
BEGIN TRANSACTION READ ONLY;
COMMIT; DROP SCHEMA public CASCADE;Where this stops: Choosing a hosted endpoint moves that responsibility rather than removing it: you are then trusting our patch cadence instead of your own, and an agent that still holds the database's own connection string is outside either.
A second agent needs different access from the first
Two credentials, two policies, one upstream. Neither the database roles nor the endpoint configuration changes, and revoking one leaves the other running.
Where this stops: If every agent genuinely needs the same access and there is only one of them, this buys you nothing, and a self-hosted server plus a read-only role is the smaller system.
The schema changes and the allowlist goes stale
A policy names relations, so a new table is outside the allowlist until somebody adds it, which fails closed. Statements against it are refused with the rule and the relation named, rather than quietly succeeding.
Where this stops: Failing closed means a legitimate new table breaks the agent until the policy is updated. That is the trade we chose, and it is the wrong trade for a database whose schema changes hourly.
We need to answer what the agent read, months later
The audit trail carries the statement, the decision, the rule, rows, bytes, latency, region and credential, hash-chained so an edited or deleted row breaks the chain and a verify endpoint reports where.
pgbeam audit list --event blocked --limit 20
pgbeam audit verifyWhere this stops: It covers what crossed PgBeam and nothing else. Tool calls to servers that are not the database do not appear in it, so a joined picture means shipping both streams to the same destination.
Our data cannot leave infrastructure we control
Then a hosted endpoint is the wrong shape and this comparison is settled. Self-host a maintained server, put a read-only role under it, and use the guides for the controls a role cannot express.
Where this stops: PgBeam has a self-hosted deployment, which changes who runs the proxy but not the decision above: evaluate it as software you operate, with the patch cadence and the on-call that implies.
Questions
Safe Postgres access for your agents
Start with a 14-day free trial. No credit card required.
Get Started