A badly scoped MCP server is a loaded gun pointed at production. Done right, scoping turns a Postgres server from "can DROP any table" into "can SELECT from one schema, read-only, for ten minutes." Here are the four patterns that matter.
Why scoping matters more for MCP than for traditional services
A regular microservice has one caller (another service) and one deploy-time config. An MCP server has many callers (every agent run is different), dynamic requests (the model invents arguments), and ambient trust (env-var credentials get shared across tools). The blast radius of an over-scoped MCP server is bigger than a normal service — and the model invents ways to exercise it you never considered.
If you need the broader security context first, read MCP security best practices for 2026 and Zero-trust MCP architecture alongside this post.
Pattern 1: schema scope
Grant the MCP server access to a named subset of data, not everything.
Postgres example
-- Bad: the default Postgres MCP setup
GRANT ALL ON DATABASE prod TO mcp_agent;
-- Good: read-only role bound to one schema
CREATE ROLE mcp_agent LOGIN;
GRANT CONNECT ON DATABASE prod TO mcp_agent;
GRANT USAGE ON SCHEMA analytics TO mcp_agent;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO mcp_agent;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics
GRANT SELECT ON TABLES TO mcp_agent;
GitHub example — scope a GitHub token to one repo and one permission set:
permissions:
contents: read
pull_requests: write
repositories:
- myorg/docs
The model can still hallucinate DROP TABLE users in its arguments — the database refuses before the statement runs.
Pattern 2: row / resource scope
Schema scope is coarse. Real systems need filtering at the row level.
Postgres RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY mcp_agent_tenant ON orders
FOR SELECT TO mcp_agent
USING (tenant_id = current_setting('app.tenant_id')::uuid);
The MCP server sets app.tenant_id from the caller's context on each call. No matter what query the model writes, it only sees rows for that tenant.
Filesystem — the official filesystem MCP server supports allowed-path lists:
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects/acme"]
}
The server refuses reads or writes outside the allowed root. Always pass a single, specific path — never $HOME.
Pattern 3: verb scope
Separate read from write from destructive actions. Most tool catalogues conflate them.
A practical tiering:
| Verb tier | Examples | Default permission |
|---|---|---|
read |
SELECT, GET, list | always allowed with schema scope |
write |
INSERT, UPDATE, POST, PR comment | requires explicit user confirmation |
destructive |
DELETE, DROP, force-push, payment | requires dual approval + reason |
Express this in the MCP server manifest (custom annotations until the spec formalises it) and enforce at the policy engine. The host UI can grey out destructive calls unless approval is present.
Pattern 4: time scope
Standing credentials are a liability. Scope by wall-clock time.
- Short-lived tokens. Use STS-style credentials with 5–15 minute TTLs. AWS IAM, GCP Workload Identity, and HashiCorp Vault all issue these.
- Connection-level expiry. Postgres
statement_timeoutandidle_in_transaction_session_timeoutprevent a single tool call from holding a connection hostage. - Tool-level windows. Some actions (deploys, merges) should only be callable during business hours unless a human opens a window explicitly.
Rotation is the flip side of time scope — see MCP credential rotation strategy.
Composing the four
In practice you compose all four. A well-scoped Postgres MCP server for a customer-support agent looks like:
identity: mcp_agent (mTLS cert, 10-minute TTL)
schema: analytics.* (SELECT only)
row: tenant_id = caller (RLS enforced)
verb: read (no write grants exist)
time: statement_timeout 5s; connection TTL 10m
The model can ask for anything. The database quietly refuses 99% of it without the server ever needing to parse the SQL.
Failure modes to watch
- Scope creep. Last month's "just this once" grant becomes permanent. Automate revocation on a timer, not on a ticket.
- Token proliferation. Ten MCP servers × one token each = ten blast radiuses. Use a secrets broker that mints per-call credentials.
- Implicit admin. Some cloud SDKs fall back to a personal CLI login silently. Set
AWS_PROFILE=none, pin credentials explicitly. - Logging gaps. Every refused call is a signal. If the policy engine denies a tool call, log it loudly — the model just tried something it shouldn't.
Where to go next
- Zero-trust MCP architecture — the larger frame scoping plugs into.
- MCP access control lists — ACL-style implementation details.
- Claude Desktop Postgres read-only setup — step-by-step for the most common pattern.