Changelog
All notable changes to MCPg are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Security
- Bumped
mcpSDK ≥ 1.28.1 (from≥ 1.25.0) to clear three advisories in the resolvedmcp1.27.1: CVE-2026-52870 and CVE-2026-52869 (fixed in 1.27.2) and CVE-2026-59950 (fixed in 1.28.1). No MCPg source change required: the tenancy per-request role path (tenancy._role_from_request→mcp.server.lowlevel.server.request_ctx+ServerMessageMetadata.request_context) is unchanged in 1.28.1, and the full unit + contract suite (incl.test_tenancy.py,test_http_runtime.py) passes.pip-audit --stricton the resolved runtime deps is clean.
Changed
- Bumped
pglast7.15 → 8.2 (the SQL-safety kernel’s parser). pglast 8 trackslibpg_query18-latest (PostgreSQL 18 grammar) and now ships type hints. No behaviour change for MCPg: the full adversarial + fuzz + differential-parity SQL-kernel suites and the PG-grammar characterisation tests all pass unchanged. The AST walker’s__slots__recursion gets a targetedtype: ignoresince pglast 8’sNodebase type doesn’t declare it (the concrete node subclasses still carry their fields there at runtime).
[0.6.11] - 2026-07-10
Added
recommend_indexesnow flags unindexed foreign keys. PostgreSQL indexes PRIMARY KEY / UNIQUE columns automatically but not foreign keys, so an unindexed FK silently forces sequential-scan joins and slow cascading UPDATE/DELETE. For each seq-scan-heavy table the advisor now recommends a btree on any single-column FK that has no covering index (leading the type-driven GIN/trigram suggestions). Closes the gap the demo walkthrough demonstrates.
Fixed
mcpg --help/-hnow prints usage instead of dying with aMCPG_DATABASE_URL is requiredconfig error; an unknown argument is reported clearly (exit 2) rather than falling through to the same misleading message.- README HTTP quickstart pointed clients at
http://localhost:8000; the handler is mounted at/mcp(/ssefor SSE) — corrected so a copy-pasted first connection doesn’t 404. - Stdio startup is no longer silent —
mcpglogs one line to stderr (ready on stdio (<mode> mode) — waiting for an MCP client) so a first-time user doesn’t think it hung.
Security
- Per-request tenancy role now works on
streamable-http/sse. TheX-MCPG-Roleheader (and per-request OIDC role claim) was set in a ContextVar in the ASGI request task, but FastMCP dispatches tool calls in a long-lived per-session task whose context is copied once at session start — so the override was silently pinned to the session’s first request, a multi-tenant isolation break (staticMCPG_DEFAULT_ROLEand stdio were unaffected). The middleware now stashes the validated role on the request, and the tenancy driver resolves it per message from the SDK’s request context at query time. Regression tests cover the frozen-ContextVar case. - Audit log: error text is now redacted. A DSN embedded in a tool’s error
message (e.g. a secondary/replica/data-movement connection) is run through
obfuscate_passwordbefore logging, matching the existing argument redaction, so a password can’t leak into the audit sink. EXPLAIN ANALYZE(io=True) now runs read-only. The only agent-SQL path that executed atforce_readonly=False, reachable in read-only mode, now wraps execution inBEGIN TRANSACTION READ ONLYlike every other path.
[0.6.10] - 2026-07-09
Fixed
-
Cross-database read-cache bleed with
MCPG_SECONDARY_DATABASE_URLS(roadmap 13.1). The process-wide read cache keyed entries by tool + arguments + tenant role but not by the targetdatabaseselector, so a cached read against the primary was served for a secondary with matching arguments (and vice-versa) — most visibly,audit_database(database=…)against a secondary returned the primary’s report. Thedatabaseselector is now folded into the cache key inside_cached_call(normalisingNone/"primary"to one entry) and threaded from every one of the 70 read tools that accept adatabaseargument. Addedtests/unit/test_multidb_cache.pyas a regression guard. -
inner_productrecall advisors reported ~0 recall (pgvector).vector_recall_at_k,recommend_hnsw_ef_search, andrecommend_ivfflat_probesranked the ANN side with pgvector’s<#>operator (the negated inner product — nearest-first underASC) but the brute-force ground-truth side with theinner_product()function (raw dot product — nearest-first underDESC). Under oneASCordering the ground truth became the k farthest rows, collapsingmetric="inner_product"recall to ~0 and wrongly advising an index rebuild. The ground-truthORDER BYdirection is now metric-correct (DESCforinner_product);l2/cosinewere always right. -
Redis cache could bleed across a multi-database fleet. The Redis backend keyed entries without any physical-database identity, so a Redis instance shared by processes serving different databases (all under the
"primary"label) served one database’s cached result for another. Keys are now namespaced by a credential-free hash of the primary’shost:port/dbname; a same-database fleet still shares correctly. -
recommend_index_dropscould recommend dropping a covering index.idx_tup_fetch == 0was treated as “returned zero rows, drop it”, but index-only scans never incrementidx_tup_fetch— so a heavily-used covering index got aDROP INDEX.scan_no_fetchnow also requiresidx_tup_read == 0(the genuine existence-check pattern), leaving covering indexes alone. -
open_cursorcould exceedMCPG_MAX_OPEN_CURSORSunder concurrency. The cap check and the cursor insert happened under two separate lock acquisitions with the connection open in between, so concurrent opens all passed the check and overshot the cap. Opens are now reserved against the cap under the lock. -
WarehousePG
check_segment_healthfalse-alarmed on mirrorless clusters.mode='n'(normal for a healthy primary with no mirror) was counted as “out of sync”. Sync is now only evaluated when the cluster actually has mirrors (role='m').
Changed
-
The primary database is now addressable by its real name (roadmap 13.1). It was registered only under the generic literal id
"primary"while secondaries use their real names, sodatabase="lookup"errored even thoughlookupis the primary. The primary’s advertised id is now its real database name (derived from theMCPG_DATABASE_URLdbname);list_databasesand error messages show it, anddatabase=<real name>works."primary"and omitting the argument remain accepted aliases. -
Install docs: per-OS commands +
docker run --name mcpg. The installation guide’s quick-start and Docker sections now carry ready-to-copy Linux/macOS, Windows PowerShell, and Windows Command Prompt blocks (the only per-OS difference is how environment variables are set and the shell’s line-continuation character), fronted by a one-time translation table. Thedocker runexamples gain--name mcpgso the container is addressable by name. -
De-vendored the SQL-safety kernel (roadmap 18.1). The formerly-vendored
crystaldba/postgres-mcpsql/subpackage (src/mcpg/_vendor/) is replaced by a first-partysrc/mcpg/sql/package —allowlist.py(the permitted statement/node/function/extension policy, as data),safety.py(SafeSqlDriver, thepglastallowlist validator), anddriver.py(SqlDriver/DbConnPool/obfuscate_password). Behaviour is identical (proven by a differential parity harness — 0 divergence — plus the ported 760-LOC adversarial suite, a fuzz pass, and a/security-reviewwith no findings); the public seam is unchanged, so no tool signatures moved. The kernel is now inside the coverage gate,mypy --strict,ruff, andbandit(the vendored code was excluded from all four). MCPg now ships no vendored runtime code. Supersedes ADR-0001 with ADR-0007.
Added
- Three more built-in NL→SQL providers (19 → 22).
translate_nl_to_sqlnow ships GLM (Zhipu AI / Z.ai —ZAI_API_KEY, defaultglm-4.5-flash), Doubao (ByteDance / Volcengine Ark —ARK_API_KEY, defaultdoubao-seed-1-6-251015; the model must be activated in the Ark console), and Sakana Fugu (SAKANA_API_KEY, defaultfugu). Each is a one-line entry in thenl2sql._PROVIDERSregistry; base URLs and key env vars were verified against each vendor’s own docs. - NL→SQL EXPLAIN dry-run pre-flight.
translate_nl_to_sqlruns a non-executingEXPLAINon the generated SQL before returning/executing, catching queries that pass the structural allowlist but reference a non-existent column/table. A planner rejection surfaces aserror="query invalid: …"(SQL returned, nothing executed); a pre-flight timeout degrades to “proceed”. Toggle via theexplain_preflightarg (default on). - NL→SQL prompt-injection hardening (boundary defence).
translate_nl_to_sqlnow fences the user question in<user_request>…</user_request>delimiters and instructs the model to treat it as data, refusing any out-of-scope request via a-- MCPG_REFUSED: <reason>sentinel. The sentinel is detected and surfaced asTranslationResult(refused=True, refusal_reason=…)with emptysql— never forwarded to execution. The AST allowlist remains the enforcement backstop. - Doc-table drift guard.
tools/generate_doc_tables.pyregenerates thedocs/tools.mdtool index and thedocs/architecture.mdmodule map from the single sources of truth (the registered tool surface and the package layout), andtests/contract/test_doc_tables.pyfails CI if either doc drifts — so a new tool or module can’t ship undocumented. - Architecture diagram.
docs/architecture.mdgains a Mermaid layered-request-path diagram (with a prose fallback for the docs site).
Changed
- Documentation refresh across the board. Regenerated the tool index (was ~90 tools stale, listed 3 phantom tools) and the module map (was ~35 modules short, several mis-named); corrected the NL→SQL provider understatement (3 → 19 built-in), stale tool counts (→ 252), Windows file path, PG/Python version claims, wrong-on-use recipe params, AWS/GCP secrets env vars, and access-mode terminology throughout; archived three fully-shipped plans and de-staled the PG 19 status table. Added a per-release documentation-validation checklist and an actionable quarterly NL→SQL default-model sweep to the release process.
restrictedaccess mode now permits data writes. Previouslyrestrictedwas functionally identical toread-only(reads only — the “additional execution constraints” it implied were never implemented). It now grants theWRITEcapability, exposing DML write tools (run_write, maintenance, query management, etc.) while still withholding schema changes (DDL), subprocess/shell,LISTEN/NOTIFY, and migrations. This makes it the intended “safe read-write” tier betweenread-onlyandunrestricted.read-only(the default) andunrestrictedare unchanged. Operators who setMCPG_ACCESS_MODE=restrictedexpecting read-only behaviour should switch toread-only.
[0.6.9] - 2026-07-07
Added
- 12 more built-in NL→SQL providers + a metadata-driven provider
registry —
translate_nl_to_sqlnow ships 19 built-in providers. The expanded fleet adds xAI (Grok), GitHub Models, Hugging Face, Groq, Mistral, Together, Fireworks, DeepInfra, Cerebras, Nebius, SambaNova, and Moonshot (Kimi) alongside the original seven. Each is plug-and-play: set the vendor’s conventional API-key env var and it’s auto-discovered — including the ones that deviate from the<VENDOR>_API_KEYconvention (Hugging Face’sHF_TOKEN, GitHub’sGITHUB_TOKEN, DeepInfra’sDEEPINFRA_TOKEN). Internally, all provider metadata now lives in a single declarative registry (nl2sql.ProviderSpec/_PROVIDERS) that every lookup table derives from, so adding or refreshing a provider — e.g. bumping a default model when a vendor retires one — is a one-line data edit, no code change. Base URLs and key env vars were verified against each vendor’s official docs. Local stacks (Ollama, vLLM, LM Studio) stay on the keylessMCPG_NL2SQL_CUSTOM_PROVIDERSpath by design. - Four new NL→SQL providers: DeepSeek, Qwen, OpenRouter, and
Perplexity —
translate_nl_to_sqlnow supports seven providers. The new four all speak the OpenAI-compatible chat API, so they reuse the existing client with vendor-preset endpoints (one dict entry per vendor, no new HTTP code). Enable with the conventional env vars —DEEPSEEK_API_KEY,DASHSCOPE_API_KEY(orQWEN_API_KEY),OPENROUTER_API_KEY,PERPLEXITY_API_KEY— exactly like the original three; auto-pick preference order keeps anthropic → openai → gemini first so existing deployments keep their current default. Self-hosted OpenAI-compatible stacks (Ollama, vLLM, LM Studio) remain reachable by pointing theopenaiprovider’sMCPG_NL2SQL_BASE_URLat them — now documented. Pairs with the client-integrations guide: Qwen Code / DeepSeek-driven-client users can now run NL→SQL on their own vendor’s models. - Pluggable NL→SQL providers (
MCPG_NL2SQL_CUSTOM_PROVIDERS) — any OpenAI-compatible vendor or local model server can be added through configuration alone, no code change: comma/newline-separatedname=base_url|modelentries (optional|KEY_ENV_VARthird segment for vendors whose key env var deviates from the<NAME>_API_KEYconvention, e.g. Hugging Face’sHF_TOKEN). Keyless declarations are allowed for loopback endpoints, making local Ollama/vLLM/LM Studio first-class; remote endpoints require HTTPS (mirroring the database TLS policy). Declared names are callable viaprovider=, listed byget_server_info, and join auto-pick after the built-ins.
Changed
- Version single-sourced from
src/mcpg/__init__.py—pyproject.tomlnow declaresdynamic = ["version"]and reads__version__via hatchling, so a release bumps exactly one line and pip,mcpg --version, and the MCPserverInfohandshake can never disagree.
Fixed
- Windows: HTTP transport now connects to Postgres. Under
streamable-http/sseon Windows, uvicorn reinstalled theProactorEventLoop, which async psycopg rejects — every database connection failed with a 30 s pool timeout.run_httpnow pins theWindowsSelectorEventLoopPolicyand runs uvicorn on it. stdio was unaffected. serverInforeports MCPg’s own version. The MCPinitializehandshake advertised the MCP SDK’s version instead of mcpg’s; the low-level server’s version is now pinned tomcpg.__version__via theAuditedFastMCPwrapper.
[0.6.8] - 2026-07-05
Added
- One-click installs for Cursor and VS Code + a client integration
guide — README install badges using the official HTTPS deeplink
endpoints (
cursor.com/install-mcp,vscode.dev/redirect; GitHub strips custom URL schemes from READMEs, so the rawcursor:///vscode:forms would render dead). The VS Code install uses a maskedpromptStringinput for the connection URL, so it never lands in plain-text settings. Newdocs/integrations.mdcovers 14 clients: Cursor, VS Code (GitHub Copilot agent mode), Claude Desktop/Code, Windsurf, JetBrains AI Assistant, Cline/Roo, Zed, Google Antigravity (+ Gemini CLI), Qwen Code, Perplexity, ChatGPT (remote connectors), Microsoft Copilot Studio, Continue, and generic HTTP clients — plus an honest “not yet coverable” section for Aider (no native MCP support upstream) and DeepSeek (a model provider, not a client). Badge payloads are pinned by a unit test that decodes them out of both README and docs and asserts cross-file consistency. - One-click Claude Desktop install (
.mcpb) — every release now attaches amcpg-<version>.mcpbdesktop-extension bundle. It uses the MCPBuvserver type, so the bundle is ~2 kB and works on every platform/architecture: Claude Desktop resolves the pinnedmcpgrelease from PyPI at install time. The connection URL is declaredsensitive(OS-keychain storage) and access mode defaults to read-only. Bundle sources live inpackaging/mcpb/; versions are synced from the release tag by the publish workflow (same never-goes-stale pattern asserver.json). - Human-readable titles on all 252 tools — auto-derived from the
snake_case name with an acronym/product-name table (
run_ddl→ “Run DDL”,recommend_ivfflat_probes→ “Recommend IVFFlat probes”), filling the one remaining metadata gap for connector-directory listings. Explicit per-registration titles still win. - Explicit
destructiveHinton every write-capable tool — the 67 write tools are now classified by a curated destructive (18) / non-destructive (49) partition instead of relying on the implicit MCP default. A contract test requires the partition to cover the write surface exactly, so a new write tool cannot ship unclassified. PRIVACY.md— states the self-hosted/no-telemetry posture, the single documented external call (translate_nl_to_sql→ your configured LLM provider), and credential-handling guarantees.
Fixed
- GitHub Release bodies were always the fallback stub. The release-notes extraction in the publish workflow used an awk range pattern whose start line also matched its end pattern (GNU awk collapses that to a single line), so every release since the job was added shipped “See CHANGELOG.md” instead of the changelog section. Replaced with an explicit flag-variable scan; this release is the first to exercise it.
[0.6.7] - 2026-07-03
Added
- MCP
ToolAnnotationson every tool — all 252 tools now publishreadOnlyHint(185 read-only / 67 write-capable) andopenWorldHinton the wire, derived mechanically from the same READ / WRITE / DDL / SHELL / LISTEN capability gates that already enforce access, so a moved tool can never ship a stale hint. Clients like Claude Desktop use these to decide which calls to auto-approve — MCPg’s safety classification is now visible to them, not just internal.openWorldHintisfalseeverywhere excepttranslate_nl_to_sql(the one tool that calls an external LLM API).destructiveHintis deliberately left unset for write-capable tools: the MCP default (true) is the cautious reading. A contract test pins the derivation exhaustively: the hinted read-only set must equal the surface actually reachable in read-only access mode. - Prompt argument descriptions — all 7 arguments across the 3 MCP
prompts (
diagnose_slow_query,bisect_slow_migration,review_rls_policy) now carry wire-visible descriptions. mcpg --demo/mcpg --demo-drop(roadmap 17.1) — one-shot CLI commands that seed / remove a small, deterministic, curated e-commerce demo dataset (400 customers, 120 products, 3,000 orders, 900 reviews) in anmcpg_demoschema inside the configured database. The dataset plants teaching moments for the pivotal tools — an un-indexed foreign key thatanalyze_query_plan/recommend_indexescatch, PII-shaped columns forfind_sensitive_columns, a camelCase naming violation forlint_naming_conventions, review prose forfull_text_search, and an optional pgvectorembeddingcolumn when the extension is installed. Safety: the seed is a single transaction, re-seeding refuses rather than clobbers, and--demo-droponly removes a schema carrying the MCPg ownership marker. A captured walkthrough of the pivotal tools running against this dataset ships asdocs/demo.md, generated from real runs bytools/generate_demo_walkthrough.pyand pinned by integration tests so it cannot silently go stale.
Changed
- Runtime moves to Python 3.14. The Docker image, CI (
lint,security, and the full PG 14-19 + WarehousePG test matrix), and the publish workflow all now build/run on Python 3.14 (previously 3.13).pyproject.tomlstill declaresrequires-python = ">=3.12"and classifies 3.12/3.13/3.14 as supported — this is a change to which version we build and test with, not a narrowing of what versionspip install mcpgaccepts.
Fixed
- Startup warning noise eliminated. Running MCPg printed a wall of
benign pydantic
Field name "schema" ... shadows an attribute in parent "BaseModel"warnings on the firsttools/listcall (themcpSDK builds a pydantic model from each tool-return dataclass, andschema— the natural name for “which Postgres schema” — collides with a deprecated pydantic v1 shim). The specific message is now suppressed at import time; renaming the field would have broken every affected tool’s JSON output shape. A contract test builds the full tool surface and asserts zero shadow warnings leak. - TestPyPI smoke-test race in the publish pipeline. The
wait-for-indexing step polled TestPyPI’s JSON API, which can report a
release “ready” before the PEP 503 simple index (what
pip installactually reads) catches up — observed on the v0.6.6 release, which needed a manual re-run. The step now polls the simple index directly and retry-wraps the install.
[0.6.6] - 2026-07-01
Added
generate_graph_projection— relational → Apache AGE graph-projection mapper (roadmap 2.10). Given a schema (and optional table subset), emits the openCypherCREATE/MERGEstatements that project rows→vertices (one label per table) and FKs→edges into an AGE graph, reusingintrospection.describe_table+introspection.list_foreign_keysand a primary-key probe. Emit-for-review, never executed (likegenerate_test_data/recommend_redistribute):row_limit=0(default) returns a schema-level template plan (one CREATE per label, one MERGE per edge type,$propplaceholders) reading only the catalog;row_limit>0also emits concrete per-row statements (values escaped'→'', NULL properties omitted, capped at 1000 rows/table). Tables without a primary key still get node CREATEs but their edges are skipped with a warning (they can’t be reliablyMATCHed). AGE presence is an advisoryavailableflag — statements are emitted regardless. Caveat surfaced in the tool + warnings: AGE materializes the projection (a graph-load, not a virtual view); run node statements before edge statements. Read-only; every schema / table / graph name is identifier-validated. Typed return →outputSchema. Lives inmcpg.graph_projection;graph_operationsbucket. Closes the roadmap.retrieve_with_context— context-packed hybrid retrieval / “one-shot RAG” (roadmap 9.11). Runs a pgvector k-NN for a caller-supplied query vector, then expands each hit 1 hop along foreign keys — parent rows (outbound FKs) and up tomax_relatedchild rows (inbound, same-schema FKs) — and packs each hit plus its related records into one structured context object, so an agent gets the matching row and its surrounding relational context in a single call. Read-only; the embedding column is dropped from returned rows. Typed return →outputSchema. Lives inmcpg.vector_ops;vector_searchbucket. (1-hop only + same-schema inbound expansion in this first cut.)recommend_ivfflat_probes— IVFFlat counterpart torecommend_hnsw_ef_search(roadmap 9.12). Samples query vectors, builds an exact brute-force top-k ground truth, sweepsivfflat.probesmeasuring mean recall@k + p50/p95 latency, and recommends the smallestprobesclearingtarget_recall. Verifies an IVFFlat index exists on the column (has_ivfflat_index=falsewith guidance otherwise). Typed return →outputSchema. Lives inmcpg.vector_tuner_advanced;vector_searchbucket.analyze_table_bloat— live table + index bloat analysis with targeting (roadmap 2.7). Returns per-table and per-index estimated bloat % (catalog-only estimate by default; precisepgstattuple/pgstatindexmode when the extension is installed, otherwise it falls back to the estimate and reportsmethod="estimate"), plus per-table dead-tuple %, live/dead tuple counts, and byte sizes — sorted worst-first and capped bylimit— so the agent targets VACUUM / REPACK precisely. Read-only;available=falseonly on driver failure. Typed return →outputSchema. Lives inmcpg.health;operations_and_healthbucket.dry_run_ddl— DDL lock / WAL impact dry-run (roadmap 2.8, DDL-gated). Runs a proposedALTER TABLE/ non-concurrentCREATE INDEXinside a transaction under a tightlock_timeout, captures the lock modes held (max_lock_mode, e.g.AccessExclusiveLock), wall-clockduration_ms, and WAL bytes generated (pg_wal_lsn_diffdelta), then always rolls back so nothing persists. CONCURRENTLY / non-transactional statements are rejected up front aseligible=false(reusing the migrations non-transactional regex); lock-timeout (SQLSTATE 55P03) and other errors return as structured results rather than raising. Honest caveat documented: a rewriting DDL still does the rewrite work — and holds its lock — before the rollback, so the dry-run measures impact by incurring it. RequiresMCPG_ALLOW_DDL. Typed return →outputSchema. Newmcpg.ddl_dryrunmodule;operations_and_healthbucket.run_select_tuned— heavy read-only SELECT with elevated, boundedwork_mem(roadmap 2.9). Validateswork_mem(and optionalmaintenance_work_mem) against^\d+(kB|MB|GB)$and a hard 2 GiB cap (unbounded values are an OOM risk), validates the SQL is read-only via the same allowlist asrun_select, then issuesSET LOCAL work_mem='…'; <sql>in a single read-only transaction so the knob applies to that one statement and never leaks back into the pool.SET LOCALis transactional-only, so non-transactional maintenance (CREATE INDEX CONCURRENTLY / VACUUM) is out of scope. Returns aQueryResultlikerun_select. Lives inmcpg.query;query_executionbucket.
Changed
audit_databasenow folds in the sequence-exhaustion (16.1) andpostgresql.confsanity (16.2) advisors as two new scorecard categories — “Sequence Exhaustion” and “Configuration Settings”. The comprehensive scan now surfaces sequences nearing their ceiling and dangerous / mis-sized GUCs (e.g.fsync=off,maintenance_work_mem<work_mem) directly in its top issues + recommendations, without a separateaudit_sequences/audit_settingscall. Both categories degrade gracefully: “Sequence Exhaustion” is omitted entirely whenpg_sequencesis unavailable (PG < 10), and either category drops out on any driver error rather than breaking the scan. The standalone tools remain available unchanged.audit_database’s return shape (AuditReport) is unchanged.dump_databasegains an optionalschemasparameter (roadmap 15.11) to scope apg_dumpto specific schemas instead of the whole database — one repeatable--schema=NAMEflag per entry, each validated against the existing identifier regex (ShellErroron a bad name). Purely additive:schemas=None(the default, and every existing caller) emits no--schema=flag and is byte-for-byte unchanged. Threaded through thedump_databasetool. Sidesteps schema collisions on restore when the source database has schemas the caller doesn’t want captured (e.g. WarehousePG’s built-ingp_toolkit), and is generally useful for scoping large dumps.
Fixed
- WarehousePG/Greenplum MPP-dialect test-fixture portability (roadmap
15.9, 15.10), root-caused from real
warehousepg-latestCI failures. (15.9) Six integration-test tables with an independentPRIMARY KEYplus a separateUNIQUEconstraint/index (a shape WarehousePG rejects — everyUNIQUE/PRIMARY KEYmust be a superset of the table’s distribution key) now append a conditionalDISTRIBUTED REPLICATEDclause via newis_warehousepg/distributed_replicated_clausefixtures intests/integration/conftest.py— a no-op on vanilla PostgreSQL. (15.10)test_copy_table_between_databases_round_trips_against_real_pg’sDROP DATABASE ... WITH (FORCE)cleanup (PG 13+ syntax, unsupported on WarehousePG’s older merge-base) now falls back to explicitly terminating lingering backends viapg_terminate_backendfollowed by a plainDROP DATABASE, preserving the original FORCE intent without needing the syntax. cancel_query/terminate_backendcould be replica-routed to the wrong server. Both sent theirpg_cancel_backend(pid)/pg_terminate_backend(pid)signal withforce_readonly=True, which marks a query “safe to route to a read replica” — but a PID names a specific backend process on whichever physical server it lives on, not something portable across a replica. WithMCPG_REPLICA_URLSconfigured, this could silently target the wrong server (a PID not found there just returnssucceeded=False; a coincidentally-matching PID would be cancelled/terminated instead, with no error at all). Dropped the flag from both calls inmcpg.liveops— a PID-targeted admin signal is a primary-only action by nature, never eligible for replica routing.-
MCP Registry publish step failing silently on every release since 0.6.1.
server.json(the manifestmcp-publisherreads) was checked in with a fixedversion: "0.6.1"at registry launch and never bumped again, so every subsequent release re-submitted “0.6.1” and the registry correctly rejected it as a duplicate version — while PyPI / GHCR / the GitHub Release all kept succeeding, so the failure went unnoticed. Thepublish-mcp-registryjob now patchesserver.json’sversion(andpackages[].version) to the release tag before callingmcp-publisher publish, the same way the sdist/wheel build derives its version frompyproject.tomlrather than a hand-maintained constant — this can’t go stale again. The checked-inserver.jsonis also bumped to0.6.5to match current state. Tests (PG warehousepg-latest)CI lane failing on every run since it was added (roadmap 15.8). The pinned image,warehousepg/warehousepg:latest, never existed on Docker Hub (confirmed 404 — “repository does not exist”); the Docker build failed at the very firstFROMline on every single CI run, masked by the matrix entry’scontinue-on-error: true. Repointed.github/ci-postgres-warehousepg.Dockerfileat the real, actively-publishedwoblerr/warehousepg:7.4.1-WHPG; switched its env vars from the (unread)POSTGRES_*names to theGREENPLUM_*ones that image’s entrypoint actually honours; and taughtci.ymlthat this lane’s superuser isgpadmin, notpostgres, with a longer readiness poll for the slower Greenplum-family single-node cluster init. Considered and rejected EnterpriseDB’swarehouse-pg-dockeras the base image — it publishes no pre-built image and needs an EDB auth token for the RPM build, so it can’t run in an unauthenticated public CI pipeline. The lane stayscontinue-on-error: true— non-gating either way.
[0.6.5] - 2026-06-30
Added
-
Container images on GHCR. The release workflow now builds the
Dockerfileand pushes it toghcr.io/devopam/mcpgon every tagged release (:<version>+:latest), so container users candocker pull ghcr.io/devopam/mcpg:latestinstead of building from source. The push runs only after the human-approved PyPI publish succeeds, so the image tracks the approved release; it authenticates with the built-inGITHUB_TOKEN(no new secrets). -
Multi-database selector with read-only secondaries (roadmap 13.1). One MCPg server can now serve multiple databases. The primary (
MCPG_DATABASE_URL) stays the default target of every tool; additional named, read-only secondaries are configured via the newMCPG_SECONDARY_DATABASE_URLSenv var (comma- or newline-separatedname=dsnentries, e.g.analytics=postgresql://…,reporting=postgresql://…). Every read-capable tool gains an optionaldatabaseargument that selects a secondary by name; omitting it targets the primary, so the primary-only path is unchanged when the var is unset.Secondaries are read-only, enforced at the PostgreSQL level: every query against a secondary runs inside a
BEGIN TRANSACTION READ ONLY, so even a stray write is rejected by Postgres rather than by convention. Write / DDL / shell / listen / migrate tools always target the primary. A secondary that fails to open at startup is marked unavailable but does not abort startup (mirrors the read-replica tolerance). Secondary DSNs get the same TLS enforcement as the primary; names must be simple identifiers ([a-z0-9_]+), unique, and not the reservedprimary.New
list_databasesREAD tool (typed return →outputSchema, routed tooperations_and_health) reports every configured database id (primary first), which is the primary, eachread_onlyflag, and a liveSELECT 1reachability probe +detail. Newmcpg.multidbmodule owns the read-only driver and the discovery shapes. Closes 13.1.
Changed
Fixed
[0.6.4] - 2026-06-27
Added
-
check_pitr_readiness— point-in-time recovery readiness advisor (mcpg.pitr, roadmap 5.3). Composesget_wal_archive_status(5.2) with the GUCs that gate PITR into one verdict: continuous archiving healthy,wal_level>= replica,max_wal_senders>= 1 (so pg_basebackup can stream a base backup), andfull_page_writeson (torn-page safety in replay). Returns a per-gate breakdown (name/ok/observed/remediation), an overallreadyverdict, and an ordered remediation list for the failing gates. Read-only advisor — changes nothing, emits no secrets. Typed return →outputSchema(manifest floor 193 → 194). Completes §5 (Backups & DR). Routed tooperations_and_health. get_wal_archive_status— WAL-archiving health probe (mcpg.wal_archive, roadmap 5.2). Readspg_stat_archiver- the archive-mode GUCs and returns a one-call verdict on whether
continuous archiving is healthy. The early-warning signal for a
failing
archive_command/archive_library(full archive volume, bad object-store credentials, network partition), which otherwise silently accumulates WAL inpg_wal/until the volume fills.healthyis false when archiving is on and the latest attempt failed (the comparison is computed in SQL on the nativetimestamptzcolumns, correct across timezone offsets). Thearchive_commandstring is never echoed (it can carry credentials) — only a booleanarchive_command_set. Read-only; never raises. Typed return → emits anoutputSchema(manifest floor 192 → 193). Companion toread_pg_wal_records(2.3, WAL records); this covers the WAL archive. Routed to theoperations_and_healthbucket.
- the archive-mode GUCs and returns a one-call verdict on whether
continuous archiving is healthy. The early-warning signal for a
failing
- Roadmap-row linkage tooling (roadmap 14.6). The PR template
now prompts for
Advances roadmap row: N.M, and a newtools/roadmap_linkage.pyparsesdocs/feature-shortlist.mdinto a{row_id: status}map so the cited row can be validated at review/merge time (check N.M --openexits non-zero if the row is missing or already ✅;listprints every id with its shipped/in_progress/open status). The convention is documented indocs/contributing/adding-tools.md§12a. Makes the previously-implicit PR↔roadmap linkage explicit and mechanically checkable.
Changed
-
outputSchemasweep — batch 7 (advisors / ops / data / write remainder). Roadmap 8.6. 47 tools converted to typed frozen-dataclass returns acrossadvisors,data_movement,config_advisor,query,workload,indexing,logical_replication,composite,migrations,naming,rls,test_data,test_row_factory,session_advisor,audit_trail,audit,autovacuum,cron,write,schema_diff,migration_ingestion,nl2sql,maintenance. Manifest floor 145 → 192.read_autovacuum_priority’s helper module was hoisted to a top-level import (its annotation can’t resolve a function-local import).prepare_migration/validate_migration_schemastaydict[str, Any]— they restructure their result (isoformat the TTL, flatten the nested diff) rather than return the dataclass verbatim, so they remain documented opaque exceptions. Additive only. -
outputSchemasweep — batch 6 (FDW / extensions family). Roadmap 8.6. 26 tools acrosspg_prewarm,turboquant,redis_fdwconverted to typed frozen-dataclass returns. Manifest floor 119 → 145. Optional (DataClass | None) returns — like list returns — are enveloped by FastMCP into a top-level{"result": ...}, so the manifest pins theresultkey for those (get_turboquant_last_scan_stats). Additive only. -
outputSchemasweep — batch 5 (vector / RAG / search family). Roadmap 8.6. 42 tools acrossvector_ops,vector_tuning,vector_tuner_advanced,rag_efficiency,rag_telemetry,pg_search,textsearch,timescaledbconverted to typed frozen-dataclass returns. Manifest floor 77 → 119. While converting, the now-enforced structured-output schema surfaced a latent JSON-safety bug indetect_vector_outliers: theVectorOutlier.zscorefield storedmath.inffor singleton / zero-variance clusters, which isn’t representable in JSON and the schema rejected. Fixed by typing the fieldfloat | Noneand emitting theNonesentinel for the infinite case (field name unchanged → no consumer break;infwas never valid JSON anyway). -
outputSchemasweep — batch 4 (schema-introspection catalogue reads). Roadmap 8.6. 26 introspection tools converted to typed frozen-dataclass returns:list_schemas,list_tables,describe_table,list_indexes,list_constraints,list_foreign_keys,list_views,list_functions,list_triggers,list_partitions,list_roles,list_grants,list_policies,list_sequences,list_enums,list_domains,list_composite_types,list_foreign_data_wrappers,list_foreign_servers,list_foreign_tables,list_user_mappings,list_publications,list_subscriptions,list_extensions,list_available_extensions,list_generated_columns. Structured-output manifest floor 51 → 77. Additive only (field sets unchanged). -
outputSchemasweep — batch 3 (live-ops / health / catalogue reads). Realises roadmap 8.6. 18 more tools converted fromdict[str, Any]returns to their typed frozen-dataclass returns, so FastMCP auto-derives a JSONoutputSchemaLangChain / LangGraph clients can validate against. Tools:list_locks,find_blocking_chains,walk_blocking_chains,read_pg_stat_io,read_pg_buffercache_summary,read_pg_buffercache_relations,read_pg_wal_records,read_pg_wal_stats,check_database_health,read_migration_history,list_active_queries,monitor_index_build,verify_connection_encryption,cancel_query,terminate_backend,list_cron_jobs,partman_run_maintenance,enable_extension. The structured-output manifest floor intest_tool_output_schemas.pyrises 33 → 51. Additive only — every dataclass field set is unchanged (theslots=Truedrop is invisible on the wire), so no consumer breaks.
Added
-
recommend_hnsw_ef_search— HNSW recall/speed advisor (mcpg.vector_tuner_advanced). Realises roadmap 9.1. The actionable companion to the existinganalyze_hnsw_recall(which sweeps one caller-supplied query vector and returns a raw curve): this one samplessample_queriesrows (default 10) as query vectors, builds an exact brute-force top-k ground truth per query (via the pgvector distance function, which the planner does not route to the ANN index), sweepsef_values(default 16/32/64/128/256) measuring mean recall@k + p50/p95 latency at each, and recommends the smallest value clearingtarget_recall(default 0.95). Crucially it verifies an HNSW index actually exists on the column (has_hnsw_index=falsewith guidance otherwise) — the single-query tool can’t tell a real index from a sequential scan and silently reports recall 1.0. The query row is excluded from its own results (a vector is its own nearest neighbour at distance 0). Returns a typedHnswRecallRecommendationwith asweepofEfSearchSweepPoints. Read-only; routed to thevector_searchbucket. Coexists withanalyze_hnsw_recallper the no-deprecation rule — the raw-curve tool stays untouched. 11 new unit tests. -
Configuration & sizing advisors bundle (
mcpg.config_advisor). pghero / pgtune coverage — three standalone tools filling the gapsaudit_databasedoesn’t cover. Realises roadmap §16 (rows 16.1 + 16.2 + 16.3):-
audit_sequences(warning_pct=80, critical_pct=95)(16.1) — walkspg_sequencesfor serial / identity / explicit sequences nearing their ceiling. Sequence overflow is catastrophic and silent until the nextnextval()raises “reached maximum value” — the int4serialceiling (2³¹-1) is hit far more often than operators expect. Lists only at-risk sequences (sorted byused_pctdesc) with absoluteremainingheadroom; never-advanced sequences (NULLlast_value) are counted but not flagged.available=falseon PG < 10. Pure read. -
audit_settings(total_ram_mb=None)(16.2) — sanity-sweepspostgresql.confviapg_settings. Flags dangerous toggles (fsync=off,full_page_writes=off,autovacuum=off,synchronous_commit=off), cross-setting issues (maintenance_work_mem<work_mem, tinyshared_buffers, lowcheckpoint_completion_target), and — whentotal_ram_mbis supplied (PostgreSQL can’t see host RAM itself) — RAM-relative ratio checks forshared_buffers/effective_cache_size. Memory GUCs are read in bytes viapg_size_bytes(current_setting(...))so the pg_settings unit column never has to be decoded by hand. Each finding carries a stablecodeand a one-linesuggestion. Pure read. -
recommend_postgres_conf(total_ram_mb, cpu_count=4, workload='mixed', storage='ssd', max_connections=None)(16.3) — a pure pgtune-style calculator (no DB connection). Returns recommended values forshared_buffers,effective_cache_size,work_mem,maintenance_work_mem,wal_buffers,min_wal_size/max_wal_size,checkpoint_completion_target,default_statistics_target,random_page_cost,effective_io_concurrency, and the parallel-worker knobs (which only lift above PG defaults whencpu_count >= 4). Memory fields are postgres-ready strings;settingsmirrors everything as a flat{guc: value}dict for direct rendering. Pairs withaudit_settings— audit first, then size.
All three classify under the
advisorsbucket. 30 new unit tests. Standalone (notaudit_databasecategories) by deliberate scope choice — folding into that subsystem’s bespoke scoring is a clean follow-up. Most of pghero’s surface (index suggestions, slow queries, bloat, replication lag, vacuum stats) is already covered by existing tools; these three are the strict net-new gaps. -
-
Agent-surface controls bundle — two related tools touching
describe_self/ agent ergonomics. Realises roadmap rows 8.8 + 14.4:-
Session-intent surface filter (
MCPG_SESSION_INTENTenv var). At server start, MCPg filters its tool surface to the capability buckets that match a declared intent — the narrowed surface is structural (tools never reachtools/list), not policy-checked at call time. Five built-in presets:lookup(read + observability),migration(schema + migrations + audit),vector_rag(catalogue + query + vector / text search + RAG telemetry),monitor(ops + advisors),admin(no filter — sentinel value). Raw bucket ids accepted alongside presets for combinations the presets don’t cover. The escape hatch —describe_selfanddescribe_tool— is always kept regardless of intent so the narrowed agent can still introspect. Big prompt-injection resilience win: a session declaredintent=lookupliterally cannot calldrop_databasebecauserun_ddlwas never registered with FastMCP. Lives inmcpg.session_intent. -
recommend_headline_tools(lookback_days=7, top_n=6)— empirical curation ofdescribe_self’s per-bucketheadline_toolsfrommcpg_audit.events. Readsstatus = 'success'events over the configured window, ranks by call count per bucket, and reportsrecommendedtuples alongsidenewcomers(recommended but not in the current hand-curated list) anddepartures(currently headlined but not in the recommendation). Reviewable recommendation, NOT an auto-applied override — operators decide whether to update the curated tuples. Returnsaudit_table_present=Falsewith a diagnostic when the audit subsystem is off. Lives inmcpg.headline_curator; routed to theobservabilitybucket alongsideanalyze_session_cost.
Combined: 30 new unit tests, one new env var (
MCPG_SESSION_INTENT), one new bucket override, snapshots regenerated. Bundled per the “fewer review cycles for related work” rule. -
-
Logical replication management writes bundle — four DDL-gated tools that close the loop on the existing
list_publications/list_subscriptionsread surface. Realises roadmap row 2.1:-
create_publication(name, all_tables=False, tables=())— emitsCREATE PUBLICATION … FOR ALL TABLESor… FOR TABLE …with each schema-qualified table validated + quoted via the same_pg_quote_identhelper used elsewhere. Rejects names that don’t match the unquoted-identifier regex so SQL injection through the publication name is impossible. -
drop_publication(name, if_exists=False, cascade=False)— symmetric DROP with the standardIF EXISTS/CASCADEmodifiers. -
create_subscription(name, connection_string, publications, enabled=True, copy_data=True, create_slot=True, slot_name=None, synchronous_commit=None)— full WITH-clause grammar.connection_stringis single-quote-doubled (libpq DSNs can contain'legitimately) and is never echoed back: the result’s__repr__redacts theCONNECTION '…'literal so DSN credentials don’t leak into logs or accidentalprint(result).synchronous_commitis validated against{on, off, local, remote_write, remote_apply}before reaching SQL. -
drop_subscription(name, if_exists=False)— plain DROP; the caller pairs it withALTER SUBSCRIPTION … DISABLE(viarun_ddl) when needed.
All four require
MCPG_ACCESS_MODE=unrestricted+MCPG_ALLOW_DDL=true, raise the typedLogicalReplicationErroron validation / driver failure, and clear the read cache on success so the matchinglist_*tools see the new state immediately. -
-
PG 19 characterisation tests bundle — defensive coverage that closes audit rows #16 + #17 + #20 under roadmap row 3.4:
-
tests/contract/test_pg19_sql_characterisation.py— feeds every SQL string MCPg’s PG 19 modules emit throughpglast.parse_sql(libpg_query bindings). Two catalogues:_PARSE_OK_CATALOGUEis the set that must parse on the pinned pglast (catalogue SELECTs,pg_enable_data_checksums(), version probes);_PARSE_FAIL_CATALOGUEpins PG 19-only grammar (REPACK,MERGE PARTITIONS,SPLIT PARTITION,WAIT FOR LSN,GRAPH_TABLE) to the exact token pglast 7.x trips on so the day pglast picks up PG 19’s parser source the test flips and we move the entry to the OK catalogue. A coverage guard ensures every PG 19 module gets at least one entry — a new module without a characterisation pin trips CI loudly. Today’s mocked-driver unit suites can’t catch a typo’d catalogue identifier (pg_state_lockforpg_stat_lock); this file is the cheapest gate that does. -
stats_resetpropagation throughpg19_statsreads —read_pg_stat_lockandread_pg_stat_recovery(both PG 19 views) now selectstats_reset::textand surface it onLockStatRow/RecoveryStatRowwith aNonedefault. PG 19 added the column to everypg_stat_*view that didn’t already have it; the no-deprecation rule says we surface it so callers can tell “no contention” from “counters were just reset”. Two new unit tests pin the propagation; the contract test pins the SQL substring against the module source. -
docs/postgres-fdw-pushdown.md— characterisation of how PG 19 widenspostgres_fdwpushdown (array operators, extended statistics, MERGE), the operator-managed setup recipe MCPg supports today, and the return conditions for a dedicated FDW tool surface. MCPg ships no FDW-specific tool today; the doc explains why and what would change that.
-
-
Agent-ergonomics bundle — two related M-effort tools that together make agentic deployments cheaper to run. Realises roadmap rows 8.1 + 8.7:
-
generate_test_row_for(schema, table)— produces ONE realistic test row designed to insert cleanly against the catalogue as it stands today. Sibling of the existinggenerate_test_data(bulk synthetic INSERTs that ignore FKs) andseed_table_with_sample_data(bulk + execute); this one is for the shadow-migration workflow where a single row that actually inserts matters more than volume. Heuristic order per column: identity /GENERATED ALWAYScolumns are skipped (server fills them in); FK columns sample one existing row from the referenced table so composite FKs stay consistent; column-name patterns produce realistic values (*_email→user_N@example.com,*_url→https://example.com/r/N,*_at→ recent timestamp,*_phone/country_code/currency_code/ip_address/first_name/ etc.); type-driven synthesis is the fallthrough. Returns aGeneratedTestRowwithinsert_sql(single ready-to-execute INSERT) and a per-columnColumnFillcarrying the chosen heuristic so reviewers see the decision trail. Lives inmcpg.test_row_factory; routed to thedata_movementbucket alongside the bulk siblings. -
analyze_session_cost(lookback_minutes=60, hot_threshold=10)— readsmcpg_audit.eventsand surfaces hot-path inefficiencies before they cost real tokens. Two finding classes:redundant_listing(catalogue-listing tools calledthreshold → suggests
get_compact_schema) andhot_repeated_call(any other tool called > threshold → suggests conversation-memory caching). Returnsaudit_table_present=Falsewith a clear diagnostic when the audit subsystem is off, distinguishing it from an idle session (which gets anidle_sessionfinding). Lookback lands as a bound parameter viamake_interval(mins => %s)— no f-string composition. Lives inmcpg.session_advisor; routed to theobservabilitybucket alongsidedescribe_tool.
Combined: 37 new unit tests, two new bucket overrides, snapshots regenerated. The two tools share the agent-ergonomics theme but no code — bundled per the “fewer review cycles for related work” rule.
-
-
warehousepg-latestCI lane — adds a new matrix entry to the CItestjob that runs the full suite against a community WarehousePG (Greenplum-derived MPP) image. Lives alongside the existing PG 14-19 lanes; uses.github/ci-postgres-warehousepg.Dockerfileto pull the upstream image.Gated
continue-on-error: true(viaexperimental: trueon the matrix entry) so a missing image tag / upstream outage doesn’t block the main suite while the community image landscape stabilises. The lane is the characterisation-test surface for the wholemcpg.warehousepg.*family — it catches SQL syntax that passes unit tests against mocked drivers but breaks on the real parser. Realises roadmap row 15.8 — closes section §15 in full.Postgres client tooling install is skipped for the WarehousePG lane (WarehousePG is wire-compatible with libpq; the runner’s default
psqlworks) and the Dockerfile branch in the test job is updated alongside the existing PG 19 special-case. -
WarehousePG advisors bundle — two MPP-aware analysis tools. Both gate on the 15.1 status probe; on vanilla PG return
available=falsecleanly. Realises roadmap rows 15.6 + 15.7 (Bundle B — advisors):analyze_mpp_query_plan(sql)— runsEXPLAIN (ANALYZE, FORMAT JSON)(viamcpg.query.explain_query(io=True), so writes / DDL stay rejected) and rolls up MPP-specific facts: slice count, motion-node inventory (Redistribute/Broadcast/Gather), per-motion senders/receivers/rows.redistribute_count > 0flags “data isn’t co-located with the join key” — the canonical signal an agent should suggest a key redistribution. Returnsavailable=falseon EXPLAIN failure with the underlying error message. (15.6)recommend_redistribute(schema, table)— distribution-key advisor. Reads the currentgp_distribution_policy, thenpg_stats.n_distinctfor every column on the table, ranks candidates by approximate distinct count (translating negative encoded fractions to absolute estimates usingreltuples), and flags a rewrite when the current key has <segment_count * 10distinct values AND a better candidate exists. Returns a ready-to-reviewALTER TABLE … SET WITH (REORGANIZE=TRUE) DISTRIBUTED BY (…)statement — never executes DDL itself. Skips RANDOM and REPLICATED tables (no key to optimize). (15.7)
Bucket overrides:
analyze_mpp_query_plan→query_execution;recommend_redistribute→advisors. Driver errors on either data probe surface asavailable=falsewith the actual error indetail. -
WarehousePG read introspection bundle — four read-only tools for the MPP-specific catalogue surface. All gate on the 15.1 status probe; on vanilla PG they return
available=falsewith a clean diagnostic rather than raising. Realises roadmap rows 15.2 + 15.3 + 15.4 + 15.5 (Bundle A — read introspection):list_distribution_policies(schema=None)— joinsgp_distribution_policy+pg_class+pg_attributeto surface per-tablemethod(HASH/RANDOM/REPLICATED),distribution_columns(in catalog order for composite hash keys), andnum_segments. Big agentic win for “is my data co-located with my joins” diagnosis on MPP workloads. (15.2)check_segment_health()— walksgp_segment_configurationand rolls up per-segmentstatus(‘u’ = up),mode(‘s’ = sync / ‘n’ = not-in-sync / ‘c’ = changetracking), androlevspreferred_role(post-failover detection). Top-levelunhealthy_count+out_of_sync_countlet agents branch without walking the segments array. (15.3)describe_ao_table(schema, table)— readspg_appendonlyfor AO / AO-CO storage metadata: row vs column orientation, compression type / level, block size, checksum. Returnsis_ao=falsecleanly for regular heap tables. (15.4)list_resource_groups()— readsgp_toolkit.gp_resgroup_statusforconcurrency,cpu_max_percent,cpu_weight,memory_limit,memory_shared_quota, plus livenum_running/num_queueing. Pairs withanalyze_workloadfor “where’s my workload time going” diagnosis. (15.5)
All four live in
mcpg.warehousepg. Bucket overrides: introspection tools classified underschema_introspection; health/ops tools underoperations_and_health. Driver errors on the data query surface asavailable=falsewith the actual error indetail. Pure read-only; no caller-supplied identifiers in identifier slots. -
get_warehousepg_statusMCP tool — gating status probe for the newmcpg.warehousepg.*family. Detects whether the connected server is a WarehousePG (Greenplum-derived MPP) cluster by combining TWO signals that must agree:- Version banner —
SELECT version()mentionsWarehousePGorGreenplum(case-insensitive; legacy Greenplum branding honoured for compatibility). - Catalog presence —
pg_catalog.gp_segment_configurationview exists (probed viato_regclass()to avoid permission errors from segment-table SELECTs).
When both agree, surfaces
coordinator_role(coordinatoron modern WarehousePG,masteron legacy Greenplum),segment_count(primary segments only, excludes mirrors + coordinator), andmirroring(Truewhen ≥1 mirror exists). On vanilla PG returnsavailable=Falsecleanly so the rest of themcpg.warehousepg.*family can stay inert without raising.Lives in
mcpg.warehousepg. Classified underoperations_and_health. Read-only; never raises — driver failures surface asavailable=Falsewith the actual error indetail. Realises roadmap row 15.1 (gating prerequisite for sub-items 15.2-15.8). - Version banner —
-
translate_nl_to_sqladvertises PG 19 SQL constructs in its system prompt when the connected server actually supports them. Probescurrent_setting('server_version_num')once per translation; on PG 19+ the prompt grows a block describing:GROUP BY ALL— emit instead of repeating non-aggregate column lists when grouping is exhaustive (the real win — the LLM will produce shorter, more readable queries).- Temporal
UPDATE(FOR PORTION OF) — mentioned for context only; the hard read-only rule still forbidsUPDATEin any form. ON CONFLICT DO SELECT— same, only valid insideINSERT, forbidden here.
Probe failures (driver error, version unparseable, empty rows) fall back to the conservative pre-PG-19 prompt — losing the shortcut is preferable to emitting syntax the server rejects. Realises sub-item #18 of roadmap row 2.5 (the PG 19 PR-10 small- tools batch) — completes the entire 2.5 batch.
-
list_grantsgains apg_get_acl()ACL string on PG 19+. PG 19 introducespg_get_acl(classid, objid, objsubid)— the canonical aclitem array ({user=privs/grantor, …}) the catalog stores natively and\dpdisplays.list_grantsnow invokes it alongside the portableinformation_schema.table_privilegesquery and attaches the returned string to every emittedGrantInfo.acl. On PG ≤ 18 (no such function) the call’s exception is swallowed andaclstaysNone— the information_schema rows still surface unchanged, so existing consumers see no behaviour change.The new field gives agents access to the privilege-flag form (
rSELECT,wUPDATE,aINSERT,dDELETE,DTRUNCATE,xREFERENCES,tTRIGGER, with*suffix forWITH GRANT OPTION) without reverse-engineering the information_schema view’s one-row-per-(grantee, privilege) shape. Realises sub-item #21 of roadmap row 2.5 (the PG 19 PR-10 small-tools batch). -
audit_databasegains anAuthentication & Password Hygienecategory. Two new security metrics surface frompg_authid:- Role Password Expiration — counts login roles whose
rolvaliduntilhas expired (CRITICAL) or expires within the next 30 days (WARNING). Carries a sample of the affected role names plus the canonicalALTER ROLE … VALID UNTILremediation. - MD5 Password Hashes (PG 19 deprecated) — flags login
roles whose
rolpasswordis still an MD5 hash. PG 19 deprecates MD5 password auth (still functional, scheduled for removal). Suggestion points at the SCRAM-SHA-256 migration path: flippassword_encryption = scram-sha-256, thenALTER ROLE … WITH PASSWORD '…'per role.
Both probes read
pg_authidwhich requires superuser; when the audit role can’t read it the category surfaces a clean per-metric WARNING (“Could not read pg_authid: …”) rather than failing the whole audit. CRITICAL on password expiry deducts 25 points from the category — enough to pull it below the GOOD threshold on its own, since expired passwords block new logins. Realises sub-item #13 of roadmap row 2.5 (the PG 19 PR-10 small-tools batch). - Role Password Expiration — counts login roles whose
-
read_autovacuum_priority(limit)— read-only catalog tool that surfaces tables most urgently needing autovacuum, ranked by how close theirn_dead_tupcount is to the per-table autovacuum threshold (autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples, honouring per-tablereloptionsoverrides). Each row carries:priority—overdue(past the threshold),watchlist(within 50%), orborderline(everything else surfaced).- The full inputs:
n_dead_tup,n_live_tup,n_mod_since_analyze,n_ins_since_vacuum(PG 13+),vacuum_threshold,analyze_threshold,last_autovacuum,last_autoanalyze,autovacuum_enabled. dead_tuple_ratio/analyze_ratiofloats for sorting beyond the categorical bucket.
Top-level
overdue_countlets agents branch without walking the list; the count reflects the cluster, not thelimit-clipped output, so pagination can’t hide an urgent backlog. Driver failures surface asavailable=False(no raise — same convention asmcpg.health). Lives inmcpg.autovacuum. Classified under theoperations_and_healthcapability bucket. Realises sub-item #14 of roadmap row 2.5 (the PG 19 PR-10 small-tools batch). -
explain_query/analyze_query_plangain anio=trueoption for PG 19’s extendedEXPLAIN (ANALYZE, BUFFERS)output. Withio=true, the tools switch fromEXPLAIN (FORMAT JSON)(plan only, no execution) toEXPLAIN (ANALYZE, BUFFERS, TIMING, FORMAT JSON)(executes the query and surfaces real buffer + I/O timing per node).On
analyze_query_plan, the rolled-upQueryPlanAnalysisgains optional fields:actual_total_time_ms,actual_rows,shared_blocks_read,shared_blocks_hit,io_read_time_ms,io_write_time_ms, plus the PG 19 asynchronous-I/O split asaio_read_blocks/aio_write_blocks. Pre-PG-19 servers don’t carry the AIO keys in BUFFERS output — those fields stayNonerather than synthesising0, so the agent can tell “no AIO observations” apart from “zero AIO observations”.Pairs with the already-shipped
recommend_io_method(PR #131) to close the AIO observability loop end-to-end: the advisor tells you whichio_methodto pick, thenanalyze_query_plan(io=True)shows what the AIO path actually does on a representative workload. Realises roadmap row 2.6.Safety:
io=Truetriggers actual execution. The safety allowlist still rejects writes / DDL — the pre-flight validates the SQL viaSafeSqlDriver._validate(EXPLAIN (FORMAT JSON) <sql>)before running the ANALYZE variant, soexplain_query(sql='DROP TABLE x', io=True)still raisesQueryErrorinstead of dropping the table. Theio=Falsedefault is unchanged — agents who don’t opt in pay no execution cost. -
describe_tool(name)MCP tool — single-tool deep-dive introspection for the agent self-recovery loop. Returns the registered description,inputSchema,outputSchema, and bucket metadata for one tool by name. Handy when:- the transport surfaces
tools/callbut nottools/list(some MCP clients don’t expose the latter as a callable surface); - an agent hits a schema-validation error and wants to verify
one tool’s shape without re-walking the full
describe_selfpayload (which on a maximal-flag server is ~250 tools).
Unknown names come back with
registered=falseplus adid_you_meanlist — close-match suggestions scored viadifflib.get_close_matchesso a single-letter typo likerun_sleectsurfacesrun_selectrather than forcing a wider catalogue scan. Emptydid_you_meandistinguishes “typo with obvious fix” from “wrong server / wrong universe”, which is itself the diagnostic.Builder lives in
mcpg.tool_introspection; bucket classification reusesmcpg.about.classify_tool. Read-only, no DB access. Classified under theobservabilitycapability bucket. Realises roadmap row 8.5. - the transport surfaces
-
get_server_infonow reportswal_level+effective_wal_level. PG 19 introduced the runtime-distincteffective_wal_levelGUC — it’s what the server is actually emitting, which can lag behind the configuredwal_level(post-ALTER SYSTEMbut pre-reload). Surfacing both viaget_server_infolets agents detect this drift at a glance without reaching for the deeperget_logical_replication_statustool. On PG ≤ 18 (no such GUC)effective_wal_levelreturnsNone, distinguishing “not known on this server” from “configured to a known value”.The GUC probe is best-effort: driver errors during the probe leave both fields
Nonerather than aborting the response, so a transiently-flapping connection doesn’t break the whole tool.build_server_infois now async + accepts an optional driver; the existing dataclass shape is backward-compatible — the new fields default toNone. Realises sub-item #19 of roadmap row 2.5 (the PG 19 PR-10 small-tools batch). -
MCP prompts surface — three pre-built investigation playbooks exposed via the MCP
prompts/list+prompts/getprotocol primitives:diagnose_slow_query(sql)— routes the agent throughexplain_query,analyze_query_plan,recommend_indexes,analyze_workloadin order, then asks for a structured root-cause + fix + impact report.bisect_slow_migration(migration_id, baseline_schema, current_schema)— confirms the migration ran, scopes what changed viacompare_schemas, validates suspects with per-queryanalyze_query_plan, and ends with a forward-fix / rollback / no-op remediation decision tree.review_rls_policy(schema, table)— RLS coverage audit:describe_table→list_policies→audit_database(category= 'security'), with gap analysis against identity-bearing columns. Diagnosis-only — proposesCREATE POLICYstatements but never applies them.
Lives in
mcpg.prompts(content builders) +mcpg.tools. _register_prompts(FastMCP wiring). Completes the MCP primitive triad alongside the resources surface (PR #157) and the tools surface. Contract test (tests/contract/test_mcp_prompts.py) pins the name + argument-shape manifest so a refactor can’t silently drop a prompt. Realises roadmap row 8.4.
Changed
-
outputSchema sweep across the 8 PG 19 modules — 28 tools converted from
dict[str, Any]returns to typed dataclass returns, so every PG 19 tool now exposes a populated MCPoutputSchemafor LangChain / LangGraph clients to validate against. Modules covered:pgq(6 tools),pg19_runtime(5),pg19_partitions(3),pg19_stats(4),pg19_skip_scan(2),wait_for_lsn(4),aio(2),repack(2).Per the established sweep playbook (
docs/contributing/adding-tools.md):- Stripped
slots=Truefrom every dataclass returned to the wire — slot descriptors block Pydantic’s schema generation. - Re-annotated each
@server.toolhandler return type, plus the 7 nested_runhelpers used by the_cached_callpattern inpgq/pg19_stats/aio/repack. - Removed
asdict(...)calls; FastMCP serialises the dataclass directly. Write tools preserve theirawait cache.clear()between the helper call and the barereturn result.
List-returning tools (
list_property_graphs,recommend_skip_scan_indexes,read_pg_stat_lock,read_pg_stat_recovery) emit FastMCP’s{"result": [...]}envelope — the per-item dataclass fields live under$defsin the schema. The manifest intest_tool_output_schemas.pypins the envelope key for these; the field-level snapshot test (test_tool_return_shapes.py) still pins the per-item fields, so a rename / removal trips both tests in concert.Realises the second batch of roadmap row 8.6 (PR-13 first sweep covered the 5 PG 19 DDL helper tools). Manifest floor bumped 5 → 33; remaining ~190 legacy
dict[str, Any]tools follow in further sweep PRs per the same checklist.Wire-format note: backward-compatible — FastMCP continues to emit the legacy text
contentarray alongside the newstructuredContentfor every converted tool. Clients reading justcontentsee no change. - Stripped
Phase 3 — PG 19 readiness (was staged for the unreleased 0.6.3)
Added
-
MCP resources (
mcpg.resources) — fourmcpg://…preload-on-connect URIs that close the gap between MCPg’s tool surface and the MCP protocol’s resources primitive. Pre-this-PR, the only way an agent could learn what MCPg does was to calldescribe_self/list_tables/get_compact_schemaas tools, burning call budget on every session start. The resources surface skips the tool-call wire overhead and the per-call context-window cost — an agent reads by URI once, caches locally, and uses tool calls only for actual work.Surface (all
application/json):mcpg://about/index— fulldescribe_self-equivalent payloadmcpg://capabilities/index— compact bucket list (id + name + summary)mcpg://capabilities/{bucket_id}— per-bucket detail (templated)mcpg://schema/{schema_name}— compact Markdown schema dump (templated)
Registered alongside READ tools (resources share the READ gate). New contract test
tests/contract/test_mcp_resources.pypins the URI set + variable shape + uniform MIME type. Realises roadmap row 8.3.Wire-format note: existing clients are unaffected — resources are a separate MCP primitive from tools, accessed via
read_resourcerather thancall_tool. Tools that emitted the same content (describe_self,get_compact_schema) continue working unchanged.
Added
-
Tool-bucket usage telemetry (
mcpg.observability+mcpg.otel_tracing) — everycall_toolinvocation now carries the capability-bucket label on both observability surfaces, so operators can slice tool usage by capability without re-deriving the routing from the tool name in PromQL / their tracing backend. Realises roadmap row 1.4.Wire-level shape:
- Prometheus:
mcpg_tool_calls_total{tool, bucket, status}— the existing series gained abucketlabel. Driver:sum by (bucket) (rate(mcpg_tool_calls_total[5m]))answers “which capabilities is this agent actually exercising?” - OpenTelemetry: every
mcp.call_toolspan gains amcp.tool.bucketattribute, so Honeycomb / Tempo / Datadog light up bucket-level facets automatically.
Bucket lookup uses the existing
mcpg.about.classify_toolrouting. Tools that don’t classify (shouldn’t happen on the maximal-flag server but defensively) land on the literal"unknown"label so the label dimension stays cardinality-stable.Pre-this-change every bucket ordering decision in
describe_selfwas hand-curated; with this telemetry in place the next iteration can driveheadline_toolsselection empirically (roadmap row 14.4).Wire-format note: existing
mcpg_tool_calls_totalqueries that don’t referencebucketkeep working — PromQL’s label-matcher semantics treat the new label as a “don’t care” dimension. The internalMetrics.record_callsignature gained an optionalbucket="unknown"kwarg; the in-tree call site (server.AuditedFastMCP.call_tool) passes the classified bucket. - Prometheus:
Added
-
PG 19 benchmark harness (
scripts/benchmark_pg19.py+scripts/benchmark_pg19.sh). Sibling of the existing smoke launcher; spins up a fresh PG 19 Beta container and quantifies the three measurable-without-restart PG 19 operational wins MCPg claims:- Skip-scan vs dedicated index — same probe query on a
composite
(status, created_at)index (PG 19 skip-scan) vs a dedicated(created_at)index. Demonstrates PG 19 closing the gap to the pre-19 catch-up index. - REPACK CONCURRENTLY vs VACUUM FULL — same bloated fixture rebuilt both ways; reports wall-clock + lock-mode contrast (VACUUM FULL holds ACCESS EXCLUSIVE; REPACK CONCURRENTLY doesn’t block reads + writes).
- LZ4 vs pglz TOAST — same compressible payload inserted
into two tables with explicit per-column
COMPRESSION, compares resultingpg_table_sizeand INSERT throughput.
Server-side timing throughout (
EXPLAIN (ANALYZE, TIMING)for query probes,clock_timestamp()deltas for maintenance ops) so the numbers don’t include psycopg / network RTT noise. Idempotent: drops + recreates a dedicatedmcpg_benchschema on each run. AIOio_uringvsworkeris aPGC_POSTMASTERGUC and needs a restart — it lives as a manual recipe indocs/plans/pg19-operations-playbook.mdrather than the harness. Realises roadmap row 3.3 and the GA-day-0 verification milestone inpg19-readiness.md. - Skip-scan vs dedicated index — same probe query on a
composite
-
Tool-surface overlap analyser (
tools/analyse_tool_overlap.py). Scans the snapshotted MCPg tool catalogue (now 223 tools after the Phase 3 sprint) for pairs likely to confuse an LLM picker — similar names, overlapping descriptions, near-identical input schemas. Output is a Markdown report ranked by a hand-tuned suspicion score (SequenceMatcher on names + Jaccard / containment on description stems + shared verb-noun + identical required-params), ready for a reviewer to triage. Deterministic, no API key required, runs in seconds against the checked-in snapshot — recovered fromclaude/tool-overlap-sweep(originally landed 2026-06-18) and rebased onto currentmain.Run with
python tools/analyse_tool_overlap.py > docs/reviews/tool-overlap-report.md. Pairs with similarity ≥ 0.70 / Jaccard ≥ 0.45 surface; tune the module-level constants if signal-to-noise drifts as the catalogue grows.
Changed
Tests (PG 19)CI step now succeeds gracefully when the pgdg apt repo hasn’t yet republishedpostgresql-client-19. Probesapt-cache showbefore installing; emits a workflow::warning::instead of a fatal exit when the package is missing. The matrix entry is alreadycontinue-on-error: trueso the job never blocked merge, but the step failure was emitting a check-run-failed webhook on every push to every open PR (~15 unnecessary notifications per day during Phase 3). This change stops the noise at the step level without changing the matrix-entry semantics — once the package is republished the install runs normally. Realises roadmap row 14.1.
Added
-
PG 19
WAIT FOR LSN+ read-your-writes advisor (mcpg.wait_for_lsn). Four new tools that turn the “read your own writes from a hot standby” pattern into a one-call primitive.get_wait_for_lsn_statusis the version probe + standby detection (never raises).get_current_wal_lsnreturns the primary write LSN or the standby replay LSN — the natural pairing for the RYW workflow.wait_for_lsnissuesWAIT FOR LSN '<lsn>' TIMEOUT <ms>with strict^[0-9A-Fa-f]+/[0-9A-Fa-f]+$LSN validation up-front (the grammar can’t parameter-bind LSN literals, so we validate then embed verbatim).recommend_read_your_writesis the advisor — combines server role + replay lag + version into a structured recommendation with reason codesprimary_no_wait_needed,standby_no_lag,standby_lag_unknown,standby_with_lag,standby_pg18_or_older,unavailable.PR-7 of the PG 19 Phase 3 plan (PO matrix row #7, PO score 22). All four tools route to the
operations_and_healthcapability bucket next to the existing replication / replica advisors.Per the no-deprecation rule, the poll-loop pattern stays valid on PG ≤ 18 —
get_wait_for_lsn_statusandrecommend_read_your_writesboth point at it;wait_for_lsnraisesWaitForLsnErroron older servers with the same fallback in the message. The wait surface also reportstimed_out=trueinstead of raising when PG’s own TIMEOUT fires; timeout detection is SQLSTATE-57014-based for locale-independence, and the advisor uses a single atomic SELECT so a transient recovery-state probe can’t mis-classify a standby as a primary. -
PG 19 skip-scan-aware index advisor (
mcpg.pg19_skip_scan). Two new tools that expose PG 19’s B-tree skip-scan optimisation as an actionable advisor.get_skip_scan_statusis the version probe (never raises).recommend_skip_scan_indexeswalkspg_indexjoined topg_statsfor composite B-tree indexes whose leading column has a low NDV — those are the ones PG 19’s skip-scan unlocks for queries that filter only on trailing columns. Each candidate’srationaleflags the dedicated single-column indexes on the trailing columns as review candidates forrecommend_index_drops.PR-8 of the PG 19 Phase 3 plan (PO matrix row #9, PO score 20). Both tools route to the existing
advisorscapability bucket next torecommend_indexes/recommend_index_drops.Per the no-deprecation rule, the existing
recommend_indexesandrecommend_index_dropskeep working on every supported PG version with the same shape and reason codes. On PG ≤ 18 the new advisor returns an empty list;get_skip_scan_statuspoints at the standard “add a dedicated single-column index” fallback. Advances #120. -
Structured MCP tool outputs (
outputSchemaon the wire). New contract surface for LangChain / LangGraph / typed-state agent clients: every tool with a typed return annotation now exposes a populated JSON Schema via MCP’soutputSchemafield. FastMCP auto-derives the schema from the function’s dataclass return type, so a LangGraph node can wire its state model directly against the tool’s output without any hand-coded translation.PR-13 (first sweep). Converts the PG 19 DDL helpers family (
get_pg19_ddl_status,get_role_ddl,get_database_ddl,get_tablespace_ddl,validate_check_constraint) fromdict[str, Any]returns to typed dataclass returns. Dropsslots=Truefrom the affected dataclasses (slot descriptors leak into Pydantic’s introspection and break schema generation). RenamesValidateCheckConstraintResult.schema→.table_schemato avoid Pydantic’s reserved-name shadow onBaseModel.schema().New contract test
tests/contract/test_tool_output_schemas.pypins the per-tool field set + locks in a monotonic-growth floor so the manifest never silently shrinks. Thedocs/contributing/adding-tools.mdskill is updated with the typed-return pattern + sweep checklist for the remaining ~200 legacydict[str, Any]tools.Wire-format note: existing clients reading the
contentarray continue to work unchanged (FastMCP emits both the legacy text payload and the newstructuredContent). Only thevalidate_check_constraintfield rename (schema→table_schema) is a breaking shape change — the tool was added in #144 yesterday, no external consumers to migrate. -
Roadmap observation loop. Extended
docs/feature-shortlist.mdwith 14 new entries capturing gaps surfaced during the Phase 3 retrospective: tool-bucket usage telemetry (1.4), PG 19 PR-10 small-tools batch (2.5),EXPLAIN ANALYZE (IO)capture (2.6), benchmark harness (3.3), PR-11 characterisation tests (3.4), MCP resources (8.3), MCP prompts (8.4),describe_tool(8.5), remainingoutputSchemasweep (8.6), session-scope cost advisor (8.7), session-intent handshake (8.8), and a new section 14 covering release engineering / hygiene papercuts (CI noise, changelog bloat, sourcery rotation, headline-tool drift, v0.7.0 release-cut, roadmap-PR linkage). Roadmap intro now documents the observation loop convention so gaps always land as a numbered row, never as a chat-thread orphan. -
PG 19 operations playbook (
docs/plans/pg19-operations-playbook.md). Operator-side reference for the PG 19 behaviour changes that aren’t primary MCPg tool surfaces — JIT-off-by-default, LZ4 TOAST compression, RADIUS removal,pg_hba.confOAuth method, MD5 deprecation warnings, per-process log levels, jsonpath string functions. Companion topg19-readiness.md, which tracks the MCPg-side tool work. PR-12 of the PG 19 Phase 3 plan (docs sweep; bundles audit rows #22, #23, plus the JSONpath / JIT / LZ4 / RADIUS doc-only entries). Advances #120. -
PG 19 DDL helpers (
mcpg.pg19_ddl). Five new tools that close the “validate-and-ship constraints, dump cluster-level DDL without pg_dumpall” gap on the agent surface.validate_check_constraintissuesALTER TABLE … VALIDATE CONSTRAINTfor a constraint originally added withNOT VALID— idempotent (returnschanged=falsewhen the constraint is already validated) and works on every supported PG version (the SQL has been around since 9.x). Three PG 19+ DDL-dump tools wrap the newpg_get_roledef(oid)/pg_get_databasedef(oid)/pg_get_tablespacedef(oid)SQL functions:get_role_ddl,get_database_ddl,get_tablespace_ddl.get_pg19_ddl_statusis the per-function presence probe; never raises.PR-9 of the PG 19 Phase 3 plan (bundles PO matrix rows #10 + #11, combined PO score 34). The four read tools route to the
schema_introspectioncapability bucket;validate_check_constraintsits inoperations_and_healthnext torun_maintenance.Per the no-deprecation rule, the
pg_dumpall --roles-only/--globals-only/--tablespaces-onlyshell-out paths stay valid on PG ≤ 18 —get_pg19_ddl_statusreturnsavailable=falsewith a pointer at the fallback, and the per-object DDL tools raisePg19DdlErrorwith the same hint in the message. Advances #120.Side win: the return-shape contract test now also recognises
mcpg.aio,mcpg.pg19_runtime,mcpg.pg19_stats, andmcpg.pg19_ddlas helper modules, so every Phase 3 PG 19 tool contributes its dataclass field set to the snapshot rather than appearing asopaque. -
PG 19 partition reorganisation (
mcpg.pg19_partitions). Three new tools that finally make partition boundaries reshapable without the detach / create / copy / attach dance.get_pg19_partitions_statusis the version probe (never raises).merge_partitionsissuesALTER TABLE … MERGE PARTITIONS (p1, p2, …) INTO new; reuses the existing partition data files.split_partitionissuesALTER TABLE … SPLIT PARTITION existing INTO (PARTITION new1 FOR VALUES …, PARTITION new2 FOR VALUES …); takes a list of{name, for_values_clause}specs because PG’s DDL grammar can’t parameter-bind partition bounds (we quote thenameidentifier; thefor_values_clauseis embedded verbatim under the existingCapability.DDL+allow_ddltrust gate, same model asrun_ddl).PR-5 of the PG 19 Phase 3 plan (PO matrix row #4, PO score 23 — next-highest after the PR-9 bundle). All three tools route to the existing
timeseries_partitioningcapability bucket next to thepartman_*lifecycle.Per the no-deprecation rule, the detach / create / attach fallback path stays valid on PG ≤ 18 —
get_pg19_partitions_statusreturnsavailable=falsewith the fallback indetail, and both write surfaces raisePg19PartitionsErroron older servers with the same hint in the message. Both writes dispatch throughDatabase.run_unmanaged(cannot run inside a transaction block, same constraint as VACUUM / REPACK). Advances #120. -
PG 19 runtime toggles (
mcpg.pg19_runtime). Five new tools that flip cluster-wide settings without a restart — previously “plan a maintenance window” tasks. Online checksums:get_data_checksums_status,enable_data_checksums,disable_data_checksumswrap PG 19’spg_enable_data_checksums()/pg_disable_data_checksums()SQL functions. On-demand logical replication:get_logical_replication_status,enable_logical_replication_on_demandflipwal_level = 'logical'viaALTER SYSTEM+pg_reload_conf()(PG 19 makes that change effective without a restart). The status probe surfaces both the configuredwal_leveland the new PG 19effective_wal_levelpreset GUC, so an agent can tell when anALTER SYSTEMhas been done but a reload is still pending.PR-6 of the PG 19 Phase 3 plan (bundles PO matrix rows #6 + #8, combined PO score 43 — highest remaining). All five tools route to the existing
operations_and_healthcapability bucket.Per the no-deprecation rule, the offline
pg_checksumsshell-out and the documented postgresql.conf-edit-and-restart wal_level path stay valid on PG ≤ 18 — both status probes returnavailable=falsewith diagnostics pointing at them. Both write surfaces raisePg19RuntimeErroron older servers with the same fallback guidance in the message, and wraprun_unmanagedfailures preserving__cause__(consistent with REPACK / AIO). Advances #120. -
PG 19 lock + recovery analytics (
mcpg.pg19_stats). Four new read tools:get_pg19_stats_status(per-view presence probe; never raises),read_pg_stat_lock(per-lock-type acquire / wait / wait_time counters from the new PG 19 view),read_pg_stat_recovery(replay LSN + lag + startup state on standbys), andanalyze_lock_hotspots(read-only advisor classifying each lock type by wait dominance with stable reason codes:contention_dominant,high_wait_time,high_wait_count,low_contention).PR-4 of the PG 19 Phase 3 plan (bundles PO matrix rows #5 + #12, combined PO score 38). Routes all four tools to the existing
operations_and_healthcapability bucket sodescribe_selfgroups them next tofind_blocking_chains/walk_blocking_chains/read_pg_stat_io. Per the no-deprecation rule, the existingfind_blocking_chains/list_lockstools inmcpg.locksare untouched and the advisor diagnostics on PG ≤ 18 point operators at them as the fallback path. Advances #120. -
PG 19 async-I/O advisor (
mcpg.aio). Two new tools:get_aio_status(version probe + currentio_method/io_min_workers/io_max_workersGUCs; never raises) andrecommend_io_method(read-only advisor that maps workload signals frompg_stat_databaseaggregates to one ofio_uring/worker/syncwith a stable reason code:high_concurrent_read_load,bursty_io_with_cache_pressure,low_io_pressure,current_setting_optimal, orinsufficient_stats).PR-3 of the PG 19 Phase 3 plan; the headline differentiator — release-blog tagline: “Tell me which
io_methodis right for this workload.” Emits aready_to_run_sql(ALTER SYSTEM SET io_method = '…';) only when the recommendation differs from the current setting; the advisor never invokes ALTER SYSTEM itself.Per the no-deprecation rule, the existing
read_pg_stat_iosurface inmcpg.io_statsis untouched. Both new tools route to theoperations_and_healthcapability bucket. Advances #120. -
PG 19 in-server
REPACKcoverage (mcpg.repack). Two new tools:get_repack_status(version probe; never raises) andrepack_table(REPACK <relation> [CONCURRENTLY], defaults toconcurrently=true). PR-2 of the PG 19 Phase 3 plan; the “online table rebuild without pg_repack — one tool, one transaction” release-blog headline. Routes to the existingoperations_and_healthcapability bucket. Per the no-deprecation rule, the pg_repack extension stays on the safe-SQL allowlist for PG ≤ 18 operators —get_repack_statusreturnsavailable=falsewith a diagnostic pointing at the pg_repack fallback. Advances #120. -
SQL/PGQ property graph queries coverage (
mcpg.pgq). PG 19’s built-in SQL standard for property-graph queries (GRAPH_TABLE(...)/MATCHpatterns) lands as six new MCPg tools, side-by-side with the existing AGE-stylegraph_operationsbucket:get_pgq_status— version probe; never raises. Returnsavailable=falseon PG < 19 with a diagnostic pointing the agent at the AGE-stylerun_cyphersurface.list_property_graphs,describe_property_graph— catalog reads overinformation_schema.sql_property_graphs. Empty on early Beta builds where the view isn’t populated yet.run_pgq— executesSELECT ... GRAPH_TABLEqueries. Refuses non-SELECT, missing-GRAPH_TABLE, and chained statements at the boundary;max_rowscaps result size.create_property_graph— DDL where the tool composes theCREATE PROPERTY GRAPH "schema"."name"header and the caller supplies theVERTEX TABLES (...)body (validated to begin with that clause and rejected if it contains;).drop_property_graph— DDL withIF EXISTSidempotency.
New
property_graph_queriescapability bucket inmcpg.about. Both surfaces (SQL/PGQ on PG 19+; AGE-style Cypher everywhere) coexist — agents choose by callingget_pgq_statusfirst. PR-1 of the PG 19 Phase 3 plan; advances #120. -
PostgreSQL 19 readiness — Phase 1 (CI matrix + Beta scaffold). PG 19 Beta added as an experimental matrix entry on the test job (
continue-on-error: trueuntil GA — failures don’t block PRs). Standalone.github/ci-postgres-pg19.Dockerfilebuilds pgvector v0.8.0 from source on top ofpostgres:19beta1(pgvector doesn’t ship apg19image tag yet). PostGIS deferred until apt package is published.docs/plans/pg19-readiness.mdcarries the per-feature audit matrix for Phase 2 (skip-scan indexes,io_methodadvisor,pg_get_acl, CHECK constraint validation, etc.) — each row becomes a focused follow-up PR. Closes Phase 1 of #120. -
pg_prewarmcache-warming coverage (mcpg.pg_prewarm). Eight new tools:get_prewarm_extension_status,list_prewarmed_relations,recommend_prewarm_targets,prewarm_relation,prewarm_recommended,schedule_autowarm,unschedule_autowarm,list_autowarm_jobs.The headline advisor —
recommend_prewarm_targets— inspectspg_stat_user_tables+pg_statio_user_tablesfor relations whose first-query latency would benefit from pg_prewarm. Caps the cumulative cost atshared_buffers_budget_pct*shared_buffers(default 60%) so the recommendation never silently exceeds the buffer pool. Ranks candidates by miss-volume descending and emits stable reason codes (seq_scan_dominant/high_cold_miss_rate/small_hot_relation_uncached/index_in_critical_path) plus a ready-to-runSELECT pg_prewarm(...)stub per row.schedule_autowarm/unschedule_autowarmdrive a pg_cron-backed “warm after restart” loop — the canonical missing piece for DBAs running clusters with frequent restarts.pg_prewarmadded toENABLEABLE_EXTENSIONS. Newcache_warmingcapability bucket inmcpg.about. Closes #119. -
Contributing playbook for new tools (
docs/contributing/adding-tools.md). Captures the consistent end-to-end shape every new MCPg capability follows: module layout, identifier validation, tool registration, description conventions (the “Returns …” sentence), naming conventions, capability bucket overrides, test patterns, snapshot regen, quality gates, commit cadence. Also installed as amcpg-add-toolClaude Code skill. -
redis_fdwcache-and-foreign-data coverage (mcpg.redis_fdw). Eight new tools across read + DDL surfaces:list_redis_foreign_servers,describe_redis_cache_table,get_redis_cache_stats,recommend_redis_cache_targets,enable_redis_fdw,create_redis_cache_server,create_redis_user_mapping,create_redis_cache_table.The advisor —
recommend_redis_cache_targets— inspectspg_stat_user_tablesfor read-heavy, low-write relations whose working set fits comfortably in Redis (defaults: read/write ratio ≥ 10, ≥ 1000 reads, ≤ 1M rows) and emits ready-to-runCREATE FOREIGN TABLEstubs.Security posture:
- Identifier allowlist on every DDL surface (server / table / user / column names).
- TLS-on by default; refuses
tls=Falseagainst non-loopback Redis hosts unlessallow_insecure_tls=Trueis set explicitly. create_redis_user_mappingnever accepts a raw password — callers passsecret_ref, resolved throughMCPG_SECRETS_BACKEND.
New
cache_and_foreign_datacapability bucket inmcpg.aboutsodescribe_selfadvertises the coverage cleanly.redis_fdwadded toENABLEABLE_EXTENSIONS. Closes #118.
[0.6.2] - 2026-06-17
Added
-
mcpg_rag.rerank_events+mcpg_rag.efficiency_observationspartitioning retrofit (mcpg.rag_telemetry.migrate_rag_telemetry_to_partitioned). PR-5 of the NL→SQL remediation arc. Same partitioning / compression / RLS treatment now applied to both RAG telemetry tables. Migration handles each table independently — either or both can be absent (telemetry never set up).Unlike
mcpg_audit.events, retention defaults on (90 days): no HMAC chain anchors these tables, so periodic chunk-drops are safe. Overrideable viaMCPG_RAG_TELEMETRY_RETENTION_DAYS. LZ4 column compression on the JSONB columns (extra,rerank_lift_curve) for the pg_partman / native paths, gated onserver_version_num >= 140000to avoid the PG-13 transaction-abort gotcha that PR #109 fixed for the events table.Native + pg_partman runs the rename-create-insert-drop dance per table under an
ACCESS EXCLUSIVElock. TimescaleDB uses in-placecreate_hypertable(migrate_data => TRUE)per table.RLS on by default; optional reader role via
MCPG_RAG_TELEMETRY_READER_ROLEgets a SELECT-only policy on both tables. Re-running on already-partitioned tables is a near-zero-cost no-op. -
mcpg_audit.eventspartitioning retrofit (mcpg.audit_trail.migrate_audit_events_to_partitioned). One-shot operator-callable migration that converts an existing unpartitioned events table onto the same partitioning / compression / RLS stack introduced formcpg_audit.nl2sql_eventsin PR #107. The HMAC chain is preserved (id values +prev_hmac/event_hmaccolumns +chain_tippointer), soverify_audit_chainkeeps working post-migration.Backend ladder (auto-detected, overrideable via
MCPG_AUDIT_EVENTS_BACKEND): TimescaleDB hypertable (create_hypertable(migrate_data => TRUE), in-place); pg_partman / native (rename + create partitioned + copy + drop dance inside oneACCESS EXCLUSIVElock). Monthly partitions cover historical data; daily partitions cover ±7 days from now.Compression: TimescaleDB columnar or LZ4 column compression on
arguments/result/error(PG 14+). Retention is off by default — the HMAC chain anchors on the oldest event, so dropping old chunks would breakverify_audit_chain; operators who explicitly want chunked retention setMCPG_AUDIT_EVENTS_RETENTION_DAYSand must disableMCPG_AUDIT_INTEGRITYfirst.RLS on by default; optional reader role via
MCPG_AUDIT_EVENTS_READER_ROLEgets a SELECT-only policy. Re-running on an already-partitioned events table is a near-zero-cost no-op.
Security
-
NL→SQL hardening sweep (PR-3 of the NL→SQL remediation arc). Five P1/P2 findings from the deep-review audit closed in one pass:
- Schema-brief character cap —
_build_schema_briefnow applies a final char limit (max_brief_chars, default 32 KB, hard cap 128 KB) after the per-table / per-column bounds, so a schema with hundreds of long column names can’t smuggle the LLM-token budget past the operator’smax_tokenssetting (P1 #4). - Vendor-egress one-time warning — first call per provider per
process logs a single
mcpg.nl2sqlwarning telling operators that catalog metadata (schema / table / column names, FK edges, sanitised DEFAULT expressions) leaves the network. The notice surfaces an operational concern most deployments don’t realise until incident time (P2 #5). QueryErrorredaction — execution-path failures now flow throughobfuscate_passwordbefore reachingTranslationResult.error. psycopg / libpq error messages routinely embed DSN fragments and password-bearing connection-string values, same vector closed formcpg_audit.events.errorin PR #96 (P2 #6).- Single-statement assertion — generated SQL is parsed with pglast
before
run_selectruns; multi-statement input ("SELECT 1; DROP TABLE x") is refused at the NL→SQL layer as defense-in-depth ahead of theSafeSqlDriverallowlist (P2 #7). - Robust fence handling —
_parse_responsenow extracts the body of a fenced JSON block when the outer JSON parse fails, instead of the leading/trailing strip-and-pray approach. Models that wrap valid JSON in explanatory prose + a fence now parse correctly instead of degrading to “raw text as explanation” (P2 #8).
- Schema-brief character cap —
Added
- NL→SQL audit table — partitioned, compressed, RLS-gated
(
mcpg.audit_nl2sql). WhenMCPG_NL2SQL_AUDIT_PERSIST=true, everytranslate_nl_to_sqlcall records one row inmcpg_audit.nl2sql_eventswith the question, generated SQL, exec outcome, and timing. First write per driver auto-provisions the table against the best partitioning backend available — TimescaleDB hypertable with native columnar compression + retention policies, pg_partman with LZ4 TOAST compression, or PostgreSQL declarative range partitioning with daily child partitions pre-created ±7 days. RLS is on by default; an optional reader role (MCPG_NL2SQL_AUDIT_READER_ROLE) gets a SELECT-only policy. Question / SQL / error text run throughobfuscate_passwordso embedded connection-string credentials never reach the audit table. All operations are idempotent and short-circuit via a per-driver cache after the first call.
Security
-
NL→SQL schema policy honours caller-supplied env (PR #106 follow-up).
translate_nl_to_sqlnow accepts anenv: Mapping[str, str]parameter that flows into_validate_schema_name—MCPG_NL2SQL_SCHEMA_DENYLIST/MCPG_NL2SQL_SCHEMA_ALLOWLISTare enforced from custom mappings (multi-tenant servers, test harnesses) instead of always falling back toos.environ. Schema-policy error messages no longer echo the full allow/deny configuration — operators get the offending schema name only, the lists stay in configuration. -
Audit error field is now redacted (#96).
mcpg_audit.events.errorused to persist the rawstr(exc)written bywrite._persist_audit; psycopg / libpq error messages routinely embed DSN fragments or password-bearing connection strings, so a write that failed because of a connection-pool error would write a plaintext credential into the audit table.record_auditnow routes the error through the sameobfuscate_passwordsweep asarguments/result, and the HMAC payload signs the redacted form soverify_audit_chainstill matches. -
OpenTelemetry spans redact
error.message(#96).tool_spanshippedstr(exc)[:200]straight to the OTel collector, bypassing every other redaction surface. Nowobfuscate_passwordruns before the 200-char cap (order is load-bearing — capping first could truncate a recognizable DSN prefix past the obfuscate match window). -
VaultSecretsProvider.__repr__no longer leaks the token (#96). The class was a plain@dataclasswithtoken: str— defaultrepr()renderedtoken='hvs.…'. Anylogging.exception(provider), pytest assert, orSettings.model_dump()that touched a provider instance leaked the Vault root token. Setrepr=Falseon the token field plus theenv/overlaymappings on every provider as defense-in-depth (those mappings can hold sibling secrets). -
TLS minimum + cipher allowlist pinned on the HTTP transport (#97).
_uvicorn_tls_kwargspreviously set only certfile / keyfile / ca_certs / cert_reqs, leaving the underlying SSLContext at uvicorn’s default — on a host with old OpenSSL a TLS 1.0/1.1 handshake could be negotiated. Now pinsssl_version=ssl.PROTOCOL_TLS_SERVERandssl_ciphers=_MOZILLA_INTERMEDIATE_CIPHERS(AEAD-only ECDHE/DHE suites; no RC4, 3DES, CBC, NULL, or anonymous). -
OIDC verifier rejects plaintext
http://issuer /jwks_url(#97). Discovery joined the issuer with/.well-known/openid-configurationand the explicitjwks_urlwas returned verbatim — either over plaintext let a path attacker swap the JWKS for keys they control and forge any JWT MCPg would then accept.OIDCVerifier.__init__now refuses non-https://URLs at construction time, with a single carve-out forhttp://localhost(and127.0.0.1,::1) so Keycloak-in-Docker / stub-IdP setups keep working. -
Audit HMAC chain anchored against tail-truncation attack (#105; re-applies the closed #98 onto the current main, which had moved with #99’s keyset pagination + #96’s
safe_errorredaction).verify_audit_chainwalkedmcpg_audit.eventsin id-ASC and stopped at the last row found, so an operator with table-write access couldDELETE FROM mcpg_audit.events WHERE id > Nandverifystill returnedstatus=ok. New single-rowmcpg_audit.chain_tiptable records the highest(last_event_id, last_event_hmac)the writer has signed;record_auditupserts it in one writable-CTE statement with the event INSERT so an attacker can’t race between them.verify_audit_chaincross-checks the highest event walked against the anchor — match →status=ok, mismatch →status=tampered, reason="truncation_detected". Backward compat: a pre-anchor DB returnsstatus=okwith ano_chain_tipwarning so operators see the upgrade gap without a false-positive alarm. -
Audit HMAC compare via
hmac.compare_digest(#99). The two!=comparisons insideverify_audit_chainnow usehmac.compare_digest. Verification-side timing isn’t a sensitive oracle (operator-initiated, not per-request) but the inconsistency with the rest of the codebase’shmac.compare_digestusage was worth fixing. -
Bounded LRU cache on cloud secret providers (#101). Vault / AWS / GCP held unbounded
dict[str, str | None]caches; a loop hittingprovider.get(unique_name)would grow them until OOM. Shared_cache_get/_cache_puthelpers enforce_SECRET_CACHE_MAX_ENTRIES = 1024with proper LRU eviction, and both are guarded by a module-levelthreading.Lockagainst theasyncio.to_threadpaths already in use (PyJWKClient, boto3). -
OIDC JWKS cache TTL pinned (#101).
PyJWKClientwas constructed withcache_keys=Trueand no other knobs, so an upstream key- rotation event required a server restart to pick up. Now pinslifespan=int(jwks_cache_seconds)(1h default) andmax_cached_keys=16so a PyJWKClient default change can’t quietly grow the cap.
Added
-
pdb.more_like_thistuning args (#91). Every documented upstream arg onpdb.more_like_this(anyelement, fields jsonb, …)is now an optional Python kwarg onpg_search_more_like_this:fields(jsonb),min_doc_frequency,max_doc_frequency,min_term_frequency,max_query_terms,min_word_length,max_word_length,boost_factor,stop_words. Omitted kwargs are not mentioned in the rendered SQL so upstream’s defaults apply. Supplied args use PG named-arg syntax (name => %s) with explicit type casts (::jsonb,::real,::text[]). -
Multi-column BM-25 search (#93).
pg_search_runandhybrid_bm25_vector_searchaccept multiple columns viacolumns=["body", "title", ...], rendered as an OR of per-column@@@predicates against the same parsed query string. The whole-index (columns=None) and single-column shapes are unchanged. New_bm25_search_predicate(columns)returns the SQL fragment + bind count so callers thread the right number of placeholders. -
docs/release-notes-0.6.0.md(#95). Mirrors the v0.5.0 release notes style; sourced from CHANGELOG[0.6.0]+ the release commit’s pre-flight numbers (1354 tests passing on PG 14-18). Highlights: TLS / mTLS, IP allowlist, cloud secrets backends, OpenTelemetry, structured JSON logging, pgvector analytics suite,recommend_index_drops, migration ergonomics, multi-provider NL→SQL routing.
Changed
-
pg_search_more_like_thisvalidates a JSON-serializablefieldsdict (#91 follow-up)._validate_mlt_fieldsonly checked the outer type; a dict containing non-encodable values (sets, datetimes, custom objects) leaked a bareTypeErrorfrom the downstreamjson.dumpsrather than the documentedPgSearchError. The helper now probes encodability up front. -
create_turboquant_index/reindex_turboquant_indexwrap driver errors asTurboQuantError(#92). The tworun_unmanagedcall sites are now intry / except Exception as exc:blocks that re-raise asTurboQuantError, matching the equivalent pattern increate_pg_search_index/reindex_pg_search_index. Thefrom excchain preserves the original cause on__cause__. -
mcpg_rag.rerank_events/mcpg_rag.efficiency_observationssetup paths wrap DDL asRagTelemetryErrorand record the executed SQL (#100).setup_rag_telemetryandsetup_efficiency_observationspreviously calleddatabase.run_unmanagedeight times total without a try/except wrap. New_run_setup_ddlhelper catchesExceptionand re-raises asRagTelemetryError, then appends each successfully-executed statement toresult.setup_sql— same record-the-SQL invariant the pg_search / turboquant DDL surfaces hold. -
MaintenanceResult.maintenance_sql(#100).run_maintenancethreads the rendered SQL through bothrun_unmanagedand the result so audit / change-review callers don’t have to reach into a side-channel database double. -
verify_audit_chainkeyset-paginates the walk (#99). Previously ranSELECT … FROM events ORDER BY id ASCwith no LIMIT — on a million-row audit table that’s gigabytes of jsonb loaded before the chain check even starts. Now walks in_VERIFY_BATCH_SIZE = 1_000batches viaWHERE id > %s ORDER BY id ASC LIMIT %s. -
Bounded sample sizes across
vector_ops+mmr_search(#99). New_MAX_SAMPLE_SIZE = 50_000(matchingrag_efficiency’s ceiling) applied via_validate_sample_sizetocluster_vectors,detect_vector_outliers,monitor_embedding_drift, andanalyze_distance_metric.mmr_searchgains_MAX_MMR_FETCH_K = 10_000and_MAX_MMR_K = 1_000since its diversity pass isO(pool · k)in pure Python. -
Hybrid BM25 + pgvector weights bound as
%sparams (#101).hybrid_bm25_vector_searchpreviously splicedbm25_weight,vector_weight, andper_leg_limitinto the SQL as Python literals — every distinct tuple produced a fresh plan-cache entry. All three now bind via%splaceholders (weights cast::float8);kstays inlined intentionally since it’s a small enumerable RRF constant where binding masks integer-arithmetic optimization. -
Typed errors across the AGE-graph + locks surfaces (#102).
graph.py,cypher.py,graph_mgmt.py,graph_diagram.py, andlocks.pypreviously raised bareValueErrorfor boundary validation. NewGraphError(shared across the four graph modules via a single import frommcpg.graph) andLocksErrormatch the*Error-per-module convention every other surface uses. -
translate_nl_to_sqlwrapper business logic relocated tomcpg.nl2sql(#102). The tool wrapper carried ~45 lines of provider selection / model override / API-key dispatch. Moved to a newresolve_provider_call_params(settings, requested_provider) → ProviderCallParamshelper; wrapper shrinks to ~5 lines. The two normalisation bugs called out in review are fixed: whitespace-onlyrequested_providerno longer buries a valid configured default behind a “no provider configured” error, and theis_defaultcomparison normalizes both sides defensively.
Docs
-
Multi-column BM-25 search + tuning-args coverage in tour, plan, and tools surfaces (#94, #95).
docs/plans/bm25-integration.mdupdates §2.1 (BM-0 spec) to mark thepdb.more_like_thistuning args as landed, Phase BM-2 to reflect the fullpg_search_more_like_thissignature, and §6 to drop the now- obsolete deferral bullet.docs/tools.mdgains pg_search observability / search / DDL rows;docs/tour.mdanddocs/user-guide.mddescribe the multi-column OR shape; the README’s licensing-matrix row drops the “planned” qualifier on pg_search. -
docs/release-notes-0.6.0.mdlinked from index.md (#95). Release-notes nav now points at v0.6.0 directly rather than the “see CHANGELOG.md for v0.5.1 and beyond” workaround. -
Stale
mcpg --versionoutput bumped 0.5.1 → 0.6.1 indocs/installation.md(#95).
Dependencies
-
Transitive bumps to close 7 Dependabot security alerts (#113). Lockfile-only refresh (
uv.lock); nopyproject.tomlchange since every flagged package reaches the project transitively under loose constraints, so downstream PyPI installs already resolve to the patched versions.cryptography48.0.0 → 49.0.0 — OpenSSL CVE in statically-linked wheels (alert #5).python-multipart0.0.29 → 0.0.32 — closes four alerts: Content-Disposition RFC 2231/5987 parameter smuggling (#1),application/x-www-form-urlencoded;separator differential (#2), negativeContent-Lengthunbounded read inparse_form(#3), quadratic-time;separator scan DoS (#4).starlette1.2.0 → 1.3.1 — closes two alerts: unvalidated request path poisonsrequest.url.hostname(#6),request.form()limits silently ignored onapplication/x-www-form-urlencoded(#7, DoS).
[0.6.1] - 2026-06-09
Added
-
MCP Registry Integration. Added
server.jsonconfiguration and automated publishing workflow to register MCPg on the official Model Context Protocol registry (registry.modelcontextprotocol.io). Addedmcp-nameverification comment inREADME.mdto satisfy PyPI ownership verification. -
RAG reranker analytics + advisor + audit category (RAG-D). Five read-only tools over the
mcpg_rag.rerank_eventstable shipped in RAG-C, plus a newaudit_rag_pipelinecategory wired intoaudit_database. Reads only; the storage layer (RAG-C) provides the writes.Analytics (each filterable by
model+retrieval_index, with adayswindow default of 7):analyze_reranker_lift— per-query Spearman / Kendall correlation between bi-encoder and cross-encoder ranks, aggregated. Surfacesreranker_idle(WARNING) when mean Kendall tau exceeds 0.85 (the reranker rarely changes ordering).analyze_topk_stability— Jaccard overlap between top-K-by-bi-rank and top-K-by-cross-rank per query. Surfacestopk_stable(WARNING) when mean Jaccard exceeds 0.90 (rerank is barely earning its place at this K).analyze_rerank_score_distribution— equal-width histogram ofcross_encoder_scorevalues + top-decile share. Surfacesscore_clustering(WARNING) when more than 50% of scores land in the top decile (the reranker isn’t discriminating).analyze_rerank_ndcg— NDCG@k under bi-ordering vs cross-ordering, averaged across labeled queries (ground_truth_relevance IS NOT NULL). Surfacesrerank_hurts_ndcg(CRITICAL, delta < -0.02) orrerank_lifts_ndcg(GOOD, delta > 0.05).
Roll-up advisor
recommend_rerank_strategyruns all four analytics for one window and produces a single headline summary picking the most actionable signal. The newaudit_rag_pipelinecategory inaudit_databaseinvokes the advisor over the default 7-day window and turns each finding into aMetricResult(same severity → score deduction asaudit_turboquant_indexesandaudit_vector_indexes). ReturnsNonewhen the events table doesn’t exist, so stock deployments are cleanly omitted from the scorecard.New helpers in
mcpg.rag_efficiency:_jaccard(set overlap),_ndcg_at_k(log-discount sum),_histogram(equal-width buckets). Pure-Python, consistent with the no-SciPy stance. Threshold constants live at the module top so the future Phase E adaptive-thresholds framework can override them in one place. All five tools are READ-gated. -
mcpg_rag.rerank_eventsschema +setup_rag_telemetry/log_rerank_eventtools (RAG-C). Storage layer for the forthcoming Phase D analytics (reranker lift, top-K stability, score-distribution clustering, NDCG, therecommend_rerank_strategyadvisor, and theaudit_rag_pipelinecategory). One row per(query, candidate)pair from a RAG reranker step.query_hash(BYTEA, caller-computed) is the join key — MCPg never sees raw query text by default, so PII stays out of the table; callers who want to retain it can stash the text in theextraJSONB column under their own responsibility. Three indexes (occurred_at,query_hash, composite(reranker_model, occurred_at)) cover the analytics’ expected access patterns.setup_rag_telemetryis idempotent — catalog probes before eachCREATE … IF NOT EXISTSlet the result honestly report first-run vs no-op ({schema_created, table_created, indexes_created}). DDL runs throughDatabase.run_unmanagedso each statement commits independently.log_rerank_eventcarries 11 required typed fields plus optionalused_in_context(defaultsFALSE),ground_truth_relevance(nullable for online traffic), andextra(free-form dict serialised as JSONB). Bool-as-int subclass trap caught explicitly (same pattern as TQ-4’sconcurrentlyvalidation).setupgated under unrestricted +MCPG_ALLOW_DDL;loggated under unrestricted (WRITE). Lives inmcpg.rag_telemetry.
Docs
-
Doc-sync after the turboquant + RAG-A/B PR wave. Tool count in
docs/tour.md,docs/user-guide.md, anddocs/tools.mdbumped from 141 to 154. Thedocs/tools.mdtool index now lists every tool registered bytools.py(verified by reconcilinggrep '@server.tool' src/mcpg/tools.pyagainst the listed tools). New rows for the pg_turboquant observability / write / DDL groupings; existing rows extended withanalyze_vector_search_efficiency(RAG-A),verify_audit_chain/prune_audit_events,verify_connection_encryption,get_compact_schema,mmr_search,optimize_query, the pgvector advisor follow-ups (tune_vector_index,vector_recall_at_k,analyze_hnsw_recall), andschedule_logical_backupunder a unified “pg_cron scheduling (gated)” row. Stalepg_cron.schedule/partman.*naming convention corrected to the actual tool names (schedule_cron_job/partman_create_parentetc.). -
Planning doc for the BM25 sparse-search integration. A focused three-way comparison of
pg_search(ParadeDB),pg_textsearch(Tiger Data), andpg_tokenizer + vchord_bm25(VectorChord) selectedpg_searchas the first integration target. Five-phase plan indocs/plans/bm25-integration.md(BM-1 observability → BM-5 advisor + audit). The other two are deferred with documented return conditions. Feature-shortlist gains a new section 12 (BM25); the old section 12 (Multi-database support) is renumbered to section 13.
Added
-
audit_vector_indexesscorecard category (RAG-B). Foldsanalyze_vector_search_efficiencyintoaudit_database. Walkspg_indexfor every HNSW / IVFFlat / turboquant index in user schemas, runs a small per-index sweep (sample_size=10, multipliers=(1, 4), 30 queries per index) and surfaces the findings as aCategoryResultnamed"ANN Index Efficiency". ReturnsNonewhen pgvector isn’t installed or no ANN indexes exist, so the category is cleanly omitted on stock clusters — no padding, no score dilution. Composite-PK and PK-less tables are skipped silently (surfaced as a GOOD baseline metric so the operator sees what was skipped). Per-index failures are isolated: one index raising doesn’t sink the rest of the audit. Same scoring convention asaudit_turboquant_indexes(CRITICAL = -30, WARNING = -15, clamped at 0). Lives inmcpg.rag_efficiency. -
analyze_vector_search_efficiencycross-backend retrieval-quality report (RAG-A). One report shape, three backends — HNSW, IVFFlat, and pg_turboquant. Detects the index’s access method (pg_am), picks the right per-backend knob (ef_searchfor HNSW,probesfor IVFFlat,candidate_limitfor turboquant), samples query vectors from the table itself, computes a brute-force exact baseline via the pgvector function-form distance (documented as non-indexed), and sweeps the approximate retrieval across a multiplier curve. Reportsrecall@k, Spearman rank correlation, Kendall tau, per-query p50/p95 wall-clock latency, and (turboquant only) the page-pruning ratio fromtq_last_scan_stats.Rule table (every signal documented, no speculation):
baseline_recall_low(CRITICAL) —recall@kat the default knob is below 0.80.rerank_lift_flat(WARNING) — recall barely moves across the sweep (knob over-provisioned), suppressed when baseline is already low so the suggested action stays correct.rerank_lift_steep(WARNING) — recall jumps from<0.70to>=0.95between the 1x and 4x multiplier (knob too tight).ranking_degraded(WARNING) — recall stays high but Spearman rank correlation drops below 0.50 (right rows, wrong order).pruning_ineffective(WARNING, turboquant only) — medianpages_pruned / pages_scanned < 0.10.
Statistical helpers (Spearman, Kendall tau-b with ties, recall@k, percentile interpolation) are pure-Python — no SciPy / NumPy at runtime. Identifier safety,
_MAX_SAMPLE_SIZE = 100cap, andSET LOCAL(transaction-scoped GUC restore) match themcpg.vector_tuningconventions. Each call burnssample_size x (1 + len(multipliers))queries; the brute-force baseline is sequential on the table, so this is an ad-hoc diagnostic — the docstring is explicit.inner_productis deferred to a follow-up because the pgvector operator (negated) and the function form (raw) order opposite directions, which requires careful handling beyond Phase A’s scope.Read-only; registered as a new
_register_rag_efficiencyfamily intools.py. Lives inmcpg.rag_efficiency. The turboquant arm composes withmcpg.turboquant.turboquant_rerank_candidatesandget_turboquant_last_scan_statsshipped in earlier PRs.
Changed (BREAKING)
-
TurboQuantIndexInfofield alignment to verified upstream keys. A direct read ofsrc/tq_extension.cconfirmed that seven fields the original TQ-1 implementation exposed (algorithm_version,quantizer_family,residual_sketch_kind,fast_path_eligible,capability_flags,delta_state,maintenance_recommended) don’t appear in upstream’s actualtq_index_metadataJSON payload — they were extracted from English prose in the README, not from the source. Those fields are removed in this PR, along with three advisor rules that depended on them (format_v1_reindex_needed,maintenance_due,fast_path_ineligible) — those rules’ keying conditions never matched against a real install and were effectively no-ops.New typed fields (sourced from verified keys):
access_method,opclass,input_type,heap_relation,heap_live_rows_estimate,capabilities(dict),operability(dict),delta_enabled,delta_head_block,delta_tail_block,delta_page_depth,delta_live_fraction,delta_merge_thresholds(dict). The previously-addeddelta_live_count,delta_batch_page_count, anddelta_merge_recommendedare kept.Remaining advisor rules:
prerequisites_unmet(CRITICAL — pgvector missing) anddelta_tier_large(WARNING — upstream’s owndelta_health.merge_recommendedadvisory). Callers needing fast-path-eligibility / algorithm-version information should readcapabilities/operability/raw_metadatadirectly until upstream documents what’s in those sub-objects.
Added
-
pg_turboquant post-investigation enhancements (TQ-5 un-deferred,
delta_tier_largerule shipped,tq_maintain_indexreturn JSON surfaced). A focused investigation of upstream’ssql/pg_turboquant--0.1.0.sqlandsrc/tq_extension.cresolved three previously-deferred items:-
delta_tier_largeadvisor rule (was deferred in TQ-2).tq_index_metadatawas found to exposedelta_live_count,delta_batch_page_count, anddelta_health.merge_recommended.TurboQuantIndexInfogains three typed fields sourced from those verified keys (the original prose-sourced fields likedelta_state/maintenance_recommendedare preserved for backwards compatibility). The new rule trusts upstream’s ownmerge_recommendedboolean rather than computing an MCPg-side threshold — no speculation, the extension decides. -
tq_maintain_indexreturn JSON (was deferred in TQ-3). Upstream’s return shape is documented (persrc/tq_maintenance.h):delta_merge_performed,merged_delta_count,recycled_delta_page_count.MaintenanceResultsurfaces these three plus the raw payload; missing keys map toNonevia the same defensive-parsing pattern used elsewhere in the module. -
TQ-5 query execution + per-query knob advisor (was deferred in TQ-3 PR). Full signatures (arguments + return tables) were read verbatim from upstream’s SQL definitions, removing every speculation gap that motivated the original defer. Three new read-only tools:
turboquant_approx_candidates(schema, table, id_column, embedding_column, query_vector, metric, candidate_limit, probes?, oversample_factor?, half_precision?)returns[{candidate_id, approximate_rank, approximate_distance}].turboquant_rerank_candidates(...)addsfinal_limitand returns the exact-rerank fields too (exact_rank,exact_distance).recommend_turboquant_query_knobs(candidate_limit, final_limit?, index_schema?, index_name?, filter_selectivity?)dispatches between upstream’s plain and index-aware overloads based on whetherindex_schema/index_nameare supplied.
metricaccepts the same public-facing names as TQ-4 (cosine/inner_product/l2) but is translated internally to upstream’s runtime token (cosine/ip/l2) via a new_TQ_METRIC_TEXT_FOR_METRICmapping — single source of truth, separate from TQ-4’s opclass mapping which lives in the same file.half_precision=Trueswitches to upstream’shalfvecoverload.query_vectoraccepts alist[float]or a pre-formatted text literal, matching the existingvector_searchconvention.
This un-defers the RAG efficiency suite’s turboquant arm (
analyze_vector_search_efficiencycan now sweeprerank_limitviatq_rerank_candidates). -
-
create_turboquant_index+reindex_turboquant_indexDDL tools (TQ-4). Completes the pg_turboquant integration. The create tool buildsCREATE INDEX … USING turboquantunder tight allowlists:metricselects the opclass from a single-source-of-truth dict (tq_cosine_ops/tq_inner_product_ops/tq_l2_ops),bitsis bounded1..64,listsis bounded0..1_000_000,transformis an explicit allowlist of{'hadamard'}only (README documents this one value — others are refused rather than guessed), andnormalizedmust be a true Python bool. Any option not supplied is omitted from theWITH (...)clause so upstream applies its default. The reindex tool wrapsREINDEX INDEX [CONCURRENTLY]and applies the same catalog pre-flight asmaintain_turboquant_indexto refuse non-turboquant indexes. Both run viaDatabase.run_unmanagedso they execute on an autocommit connection (CONCURRENTLYcannot run inside a transaction). Every identifier (schema/table/column/index/name) passes through_validate_identifierand then_pg_quote_ident; every option value passes through bounds or allowlist checks. The rendered SQL is preserved increate_sql/reindex_sqlon the result for auditability. Gated under unrestricted +MCPG_ALLOW_DDL; registered in a new_register_turboquant_ddlfamily. -
maintain_turboquant_indexwrite tool (TQ-3). Wraps upstreamtq_maintain_index(<schema>.<index>::regclass)for lightweight merge / compaction of a turboquant index’s physical delta tier. Identifier validation runs first; a catalog pre-flight (pg_index ⨝ pg_am WHERE am.amname = 'turboquant') then confirms the named index is actually a turboquant index before invoking upstream — without this, an attacker could probe arbitrary catalogs via upstream’s error messages. The wrapper measures client-side timings (start, end, elapsed) and returns those in aMaintenanceResult; the PG return value oftq_maintain_indexis intentionally not parsed because upstream doesn’t document a return shape and inventing one would invite a breaking change later. Gated under WRITE (unrestricted mode); registered in a new_register_turboquant_writesfamily. - pg_turboquant maintenance advisor + audit category (TQ-2). New
recommend_turboquant_maintenancetool walks every turboquant index and emits stable-coded findings:prerequisites_unmet(CRITICAL) — pg_turboquant is installed but its hard dependency (pgvector) is not. Short-circuits before any per-index work since no working index can exist in that state.format_v1_reindex_needed(CRITICAL) —algorithm_versionstarts withv1; emitsREINDEX INDEX CONCURRENTLYas the suggested action.maintenance_due(WARNING) —tq_index_metadatareportsmaintenance_recommended=true; emitsSELECT tq_maintain_index(...).fast_path_ineligible(WARNING) —fast_path_eligible=false(explicitFalse—Nonemeans “not reported” and does not fire). The advisor reads exclusively fromTurboQuantIndexInfofields TQ-1 already surfaces, so it composes cleanly without duplicating catalog queries. A scorecard adapter (audit_turboquant_indexes) produces aCategoryResultnamed"pg_turboquant Indexes", scored 100-down with CRITICAL = -30 / WARNING = -15 (clamped at 0). When the extension is not installed, the adapter returnsNoneandaudit_databasecleanly omits the category — stock clusters’ scorecards aren’t padded or score-diluted. The fifth planned rule (delta_tier_large) is deferred to backlog: the upstreamtq_index_heap_statspayload doesn’t yet document a delta-row key we can rely on, and shipping a rule against an unverified contract would produce noise rather than signal. Will return when the contract is verifiable.
-
pg_turboquant read advisors (TQ-1). Four read-only tools wrapping the pg_turboquant ANN index extension’s observability surface:
list_turboquant_indexes,get_turboquant_index_metadata,get_turboquant_heap_stats, andget_turboquant_last_scan_stats. Each function returns cleanly (empty list /None) when the extension is not installed, so callers can treat absence as “no turboquant in use” rather than a hard error. Documented metadata keys are surfaced as typed dataclass fields; the full upstream JSON payload is preserved inraw_metadata/rawso future-added fields remain accessible without a code change. Identifier validation (matching thevector_tuningrule) runs before any SQL is built.pg_turboquantis also added to theENABLEABLE_EXTENSIONSallowlist so the existingenable_extensiontool can install it. EachTurboQuantIndexInfoalso carries the index’sWITH (...)build-time options (bits,lists,transform,normalized) parsed frompg_class.reloptionsinto a typedindex_optionsdict — agents see the configuration at a glance without a second round-trip. Unknown reloption keys are preserved as strings rather than rejected, so a future upstream option doesn’t break catalog reads. The fifth planned tool (recommend_turboquant_query_knobs, wrappingtq_recommended_query_knobs) is intentionally deferred — its upstream signature is not yet documented at the field level, and shipping a guessed signature would force a breaking change later. schedule_logical_backuptool. Schedules a recurringpg_dumpviapg_cron+COPY TO PROGRAM: the cron job runspg_dumpon the database host’s filesystem and writes the dump to a caller- supplied destination. Supportsplain/custom/tarformats,schema_only, optionalgzipcompression, and an explicitport(default5432).databaseis required —pg_dumpinvoked throughCOPY TO PROGRAMdoes not inherit the connection’s database and falls back to the OS user name without-d.destinationandpg_dump_pathare matched against a tight[A-Za-z0-9_./-]allowlist;databaseis matched against a slightly looser allowlist that admits the hyphen common in real database names (app-prod) but still rejects shell metacharacters;portis bounded to1..65535.COPY TO PROGRAMis PostgreSQL-superuser-only, so the connected role must be superuser for the scheduled job to succeed at runtime. WRITE-gated; requirespg_croninstalled.
[0.6.0] - 2026-06-05
Added
recommend_index_dropstool. Sibling ofrecommend_indexesfor what to drop — walkspg_stat_user_indexes+pg_stat_user_tablesand flags existing indexes that look like pure cost (large on disk, never or barely scanned). Three reason codes, descending strength:never_used(idx_scan = 0; safe drop),scan_no_fetch(planner picks it but it returns no rows — usually an existence-check pattern that a partial index would serve more cheaply),rarely_used(scan rate belowlow_scan_ratio— default 1% — of the table’s total scan activity). Primary-key, unique, and exclusion-constraint indexes are excluded (dropping those would be a schema change, not a performance win); indexes belowmin_index_size_bytes(default 1 MB) are skipped so the report focuses on candidates worth an operator’s attention. Each result carries a ready-to-runDROP INDEX CONCURRENTLYstatement; results are sorted by reason strength then size descending so the highest- confidence, highest-impact drops come first. Read-only advisor — execution is on the operator. Lives inmcpg.indexing.
Docs
- Living-doc refresh. Reconciled
docs/tools.md,docs/user-guide.md,docs/security.md,docs/installation.md,docs/architecture.md, anddocs/PROGRESS.mdwith the post-v0.5.0 work that had landed in code but not in docs:- HTTP transport gates table in
docs/tools.mdnow covers the IP allowlist, TLS / mTLS, cloud secrets backends, OpenTelemetry, slow-call logging, and HSTS / body-cap / CORS settings. docs/tools.mdtool index gains the missing rows forrecommend_index_drops,monitor_index_build,list_unapplied_migration_scripts, and the pgvector-analytics family (cross_table_similarity,cluster_vectors,detect_vector_outliers,monitor_embedding_drift,migrate_vector_to_halfvec,analyze_distance_metric,import_vectors); reference section added forrecommend_index_drops.docs/security.mdT6 covers IP allowlist + in-process TLS / mTLS; new T9a / T9b threats document the cloud secrets backends and the deliberately-narrow OpenTelemetry span payload (argument values never attached so exporters can’t become side channels for credentials / PII).docs/installation.mdadds rows to Common scenarios for IP allowlist, in-process TLS, mTLS, the three secrets backends, OpenTelemetry tracing, and slow-call logging, plus an HTTP transport TLS / mTLS section. Stale “all 38MCPG_*variables” claim corrected.docs/architecture.mdmodule map gainsmcpg.secretsandmcpg.otel_tracingrows;mcpg.http_runtimerow expanded to cover IP allowlist and TLS termination.docs/user-guide.mdadds arecommend_index_dropsbullet next torecommend_indexes, plus an OpenTelemetry tracing section explaining themcpg[otel]extra and the deliberate no-argument-values policy.docs/PROGRESS.mdtop-of-file metadata refreshed (date 2026-05-27 → 2026-06-05, tool count 114 → 141) and the post-v0.5.0 wave summary added; Next action block refreshed.
- HTTP transport gates table in
-
Tool count sync.
docs/tour.md,docs/user-guide.md, anddocs/tools.mdnow report 141 tools (was 124) — reconciled fromgrep -c '@server.tool' src/mcpg/tools.pyper the parallel-roadmap §6 process. Added therecommend_index_dropsline todocs/tour.md’s health section. -
TLS / mTLS for the HTTP transports (
MCPG_HTTP_TLS_CERTFILE/MCPG_HTTP_TLS_KEYFILE/MCPG_HTTP_TLS_CA_CERTS/MCPG_HTTP_TLS_CLIENT_CERT_REQUIRED). Whencertfile+keyfileare set,run_httpinstructs uvicorn to terminate TLS itself. Addingca_certsplus flippingclient_cert_required=trueupgrades the listener to full mutual TLS — connections without a client cert signed by a CA in the bundle are refused at the handshake layer, before any ASGI middleware sees the request. Settings are cross-validated at boot:certfileandkeyfilemust both be set (or both be unset),client_cert_requiredrequiresca_certs(otherwise the listener would either reject everyone or accept any self- signed cert depending on the SSL backend), and every path must exist on disk so a typo failsload_settingsinstead of the first uvicorn startup. Lives inmcpg.http_runtime; disabled by default — operators behind a reverse proxy continue to terminate TLS at the proxy as before. -
IP allowlist for the HTTP transports (
MCPG_HTTP_IP_ALLOWLIST). When set, every request to the HTTP transports (streamable-http / sse) is matched against a comma-separated list of IP addresses and / or CIDR ranges before any other middleware runs; non-matching clients receive a minimal 403 with no body specifics so a scanner can’t fingerprint the allowlist. Entries are validated at boot (a malformed entry failsload_settingsinstead of the first request). The matching IP is the immediate connecting peer (scope["client"][0]);X-Forwarded-Foris deliberately not honoured because trusting a forwarded header without a verified upstream is a well-known spoofing vector — operators behind a reverse proxy should enforce the allowlist at the proxy layer where TLS terminates. Disabled (empty) by default; the middleware is only added to the stack when an allowlist is configured. Lives inmcpg.http_runtime. - Cloud secrets backends — Vault / AWS / GCP
(
MCPG_SECRETS_BACKEND=vault|aws|gcp). Three new optional providers join the existingenv+fileones, each behind its own extra so a deployment doesn’t pay for every cloud SDK when it only uses one:vault(mcpg[vault], via thehvacSDK) — HashiCorp Vault KV v2. Configured withMCPG_VAULT_ADDR+MCPG_VAULT_TOKEN, plus optionalMCPG_VAULT_NAMESPACEandMCPG_VAULT_PATH_PREFIX(defaultsecret/mcpg). Looked-up names split on the last/so callers can address sub-paths (foo/bar→ read thebarfield at<prefix>/foo); a bare name reads thevaluefield by convention.aws(mcpg[aws], viaboto3) — AWS Secrets Manager. Authentication uses the standard AWS env / IAM-role chain; the optionalMCPG_AWS_SECRETS_PREFIXis prepended to every name on lookup.SecretStringpayloads are auto-detected as JSON (per-field) or single-value strings.gcp(mcpg[gcp], viagoogle-cloud-secret-manager) — GCP Secret Manager.MCPG_GCP_PROJECT_IDis required; the optionalMCPG_GCP_SECRETS_PREFIXjoins it to form the full resource pathprojects/{project}/secrets/{prefix+name}/versions/latest.
Every provider preserves the
file-backend semantics: a name present in the backend wins, anything absent falls back to the process environment, so partial backends and vendor-conventional API-key env vars keep working. Each cloud provider caches lookups in-process for the lifetime of the server (the latency + per-call cost of a real secrets manager makes re-fetching on every config touch impractical — restart to pick up rotated values), lazily imports its SDK so the install error only fires on first lookup (with a clearinstall mcpg[xxx]hint), and distinguishes auth / permission failures (raised asSecretsErrorso operators see the real problem) from resource-not-found (silent env fallback). -
Inline usage examples in MCP tool descriptions. Wrapped the descriptions of ~25 high-traffic tools (introspection —
list_schemas,list_tables,describe_table,list_indexes,list_constraints,list_foreign_keys; query —run_select,explain_query,analyze_query_plan,translate_nl_to_sql; composite —summarize_table,why_is_this_slow; health —check_database_health,analyze_workload,recommend_indexes; search —vector_search,mmr_search,hybrid_search,full_text_search,fuzzy_search; diagrams —generate_schema_diagram,generate_schema_docs; schema-diff —compare_schemas; vector analytics —cluster_vectors,monitor_embedding_drift; migrations —prepare_migration; data movement —export_query) with a canonical pseudo-Python invocation example so agents have a concrete starting point for tools whose argument shape isn’t obvious from the name. The_with_example(description, example)helper inmcpg.toolsis the contract for new tools; the rendered format ends every wrapped description withExample: `tool(...)`. -
list_unapplied_migration_scriptstool (MCPG_MIGRATION_SCRIPTS_ROOTS). Pairs the on-disk migration scripts (the source of truth for what should run) with the database’s history table (what has run) and reports the delta. Walksscripts_dir(one level deep) for framework-specific files: FlywayV<version>__<desc>.sql, Alembic<revision>_<slug>.py, Liquibase<changeset>.sql. Extracts each script’s identifier and cross-references againstflyway_schema_history/alembic_version/databasechangelog. Returns the pending list (with a one-line first-comment preview per script), the applied identifiers, andavailable=falsedistinctly frompending_count=0for the greenfield case (no history table yet → every on-disk script surfaces as pending so a from-scratch plan is still possible). Filesystem access is gated — operators opt in by settingMCPG_MIGRATION_SCRIPTS_ROOTSto a colon-separated list of absolute directory prefixes; by default the tool refuses every path.scripts_diris canonicalised (symlinks dereferenced,..resolved) before the prefix check, so traversal escapes are caught at the allowlist gate. DDL-gated alongside the rest of the migration family. Lives inmcpg.migration_ingestion. -
OpenTelemetry tracing — one span per
call_tool(MCPG_OTEL_ENABLED). Optional via themcpg[otel]extra. Wraps every MCP tool invocation in a span on themcpg.toolstracer withmcp.tool.name,mcp.tool.argument_count,mcp.tool.status, anderror.type/error.messageon failure (message truncated at 200 chars). Span status is set to OK / ERROR so backends that surface that field light up failure cases without parsing attribute text. Raw argument values are deliberately not attached because tool arguments can carry secrets / PII. StandardOTEL_*env vars (collector endpoint, headers, resource attributes, sampler) take precedence;MCPG_OTEL_SERVICE_NAMEis the only project- specific knob and only applies whenOTEL_RESOURCE_ATTRIBUTESdoesn’t already setservice.name. Lives inmcpg.otel_tracing. Disabled by default. -
monitor_embedding_drifttool (pgvector). Compare two time windows of an embedding column and flag distributional drift. Samples up tosample_size(default 5000) non-NULL embeddings from each window (filtered bytimestamp_column), computes the centroid (per-dimension mean vector) and L2-norm distribution of each, and reports the cosine distance between the two centroids (the main drift signal), the relative change in mean / std of the L2-norm distribution, and a booleandrift_detectedthat flips when cosine distance exceedsdrift_threshold(default 0.05). Each window is a half-open[start, end)interval — using the same instant as one window’s end and the next window’s start doesn’t double-count rows.insufficient_data=trueis returned distinctly fromdrift_detected=falsewhen either window is empty. Read-only;available=falsewithout pgvector. Lives inmcpg.vector_ops. -
migrate_vector_to_halfvectool (pgvector). Read-only DDL planner that converts avector(N)column tohalfvec(N)(halving per-element storage: 4 → 2 bytes, with negligible recall impact at d ≥ 768). Reads the column’s type, dimension, row count, and every index touching the column from the catalog, and emits an orderedmigration_sqlplan: DROP each affected index, ALTER COLUMN to halfvec(N) via aUSING col::halfvec(N)cast, then recreate each index with itshalfvec_*_opssibling (vector_l2_ops→halfvec_l2_ops, same for cosine / ip / l1). Also emits a mirrorrollback_sqlthat restores the original vector(N) type plus each index’s original definition. Returnsalready_halfvec=true(empty plan) when the column is already at the target type, and refuses any ANN index whose opclass has no halfvec sibling rather than rewriting it incorrectly. Nothing is executed — the caller is expected to validate via the shadow- migration workflow before applying. Lives inmcpg.vector_tuning. -
detect_vector_outlierstool (pgvector). Flags rows whose embedding sits far from any cluster centroid. Samples up tosample_size(default 5000) non-NULL rows, clusters them with the same k-means engine ascluster_vectors, then per cluster computes a z-score on the distance from each row to its centroid and flags rows whose z-score exceedszscore_threshold(default 3.0). Per-cluster scoring catches rows that are weird-for-their-group rather than weird-overall, which is usually what users mean by “outlier”. Singleton clusters (k-means’ way of saying “this row didn’t fit anywhere”) have their lone member flagged automatically with infinite z-score. Returnsoutlierssorted by z-score descending (capped atmax_results),total_outliers(the unclipped count), andcluster_stats(per- cluster mean / std of within-cluster distances). Read-only;available=falsewithout pgvector. Lives inmcpg.vector_ops. -
cluster_vectorstool (pgvector). k-means clusters an embedding column in-process: samples up tosample_size(default 5000) non-NULL rows ofschema.table.embedding_column, runs Lloyd’s algorithm with k-means++ seeding (deterministic viaseed), and returnscentroids(one per cluster with size) +assignments(per-row cluster index + distance). Whenid_columnis set each assignment carries that column’s value; otherwise the row’s positional sample index.metricsupportsl2(default — squared Euclidean) orcosine(vectors normalised, centroids re-normalised every iteration so Lloyd still converges). Includes empty-cluster re-seeding and a centroid-drift convergence check. Read-only;available=falsewithout pgvector. Lives inmcpg.vector_ops. -
monitor_index_buildtool. Surfaces every activeCREATE INDEXoperation frompg_stat_progress_create_index(PG12+, no extension required). One row per build with PID, resolvedschema.relation.index_name, the command, the phase label, rawblocks_done/blocks_total+tuples_done/tuples_totalcounters, and a computedprogress_pct(blocks first, tuples as fallback,nullwhen neither phase reports a denominator). Useful next tolist_active_querieswhen an HNSW / IVFFlat build on a big table is taking longer than expected. Lives inmcpg.liveops; read-only. -
validate_migration_schematool. Verify a candidate migration SQL against a reference schema definition. Clones the target schema (production snapshot) into a transient shadow schema, applies the candidate DDL, and runscompare_schemasagainst the reference schema. Gated under DDL (unrestrictedaccess mode +MCPG_ALLOW_DDL=true). -
seed_table_with_sample_datatool. Generate and execute syntheticINSERTstatements to seed a table with sample data. Values respect column types, NOT NULL, and DEFAULT constraints. Gated under WRITE (unrestrictedaccess mode). -
Zero-Downtime Migration Cookbook. Added a comprehensive guide under
docs/cookbook.mdoutlining recipes for safe, zero-downtime operations in PostgreSQL (such as concurrent indexes, validating constraints/FKs usingNOT VALID+VALIDATE CONSTRAINT, column renames, and type changes). -
cross_table_similaritytool (pgvector). Locates a specific row insource_schema.source_tablebysource_id_column = source_id_value, reads its embedding fromsource_embedding_column, and issues a pgvector k-NN againsttarget_schema.target_table.target_embedding_column. Both columns must bevector(N)with matchingN— checked from the catalog up front so a mismatch fails with a clear error rather than a pgvector cast error on the inner query. Returnssource_embedding_found=falsedistinctly from “no neighbours”. Read-only;available=falsewithout pgvector. Lives inmcpg.vector_ops. -
read_migration_historytool. Adds a read-only tool to inspect and parse the native migration bookkeeping tables of popular migration frameworks (Alembic, Flyway, Diesel, Django, Prisma, Golang Migrate, Goose, Sequelize). Reports migration history records with full framework-specific metadata fields. -
pg_walinspectintegration. Addsread_pg_wal_recordsandread_pg_wal_statstools to analyze Write-Ahead Log (WAL) record details and aggregated stats over specified LSN ranges. Degrades gracefully if the extension is not installed (available=false). Addspg_walinspectto the list of programmatically enableable extensions. -
pg_buffercacheintegration. Addsread_pg_buffercache_summaryandread_pg_buffercache_relationstools to analyze shared buffer cache usage at the cluster and relation levels. Degrades gracefully if the extension is not installed (available=false). Addspg_buffercacheto the list of programmatically enableable extensions. -
analyze_distance_metrictool (pgvector). Samples up tosample_size(default 1000) non-NULL embeddings fromschema.table.column, computes each one’s L2 norm, and recommends a distance metric from the magnitude distribution: pre-normalised vectors (CV < 5%, mean ≈ 1.0) →inner_product; nearly-constant but off-unit magnitudes →cosine(same ranking as L2, safer default); variable magnitudes →cosine(normalises out heterogeneous sources). Returns the metric + a rationale + the underlying distribution stats. Newmcpg.vector_opsmodule — first resident of a vector-analytics namespace separate from search (textsearch) and storage tuning (vector_tuning,vector_tuner_advanced). Read-only;available=falsewithout the pgvector extension. -
import_vectorstool (pgvector). Bulk-load embeddings into a pgvectorvector(N)column from JSON (array of objects) or CSV. Reads the column’s declaredNfrom the catalog up-front and validates every row in the payload BEFORE any INSERT runs — a dimension mismatch on row 1000 fails the whole call rather than leaving 999 partial inserts behind. CSV cells accept bracketed pgvector literals or comma-separated numbers; JSON accepts lists or literal strings. Optional parallelid_columnfor anINSERT (id, embedding) VALUES (%s, %s::vector)shape. Errors when the target column isn’t pgvector. Write-gated (unrestricted mode). -
Slow-call logging from the MCP layer (
MCPG_SLOW_CALL_THRESHOLD_MS). Logs a warning to themcpg.serverlogger when any tool execution duration exceeds the configured threshold. Defaults to1000ms. A value of0or less disables this logging. -
Structured JSON logging toggle (
MCPG_LOG_FORMAT). Adds an opt-in structured JSON logging format. WhenMCPG_LOG_FORMAT=jsonis set, all logging output from themcpgserver is formatted as a structured JSON object. Standard log messages carrytimestamp,level,logger, andmessagekeys, whilemcpg.audittool calls merge the audit payload (tool,status,arguments,error) directly into the top level. -
walk_blocking_chainstool (deadlock cycle walker). Walks and reconstructs the active lock-wait graph of the database usingpg_blocking_pids. Identifies all simple deadlock cycles, linear blocking paths leading to root blockers or cycles, list of root blocker PIDs, and renders a styling-annotated Mermaid flowchart representing the lock dependency graph. Read-only. -
generate_schema_docstool (schema documentation). Generates a comprehensive Markdown catalog reference for a database schema’s tables, views, foreign tables, custom enums, constraints, indexes, and comments. Supports an optionalinclude_samplesflag to sample and display the first few non-null, distinct, truncated values for each column. Gated under heavy diagnostics (MCPG_ENABLE_HEAVY_DIAGNOSTICS) and supports caching. -
mmr_searchtool (pgvector). Diversity-aware vector search: fetchesfetch_knearest candidates by pgvector distance, then re-ranks with Maximal Marginal Relevance to returnkrows that are relevant but not near-duplicates — better LLM context than raw top-k.lambda_mult(0–1) trades relevance for diversity; both the relevance and diversity terms are cosine similarities computed in-process over candidate embeddings, so the result is independent of the recall-passmetric. Each hit carries itsrelevance,mmr_score, and selectionrank. Read-only;available=falsewithout the pgvector extension. -
Pluggable secrets backend (
MCPG_SECRETS_BACKEND). ASecretsProviderabstraction (mcpg.secrets) that the secrets read inload_settingsroute through — the NL→SQL provider API keys,MCPG_HTTP_AUTH_TOKEN, andMCPG_AUDIT_HMAC_KEY. Two backends ship:env(default — unchanged behaviour, zero new deps) andfile, which overlays a JSON (or YAML, when PyYAML is importable)name → valuemap fromMCPG_SECRETS_FILE_PATHon top of the environment (a name in the file wins; anything absent falls back to its env var). Cloud backends (Vault / AWS / GCP) will follow behind optional extras using the same switch.Settings.secrets_backendrecords the active choice (values never appear inrepr). -
verify_connection_encryptiontool. Reports whether MCPg’s own connection to PostgreSQL is TLS-encrypted — negotiated protocol version, cipher, and key bits frompg_stat_ssl— plus a cluster-wide encrypted/unencrypted backend tally. A runtime complement to the startup TLS-enforcement check. Read-only; available in every access mode. -
prune_audit_eventsretention tool. Deletes persisted audit events older thanolder_than_daysfrommcpg_audit.events, a cron-friendly cap on the otherwise-unbounded audit table. Returns the number deleted, the cutoff timestamp, and rows remaining. Refuses to run whenMCPG_AUDIT_INTEGRITYis enabled (pruning would break the HMAC signature chain). Write-gated (unrestricted mode). -
Subprocess hardening for the shell-gated PG binaries.
run_pg_binarynow (a) validates the resolvedpg_dump/pg_restore/psqldirectory againstMCPG_SUBPROCESS_BIN_ALLOWLIST(empty = trustPATH), rejecting a PATH shim in an untrusted directory while still allowing distropg_dump -> pg_wrappersymlinks; (b) appliesRLIMIT_CPU/RLIMIT_ASvia apreexec_fnwhenMCPG_SUBPROCESS_CPU_SECONDS/MCPG_SUBPROCESS_MEMORY_MBare set (POSIX only); and (c) spawns in a throwaway temp working directory. All opt-in; defaults preserve prior behaviour. -
Opt-in per-request HTTP timeout.
MCPG_HTTP_REQUEST_TIMEOUT_SECONDS(default0= disabled) caps wall-clock time per request on the HTTP transports, returning504on expiry. Disabled by default so long-lived SSE / streamable-http streams keep working; intended for plain request/response deployments wanting a DoS backstop. Completes the HTTP-hardening set started in the security-diagnostics release. -
Advanced security hardening, lifecycles & diagnostics (#37). HTTP hardening middleware (security headers, request-size limit, CORS allowlist); graceful-shutdown draining of in-flight tool calls (
MCPG_SHUTDOWN_DRAIN_SECONDS); audit-log HMAC integrity chain with theverify_audit_chaintool (MCPG_AUDIT_INTEGRITY+MCPG_AUDIT_HMAC_KEY); a redundant-index advisor; and ananalyze_hnsw_recallpgvector tuner. -
Adaptive caching, feature flags & related (#36). See the PR for detail.
-
Multi-provider routing for
translate_nl_to_sql. MCPg now auto-discovers every configured NL→SQL provider from the environment at startup (ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY/GOOGLE_API_KEY) — each one becomes callable through the tool, not just the configured default. The tool gains an optionalprovider="anthropic"|"openai"|"gemini"argument so a caller can route per-call across configured providers; without it, MCPg falls back toMCPG_NL2SQL_PROVIDER(the default), then to the first available in preference order anthropic → openai → gemini.get_server_infosurfacesnl2sql_default_providerandnl2sql_available_providersso agents can introspect.Enables the “one MCPg server, many MCP clients” deployment shape: set every vendor key on the host, run one MCPg over HTTP, let each agent / IDE pick its preferred LLM per call.
Changed
-
Settings.nl2sql_api_key(single value) →Settings.nl2sql_api_keys(tuple of(provider, key)pairs). Backward-incompatible only for code that importsSettingsdirectly — the env-var surface stays compatible:MCPG_NL2SQL_PROVIDER+ vendor-conventional env vars still work as before, andMCPG_NL2SQL_API_KEY(when set) still supplies the key for the configured default provider.MCPG_NL2SQL_API_KEYnow requiresMCPG_NL2SQL_PROVIDERto also be set (MCPg can’t tell which provider a stray key belongs to); startup fails with a clear message if only the key is set. -
MCPG_NL2SQL_PROVIDERis now optional when at least one vendor key is in the env — MCPg auto-picks a default. Setting it explicitly still pins the default.
[0.5.1] - 2026-05-29
Inaugural PyPI release. Three landings since 0.5.0: security hardening, license switch to MIT, and the packaging surface needed to publish to PyPI.
Added
-
mcpg --version(mcpg -V) flag. Printsmcpg <version>so bug reporters can paste the lineSECURITY.mdasks for. Backed bymcpg.__version__. -
PyPI publishing pipeline. New
.github/workflows/publish.ymltriggers onv*.*.*tag pushes and runs: build → twine check → TestPyPI upload → install-smoke against the published TestPyPI artifact (deps resolved from real PyPI first, thenmcpgwith--no-depsfrom TestPyPI — closes a dependency-confusion vector) → reviewer-gated PyPI upload via Trusted Publishing OIDC → cut a GitHub Release with notes pulled fromCHANGELOG.md. The full playbook lives atdocs/release-process.md. -
pyproject.tomlpackaging metadata. Addsauthors,maintainers,keywords,classifiers,[project.urls](Homepage / Documentation / Repository / Issues / Changelog / Release notes / Security), and[tool.hatch.build.targets.sdist]to keep the sdist tight. Addsbuild>=1.2+twine>=5.1to thedevdep group. -
“Install from PyPI” section in
docs/installation.md. Covers bothpip install mcpganduv tool install mcpg. -
Per-session
statement_timeout/lock_timeout(PR #32). Every checked-out pool connection hasstatement_timeout=MCPG_STATEMENT_TIMEOUT_MS(default 30000) andlock_timeout=MCPG_LOCK_TIMEOUT_MS(default 5000) applied once per connection via a single batchedSET— runaway queries and hanging lock waits self-terminate without operator intervention. Applies to the primary pool and every replica pool.
Changed
- Hardened multi-stage Docker image (PR #32). New runtime stage
drops the build toolchain entirely; runs as a non-root user
(
uid=10001 / gid=10001) with anologinshell. Application files stay owned by root and read-only to themcpguser so a remote-code-execution bug can’t modify the application on disk to persist. Entrypoint switches fromuv runtopython -m mcpgfor a smaller process tree.
Security
-
PG TLS enforcement at startup.
load_settingsnow refuses to start whenMCPG_DATABASE_URL(or any entry inMCPG_REPLICA_URLS) points at a non-loopback host with ansslmodeofdisable/allow/prefer(or nosslmodeset — libpq’s default falls back to plaintext). Usespsycopg.conninfo.conninfo_to_dictso the check covers both URI DSNs and keyword/value DSNs (e.g.host=db sslmode=disable) plus failover multi-host URIs (e.g.postgresql://h1,h2/db); the earlierurllib.parse-based path silently bypassed the check on both shapes. DSNs with no explicit host are refused too — libpq can resolvePGHOSTto a non-loopback default. Bypassed with the explicit opt-outMCPG_ALLOW_INSECURE_TLS=true. Loopback hosts (localhost,127.0.0.1,::1) are exempt. Replica errors identify the offending index (MCPG_REPLICA_URLS[1]) for diagnostics.Upgrade note. This is a default-tightening change. Any deployment already configured with a remote DSN whose
sslmodeisdisable/allow/prefer(or unset) will fail to start after upgrade. Either setsslmode=require(orverify-ca/verify-full) in the DSN — strongly recommended — or opt out withMCPG_ALLOW_INSECURE_TLS=true. -
Tool-argument audit redaction upgraded to regex pattern match.
mcpg.audit.redact_argumentsand the persisted audit-trail walker (mcpg.audit_trail._redact) now share a case-insensitive regex matched viare.searchagainst argument key names, sopasswordalso masksPGPASSWORD/user_password/app.password. Default patterns:password,passwd,secret,token,api[_-]?key,bearer,authorization,database_url,dsn,conninfo. Operators extend the list viaMCPG_AUDIT_REDACT_KEYS(comma-separated regex fragments). Walks nested dicts / lists / tuples so credentials buried in result payloads are masked too. -
Supply-chain CI hardening (dependency audit fix). The
securityjob in.github/workflows/ci.ymlwas invokinguv audit— a subcommanduvdoes not provide — so the dependency-vulnerability step has been a silent no-op since it was added. Replaced withpip-audit --strictover theuv export-resolved runtime requirements; the existing bandit SAST step is unchanged. Addspip-audit>=2.7to thedevdependency group. -
Vulnerability-reporting policy + hardening roadmap shipped. New
SECURITY.mdat the repo root documents supported versions, the reporting address (devopam@gmail.com), the 3-business-day acknowledgement target, and the 90-day coordinated-disclosure window. Newdocs/security-hardening.mdis a living checklist of robust-security features with status indicators (✅shipped /🟡partial /⬜queued) covering what’s onmaintoday plus the queued items (HTTP request limits + security headers, audit-log HMAC chain, pluggable secrets backend, subprocess hardening, graceful shutdown).
Changed
- License: relicensed from AGPL-3.0-or-later to MIT. New
LICENSEat the repo root carries the standard MIT text.pyproject.tomllicensefield,NOTICE, ADR-0001, and the README “License” section updated to match. The vendored SQL-safety kernel atsrc/mcpg/_vendor/sql/is and remains MIT-licensed; no upstream attribution changed. No code surface changed.
Added
- Apache AGE graph + Cypher support (PR #24). Six new tools
wired into the read / DDL surfaces:
list_graphs()anddescribe_graph(graph_name)readag_catalog, returning the graphs in the database plus per-graph label / edge / property statistics. Read-only.run_cypher(graph_name, cypher_query)executes arbitrary Cypher against a named graph and returns a typed result (columns/rows/row_count). Thegraph_nameidentifier is validated (alphanumeric / underscores, not starting with a digit). The query is scanned for write keywords (CREATE/SET/DELETE/REMOVE/MERGE) and gated under the WRITE capability when any are present — reads stay under READ.generate_graph_diagram(graph_name, max_labels=50)emits a Mermaid graph of label-to-label relationships — the graph equivalent ofgenerate_schema_diagram.create_graph(graph_name)/drop_graph(graph_name, cascade=true)are DDL, gated under unrestricted +MCPG_ALLOW_DDL.- Composes with the existing advisor surface —
run_advisorsnow reports arecommend_graph_indicesrule when AGE labels lack a property-search index that an expected access pattern would need.
-
Read-replica routing (Shortlist 1.6). When
MCPG_REPLICA_URLSis set (comma-separated DSN list), everyforce_readonly=Truequery is round-robin routed to a healthy replica; writes always go to the primary. Newmcpg.replicasmodule owns per-replica pools + degraded-replica state with a 30s retry window; replica connection failures at startup are logged + marked degraded individually rather than aborting startup. Composes with the Phase-1.4 tenancy driver — each replica gets its ownTenantSqlDriver. Newlist_replicasMCP tool reports index / password-obfuscated DSN / degraded / last_error / seconds_until_retry per replica. Routing decisions land in Prometheus metrics undermcpg_tool_calls_totalwith synthetic tool name__replica_routeand statusesprimary/primary_no_healthy/fallback/replica_<n>. -
OIDC bearer-token validation (Shortlist 6.5). New
MCPG_AUTH_MODE=oidcswaps the static-token compare for full JWT validation against an OIDC issuer’s JWKS. Newmcpg.oidc.OIDCVerifierfetches the discovery doc on first use (cached), caches JWKS keys viaPyJWKClient(default 1h), and validates each request’s JWT against signature + iss + aud + exp + nbf with 30s clock leeway. Only asymmetric algorithms (RS256/RS384/RS512 + ES256/ES384/ES512) are accepted — HS-family is excluded to preserve the OIDC trust model. WhenMCPG_OIDC_ROLE_CLAIMis set the claim’s value is validated as a safe PG identifier and stashed in the samecurrent_roleContextVar the X-MCPG-Role middleware uses, so the tenanted driver issuesSET LOCAL ROLE "<role>"for the request. Addspyjwt[crypto]as a runtime dependency. The staticMCPG_HTTP_AUTH_TOKENpath is unchanged whenMCPG_AUTH_MODE=static(the default). - Docs refresh —
docs/tour.mdtool count 90 → 108 + new sections for cursors / linting / RLS / replicas / NL→SQL.docs/cookbook.mdadds two new recipes (replica routing, OIDC). README headline updated.
[0.5.0] - 2026-05-27
Thirty-three new MCP tools and four major runtime features, closing
the full docs/feature-shortlist.md (Tier A + Tier B + Tier C)
plus an NL→SQL helper. Brings the total MCP tool surface from 74
to 107 and adds: HTTP transport bearer-token auth, Prometheus
/metrics, TimescaleDB wrappers, hybrid (vector + FTS) search,
per-request SET ROLE multi-tenancy, server-side cursors, RLS
testing, synthetic test-data generation, FK cascade graphs, and a
natural-language → SQL helper.
Headline features
- Per-request multi-tenancy.
MCPG_DEFAULT_ROLE+X-MCPG-Roleheader drive aTenantSqlDriverthat wraps every query inBEGIN ... SET LOCAL ROLE "<role>" ...so one MCPg process can serve N tenants from a single connection pool. Role names validated, allowlist viaMCPG_ALLOWED_ROLES. - HTTP transport bearer-token auth + Prometheus
/metrics.MCPG_HTTP_AUTH_TOKENgates the streamable-http / sse surface;/metrics,/healthz,/readyzare exempt so a Prometheus scraper doesn’t hold the MCP credential. - NL→SQL.
translate_nl_to_sqlsends a schema brief to a pluggable LLM provider (Anthropic / OpenAI / Gemini), parses the JSON response, and optionally executes the generated SQL through the existingSafeSqlDriverallowlist. - Server-side cursors via a
CursorManagerholding dedicated per-cursor connections — pageable reads through millions of rows without starving the main pool.
Added
- Tier-A milestone closed — three picks from
docs/feature-shortlist.mdshipped together. Tool surface 84 → 90.- HTTP transport bearer-token auth (shortlist 1.1). New
mcpg.http_runtimemodule wraps FastMCP’sstreamable_http_app()/sse_app()with an ASGI_BearerAuthMiddleware. WhenMCPG_HTTP_AUTH_TOKENis set, every request needsAuthorization: Bearer <token>(constant-time compared viahmac.compare_digest); missing / wrong tokens get a 401 withWWW-Authenticate: Bearer realm="mcpg"./metrics,/healthz, and/readyzare exempt so a Prometheus scraper / load-balancer probe doesn’t need the MCP token. New settings fieldSettings.http_auth_token. Thestdiotransport is unaffected (no HTTP surface). The runtime logs a WARNING when an HTTP transport starts without a token. - Prometheus
/metricsendpoint +get_metrics_expositiontool (shortlist 2.1). Newmcpg.observabilitymodule records everycall_toolinvocation in an in-processMetricsstore and renders the standard text-exposition format (v0.0.4) — zero runtime dependency. Three series:mcpg_tool_calls_total{tool, status}(counter),mcpg_tool_duration_seconds_bucket{tool,le}(histogram with default Prometheus buckets + 30s/60s overflow),mcpg_tool_duration_seconds_sum/_count{tool}.AuditedFastMCPtimes every tool call and records (ok|error) + wall-clock seconds. The newget_metrics_expositionMCP tool returns the same payload over the MCP protocol for stdio transports where/metricsisn’t reachable. - TimescaleDB hypertable wrappers (shortlist 4.2). New
mcpg.timescaledbmodule adds five tools — two read-only (list_hypertables,list_chunks) plus three DDL-gated writes (create_hypertable,add_compression_policy,add_retention_policy). Every interval / identifier is allowlist-validated before being inlined into SQL (TimescaleDB’s management functions take interval expressions as positional args, not bound params). Each tool degrades toavailable=Falsewhen thetimescaledbextension is missing — same pattern as the existing pg_trgm / pgvector / postgis integrations.
- HTTP transport bearer-token auth (shortlist 1.1). New
- Tier-B milestone closed — four picks from the feature shortlist
shipped together. Tool surface 90 → 93 plus the runtime tenancy
feature.
find_sensitive_columns(6.2). Scanspg_attributefor columns whose names or types look like they hold PII / secrets: seven categories (credential, financial, contact, identifier, health, government_id, location) with high / medium / low confidence. Pure heuristic — no row sampling. Lives inmcpg.advisors.detect_n_plus_one(8.4). Walkspg_stat_statementsfor the classic N+1 shape: query templates called hundreds-to- thousands of times, each returning ≤max_rows_per_callrows and accumulating ≥min_total_msof wall-clock time. Sorted by total time desc; degrades toavailable=falseon databases withoutpg_stat_statements.validate_migration(9.2). Appliescandidate_sqlto a TRANSIENT shadow oftarget_schemapre-populated with up tosample_rows_per_tablerows from each base table. Catches failure modes a structural diff misses: NOT NULL added to a column with NULLs, CHECK constraints violated by live rows, type narrowings that fail. Always drops the shadow before returning. Gated under MIGRATE.- Per-request
SET ROLEmulti-tenancy (1.4). Newmcpg.tenancy.TenantSqlDriversubclasses the vendored driver and wraps every query in an explicit transaction withSET LOCAL ROLE "<role>". Role resolved per-request from theX-MCPG-Roleheader (HTTP only) or falls back toMCPG_DEFAULT_ROLE. Role names validated against[A-Za-z_][A-Za-z0-9_]*;MCPG_ALLOWED_ROLESconfigures an allowlist enforced both at startup (default must be in it) and per request (403 if not in it).SET LOCALauto-resets at txn end — no state leak into the pool._TenantRoleMiddlewaresits above bearer auth so unauthenticated requests can’t reach the role parser.
- Tier-C milestone closed — every remaining pick from the
shortlist. Tool surface 93 → 106 (13 new tools), plus a small
follow-up fix.
- Catalog readers —
list_generated_columns(4.7) readspg_attribute.attgeneratedfor stored-generated columns;list_locks+find_blocking_chains(4.5, new modulemcpg.locks) joinpg_locks/pg_blocking_pidswithpg_stat_activity;read_pg_stat_io(4.3, new modulemcpg.io_stats) wraps the PG16+ I/O stats view (degrades on 14/15). lint_naming_conventions(8.1, new modulemcpg.naming). Detects the majority case style per schema and per table (snake_case / camelCase / PascalCase / SCREAMING_SNAKE), flags outliers, plus an index-prefix rule.generate_fk_cascade_graph(8.5). Mermaidgraph LRof foreign-key cascade chains; only CASCADE / SET NULL / SET DEFAULT FKs by default. Cross-schema targets get their schema prefix preserved.run_select_parallel(3.4). Up toparallel_limitconcurrent SELECTs viaasyncio.gather; each goes through the same safety allowlist asrun_select; one bad query doesn’t abort the others.- Server-side cursors (3.1, new module
mcpg.cursors). Four tools —open_cursor,fetch_cursor,close_cursor,list_cursors. Each cursor holds a DEDICATED psycopg connection (not a pool checkout) inside aREAD ONLYtransaction, with a per-cursorasyncio.Lockso concurrent fetch / close on the same cursor can’t corrupt the wire protocol. Hard cap of 16 concurrent cursors; 5-min idle TTL with lazy sweep. test_rls_for_role(4.8, new modulemcpg.rls). Runs a SELECT as a target role insideREAD ONLY+SET LOCAL ROLE, reports applicable policies, visible row count, and a bounded sample. Identifier-validated.generate_test_data(10.3, new modulemcpg.test_data). Produces synthetic INSERT statements honouring column type, NOT NULL, DEFAULT. Deterministic with a seed; covers numeric / text / boolean / date / timestamp / json / uuid types. Unsupported types (geometry, hstore, vector, …) listed inskipped_columns. Does NOT execute — returns SQL for review.
- Catalog readers —
-
NL → SQL helper (shortlist 10.2). New
mcpg.nl2sqlmodule with a pluggableLLMProvider(Anthropic / OpenAI / Gemini) speaking each vendor’s HTTPS API viahttpx— no SDK dependency.translate_nl_to_sql(question, schema, execute=False, ...)gathers a compact schema brief (tables, columns, FKs), asks the configured model to emit JSON withsql+explanation, and — whenexecute=true— passes the generated SQL throughSafeSqlDriverbefore running. Writes / DDL / multi-statement input rejected even if the model produced them. New settings:MCPG_NL2SQL_PROVIDER/MCPG_NL2SQL_API_KEY(with vendor-env fallbacks) /MCPG_NL2SQL_MODEL/MCPG_NL2SQL_BASE_URL/MCPG_NL2SQL_MAX_TOKENS(hard cap 16384). API key never appears inrepr(Settings). -
Agent cookbook (
docs/cookbook.md). Practical recipes for common workflows: schema discovery, slow-query diagnosis, migration safety, cursor streaming, multi-tenancy, NL→SQL, observability scraping, RLS testing, data import / export, ORM model emission, TimescaleDB inspection. Linked from the README and the docs index. - Three new agent-UX-focused tools (more Tier-A picks). Tool surface
81 → 84. All read-only.
summarize_table— one-stop snapshot of a table: columns, primary key, foreign keys, every other constraint, indexes, storage / row-count / last-vacuum/analyze stats, and an optional short row sample. Replaces what would otherwise be 4-5 individual tool calls. Lives in new modulemcpg.composite.why_is_this_slow— one-call diagnosis: runsEXPLAIN (FORMAT JSON)(does NOT execute the query), walks the plan tree, snapshots concurrent active queries and blocking lock pairs, reads the cluster-wide cache hit ratio, and produces categorised suggestions (plan / contention / cache / maintenance). Safe to run on a statement the agent doesn’t want to materialise yet. Lives inmcpg.composite.find_unused_objects— scanspg_stat_user_tablesandpg_stat_user_indexesfor tables/indexes with zero scans since stats were last reset. Tables also need zero writes (the row never moved) to qualify; indexes backing PRIMARY KEY / UNIQUE constraints are excluded since PG needs them for enforcement. Returns context (scan + write counts, size, definition) so the agent can decide whether the object is safe to drop. Documented as a SIGNAL not a verdict — recent stats resets produce false positives. Lives inmcpg.advisorsalongsiderun_advisors.
- Three new pgvector tools (Tier-A picks from the feature shortlist).
Tool surface 78 → 81. All read-only; all extend
mcpg.textsearchalongside the existingvector_search/recommend_vector_indexfamily.hybrid_search— fuses vector and full-text ranking via reciprocal-rank fusion (RRF). Pullscandidate_poolcandidates from each source (vector k-NN onvector_column, FTS viawebsearch_to_tsqueryontext_column), then merges them withscore = Σ 1/(rrf_k + rank). Each match carriesvector_rank,fts_rank, the fusedrrf_score, and the original distance + ts_rank values. Tunables:metric,text_config,candidate_pool(default 50),rrf_k(default 60),limit. Closes the biggest unmet need in agentic RAG: pure vector misses keyword/identifier matches, pure FTS misses semantic synonyms.vector_range_search— finds every row withinmax_distanceof a query vector (not top-k). Useful for de-duplication, similarity gating, clustering pre-passes. Results still ordered by distance and capped atlimitso a too-loose threshold cannot pull the whole table.recommend_vector_quantization— scans a schema forvector(N)columns whose storage could shrink by switching to pgvector v0.7+’shalfvec(N)(16-bit float). Returns per-column current vs suggested bytes, the savings ratio, and a rationale. Skips columns that are already non-vectorand small tables where the absolute saving wouldn’t justify the migration. Catalog query usespg_attribute.atttypmod+ at.typname IN ('vector','halfvec','sparsevec')filter so PG’s built-inbit(N)doesn’t false-positive.
- Four more catalog → DSL exporters under the same Batch G umbrella.
Tool surface 74 → 78. All read-only, no new capability or env-var
gates. Coverage matches the existing exporters (Prisma / Drizzle /
SQLAlchemy 2.0 / sqlc): base tables, columns, primary keys, single-
column intra-schema foreign keys, enums. Cross-schema FKs and
composite FKs are documented v1 gaps.
generate_diesel_schema— emits a Diesel ORM (Rust)schema.rswith onetable!macro per table,Nullable<T>wrappers for nullable columns,joinable!lines for intra-schema FKs, and anallow_tables_to_appear_in_same_query!macro so multi-table joins type-check. Enum types are emitted as Text-backed wrapper enums in apg_enummodule so the output works withoutdiesel_derive_enum.generate_jooq_config— emits ajooq-codegenconfiguration XML pointing at the database. Unlike the other exporters, jOOQ generates Java code itself from the live database at build time; the artefact here is the XML the user feeds tomvn jooq-codegen:generate. Includes an explicit<includes>regex naming every base table, an<excludes>covering MCPg’s bookkeeping schemas, and a<forcedType>for every json / jsonb column so they map toorg.jooq.JSON/org.jooq.JSONB.generate_ent_schemas— emits Ent (Go) Schema struct files, one.goper table. Each struct listsfield.X(...)calls,edge.To(...)lines for single-column FKs, andfield.Enum().Values()for enum-typed columns. Returns a{filename: source}dict.generate_ecto_schemas— emits Ecto (Elixir) schema modules, one.exper table named after the singularised table (matching the Phoenixlib/my_app/<singular>.exconvention). Each module usesuse Ecto.Schema, declares@primary_key,fieldfor each column,belongs_tofor single-column FKs, andtimestamps()when bothinserted_at+updated_atexist. The Elixir top-level module is configurable via theapp_modulearg (defaultMyApp).
[0.4.0] - 2026-05-26
Twenty-nine new MCP tools, closing Batches D / E / F / G of the
post-0.3.0 roadmap (PLAN.md §11). Brings the total MCP tool surface
from 45 to 74 and ships the long-planned cross-cutting features:
the data-movement family, the LISTEN/NOTIFY bridge, the agent-driven
migration shadow workflow, and three new ORM-DSL exporters
(Drizzle / SQLAlchemy 2.0 / sqlc) alongside the existing Prisma one.
Headline features
- Batch D — data movement (5 tools).
dump_database/restore_databaseround-trip a database throughpg_dump/psql/pg_restorevia the ADR-0004 subprocess gate.copy_table_between_databasespipes one database’s table into another in one shell pipeline.import_csv/import_jsonbulk-load via in-processCOPY ... FROM STDINand parametrisedexecutemany— no subprocess gate needed. - Batch E — LISTEN/NOTIFY bridge (4 tools), ADR-0005.
subscribe_channel/poll_notifications/unsubscribe_channel/list_notification_subscriptionslet an agent react to PostgreSQL events through a polled, per-subscription bounded queue. NewCapability.LISTEN+MCPG_ALLOW_LISTENopt-in. - Batch F — staged-migration workflow (4 tools), ADR-0006.
prepare_migrationclones a target schema’s structure into a shadow schema via introspection, applies a candidate SQL there, and runscompare_schemasso the agent reviews the structural delta.complete_migrationlands it on the target.cancel_migration/list_pending_migrationsround out the workflow. Same-database shadow (no full-DB clone). NewCapability.MIGRATEreuses the existingMCPG_ALLOW_DDLopt-in. - Batch G — catalog → DSL exporters (3 new tools).
generate_drizzle_schema(Drizzle ORM TypeScript),generate_sqlalchemy_models(SQLAlchemy 2.0 declarative Python),generate_sqlc_schema(replayable plain DDL for sqlc). All read-only — drop into any agentic project as a starting point.
Fixed
- PR #17 code-review findings (10 fixes across the Batches D / E / F / G
surfaces):
restore_databasefor custom/tar formats now passes--dbname=postgresql:///so pg_restore actually connects (it previously fell into “convert to SQL script” mode without-d).ListenManagerrecovers from a dead listener connection — the reader-loop clears_connand sets_needs_resubscribe, the next subscribe opens a fresh conn and re-issues LISTEN for every active channel (previously the manager silently stopped delivering after any PG restart).- Migration DDL replay only rewrites schema references on
foreign_keyconstraints, not on every constraint type — a CHECK constraint whose literal happens to contain the target schema name (e.g.CHECK (path LIKE 'public.%')) is no longer corrupted. mcpg.sqlcenum labels are now apostrophe-escaped (PG-standard''doubling) so labels likeO'Briendon’t break the DDL.mcpg.sqlalchemy_exportenum generator falls back to the functionalenum.Enum("Name", {...})form when any label isn’t a valid Python identifier (in-progress,1st,class, …), keeping the generated file importable.mcpg.drizzledefault rendering now translates PG escape rules to JS escape rules in the right order:''→', backslash →\\,"→\". Previously'it''s'became"it''s"and'a\nb'silently injected a newline.- Shadow schema names are capped to fit PostgreSQL’s 63-byte NAMEDATALEN limit, preventing silent truncation that would leak shadow schemas the workflow couldn’t clean up.
- The migration shadow-workflow now refuses candidate SQL containing
statements PG won’t run inside a transaction block (CREATE INDEX
CONCURRENTLY, VACUUM, ALTER SYSTEM, …) with a clear error
pointing the user at
run_ddlinstead. mcpg.shell._write_stdinalways closes the child’s stdin in afinallyblock — a non-BrokenPipeErrorfromwrite/drainno longer leaks the pipe and wedges the child.ListenManager.close()bounds theconn.close()await at 2s so a libpq close hanging on a half-open socket can’t wedge server shutdown.
- PR #17 code-review findings (10 fixes across the Batches D / E / F / G
surfaces):
restore_databasefor custom/tar formats now passes--dbname=postgresql:///so pg_restore actually connects (it previously fell into “convert to SQL script” mode without-d).ListenManagerrecovers from a dead listener connection — the reader-loop clears_connand sets_needs_resubscribe, the next subscribe opens a fresh conn and re-issues LISTEN for every active channel (previously the manager silently stopped delivering after any PG restart).- Migration DDL replay only rewrites schema references on
foreign_keyconstraints, not on every constraint type — a CHECK constraint whose literal happens to contain the target schema name (e.g.CHECK (path LIKE 'public.%')) is no longer corrupted. mcpg.sqlcenum labels are now apostrophe-escaped (PG-standard''doubling) so labels likeO'Briendon’t break the DDL.mcpg.sqlalchemy_exportenum generator falls back to the functionalenum.Enum("Name", {...})form when any label isn’t a valid Python identifier (in-progress,1st,class, …), keeping the generated file importable.mcpg.drizzledefault rendering now translates PG escape rules to JS escape rules in the right order:''→', backslash →\\,"→\". Previously'it''s'became"it''s"and'a\nb'silently injected a newline.- Shadow schema names are capped to fit PostgreSQL’s 63-byte NAMEDATALEN limit, preventing silent truncation that would leak shadow schemas the workflow couldn’t clean up.
- The migration shadow-workflow now refuses candidate SQL containing
statements PG won’t run inside a transaction block (CREATE INDEX
CONCURRENTLY, VACUUM, ALTER SYSTEM, …) with a clear error
pointing the user at
run_ddlinstead. mcpg.shell._write_stdinalways closes the child’s stdin in afinallyblock — a non-BrokenPipeErrorfromwrite/drainno longer leaks the pipe and wedges the child.ListenManager.close()bounds theconn.close()await at 2s so a libpq close hanging on a half-open socket can’t wedge server shutdown.
Added
- ORM-bridge exporters — Batch G follow-ons (Phase 28b/c/d). Three
new MCP tools sit alongside the existing
generate_prisma_schemaunder the schema→DSL umbrella:generate_drizzle_schema— emit a Drizzle ORM TypeScript schema (drizzle-orm/pg-core) covering tables, columns with PG-native types (incl.serial/bigserialfromnextvaldefaults, length on varchar,withTimezoneon timestamptz), single-column FKs as column-level.references(() => ...), primary/unique/check constraints, indexes, defaults, and enums viapgEnum. The helper-import line is computed from what was actually emitted, so unused helpers don’t clutter the output.generate_sqlalchemy_models— emit a SQLAlchemy 2.0 declarative models file (DeclarativeBase+Mapped[T]+mapped_column) with PG types from bothsqlalchemycore andsqlalchemy.dialects.postgresql(jsonb), single-column FKs viaForeignKey("schema.table.col"), composite uniques in__table_args__, enum types emitted as Pythonenum.Enumclasses, andserver_default=text(...)/func.now()for defaults. Composite FKs are a documented v1 gap.generate_sqlc_schema— emit a sqlc-friendlyschema.sql(plain DDL) ordered for clean replay:CREATE SCHEMA→CREATE TYPEenums →CREATE TABLE(columns only) →ALTER TABLE ADD CONSTRAINT(PK / unique / check / FK in that order) →CREATE INDEXfor non-constraint indexes. In-process — noMCPG_ALLOW_SHELLneeded. All three are read-only; gated by the standard READ capability.
- Staged-migration workflow — Batch F (Phase 27), per ADR-0006. New
mcpg.migrationsmodule implements Neon-style “branch the schema, test the migration, merge” with same-database shadow schemas (nopg_dumpshell-out, no cross-batch dependency on Batch D). Four new MCP tools:prepare_migration(name, target_schema, candidate_sql, ttl_minutes=60)clones the target schema’s structure into a freshmcpg_shadow_<id>schema via introspection-driven DDL replay (tables + columns, PK / UNIQUE / CHECK / FK constraints, indexes), appliescandidate_sqlagainst the shadow withSET LOCAL search_pathso unqualified identifiers resolve there, runscompare_schemas(target, shadow), and persists the staged row inmcpg_migrations.staged. Returns the migration id + shadow schema name + TTL + structural diff for review.complete_migration(id)applies the candidate SQL to the target schema and drops the shadow. Refuses if status is notpreparedor TTL has expired.cancel_migration(id)drops the shadow and marks the rowcancelled. Idempotent.list_pending_migrations()lists prepared migrations newest first; sweeps any expired prepared rows before listing. Intra-schema FK references are rewritten to point at the shadow; cross-schema FKs are left pointing at the original and surface in the diff as removed (documented limitation per ADR-0006).
- New
Capability.MIGRATEenum entry; the migration tools register under unrestricted mode + the existingMCPG_ALLOW_DDLopt-in (the underlying ops are DDL). -
New
mcpg_migrationsschema +stagedtable created idempotently on first migration call. State columns:id,prepared_at,target_schema,shadow_schema,candidate_sql,status(prepared/completed/cancelled/expired),ttl_expires_at,completed_at. - LISTEN/NOTIFY bridge — Batch E first slice, per ADR-0005. New
mcpg.listenmodule owns the server-lifetime subscription state. Four new MCP tools:subscribe_channel(channel)opens a PostgreSQLLISTENon the given channel (validated against the standard plain-identifier allowlist) and returns a subscription id. Notifications buffer in a per-subscription bounded queue.poll_notifications(subscription_id, timeout_ms, max_messages)drains up tomax_messagesfrom the queue, waiting at mosttimeout_msfor the first one when the queue is empty. Each{channel, payload, delivered_at, dropped_count}notification surfaces drop count only on the first message after an overflow so the caller is informed exactly once.unsubscribe_channel(subscription_id)removes a subscription;UNLISTENfires when the last subscription on a channel is gone.list_notification_subscriptions()reports the active{subscription_id, channel}pairs for visibility. A single dedicated PostgreSQL connection (separate from the request pool) holds every active LISTEN, opened lazily on first subscribe. A backgroundasyncio.Taskdrains psycopg’s notifies generator with a short polling timeout so subscribe/unsubscribeexecute()calls can land between iterations (the psycopg connection lock would otherwise deadlock concurrent admin commands). Queue overflow drops the oldest message and surfacesdropped_counton the next poll.
- New
Capability.LISTENenum entry. Two new env vars:MCPG_ALLOW_LISTEN(bool, defaultfalse) toggling the subscription tool surface;MCPG_LISTEN_QUEUE_MAX(default 1000) capping per-subscription buffer size. -
AppContext.listen_managerexposes the manager to every tool;create_serveraccepts an optionallisten_managerkeyword arg so tests can inject a fake connection factory. copy_table_between_databasestool — copy a single table from one database to another by pipingpg_dump --format=custom --table=...(source) intopg_restore --format=custom --single-transaction --exit-on-error(destination). Both legs run through the ADR-0004 shell runner with separate libpq env dicts derived from the source and destination URLs; credentials never appear on argv.include_schemaandinclude_dataflags are required (no implicit default) so the caller can’t accidentally copy the wrong half. If the captured pg_dump archive exceedsMCPG_SHELL_MAX_OUTPUT_BYTES, the tool raises before invoking pg_restore — a truncated custom-format archive would either fail obscurely or partially restore. A failed pg_dump short-circuits the same way, returning the dump stderr_tail withrestore_exit_code=-1as a sentinel. Gated under unrestricted modeMCPG_ALLOW_SHELL.
import_csvtool — bulk-load CSV content intoschema.tableviaCOPY ... FROM STDIN. CSV text is sent verbatim;headertoggles header-row skipping; optionalcolumnsrestricts loading to named columns (each validated against the plain-identifier allowlist). Delimiter is restricted to a single non-newline, non-quote character so it cannot terminate the COPY options list early. Returns the server-reported row count. Gated under unrestricted mode (WRITE capability) — no subprocess, noMCPG_ALLOW_SHELLneeded.import_jsontool — bulk-load a JSON array of objects intoschema.tablevia parametrisedINSERT ... executemany. Columns are derived from the first row’s keys (or supplied explicitly); nesteddict/listvalues are JSON-serialised so they round-trip intojsonbcolumns; missing keys in later rows bind asNULL. Values are bound — never spliced into SQL — so they cannot inject statements. Gated under unrestricted mode (WRITE capability).-
Database.copy_from_stdinandDatabase.execute_manyhelpers — in-process plumbing for COPY FROM STDIN andexecutemany, used by the new import tools. The vendoredSqlDriverexposes neither, so imports go through theDatabasewrapper for raw connection access. restore_databasetool — restore a dump into the connected database via the ADR-0004 subprocess gate.format='plain'pipes SQL text throughpsql --single-transaction --set=ON_ERROR_STOP=onso a syntax error rolls back the whole restore;format='custom'/'tar'base64-decode the payload and pipe the binary archive intopg_restore --single-transaction --exit-on-error. Credentials reach the binary via libpq env vars; the dump bytes flow through stdin and are never interpolated into argv. Gated on unrestricted mode +MCPG_ALLOW_SHELL.
Fixed
-
mcpg.shell.run_pg_binarynow writes the optionalstdinpayload concurrently with the stdout/stderr drain. The previous “write stdin after wait()” ordering would have deadlocked any subprocess that consumes stdin (pg_restore,psql -f -); no shipped tool used stdin yet, but the bug blockedrestore_databasefrom working. dump_databasetool — wrapspg_dumpto capture the connected database’s schema (and optionally data) as a plain-SQL string or base64-encoded binary archive. Implements the ADR-0004 subprocess policy: argv-only invocation, allowlisted binaries, hard timeout, output cap with truncation flag, credentials passed via libpq env vars (never on the command line). Gated behind a newCapability.SHELL+MCPG_ALLOW_SHELLopt-in on top of unrestricted access mode.- New
MCPG_ALLOW_SHELLenv var (bool, defaultfalse) toggling the whole subprocess-tool surface. Two companion knobs:MCPG_SHELL_TIMEOUT_SEC(default 60) andMCPG_SHELL_MAX_OUTPUT_BYTES(default 64 MiB). Capability.SHELLadded to the policy table; required for any tool that invokes an external binary.export_querytool — run a read-only SQL query and serialise the rows to CSV or JSON. Reuses the safety checks ofrun_selectand truncates at the supplied row limit with atruncatedflag in the result so callers can paginate.export_tabletool — serialise every row in aschema.table(up to the supplied limit) to CSV or JSON. Identifier names must match the plain SQL allowlist; anything that needs delimited-identifier quoting is rejected.list_audit_eventstool — read recent rows frommcpg_audit.events(newest first). Returns an empty list whenMCPG_AUDIT_PERSISThas never been turned on (no audit table yet). Optional tool-name filter.- New
MCPG_AUDIT_PERSISTenv var (bool, defaultfalse). When on, everyrun_write/run_ddlcall appends one row tomcpg_audit.eventscontaining redacted arguments, status, error, and result. Persistence failures are swallowed so audit logging never masks the real write outcome. run_ddlgains optionalschema/tablehints. When both are supplied, the call snapshots the table’s columns before and after the DDL and attaches the structured before/after lists to the result as aSchemaDiffSnapshot. The snapshot is also stored in the persisted audit row whenMCPG_AUDIT_PERSISTis on.- PostgreSQL 18 added to the CI test matrix (was 14–17; now 14–18). The integration suite runs against every supported version on every PR.
run_advisorstool — runs a set of codified, catalog-driven lint rules against a schema and returns a typed report of findings. First cut covers:missing_primary_key,unindexed_foreign_key(leading- column heuristic),duplicate_indexes(same column-keys + access method), andnullable_timestamp_without_tz. Each finding carries a rule id, severity (warning/info), a qualified object name, and a human-readable message. Advisory only — no writes.generate_prisma_schematool — read a PostgreSQL schema and emit a valid Prisma.prismaschema string, mirroringprisma db pullbut driven by MCPg. Covers tables, columns, primary/foreign keys (including composite), unique constraints, secondary indexes, and enums; standard defaults (nextval(...)→autoincrement(),now()→now(),gen_random_uuid()→uuid(), literals) and array types are mapped; unmappable types (vectors, custom domains) fall back toUnsupported("...")exactly likeprisma db pull. Views, foreign tables, partitions, triggers, functions, policies, and composite types are out of scope for v1. First USP-tier tool — no other PG MCP server bridges to an ORM schema DSL.tune_vector_indextool — recommends anivfflatorhnswconfiguration for a pgvector column. Reads the live row count (pg_class.reltuples) and column dimension, applies the standard pgvector heuristics (lists ≈ rows/1000 or sqrt for ivfflat; m scales with size, ef_construction with size for hnsw), and returns the parameters plus a ready-to-runCREATE INDEXstatement.vector_recall_at_ktool — measures recall@k of an existing pgvector index by comparing its top-k results against a brute-force ground truth for the same query vectors. Uses pgvector’s distance functions (l2_distance/cosine_distance/inner_product) as the non-indexed baseline; the operator form (<->,<=>,<#>) triggers the ANN index.list_cron_jobstool — read pg_cron’scron.jobcatalog. Returns an empty list when pg_cron is not installed (graceful degradation).schedule_cron_jobandunschedule_cron_jobtools (write-gated) — thin wrappers overcron.schedule()/cron.unschedule(). RaiseCronErrorwhen pg_cron is not installed.partman_create_parent,partman_run_maintenance,partman_drop_partitiontools (write-gated) — pg_partman partition-set creation, periodic maintenance (forward partitions + retention drops), and explicit retention-based drops (time- or id-controlled).partition_typeis allowlisted to range/list/native. RaisePartmanErrorwhen pg_partman is not installed.pg_cronandpg_partmanadded toENABLEABLE_EXTENSIONS— agents can request enabling them (still gated on unrestricted mode +MCPG_ALLOW_DDL; pg_cron also requires server-sideshared_preload_libraries).
[0.3.0] - 2026-05-23
Twelve new MCP tools, closing Batch A of the post-0.2.0 roadmap
(PLAN.md §11): catalog completeness (Phase 16), schema visualisation
(Phase 17), and structural schema diff (Phase 18). Brings the total
MCP tool surface from 33 to 45 and lays the structural foundation for
Phase 27 shadow migrations.
Added
list_foreign_keystool — every foreign key in a schema, resolved to its from-columns, referenced schema, referenced table, and to-columns. The two column arrays are aligned by ordinal position.generate_schema_diagramtool — renders a Mermaid ER diagram for a schema (entities with PK/FK column markers, edges parent → child). Views and foreign tables are excluded; partitions are excluded by default and can be included withinclude_partitions=true.compare_schemastool — structural diff between two schemas. Reports tables / columns / indexes / constraints / foreign keys as added, removed, or changed; column changes include the list of differing ColumnInfo fields. Object identity is by name; renames surface as a paired add + remove. Foundation for the Phase-27 shadow-migration workflow.list_constraintstool — a table’s primary-key, foreign-key, unique, check, and exclusion constraints.list_viewstool — the views and materialized views in a schema, with their definitions.list_functionstool — the functions and procedures in a schema, with kind, arguments, return type, and language.list_triggerstool — the user-defined triggers on a table.list_sequencestool — the sequences in a schema, with each sequence’s data type, range, increment, cycle flag, and last value.list_partitionstool — how a table is partitioned (range, list, or hash) and its partitions, each with its bound expression.list_policiestool — the Row-Level-Security policies on a table, with each policy’s command, permissive flag, roles, and predicates, plus whether row security is enabled on the table.list_rolestool — the database roles and their attributes (superuser, create-role/db, login, replication, bypass-RLS, connection limit, and role membership).list_grantstool — the privileges granted on a table, with each grant’s grantee, privilege, grantable flag, and grantor.list_active_queriestool — the queries currently running on the server, each with its wait event, duration, and blocking PIDs.check_database_healthgains two checks — replication lag (how far connected standbys trail) and table bloat (tables far larger than their estimated minimum size).run_maintenancetool — runsVACUUMorANALYZEagainst one table; requires unrestricted mode. Runs on an autocommit connection, sinceVACUUMcannot run inside a transaction.cancel_queryandterminate_backendtools — signal a backend PID to cancel its current query or close its connection; require unrestricted mode.list_enums,list_domains,list_composite_typestools — the user-defined types in a schema. Composite types report each attribute with its rendered type; the catalog’s implicit table row-types are excluded.list_foreign_data_wrappers,list_foreign_servers,list_foreign_tables,list_user_mappingstools — the FDW catalog, with each entry’s options array parsed into a typed dict.list_publicationsandlist_subscriptionstools — read-only view of logical-replication publications (with the tables and operations they cover) and subscriptions; reading subscriptions requires superuser, by PostgreSQL design.postgres_fdwadded toENABLEABLE_EXTENSIONS— agents can now enable the wrapper they can already introspect (gated on unrestricted mode +MCPG_ALLOW_DDL).
Changed
list_tablesnow flags each table withpartitioned(a partitioned parent) andis_partition(itself a partition).list_indexesnow flags each index withpartitioned(a partitioned-index template).recommend_indexesnow rolls a flagged partition up to its partitioned parent — summing scan and row counts and setting apartitionedflag — since an index created on the parent propagates to every partition.- The “every introspection tool is callable” check moved from the unit suite (fakes-only) to the integration suite — it now runs against the real catalog across the PG 14–17 CI matrix, closing a trust gap the unit-level fake driver couldn’t reach.
[0.2.0] - 2026-05-21
Extension support: index-method intelligence, extension management, and similarity-search tools (trigram, full-text, pgvector, PostGIS) — six new tools, each degrading gracefully when its extension is absent.
Added
list_available_extensionstool — lists every extension available to the database with its installed-vs-available status.enable_extensiontool — enables an allowlisted PostgreSQL extension; requires unrestricted mode andMCPG_ALLOW_DDL.fuzzy_searchtool — ranks a text column bypg_trgmtrigram similarity to a search term, with awordmode (fragment matching, the default) and afullmode (whole-string comparison).full_text_searchtool — ranks documents with PostgreSQL’s built-intsvector/tsqueryfull-text search.vector_searchtool — finds the rows nearest to a query vector bypgvectordistance (l2,cosine, orinner_product).geo_searchtool — finds the rows nearest to a lon/lat point by PostGIS distance.
Changed
list_indexesnow reports each index’s access method (btree,gin,gist,brin,hash,spgist).recommend_indexesnow suggests per-column index types from column data types — GIN forjsonb/array columns, trigram GIN for text columns.describe_tablenow reads the catalog directly and reports thepgvectordimension forvector(N)columns.- Documentation reorganised into living guides:
docs/installation.md,docs/user-guide.md, anddocs/architecture.md(replacingdocs/usage.md).
[0.1.0] - 2026-05-21
First release: a production-grade PostgreSQL MCP server with 14 tools across introspection, querying, writes, and tuning — read-only by default, every statement validated, every tool call audited.
Added
- Project plan, phased roadmap, and session-resume protocol (
PLAN.md,docs/PROGRESS.md). - ADR-0001 (build approach: hard-fork) and ADR-0002 (technology stack).
- Vendored the self-contained
sql/SQL-safety kernel fromcrystaldba/postgres-mcp@07eb329(MIT) intosrc/mcpg/_vendor/sql/, with the upstream unit tests that port cleanly. - Project scaffold:
pyproject.toml, packaging,ruff/mypy/pytest/ coverage configuration,NOTICE. - GitHub Actions CI (
.github/workflows/ci.yml): lint, format, type-check, and test jobs. CONTRIBUTING.md, localpre-commithooks, and GitHub issue/PR templates.- Env-driven configuration (
mcpg.config):Settings,AccessMode,Transport, andload_settings. Read-only is the default access mode and the settings repr redacts database credentials. - Database connection lifecycle (
mcpg.database):Databasewraps the pool with connect/close, async-context-manager support, and a typedDatabaseError. - MCP server bootstrap (
mcpg.server):create_serverbuilds a configuredFastMCPwhose lifespan owns the settings and database (no global state);runserves over the stdio, streamable-HTTP, or SSE transport. - First MCP tool,
get_server_info(mcpg.tools): reports the server version, access mode, transport, and database connection status. - Console entry point:
mcpg(andpython -m mcpg) loads configuration and runs the server. - CI now enforces the test-coverage gate (90% of authored code).
- Integration-test harness (
tests/integration/) running against a live PostgreSQL; CI exercises the suite against PostgreSQL 14, 15, 16, and 17. - Schema-introspection tools (
mcpg.introspection):list_schemas,list_tables,describe_table,list_indexes, andlist_extensions, using parameterised read-only catalog queries. - Safe query execution (
mcpg.query): therun_selecttool validates agent-supplied SQL against an allowlist and runs it read-only, returning a typed result; unsafe statements are rejected. - The
explain_querytool returns a query’sEXPLAIN (FORMAT JSON)execution plan without running the query. run_selectcaps results at a configurablemax_rows(default 1000) and reports whether the result wastruncated.- Access-mode policy engine (
mcpg.policy): tool registration is gated by capability, so the available tools depend on the configured access mode. - Adversarial SQL-safety regression suite covering statement stacking,
comment and transaction-control escapes, DDL/DML,
COPY, andDOblocks. - Audit logging (
mcpg.audit): every tool invocation is logged to themcpg.auditlogger with its outcome and arguments, with secrets masked. - Security documentation (
docs/security.md): threat model, trust boundaries, mitigations, and operator responsibilities. - Write execution (
mcpg.write): therun_writetool executes a single validated INSERT/UPDATE/DELETE statement, available only in unrestricted access mode; statement stacking is rejected. - The
run_ddltool executes a single validated DDL statement; it requires unrestricted access mode and theMCPG_ALLOW_DDLopt-in. - Database health checks (
mcpg.health): thecheck_database_healthtool reports connection utilisation, buffer cache hit ratio, tables needing vacuum, and invalid indexes. - Workload analysis (
mcpg.workload): theanalyze_workloadtool reports the slowest queries viapg_stat_statements, degrading gracefully when the extension is not installed. - Index recommendations (
mcpg.indexing): therecommend_indexestool flags large tables read mostly by sequential scan. - Query plan analysis (
mcpg.query): theanalyze_query_plantool summarises a query’s execution plan — total cost, estimated rows, node types, and sequentially-scanned tables. - Configurable connection-pool sizing via
MCPG_POOL_MIN_SIZEandMCPG_POOL_MAX_SIZE(defaults 1 and 5). - Multi-tenancy / Row-Level Security guidance in
docs/security.md. - Scaling documentation (
docs/scaling.md) and a benchmark harness (benchmarks/bench.py). - Usage guide (
docs/usage.md), tool reference (docs/tools.md), and auv-basedDockerfile.