MCPg User Guide
How to use MCPg once it’s installed. For getting it installed see
installation.md; for the full per-tool
parameters see tools.md; for short task-shaped
recipes see cookbook.md.
This guide is the narrative walkthrough — read it top-to-bottom the first time, skim it as a reference after.
Contents
- What MCPg is
- Access modes & capability gates
- Connecting an MCP client
- Working with the tools
- Multi-tenancy
- Read-replica routing
- Natural-language SQL
- Server-side cursors
- Data movement
- Reactive workflows: LISTEN / NOTIFY
- Staged migrations
- Migration history
- ORM bridges
- Audit trail
- Observability
- Rate limiting
- Caching
- Feature Flags
- Security defaults
- Troubleshooting
What MCPg is
MCPg is an MCP server that exposes a PostgreSQL database to an AI
agent through a fixed, audited set of 252 tools (read-only mode exposes ~185). The agent never
gets a raw database connection — it can only call the tools MCPg
registers, every call is validated, and every call is logged. MCPg
runs as a single async process and ships as both a PyPI package
(pip install mcpg) and a hardened Docker image.
Access modes & capability gates
The MCPG_ACCESS_MODE setting decides the default tool surface;
additional capability gates unlock higher-blast-radius families
within unrestricted mode.
| Mode | What’s exposed |
|---|---|
read-only (default) |
Catalog introspection, querying, search, health, EXPLAIN — all read-only. |
restricted |
The above plus data writes (DML): run_write, run_maintenance, cancel_query, terminate_backend. No schema changes (DDL), subprocess/shell, LISTEN/NOTIFY, or migrations — the “safe read-write” tier. |
unrestricted |
Everything: the write tools above plus the gated DDL / shell / listen / migrate families below. |
Within unrestricted, the gate vars decide which additional
families come along:
| Gate | Unlocks |
|---|---|
MCPG_ALLOW_DDL=true |
run_ddl, enable_extension, hypertable tools (create_hypertable, add_compression_policy, add_retention_policy), AGE graph DDL (create_graph, drop_graph), staged migrations (prepare_migration / complete_migration / cancel_migration / validate_migration / validate_migration_schema / list_pending_migrations). |
MCPG_ALLOW_SHELL=true |
Subprocess tools — dump_database, restore_database, copy_table_between_databases. Requires the PostgreSQL client binaries (pg_dump / pg_restore / psql) on PATH. |
MCPG_ALLOW_LISTEN=true |
subscribe_channel, poll_notifications, unsubscribe_channel, list_notification_subscriptions. |
Read-only is the default because the typical agent workflow is inspect first, modify second. Each gate is a deliberate opt-in so an experimentation deployment can’t drop a table by accident.
Connecting an MCP client
stdio (Claude Desktop and other local clients)
{
"mcpServers": {
"mcpg": {
"command": "uvx",
"args": ["mcpg"],
"env": {
"MCPG_DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb",
"MCPG_ACCESS_MODE": "read-only"
}
}
}
}
(Use "command": "mcpg" with no args if you’ve done a
pip install mcpg on the system Python.)
Streamable HTTP (Cursor, Continue, custom web clients, etc.)
export MCPG_DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
export MCPG_TRANSPORT=streamable-http
export MCPG_HTTP_AUTH_TOKEN=<random_long_token>
mcpg
Then point the client at http://<host>:8000/mcp and have it send
Authorization: Bearer <random_long_token>.
For production, prefer OIDC over a static token — set:
export MCPG_AUTH_MODE=oidc
export MCPG_OIDC_ISSUER=https://your-issuer/
export MCPG_OIDC_AUDIENCE=mcpg
# Optional — map a JWT claim to the per-request PG role:
export MCPG_OIDC_ROLE_CLAIM=pg_role
OIDC validates every request’s JWT against the issuer’s JWKS (asymmetric algorithms only — RS256/RS384/RS512 + ES256/ES384/ES512).
Working with the tools
The tools group into a few common workflows. Full discovery surface
is in tour.md; long-form parameters in
tools.md; task-shaped recipes in
cookbook.md.
Explore a schema
list_schemas → list_tables → describe_table maps a database.
list_indexes shows a table’s indexes and their access method
(B-tree / GIN / HNSW / IVFFlat / GiST / BRIN). list_extensions
and list_available_extensions show what extensions are installed
or available; describe_table reports the dimension of a
pgvector vector(N) column.
For a one-call snapshot use summarize_table(schema, table) —
columns + PK + FKs + indexes + stats + sample rows, all in one
round-trip. Excellent agent UX.
Query data
run_select(sql, max_rows=1000) runs a read-only SQL query —
validated against the SafeSQL allowlist, executed under a forced
read-only transaction, and capped with a truncated flag.
run_select_parallel(statements) fans out concurrently with
per-statement error isolation.
explain_query(sql) returns a query plan; analyze_query_plan(sql)
summarises it (cost, node types, sequential scans);
why_is_this_slow(sql) rolls EXPLAIN + active queries + locks +
cache + index suggestions into one call.
Search
fuzzy_search (trigram, needs pg_trgm), full_text_search
(tsvector / tsquery, web-search syntax), vector_search (pgvector
k-NN, choose <-> / <#> / <=> operator), vector_range_search
(distance threshold), hybrid_search (vector + FTS via
reciprocal-rank fusion), geo_search (PostGIS k-NN over a
geography column).
When the ParadeDB pg_search extension is installed,
pg_search_run adds BM25 keyword search (single or multi-column
OR), pg_search_more_like_this does similar-document recall with
nine optional pdb.more_like_this tuning knobs, and
hybrid_bm25_vector_search fuses a BM25 leg with a pgvector leg
via reciprocal-rank fusion. See tools.md for the
full pg_search row and the BM-25 integration plan
for design context.
For vector index sizing: recommend_vector_index (HNSW vs IVFFlat)
and recommend_vector_quantization (vector → halfvec / bit
storage advisor).
Diagnose and tune
check_database_health— connections, cache hit ratio, dead tuples, invalid indexes, replication lag, bloat.audit_database(schema)— comprehensive 5-category DBA report (Memory & I/O, Transactions & Connections, Concurrency & Locks, Cleanliness & Bloat, Slow Queries) with per-metric scores, top issues, and prescriptive recommendations.analyze_workload— slowest query templates frompg_stat_statements.read_pg_buffercache_summaryandread_pg_buffercache_relations— shared buffer cache usage summary and relation-level breakdown (requires thepg_buffercacheextension).read_pg_wal_recordsandread_pg_wal_stats— read WAL record information and aggregated statistics over LSN ranges (requires thepg_walinspectextension).recommend_indexes— missing-index heuristics.recommend_index_drops(schema=None, min_index_size_bytes=1_000_000, low_scan_ratio=0.01)— sibling for what to drop: walkspg_stat_user_indexesfor indexes that look like pure cost (never_used,scan_no_fetch,rarely_used), emits a ready-to-runDROP INDEX CONCURRENTLYper candidate. Execution stays with the operator.find_unused_objects(schema)— zero-scan tables and user indexes; great for “what can I drop?”.detect_n_plus_one(min_calls=100)— walkspg_stat_statementslooking for ORM lazy-load loops.
Change data (restricted or unrestricted mode)
run_write(sql)— oneINSERT/UPDATE/DELETE. Add aRETURNINGclause to get affected rows back. Available inrestrictedmode and up.run_ddl(sql)(unrestricted+MCPG_ALLOW_DDL=true) — one DDL statement; can optionally snapshot the structural diff for anaudit_databasefollow-up.enable_extension(name)— allowlisted extensions only.
Visualise
generate_schema_diagram(schema)— Mermaid ER, paste-ready into GitHub / Notion / Obsidian.generate_fk_cascade_graph(schema)— Mermaid blast-radius graph ofON DELETE CASCADE/SET NULL/SET DEFAULTFKs.generate_graph_diagram(graph_name)— Mermaid view of an Apache AGE property graph.
Multi-tenancy
MCPg supports per-request PostgreSQL role switching via
SET LOCAL ROLE so one process can safely serve many tenants from
a single pool.
# Static default role applied to every query that doesn't override:
export MCPG_DEFAULT_ROLE=tenant_a
# Allowlist — when set, the X-MCPG-Role header / OIDC claim must
# be in the list (otherwise 403):
export MCPG_ALLOWED_ROLES=tenant_a,tenant_b,tenant_c
HTTP requests override the default by sending
X-MCPG-Role: tenant_b. OIDC deployments override via
MCPG_OIDC_ROLE_CLAIM — the named JWT claim’s value becomes the
per-request role automatically.
Per-request role selection is honoured per message on the
streamable-http / sse transports — every tool call resolves the role
from its own request, so two tenants sharing one MCP session each run
under their own role. (This is threaded through the SDK’s per-message
request context; earlier releases up to 0.6.10 pinned the role to a
session’s first request — fixed in 0.6.11.)
Role names are identifier-validated
([A-Za-z_][A-Za-z0-9_]*) so they’re safe to inline into
SET LOCAL ROLE "<name>".
RLS policies keyed on current_user then isolate tenants
correctly, and the audit trail records which role ran each call.
Read-replica routing
export MCPG_REPLICA_URLS=postgresql://u:p@replica-1/db?sslmode=require,postgresql://u:p@replica-2/db?sslmode=require
When MCPG_REPLICA_URLS is non-empty, every force_readonly=true
query is round-robin-routed to a healthy replica; writes always go
to the primary. Replica failures fall back to the primary once and
mark the replica degraded for 30 s before re-trying.
Each replica has its own connection pool; per-request SET LOCAL
ROLE composes across replicas (each replica’s pool gets its own
TenantSqlDriver).
list_replicas() reports per-replica index, password-obfuscated
DSN, degraded flag, last error, and seconds-until-retry. Routing
decisions land in the Prometheus mcpg_tool_calls_total counter
under the synthetic tool name __replica_route with statuses
primary / primary_no_healthy / fallback / replica_<n>.
Natural-language SQL
translate_nl_to_sql(question, schema, provider=None, execute=false)
asks an LLM provider to produce read-only SQL for the supplied
natural-language question, given the named schema’s catalog. The
generated SQL is passed through the same SafeSQL allowlist as
any hand-written query before any optional execution.
Provider configuration
MCPg auto-discovers every configured provider from the environment at startup. Set as many of these as you want active — each becomes callable through the tool:
| Provider | Set in env | Default model |
|---|---|---|
anthropic |
ANTHROPIC_API_KEY |
recent Claude Sonnet |
openai |
OPENAI_API_KEY |
gpt-4o-mini |
gemini |
GEMINI_API_KEY (falls back to GOOGLE_API_KEY) |
recent Gemini Flash |
deepseek |
DEEPSEEK_API_KEY |
deepseek-chat |
qwen |
DASHSCOPE_API_KEY (falls back to QWEN_API_KEY) |
qwen-plus |
openrouter |
OPENROUTER_API_KEY |
openai/gpt-4o-mini |
perplexity |
PERPLEXITY_API_KEY |
sonar |
xai |
XAI_API_KEY |
grok-3-mini |
groq |
GROQ_API_KEY |
llama-3.1-8b-instant |
mistral |
MISTRAL_API_KEY |
mistral-small-latest |
together |
TOGETHER_API_KEY |
meta-llama/Llama-3.3-70B-Instruct-Turbo |
fireworks |
FIREWORKS_API_KEY |
accounts/fireworks/models/llama-v3p1-8b-instruct |
deepinfra |
DEEPINFRA_TOKEN |
meta-llama/Meta-Llama-3.1-8B-Instruct |
cerebras |
CEREBRAS_API_KEY |
qwen-3-32b |
nebius |
NEBIUS_API_KEY |
meta-llama/Meta-Llama-3.1-8B-Instruct |
huggingface |
HF_TOKEN |
openai/gpt-oss-20b |
github |
GITHUB_TOKEN (GitHub Models) |
openai/gpt-4o-mini |
sambanova |
SAMBANOVA_API_KEY |
Meta-Llama-3.1-8B-Instruct |
moonshot |
MOONSHOT_API_KEY (Kimi) |
kimi-k2.5 |
glm |
ZAI_API_KEY (falls back to ZHIPUAI_API_KEY) |
glm-4.5-flash |
doubao |
ARK_API_KEY (Volcengine Ark; model must be activated in the console) |
doubao-seed-1-6-251015 |
sakana |
SAKANA_API_KEY (Fugu) |
fugu |
All but the first three (anthropic / openai / gemini) are
OpenAI-compatible vendors: MCPg drives them through one chat-completions
client with vendor-preset endpoints. The whole list is a single
declarative registry (_PROVIDERS in nl2sql.py), so adding a vendor or
refreshing a default model as vendors retire them is a one-line data
change. Base URLs and key env vars were verified against each vendor’s
docs; the default_model column is the volatile one — override it in
configuration with MCPG_NL2SQL_MODEL (applies to the default provider).
Bring your own provider (no code change)
Any other OpenAI-compatible vendor — or a local model server — is declared through configuration alone:
export MCPG_NL2SQL_CUSTOM_PROVIDERS="
myvendor=https://api.myvendor.example/v1|big-model,
privategw=https://llm.internal/v1|my-model|PRIVATEGW_TOKEN,
ollama=http://localhost:11434/v1|llama3.1
"
export MYVENDOR_API_KEY=... # <NAME>_API_KEY convention
export PRIVATEGW_TOKEN=... # explicit third segment for a deviating key var
# ollama needs no key — it's a loopback endpoint
The names above must not collide with a built-in provider (the 19 in the table); a clash is rejected at startup. Use custom entries for vendors not already built in, or for a local model server.
Each entry is name=base_url|model with an optional |KEY_ENV_VAR
third segment. Rules, stated plainly:
- The API key is read from
<NAME>_API_KEY(the convention most vendors follow); the third segment names the env var explicitly when a vendor deviates from it. - Keyless is allowed for loopback endpoints — local Ollama / vLLM / LM Studio are first-class, no dummy key needed.
http://is accepted only for loopback hosts; anything remote must behttps://(mirroring MCPg’s database TLS policy).- The model segment is required — custom endpoints have no sensible
default.
MCPG_NL2SQL_MODELstill overrides it when the custom provider is the configured default. - Every declared name becomes callable via the tool’s
provider=argument and is listed byget_server_info; auto-pick prefers the built-in vendors first, then declared customs in order.
export ANTHROPIC_API_KEY=sk-ant-... # configures anthropic
export OPENAI_API_KEY=sk-... # configures openai too
# Both available; MCPg auto-picks anthropic as the default. Preference
# is the registry order — anthropic → openai → gemini stay first, then
# the OpenAI-compatible fleet (deepseek, qwen, …, xai, groq, …) — so
# existing setups keep their current default.
To pin a specific default, set MCPG_NL2SQL_PROVIDER explicitly:
export MCPG_NL2SQL_PROVIDER=openai # default is now openai
MCPG_NL2SQL_API_KEY (when set) supplies the key for the configured
default provider; it requires MCPG_NL2SQL_PROVIDER to also be set so
MCPg knows which provider it’s for.
Per-call routing
Multiple providers configured? Any caller can route per-call by
passing the optional provider= argument:
translate_nl_to_sql(question, schema, provider="anthropic")
translate_nl_to_sql(question, schema, provider="openai")
translate_nl_to_sql(question, schema, provider="gemini")
translate_nl_to_sql(question, schema) # uses default
This is the recommended shape for one MCPg server, many MCP
clients — set every vendor key on the host, run one MCPg over the
HTTP transport, let each agent / IDE pick its preferred LLM per call.
get_server_info() reports nl2sql_default_provider and
nl2sql_available_providers so a caller can introspect.
Model + endpoint overrides
Override the default provider’s model with MCPG_NL2SQL_MODEL, or
point at an OpenAI-compatible self-hosted endpoint (Ollama, vLLM,
OpenRouter) with MCPG_NL2SQL_BASE_URL. Both apply only when the
call targets the default provider — an explicit provider="openai"
call when the default is anthropic falls back to OpenAI’s default
model + endpoint, since the operator’s overrides are
provider-specific.
The per-call response budget is capped by MCPG_NL2SQL_MAX_TOKENS
(default 2048, hard limit 16384).
Server-side cursors
For pageable reads over millions of rows.
open_cursor(sql) → { cursor_id: "mcpg_e3a91f", … }
fetch_cursor(cursor_id, batch_size=100) → { rows: [...], exhausted: false }
fetch_cursor(cursor_id, batch_size=100) → { rows: [...], exhausted: true } # stop polling
close_cursor(cursor_id) # idempotent
list_cursors()
Each cursor holds a dedicated connection so long-lived cursors
don’t starve the main pool. Cursors auto-close after a 5-minute
idle timeout. The opening SQL passes through the same SafeSQL
allowlist as run_select, so cursors can only read.
Data movement
Five tools, three blast-radius tiers:
- In-process exports (no opt-in needed):
export_query(sql, format="csv|json"),export_table(schema, table, format="csv|json", limit=10000). - In-process imports (
unrestricted): bulk-load viaCOPY … FROM STDIN+ parametrisedexecutemany.import_csv(schema, table, content, header=true, delimiter=","),import_json(schema, table, content). - Subprocess dump / restore
(
unrestricted+MCPG_ALLOW_SHELL=true):dump_database,restore_database,copy_table_between_databasesshell out topg_dump/psql/pg_restorewith hard timeouts (MCPG_SHELL_TIMEOUT_SEC, default 60 s) and output caps (MCPG_SHELL_MAX_OUTPUT_BYTES, default 64 MiB).
Reactive workflows: LISTEN / NOTIFY
subscribe_channel(channel) opens a PG LISTEN on a dedicated
connection and returns a subscription_id. poll_notifications(id,
timeout_ms=0, max_messages=100) drains the per-sub bounded queue,
optionally waiting up to timeout_ms. unsubscribe_channel(id)
removes the subscription; list_notification_subscriptions()
reports the active ones.
Queue size cap: MCPG_LISTEN_QUEUE_MAX (default 1000); overflow
drops the oldest message and surfaces dropped_count on the next
poll. Requires MCPG_ACCESS_MODE=unrestricted +
MCPG_ALLOW_LISTEN=true.
Staged migrations
prepare_migration(name, target_schema, candidate_sql,
ttl_minutes=60) clones the target schema’s structure into a shadow,
applies the candidate SQL there, and runs compare_schemas so you
can review the structural diff. validate_migration separately
applies the candidate to a transient shadow seeded with sampled
real data so failures the diff misses (NOT NULL on existing NULLs,
CHECK violations, type narrowings) surface before apply.
validate_migration_schema(target_schema, reference_schema, candidate_sql)
clones the target structure, applies the candidate SQL, and diffs the
resulting shadow schema against a reference schema using compare_schemas
to ensure the proposed migration exactly achieves the reference state.
complete_migration(id) applies to the target;
cancel_migration(id) drops the shadow without applying.
list_pending_migrations() shows what’s staged.
All staged-migration tools require unrestricted +
MCPG_ALLOW_DDL=true.
Migration history
read_migration_history(schema=None) queries and summarizes applied migrations for popular frameworks (Alembic, Flyway, Diesel, Django, Prisma, Golang Migrate, Goose, Sequelize). Since it is read-only, it does not require DDL privileges or unrestricted mode.
ORM bridges
Eight read-only exporters generate a starting schema/model file from the live PG catalog:
generate_prisma_schema(TypeScript / Prisma)generate_drizzle_schema(TypeScript / Drizzle)generate_sqlalchemy_models(Python / SQLAlchemy 2.0)generate_sqlc_schema(Go / sqlc)generate_diesel_schema(Rust / Diesel)generate_jooq_config(Java / jOOQ codegen XML)generate_ent_schemas(Go / Ent — one file per table)generate_ecto_schemas(Elixir / Ecto — one file per table)
All eight cover: base tables, columns, primary keys, single-column intra-schema FKs, and enums. Cross-schema and composite FKs are documented gaps.
Audit trail
Every tool call — success or failure — is logged to the
mcpg.audit Python logger with the tool name, redacted
arguments (see below), and outcome. Configure where that logger’s
records are shipped via your deployment’s logging stack.
Log Format
By default, the server writes human-readable log messages. You can toggle structured JSON logging using the MCPG_LOG_FORMAT environment variable:
export MCPG_LOG_FORMAT=json # text (default) or json
When set to json, all logging events from mcpg (including tool execution audit events) are serialized as a single-line JSON object containing standard keys (timestamp, level, logger, message). For audit events, the fields (tool, status, arguments, and optionally error) are merged directly into the top level of the JSON log record.
Persistence
With MCPG_AUDIT_PERSIST=true, every run_write and run_ddl is
also persisted to a mcpg_audit.events table (auto-created
idempotently on first write) with redacted arguments + result +
status. Query the table via the list_audit_events tool.
Redaction
Argument values are masked when their key name matches a
case-insensitive regex (matched via re.search, so password
also covers PGPASSWORD, user_password, app.password).
Default patterns:
password, passwd, secret, token, api[_-]?key, bearer,
authorization, database_url, dsn, conninfo
Extend the pattern list via MCPG_AUDIT_REDACT_KEYS (comma-separated
regex fragments). Walks nested dicts / lists / tuples so credentials
buried in result payloads are masked too.
String leaves are passed through the obfuscate_password helper so
an embedded connection-string credential nested anywhere is
scrubbed.
Integrity
To prevent tampering (unauthorized alterations, insertions, or deletions) of your persisted audit events, MCPg supports a signature chain:
MCPG_AUDIT_INTEGRITY=true— Enables the HMAC-SHA256 signature chain.MCPG_AUDIT_HMAC_KEY=<key>— The secret key used to compute and verify the signature chain (required when integrity is enabled).
When enabled, each event carries a signature computed over the deterministic payload and the preceding event’s signature. Verify the entire log using the verify_audit_chain tool, which sequentially checks each link and reports any tampering or deletions.
Observability
The HTTP transports expose:
GET /metrics— Prometheus text-format snapshot. Two series:mcpg_tool_calls_total{tool,status}— counter;status∈ok/error/primary/fallback/replica_<n>/primary_no_healthy(the last four come from the synthetic__replica_routetool).mcpg_tool_duration_seconds_*— histogram per tool.
GET /healthz— liveness probe.GET /readyz— readiness probe (verifies a pool connection).
For stdio deployments, the get_metrics_exposition() MCP tool
returns the same Prometheus payload as a string the agent can hand
to a scrape target.
Slow-call latency logging
To flag slow MCP-side tool calls, MCPg supports a latency threshold warning log:
export MCPG_SLOW_CALL_THRESHOLD_MS=1000 # Threshold in milliseconds (default: 1000)
- When configured (defaults to
1000ms), tool calls that exceed this threshold are logged as a warning tomcpg.server. - Set this to
0or a negative value to disable slow-call logging.
OpenTelemetry tracing
MCPg can emit one OpenTelemetry span per call_tool so traces from
the agent → MCPg → PostgreSQL hop can be stitched together in
Jaeger / Tempo / Honeycomb / your APM of choice.
pip install 'mcpg[otel]' # adds the OTel SDK + exporters
export MCPG_OTEL_ENABLED=true
export MCPG_OTEL_SERVICE_NAME=mcpg-prod # optional, default "mcpg"
Standard OTel env vars (OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_RESOURCE_ATTRIBUTES, …) are honoured by the SDK as usual.
Each span carries mcp.tool.name, mcp.tool.argument_count, and
the outcome status. Argument values are deliberately not
attached — span exporters often ship to third-party SaaS, and
attaching agent-supplied SQL or other args would turn the trace
backend into a side channel for credentials or PII. Audit logging
(local-only by default) is the place to look for full per-call
payloads.
Rate limiting
export MCPG_RATE_LIMIT_ENABLED=true
export MCPG_RATE_LIMIT_MAX_REQUESTS=60 # global cap per window
export MCPG_RATE_LIMIT_WINDOW_SECONDS=60 # window length
export MCPG_RATE_LIMIT_HEAVY_MAX=5 # cap for heavy tools
export MCPG_RATE_LIMIT_HEAVY_WINDOW=60 # heavy-window length
Token-bucket per-tool rate limiting. “Heavy” tools include
run_write, run_ddl, dump_database, restore_database, and
similarly expensive operations. A rate-limited call returns an
error immediately rather than queueing.
Caching
export MCPG_CACHE_ENABLED=true # enable or disable caching (default: true)
export MCPG_CACHE_TTL_SECONDS=300 # default cache TTL in seconds (default: 300)
export MCPG_CACHE_MAXSIZE=1024 # maximum LRU cache entry capacity (default: 1024)
export MCPG_REDIS_URL=redis://localhost:6379/0 # optional Redis connection string for external caching
MCPg provides a high-performance caching layer to save context window tokens and prevent database connection pool saturation from duplicate schema reads:
- Adaptive Caching: Wraps all schema introspection, diagram generators, property graphs, and DBA performance advisors.
- Automatic Invalidation: Any write or DDL statement executed on the database automatically clears the entire cache to prevent serving stale metadata.
- Optional Redis Backend: Soft-dependency Redis async support. If
redisis configured but the library is not installed, the server logs a warning and falls back to a thread-safe, memory-bounded in-memory LRU cache rather than crashing.
Feature Flags
export MCPG_ENABLE_HEAVY_DIAGNOSTICS=true # toggle computationally heavy diagnostics (default: true)
Enables operational gating for administrators over expensive diagnostic tools. When set to false, diagnostic, diagramming, and advisor tools (run_advisors, recommend_indexes, generate_schema_diagram, etc.) remain registered for client discovery but raise a friendly, administrator-disabled RuntimeError at call-time.
Security defaults
MCPg ships with defence-in-depth defaults:
- Read-only access mode by default. Writes and DDL need explicit opt-in.
- SafeSQL allowlist. Every agent-supplied SQL statement parses
through the vendored
SafeSqlDriver(built onpglast) — only whitelisted AST nodes pass through. Statement stacking, comment escapes, transaction-control escapes, DDL insiderun_select,COPY, andDOblocks are refused before execution. - Identifier allowlist. Every interpolated identifier (schema
/ table / column / role names) goes through a
[A-Za-z_][A-Za-z0-9_]*regex. - PG TLS enforcement on startup. MCPg refuses to start if
MCPG_DATABASE_URL(or anyMCPG_REPLICA_URLSentry) points at a non-loopback host withoutsslmode=require(or stronger). UseMCPG_ALLOW_INSECURE_TLS=trueto opt out for non-prod. - Per-session timeouts (
MCPG_STATEMENT_TIMEOUT_MS, default 30 s;MCPG_LOCK_TIMEOUT_MS, default 5 s) self-terminate runaway queries and lock waits. - HTTP authn. Static bearer (
MCPG_HTTP_AUTH_TOKEN) with constant-time compare, or OIDC JWT validation against the issuer’s JWKS (asymmetric algorithms only). - Audit redaction as documented above.
- Graceful shutdown draining (controlled by
MCPG_SHUTDOWN_DRAIN_SECONDS, default 30 s) ensures the server lifespan exit drains all in-flight tool calls before releasing database connection pools. - Multi-tenancy via
SET ROLE. One process can serve many tenants without the RLS-meets-pool footgun.
See security.md for the full threat model and
security-hardening.md for the living
roadmap of shipped (✅) and queued (⬜) hardening items.
Troubleshooting
mcpg: configuration error: …— A requiredMCPG_*env var is missing or invalid; the message names it. See the README env-var reference.MCPG_DATABASE_URL points at a remote host (…) but its sslmode is 'prefer'— The TLS enforcement guard caught a plaintext-capable DSN. Add?sslmode=requireto the DSN or setMCPG_ALLOW_INSECURE_TLS=trueif intentional.- A write tool is missing. Set
MCPG_ACCESS_MODE=unrestrictedplus the matching gate (MCPG_ALLOW_DDL=true,MCPG_ALLOW_SHELL=true, orMCPG_ALLOW_LISTEN=true). fuzzy_search/analyze_workload/vector_search/read_pg_buffercache_summary/read_pg_buffercache_relations/read_pg_wal_records/read_pg_wal_statsreportsavailable: false. The corresponding extension isn’t installed in the target DB. MCPg degrades gracefully — install when ready.- A query is rejected.
run_selectonly permits safe read-only statements; writes, DDL, and multi-statement input are refused by design. Userun_write/run_ddl(underunrestrictedmode) instead. prepare_migrationrefuses with “cannot run inside a transaction” — the candidate SQL contains aCONCURRENTLY/VACUUM/ALTER SYSTEMstatement. The staged-migration workflow always wraps the candidate in a transaction; for those, userun_ddldirectly.- Rate-limit errors. With
MCPG_RATE_LIMIT_ENABLED=true, exceeding the per-tool quota returns “Rate limit exceeded for tool ‘'". Wait the window out or raise `MCPG_RATE_LIMIT_*` defaults. - Replica request fell back to primary. Check
list_replicas()— the affected replica will bedegraded=truewithlast_errorandseconds_until_retry. The 30 s window auto-clears once the replica is healthy. - JWT was rejected. OIDC validates against
iss+aud+exp+nbf+ signature, with 30 s clock leeway. ConfirmMCPG_OIDC_ISSUER+MCPG_OIDC_AUDIENCEmatch what your IdP is minting; check that the JWT algorithm is asymmetric (HS-family is rejected by design). audit_databasereturnspg_stat_statements: Not Installed. Addpg_stat_statementstoshared_preload_libraries, restart Postgres, thenCREATE EXTENSION pg_stat_statements;.