pg-admin-mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +608 -2
- package/dist/config/env.js +67 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.js +257 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/core/audit-context.js +21 -0
- package/dist/core/audit-context.js.map +1 -0
- package/dist/core/audit.js +91 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/brands.js +24 -0
- package/dist/core/brands.js.map +1 -0
- package/dist/core/classifier.js +279 -0
- package/dist/core/classifier.js.map +1 -0
- package/dist/core/errors.js +197 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.js +258 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/guards.js +513 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/identifiers.js +431 -0
- package/dist/core/identifiers.js.map +1 -0
- package/dist/core/limits.js +79 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/logger.js +34 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paginate.js +195 -0
- package/dist/core/paginate.js.map +1 -0
- package/dist/core/paths.js +392 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.js +98 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/shutdown.js +10 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/stdout-guard.js +25 -0
- package/dist/core/stdout-guard.js.map +1 -0
- package/dist/db/catalog.js +1006 -0
- package/dist/db/catalog.js.map +1 -0
- package/dist/db/pool.js +158 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/session.js +258 -0
- package/dist/db/session.js.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/index.js +88 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/runtime/binaries.js +356 -0
- package/dist/runtime/binaries.js.map +1 -0
- package/dist/runtime/env-file.js +62 -0
- package/dist/runtime/env-file.js.map +1 -0
- package/dist/runtime/environment.js +34 -0
- package/dist/runtime/environment.js.map +1 -0
- package/dist/server.js +106 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot/capture.js +189 -0
- package/dist/snapshot/capture.js.map +1 -0
- package/dist/snapshot/diff.js +254 -0
- package/dist/snapshot/diff.js.map +1 -0
- package/dist/snapshot/migrate.js +1596 -0
- package/dist/snapshot/migrate.js.map +1 -0
- package/dist/snapshot/model.js +80 -0
- package/dist/snapshot/model.js.map +1 -0
- package/dist/tools/_archive_meta.js +138 -0
- package/dist/tools/_archive_meta.js.map +1 -0
- package/dist/tools/_batch_shared.js +73 -0
- package/dist/tools/_batch_shared.js.map +1 -0
- package/dist/tools/_export_format.js +107 -0
- package/dist/tools/_export_format.js.map +1 -0
- package/dist/tools/_object_kinds.js +97 -0
- package/dist/tools/_object_kinds.js.map +1 -0
- package/dist/tools/_relation_detail.js +55 -0
- package/dist/tools/_relation_detail.js.map +1 -0
- package/dist/tools/_shared.js +17 -0
- package/dist/tools/_shared.js.map +1 -0
- package/dist/tools/_snapshot_io.js +238 -0
- package/dist/tools/_snapshot_io.js.map +1 -0
- package/dist/tools/admin/_shared.js +18 -0
- package/dist/tools/admin/_shared.js.map +1 -0
- package/dist/tools/admin/extensions.js +80 -0
- package/dist/tools/admin/extensions.js.map +1 -0
- package/dist/tools/admin/grants.js +207 -0
- package/dist/tools/admin/grants.js.map +1 -0
- package/dist/tools/admin/index.js +138 -0
- package/dist/tools/admin/index.js.map +1 -0
- package/dist/tools/admin/maintenance.js +106 -0
- package/dist/tools/admin/maintenance.js.map +1 -0
- package/dist/tools/admin/roles.js +144 -0
- package/dist/tools/admin/roles.js.map +1 -0
- package/dist/tools/admin/sessions.js +104 -0
- package/dist/tools/admin/sessions.js.map +1 -0
- package/dist/tools/admin/stats.js +63 -0
- package/dist/tools/admin/stats.js.map +1 -0
- package/dist/tools/backup.js +288 -0
- package/dist/tools/backup.js.map +1 -0
- package/dist/tools/connection_status.js +138 -0
- package/dist/tools/connection_status.js.map +1 -0
- package/dist/tools/copy.js +355 -0
- package/dist/tools/copy.js.map +1 -0
- package/dist/tools/delete_batch.js +73 -0
- package/dist/tools/delete_batch.js.map +1 -0
- package/dist/tools/describe_object.js +87 -0
- package/dist/tools/describe_object.js.map +1 -0
- package/dist/tools/execute_sql.js +395 -0
- package/dist/tools/execute_sql.js.map +1 -0
- package/dist/tools/explain.js +134 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/export.js +215 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/get_schema.js +148 -0
- package/dist/tools/get_schema.js.map +1 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insert_batch.js +212 -0
- package/dist/tools/insert_batch.js.map +1 -0
- package/dist/tools/list_objects.js +160 -0
- package/dist/tools/list_objects.js.map +1 -0
- package/dist/tools/prepare_migration.js +112 -0
- package/dist/tools/prepare_migration.js.map +1 -0
- package/dist/tools/restore.js +301 -0
- package/dist/tools/restore.js.map +1 -0
- package/dist/tools/schema_diff.js +79 -0
- package/dist/tools/schema_diff.js.map +1 -0
- package/dist/tools/schema_snapshot.js +57 -0
- package/dist/tools/schema_snapshot.js.map +1 -0
- package/dist/tools/select.js +162 -0
- package/dist/tools/select.js.map +1 -0
- package/dist/tools/update_batch.js +154 -0
- package/dist/tools/update_batch.js.map +1 -0
- package/package.json +47 -5
package/README.md
CHANGED
|
@@ -1,3 +1,609 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pg-admin-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server, stdio transport, that gives an AI agent full
|
|
4
|
+
PostgreSQL access: connect, browse the catalog, read and paginate rows, run arbitrary SQL, bulk
|
|
5
|
+
insert/update/delete, DDL, snapshot and diff schemas, plan migrations, administer roles/grants/extensions/
|
|
6
|
+
sessions/maintenance, and back up or restore with `pg_dump`/`pg_restore`. 18 tools, 3 read-only resources.
|
|
7
|
+
|
|
8
|
+
Every connection profile is **read-only by default**. Writes, DDL, admin operations, `EXPLAIN ANALYZE`,
|
|
9
|
+
backup and restore are each a separate opt-in flag, set per profile via environment variables — nothing is
|
|
10
|
+
unlocked implicitly. Every destructive operation (bulk update/delete, guarded raw SQL, restore, role drop,
|
|
11
|
+
`pg_terminate_backend`, `REINDEX`, …) goes through a dry-run/confirm protocol: the first call previews the
|
|
12
|
+
change and returns a token, the second call must hand that exact token back to commit it.
|
|
13
|
+
|
|
14
|
+
**What it is not.** This server never elevates privileges — it only restricts. It runs as whatever
|
|
15
|
+
PostgreSQL role the connection string authenticates as; capability flags gate which of *that role's own*
|
|
16
|
+
privileges the server is willing to exercise, they do not grant new ones. A read-only transaction is not a
|
|
17
|
+
sandbox either: it won't stop `COPY … TO PROGRAM`, `lo_export()`, or advisory locks, so the documented
|
|
18
|
+
production posture is a role that is itself read-only at the database level
|
|
19
|
+
(`ALTER ROLE … SET default_transaction_read_only = on`, no `pg_write_server_files`).
|
|
20
|
+
|
|
21
|
+
Two ways to run it: **`npx -y pg-admin-mcp`** (primary — runs on your machine, needs Node.js >= 20.12)
|
|
22
|
+
or the **Docker image** (alternative — carries its own pinned PostgreSQL client tools; see
|
|
23
|
+
[Docker packaging](docs/design.md#docker-packaging) in the design doc).
|
|
24
|
+
|
|
25
|
+
## Quick start (npx)
|
|
26
|
+
|
|
27
|
+
1. **Copy the per-project template** from [`examples/per-project/`](examples/per-project/) into your
|
|
28
|
+
project root: `.mcp.json` (committed — profiles, environment classes, capability flags) and
|
|
29
|
+
`.env.example` (copy to `.env`, gitignored — the `PG_<ALIAS>_URL` lines with passwords).
|
|
30
|
+
```bash
|
|
31
|
+
cp .env.example .env # then fill in the URLs
|
|
32
|
+
mkdir -p pg-workspace # exports, backups, snapshots land here
|
|
33
|
+
```
|
|
34
|
+
`.mcp.json` launches `npx -y pg-admin-mcp` with `PG_MCP_ENV_FILE: ".env"`; the server reads that
|
|
35
|
+
file itself, and any key also set in `.mcp.json` wins over `.env`. On native Windows use
|
|
36
|
+
`"command": "cmd", "args": ["/c", "npx", "-y", "pg-admin-mcp"]`.
|
|
37
|
+
2. **Start `claude` from the project root** — `.env` and `pg-workspace` are relative paths resolved
|
|
38
|
+
against the directory the client was launched from.
|
|
39
|
+
3. **Verify** by calling `postgres_connection_status`. It reports `runtime: "host"`, reachability, the
|
|
40
|
+
server version, your role's privileges, and the client tool versions.
|
|
41
|
+
|
|
42
|
+
Host-mode notes:
|
|
43
|
+
|
|
44
|
+
- **The database host is plain `localhost`** for a database on your machine — the server is not in a
|
|
45
|
+
container, so no `host.docker.internal`.
|
|
46
|
+
- **Only `postgres_backup`/`postgres_restore` need the PostgreSQL client tools** (`pg_dump`,
|
|
47
|
+
`pg_restore`); the other 16 tools need nothing beyond Node. Install a client matching your server's
|
|
48
|
+
major — Windows `winget install PostgreSQL.PostgreSQL.17`, Debian/Ubuntu
|
|
49
|
+
`sudo apt install postgresql-client`, macOS `brew install libpq` — and, if it is not on `PATH`, set
|
|
50
|
+
`PG_MCP_PG_BIN_DIR` in `.env` (Windows: `C:\Program Files\PostgreSQL\17\bin`).
|
|
51
|
+
- `bunx pg-admin-mcp` should work too — untested.
|
|
52
|
+
|
|
53
|
+
## Quick start (Docker, alternative)
|
|
54
|
+
|
|
55
|
+
1. **Pull the image** (client binaries are pinned to a PostgreSQL major; use the tag matching your
|
|
56
|
+
server — `linux/amd64` only):
|
|
57
|
+
```bash
|
|
58
|
+
docker pull mrrainx/pg-admin-mcp:pg17
|
|
59
|
+
```
|
|
60
|
+
`manifest unknown` means no release has been cut yet — see [Status](#status) for how to tell, and
|
|
61
|
+
[Building the image locally](#building-the-image-locally) for the interim path.
|
|
62
|
+
2. **Create your env file:**
|
|
63
|
+
```bash
|
|
64
|
+
cp .env.example .env
|
|
65
|
+
```
|
|
66
|
+
Edit `.env`: set `PG_MCP_PROFILES`, and for each alias a `PG_<ALIAS>_URL` plus whichever
|
|
67
|
+
`PG_<ALIAS>_ALLOW_*` flags you want on. See [Configuration](#configuration) and [Networking](#networking)
|
|
68
|
+
for the connection host — `localhost` will not reach your host machine from inside the container.
|
|
69
|
+
3. **Create the workspace** the container writes files into (exports, backups, snapshots):
|
|
70
|
+
```bash
|
|
71
|
+
mkdir -p pg-workspace
|
|
72
|
+
```
|
|
73
|
+
4. **Register it in your MCP client** — see [Connecting a client](#connecting-a-client) for the exact form
|
|
74
|
+
for Claude Code, Claude Desktop, Codex CLI, Cursor and VS Code.
|
|
75
|
+
5. **Verify** by calling `postgres_connection_status` from the client. It reports `runtime: "container"`,
|
|
76
|
+
reachability, the server version, your role's privileges, and the `pg_dump`/`pg_restore`/`psql` client
|
|
77
|
+
versions — everything needed to confirm the server actually reached your database.
|
|
78
|
+
|
|
79
|
+
## Using it from several projects
|
|
80
|
+
|
|
81
|
+
Run **one server process per project**, not one shared server for every project on the machine. The
|
|
82
|
+
profile/capability model (`PG_MCP_PROFILES`, `PG_<ALIAS>_URL`, `PG_<ALIAS>_ALLOW_*`) is scoped to the
|
|
83
|
+
process, not to the caller — it assumes the process belongs to a single client. A server shared
|
|
84
|
+
across projects would let one project's agent name another project's profile alias and reach its
|
|
85
|
+
database, `prod` included. One process per project, launched from that project's own config, keeps
|
|
86
|
+
each project's agent limited to the profiles that project itself defines.
|
|
87
|
+
|
|
88
|
+
**With npx (primary)** this is what [Quick start (npx)](#quick-start-npx) already sets up: the client
|
|
89
|
+
spawns a fresh `npx -y pg-admin-mcp` per session from the project's own `.mcp.json` + `.env`. The
|
|
90
|
+
template is [`examples/per-project/`](examples/per-project/).
|
|
91
|
+
|
|
92
|
+
**With Docker (alternative)** it is one container per project, launched by a small Compose file.
|
|
93
|
+
Pull the image once — the same image is reused across every project (see [Quick start (Docker, alternative)](#quick-start-docker-alternative)
|
|
94
|
+
if this returns `manifest unknown`):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
docker pull mrrainx/pg-admin-mcp:pg17
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Each project then carries three small committed files plus a gitignored `.env`:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
myproject/
|
|
104
|
+
.mcp.json # committed — identical in every project, never edited
|
|
105
|
+
pg_mcp.yml # committed — this project's profiles and environments, no passwords
|
|
106
|
+
.env # gitignored — passwords only
|
|
107
|
+
pg-workspace/ # gitignored — where exports, dumps and snapshots land
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`pg_mcp.yml` is a Docker Compose file. The server itself has no config-file format — it only reads
|
|
111
|
+
environment variables — so Compose here is nothing more than a launcher that sets those variables and
|
|
112
|
+
interpolates the passwords in from `.env`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
services:
|
|
116
|
+
pg-mcp:
|
|
117
|
+
image: mrrainx/pg-admin-mcp:pg17
|
|
118
|
+
stdin_open: true
|
|
119
|
+
extra_hosts: ["host.docker.internal:host-gateway"] # harmless on Docker Desktop, required on Linux
|
|
120
|
+
volumes: ["./pg-workspace:/workspace"]
|
|
121
|
+
environment:
|
|
122
|
+
PG_MCP_PROFILES: dev,prod
|
|
123
|
+
PG_MCP_DEFAULT_PROFILE: dev
|
|
124
|
+
|
|
125
|
+
# development: writes and DDL allowed
|
|
126
|
+
PG_DEV_URL: postgres://app:${DEV_PASSWORD}@host.docker.internal:5432/myproject
|
|
127
|
+
PG_DEV_ENV: development
|
|
128
|
+
PG_DEV_ALLOW_WRITE: "true"
|
|
129
|
+
PG_DEV_ALLOW_DDL: "true"
|
|
130
|
+
|
|
131
|
+
# production: no ALLOW_* keys, so it stays read-only
|
|
132
|
+
PG_PROD_URL: postgres://readonly:${PROD_PASSWORD}@prod.internal:5432/myproject
|
|
133
|
+
PG_PROD_ENV: production
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`.mcp.json` — commit this byte-identical across every project, never edit it:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"pg": {
|
|
142
|
+
"command": "docker",
|
|
143
|
+
"args": [
|
|
144
|
+
"compose", "-f", "${CLAUDE_PROJECT_DIR:-.}/pg_mcp.yml",
|
|
145
|
+
"run", "--rm", "-T", "pg-mcp"
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**`-T`, never `-t`** — the Compose spelling of the same rule as `docker run`: a TTY echoes stdin back
|
|
153
|
+
onto stdout and corrupts the JSON-RPC frame stream this server relies on.
|
|
154
|
+
|
|
155
|
+
**The launch directory is load-bearing.** Verified against Claude Code 2.1.220: `CLAUDE_PROJECT_DIR` is
|
|
156
|
+
not set at the point `.mcp.json` is substituted, so `${CLAUDE_PROJECT_DIR:-.}` always takes its `.`
|
|
157
|
+
fallback, which resolves against the spawned server's CWD — the directory `claude` was launched from,
|
|
158
|
+
not the directory `.mcp.json` lives in and not `$HOME`. The dangerous case is not the obvious failure
|
|
159
|
+
(launching from a directory with no `pg_mcp.yml` — Compose exits, no server); it's launching from a
|
|
160
|
+
*parent* directory that happens to hold its **own** `pg_mcp.yml` (a monorepo root, or any other project
|
|
161
|
+
set up this same way): Compose silently loads that file instead, and the session connects to a
|
|
162
|
+
different project's databases, `prod` profile included, with no error at all. Always start `claude`
|
|
163
|
+
from the project root that holds this `pg_mcp.yml`.
|
|
164
|
+
|
|
165
|
+
Add to the project's `.gitignore`:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
.env
|
|
169
|
+
pg-workspace/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The full template — copy-ready files, adding a third profile, the networking table, and how to confirm
|
|
173
|
+
it worked — lives in [`examples/per-project-docker/`](examples/per-project-docker/); start there rather than
|
|
174
|
+
retyping any of the above.
|
|
175
|
+
|
|
176
|
+
## The 18 tools
|
|
177
|
+
|
|
178
|
+
All tool names are prefixed `postgres_`. "Capability flag" is the `PG_<ALIAS>_ALLOW_*` env var that must be
|
|
179
|
+
`true` on the profile for that tool (or that part of it) to work; a blank cell means no flag is needed.
|
|
180
|
+
|
|
181
|
+
**Discovery** — always available, read-only:
|
|
182
|
+
|
|
183
|
+
| Tool | What it does | Capability flag |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `postgres_connection_status` | Resolved config, reachability, server version, current-role privileges, and `pg_dump`/`pg_restore`/`psql` client versions for one profile. | — |
|
|
186
|
+
| `postgres_list_objects` | Paginated listing of schemas, tables, views, materialized views, sequences, functions, enum types, and extensions in scope for a profile. | — |
|
|
187
|
+
| `postgres_describe_object` | Full detail for one object: columns, PK/FK (both directions), unique/check constraints, indexes (incl. partial), triggers, RLS policies. | — |
|
|
188
|
+
| `postgres_get_schema` | Structured live snapshot of a database or a filtered scope (schemas/kinds/name pattern); `detail:"summary"` or full. | — |
|
|
189
|
+
|
|
190
|
+
**Read:**
|
|
191
|
+
|
|
192
|
+
| Tool | What it does | Capability flag |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `postgres_select` | Structured, parameterized, paginated (keyset) read of one table or view — the primary way to fetch row data. | — |
|
|
195
|
+
| `postgres_explain` | `EXPLAIN` a statement (TEXT/JSON/XML/YAML, costs/buffers/verbose/settings). `analyze:true` actually executes the statement. | `ALLOW_EXPLAIN_ANALYZE` (only for `analyze:true`) |
|
|
196
|
+
| `postgres_export` | Streams a table selection or a `SELECT` to a CSV/JSON/JSONL file under `/workspace`, batched via a server-side cursor — never buffers the full result. | — |
|
|
197
|
+
|
|
198
|
+
**Write** (the escape hatch plus the batch mutation tools, all destructive-annotated):
|
|
199
|
+
|
|
200
|
+
| Tool | What it does | Capability flag |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `postgres_execute_sql` | Arbitrary parameterized SQL — reads always work; `INSERT`/`UPDATE`/`DELETE`/`MERGE` need `ALLOW_WRITE`, `CREATE`/`ALTER`/`DROP`/`TRUNCATE` additionally need `ALLOW_DDL`, `GRANT`/`REVOKE` need `ALLOW_ADMIN`. Mutating statements go through dry-run/confirm. | `ALLOW_WRITE` / `ALLOW_DDL` / `ALLOW_ADMIN` (statement-dependent) |
|
|
203
|
+
| `postgres_insert_batch` | Bulk `INSERT` — set-based `INSERT … SELECT * FROM unnest()`, switching to a bulk strategy above a row threshold. Confirmed on row count alone (no pre-existing rows to checksum). | `ALLOW_WRITE` |
|
|
204
|
+
| `postgres_update_batch` | Filtered bulk `UPDATE`. `where` is a structured, parameterized filter and is mandatory — an empty filter is refused before any SQL reaches the server. Dry-run/confirm. | `ALLOW_WRITE` |
|
|
205
|
+
| `postgres_delete_batch` | Filtered bulk `DELETE`, same mandatory-`where` rule as update. Dry-run/confirm. | `ALLOW_WRITE` |
|
|
206
|
+
| `postgres_copy` | High-throughput whole-table (or column-subset) transfer via PostgreSQL's `COPY` protocol, streamed to/from a file. Export direction is read-only; import direction needs write and goes through dry-run/confirm. | `ALLOW_WRITE` (import direction only) |
|
|
207
|
+
|
|
208
|
+
**Schema:**
|
|
209
|
+
|
|
210
|
+
| Tool | What it does | Capability flag |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `postgres_schema_snapshot` | Captures a normalized, byte-stable schema snapshot (tables, views, sequences, functions, enums, extensions) and writes it to a workspace JSON file. | — |
|
|
213
|
+
| `postgres_schema_diff` | Structural diff between two snapshots, each independently `kind:"live"` (fresh capture) or `kind:"file"` (a `postgres_schema_snapshot` output). | — |
|
|
214
|
+
| `postgres_prepare_migration` | Diffs two snapshots and returns an ordered, dependency-safe, risk-annotated SQL plan. **Emits SQL only — never executes it.** | — |
|
|
215
|
+
|
|
216
|
+
**Admin:**
|
|
217
|
+
|
|
218
|
+
| Tool | What it does | Capability flag |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `postgres_admin` | Widest blast radius in the server. Six sub-operations selected by `op`: `roles_*` (create/alter/drop a role), `grants_*` (GRANT/REVOKE), `extensions_*` (CREATE/DROP EXTENSION — installing runs arbitrary SQL as superuser), `sessions_*` (cancel/terminate a backend), `maintenance_*` (VACUUM/ANALYZE/REINDEX), `stats_*` (table/index/activity stats). Mutating sub-ops go through dry-run/confirm. `grants_get`, `extensions_list`, and `stats_*` are read-only and need no flag; everything else needs admin. | `ALLOW_ADMIN` (all sub-ops except `grants_get` / `extensions_list` / `stats_*`) |
|
|
221
|
+
|
|
222
|
+
**Backup:**
|
|
223
|
+
|
|
224
|
+
| Tool | What it does | Capability flag |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `postgres_backup` | Runs `pg_dump` into a workspace file — whole database (default), or named schemas/tables. Formats: custom (default), directory, tar, plain. Not wrapped in dry-run/confirm (it only reads the database and writes a file); gated by the capability plus the workspace path rules. | `ALLOW_BACKUP` |
|
|
227
|
+
| `postgres_restore` | **The most destructive tool in this server.** `mode:"inspect"` (default) lists archive contents via `pg_restore --list`, touching no database. `mode:"restore"` runs `pg_restore` and can overwrite, or with `clean:true` drop-and-recreate, every object in the archive — no undo, not wrapped in a transaction guard, refused without an explicit `confirm:true`. | `ALLOW_RESTORE` |
|
|
228
|
+
|
|
229
|
+
## The 3 resources
|
|
230
|
+
|
|
231
|
+
Same read-only catalog listings the discovery tools use, exposed as MCP resources:
|
|
232
|
+
|
|
233
|
+
| URI | Returns |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `postgres://<profile>/schemas` | Every schema in scope for the profile. |
|
|
236
|
+
| `postgres://<profile>/schema/<schema>` | Every object in one schema. |
|
|
237
|
+
| `postgres://<profile>/table/<schema>/<table>` | Full detail for one table-like relation. |
|
|
238
|
+
|
|
239
|
+
## Safety model
|
|
240
|
+
|
|
241
|
+
**Read-only by default.** A profile with no `ALLOW_*` flags set can only discover, `SELECT`, `EXPLAIN`
|
|
242
|
+
(without `ANALYZE`), export, and diff schemas. Every other capability is a separate, explicit,
|
|
243
|
+
per-profile opt-in:
|
|
244
|
+
|
|
245
|
+
| Flag | Unlocks |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `PG_<ALIAS>_ALLOW_WRITE` | `INSERT`/`UPDATE`/`DELETE`/`MERGE` via `postgres_execute_sql`; `postgres_insert_batch` / `update_batch` / `delete_batch`; `postgres_copy` import direction |
|
|
248
|
+
| `PG_<ALIAS>_ALLOW_DDL` | `CREATE`/`ALTER`/`DROP`/`TRUNCATE` via `postgres_execute_sql` |
|
|
249
|
+
| `PG_<ALIAS>_ALLOW_ADMIN` | `postgres_admin`'s mutating sub-ops (roles, grants, extensions, sessions, maintenance) and `GRANT`/`REVOKE` via `postgres_execute_sql` |
|
|
250
|
+
| `PG_<ALIAS>_ALLOW_EXPLAIN_ANALYZE` | `postgres_explain` with `analyze:true` (this actually executes the statement) |
|
|
251
|
+
| `PG_<ALIAS>_ALLOW_BACKUP` | `postgres_backup` |
|
|
252
|
+
| `PG_<ALIAS>_ALLOW_RESTORE` | `postgres_restore` |
|
|
253
|
+
|
|
254
|
+
All six default to `false`.
|
|
255
|
+
|
|
256
|
+
**Dry-run/confirm protocol.** Every mutating tool shares one parameter fragment:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
{ profile?: string,
|
|
260
|
+
confirm?: boolean, // default false = dry run
|
|
261
|
+
expect?: { sqlHash: string, rowCount: number, keyChecksum?: string },
|
|
262
|
+
allowRowDrift?: number } // default 0
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Concrete example, `postgres_delete_batch`:
|
|
266
|
+
|
|
267
|
+
**Call 1 — dry run** (`confirm` omitted). The server resolves identifiers, builds parameterized SQL, checks
|
|
268
|
+
the `where` guard, runs the delete inside a transaction, captures the row count and a checksum of the
|
|
269
|
+
affected primary keys, then rolls back:
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{ "profile": "dev", "table": "customers", "where": { "region": { "eq": "closed" } } }
|
|
273
|
+
```
|
|
274
|
+
→
|
|
275
|
+
```json
|
|
276
|
+
{ "rowCount": 42, "expect": { "sqlHash": "a1b2…", "rowCount": 42, "keyChecksum": "9f3e…" } }
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Call 2 — confirm.** Hand the `expect` object straight back with `confirm: true`. The server recomputes
|
|
280
|
+
`sqlHash` from the incoming arguments — a mismatch is `GUARD_HASH_MISMATCH` — re-checks the row count and
|
|
281
|
+
(if supplied) the key checksum inside the committing transaction, and only then commits:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{ "profile": "dev", "table": "customers", "where": { "region": { "eq": "closed" } },
|
|
285
|
+
"confirm": true, "expect": { "sqlHash": "a1b2…", "rowCount": 42, "keyChecksum": "9f3e…" } }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A concurrent change between the two calls that alters the row count or the key set beyond `allowRowDrift`
|
|
289
|
+
aborts the commit with `GUARD_ROW_DRIFT` instead of silently widening the blast radius.
|
|
290
|
+
|
|
291
|
+
**`envClass: production` makes `expect` (including `keyChecksum`) mandatory and forces `allowRowDrift = 0`.**
|
|
292
|
+
`development` accepts a bare `confirm: true`. `envClass` comes from `PG_<ALIAS>_ENV`; an unset or
|
|
293
|
+
unrecognized value defaults to `production` — the safest failure mode.
|
|
294
|
+
|
|
295
|
+
**Honest limitation.** `keyChecksum` is an MD5 over the sorted *primary keys* of the affected rows, so it
|
|
296
|
+
proves *which* rows a statement touches, never their contents. A row whose non-key columns changed between
|
|
297
|
+
the dry run and the confirm — leaving the primary-key set and the row count identical — is undetectable by
|
|
298
|
+
either check. What `keyChecksum` does catch, that `rowCount` alone does not, is *substitution*: one matching
|
|
299
|
+
row disappearing while a different one starts matching keeps the count identical while changing the key set.
|
|
300
|
+
|
|
301
|
+
## Configuration
|
|
302
|
+
|
|
303
|
+
`.env.example` is the full, commented contract — copy it to `.env` and fill it in. The tables below are
|
|
304
|
+
what exists, not the full detail.
|
|
305
|
+
|
|
306
|
+
**Global:**
|
|
307
|
+
|
|
308
|
+
| Var | Default |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `PG_MCP_PROFILES` | **required** — csv of profile aliases |
|
|
311
|
+
| `PG_MCP_DEFAULT_PROFILE` | first entry |
|
|
312
|
+
| `PG_MCP_ALLOWED_DIRS` | `/workspace` in the image; empty on a bare host run disables file tools. Comma-separated (Windows `D:\…` paths are fine); a relative entry resolves against the launch directory |
|
|
313
|
+
| `PG_MCP_ENV_FILE` | unset — path to a dotenv file the server loads at startup (relative = launch directory); keys already in the environment win; unreadable file = exit 1 |
|
|
314
|
+
| `PG_MCP_AUDIT_PATH` | unset — audit logging disabled |
|
|
315
|
+
| `PG_MCP_AUDIT_PARAMS` | `false` — redact statement params in the audit log |
|
|
316
|
+
| `PG_MCP_LOG_LEVEL` | `warn` — stderr only, never stdout |
|
|
317
|
+
| `PG_MCP_MAX_ROWS` / `PG_MCP_MAX_BYTES` | `500` / `262144` |
|
|
318
|
+
| `PG_MCP_CONNECT_TIMEOUT_MS` | `10000` — pool checkout timeout; exceeding it is `PROFILE_UNREACHABLE` |
|
|
319
|
+
| `PG_MCP_MAX_IMPORT_BYTES` | `536870912` (512 MiB) — `postgres_copy` import source cap |
|
|
320
|
+
| `PG_MCP_MAX_SNAPSHOT_BYTES` | `67108864` (64 MiB) — snapshot JSON read cap (a real memory bound) |
|
|
321
|
+
| `PG_MCP_MAX_ARCHIVE_BYTES` | `21474836480` (20 GiB) — `pg_dump` archive cap for `postgres_restore` |
|
|
322
|
+
| `PG_MCP_IN_CONTAINER` | set to `1` by the Dockerfile — **never set this yourself** |
|
|
323
|
+
| `PG_MCP_HOST_REWRITE` | unset — opt-in rewrite of loopback hosts, see [Networking](#networking) |
|
|
324
|
+
| `PG_MCP_WORKSPACE_HOST_PATH` | unset — cosmetic host-side path shown in responses |
|
|
325
|
+
| `PG_MCP_PG_BIN_DIR` / `PG_MCP_PG_MAJOR` | resolved from `PATH` / the image's build arg — on a host without the client on `PATH`, e.g. `C:\Program Files\PostgreSQL\17\bin` |
|
|
326
|
+
|
|
327
|
+
**Per profile** (alias uppercased, non-alphanumerics → `_`, prefixed `PG_<ALIAS>_`):
|
|
328
|
+
|
|
329
|
+
| Var | Default |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `URL` | **required** — percent-encode special characters in the password |
|
|
332
|
+
| `ENV` | `production` (`development` \| `staging` \| `production`) |
|
|
333
|
+
| `ALLOW_WRITE` / `ALLOW_DDL` / `ALLOW_ADMIN` / `ALLOW_EXPLAIN_ANALYZE` / `ALLOW_BACKUP` / `ALLOW_RESTORE` | all `false` |
|
|
334
|
+
| `STATEMENT_TIMEOUT_MS` / `LOCK_TIMEOUT_MS` / `IDLE_TX_TIMEOUT_MS` | `30000` / `5000` / `60000` |
|
|
335
|
+
| `POOL_MAX` | `4` |
|
|
336
|
+
| `SCHEMAS` | unset — csv allowlist, unset = all non-system schemas |
|
|
337
|
+
| `ALLOWED_DIRS` / `MAX_ROWS` | inherit global |
|
|
338
|
+
| `SSL_MODE` / `SSL_ROOT_CERT` | unset |
|
|
339
|
+
| `EXPORT_TIMEOUT_MAX_MS` | `600000` (10 min) — ceiling for `postgres_export` / `postgres_copy`'s per-call `timeoutMs` |
|
|
340
|
+
| `BACKUP_TIMEOUT_MAX_MS` | `3600000` (1 hour) — ceiling for the `pg_dump`/`pg_restore` child process, deliberately separate from `STATEMENT_TIMEOUT_MS` |
|
|
341
|
+
|
|
342
|
+
A broken profile does not stop the server at startup — other profiles stay usable, and
|
|
343
|
+
`postgres_connection_status` reports the breakage for the affected alias.
|
|
344
|
+
|
|
345
|
+
## Connecting a client
|
|
346
|
+
|
|
347
|
+
Every client below takes the same stdio `command`/`args` shape. **npx (primary):** `"command": "npx"`,
|
|
348
|
+
`"args": ["-y", "pg-admin-mcp"]` plus an `env` block — the per-project `.mcp.json` in
|
|
349
|
+
[`examples/per-project/`](examples/per-project/) is the complete Claude Code form. On native Windows,
|
|
350
|
+
`npx` is a `.cmd` shim that most clients cannot spawn directly: use `"command": "cmd"`,
|
|
351
|
+
`"args": ["/c", "npx", "-y", "pg-admin-mcp"]`. Clients launched outside a project (Claude Desktop) have
|
|
352
|
+
no meaningful launch directory, so give `PG_MCP_ENV_FILE` and `PG_MCP_ALLOWED_DIRS` absolute paths there.
|
|
353
|
+
|
|
354
|
+
The Docker forms (alternative) follow.
|
|
355
|
+
|
|
356
|
+
### Claude Code
|
|
357
|
+
|
|
358
|
+
Verified against Claude Code 2.1.220 (`claude mcp add --help`). Stdio is the default transport, so it does
|
|
359
|
+
not need to be specified:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
claude mcp add pg-admin-mcp -- docker run --rm -i --init \
|
|
363
|
+
--add-host=host.docker.internal:host-gateway \
|
|
364
|
+
--env-file /absolute/path/to/.env \
|
|
365
|
+
-v /absolute/path/to/pg-workspace:/workspace \
|
|
366
|
+
mrrainx/pg-admin-mcp:pg17
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
This writes an entry shaped like this into Claude Code's config (`~/.claude.json`, under `mcpServers`):
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"mcpServers": {
|
|
374
|
+
"pg-admin-mcp": {
|
|
375
|
+
"type": "stdio",
|
|
376
|
+
"command": "docker",
|
|
377
|
+
"args": [
|
|
378
|
+
"run", "--rm", "-i", "--init",
|
|
379
|
+
"--add-host=host.docker.internal:host-gateway",
|
|
380
|
+
"--env-file", "/absolute/path/to/.env",
|
|
381
|
+
"-v", "/absolute/path/to/pg-workspace:/workspace",
|
|
382
|
+
"mrrainx/pg-admin-mcp:pg17"
|
|
383
|
+
],
|
|
384
|
+
"env": {}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Use absolute paths for `--env-file` and the volume mount — relative paths are resolved against wherever
|
|
391
|
+
the client process's cwd happens to be, not this repo.
|
|
392
|
+
|
|
393
|
+
### Claude Desktop
|
|
394
|
+
|
|
395
|
+
Edit `claude_desktop_config.json` directly:
|
|
396
|
+
|
|
397
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
398
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"mcpServers": {
|
|
403
|
+
"pg-admin-mcp": {
|
|
404
|
+
"command": "docker",
|
|
405
|
+
"args": [
|
|
406
|
+
"run", "--rm", "-i", "--init",
|
|
407
|
+
"--add-host=host.docker.internal:host-gateway",
|
|
408
|
+
"--env-file", "/absolute/path/to/.env",
|
|
409
|
+
"-v", "/absolute/path/to/pg-workspace:/workspace",
|
|
410
|
+
"mrrainx/pg-admin-mcp:pg17"
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Check your Claude Desktop version's docs for this exact key set — not verified locally against Claude
|
|
418
|
+
Desktop, only against the standard stdio-server JSON shape.
|
|
419
|
+
|
|
420
|
+
### Codex CLI
|
|
421
|
+
|
|
422
|
+
`~/.codex/config.toml`:
|
|
423
|
+
|
|
424
|
+
```toml
|
|
425
|
+
[mcp_servers.pg-admin-mcp]
|
|
426
|
+
command = "docker"
|
|
427
|
+
args = [
|
|
428
|
+
"run", "--rm", "-i", "--init",
|
|
429
|
+
"--add-host=host.docker.internal:host-gateway",
|
|
430
|
+
"--env-file", "/absolute/path/to/.env",
|
|
431
|
+
"-v", "/absolute/path/to/pg-workspace:/workspace",
|
|
432
|
+
"mrrainx/pg-admin-mcp:pg17",
|
|
433
|
+
]
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Not verified locally against the Codex CLI — this is the standard `[mcp_servers.<name>]` stdio form; check
|
|
437
|
+
`codex --help` / the Codex docs for your installed version before relying on it.
|
|
438
|
+
|
|
439
|
+
### Cursor / VS Code
|
|
440
|
+
|
|
441
|
+
Both use the same `command`/`args` stdio shape as Claude Desktop, in a project-local file:
|
|
442
|
+
|
|
443
|
+
- Cursor: `.cursor/mcp.json`, top-level key `mcpServers`
|
|
444
|
+
- VS Code: `.vscode/mcp.json`, top-level key `servers`
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"pg-admin-mcp": {
|
|
449
|
+
"command": "docker",
|
|
450
|
+
"args": [
|
|
451
|
+
"run", "--rm", "-i", "--init",
|
|
452
|
+
"--add-host=host.docker.internal:host-gateway",
|
|
453
|
+
"--env-file", "/absolute/path/to/.env",
|
|
454
|
+
"-v", "/absolute/path/to/pg-workspace:/workspace",
|
|
455
|
+
"mrrainx/pg-admin-mcp:pg17"
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Not verified locally against Cursor or VS Code — check each editor's MCP docs for the current top-level
|
|
462
|
+
key and file location before relying on it.
|
|
463
|
+
|
|
464
|
+
## Networking
|
|
465
|
+
|
|
466
|
+
**npx / host mode:** the server is an ordinary process on your machine, so URL hosts mean what they
|
|
467
|
+
mean there — `localhost` for a local database (or a container's published port), the hostname for a
|
|
468
|
+
remote one. Nothing below applies.
|
|
469
|
+
|
|
470
|
+
**Docker:** the DB's location relative to the container decides the URL host and any extra `docker run` flags:
|
|
471
|
+
|
|
472
|
+
| DB location | Extra run flags | URL host |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| Host machine, Docker Desktop (Windows/macOS) | none | `host.docker.internal` |
|
|
475
|
+
| Host machine, Linux | `--add-host=host.docker.internal:host-gateway` | `host.docker.internal` |
|
|
476
|
+
| Another container | `--network <shared_network>` | that container's service name |
|
|
477
|
+
|
|
478
|
+
`localhost` inside the container **is the container** — not your host. The server does not silently rewrite
|
|
479
|
+
a loopback host to something that might work, because that could connect the agent to a different database
|
|
480
|
+
than the one it thinks it named. Instead it fails fast with `PROFILE_UNREACHABLE`, reported both from
|
|
481
|
+
`postgres_connection_status` before any connection attempt and from the error path on a runtime
|
|
482
|
+
`ECONNREFUSED`/`ENOTFOUND`. Opt-in rewrite is available via `PG_MCP_HOST_REWRITE=host.docker.internal`
|
|
483
|
+
(applied only to loopback hosts, always reported in the response) if you want the old silent behavior.
|
|
484
|
+
|
|
485
|
+
## The `/workspace` volume
|
|
486
|
+
|
|
487
|
+
One mounted volume, `-v <HOST>/pg-workspace:/workspace`. Every tool that writes a file —
|
|
488
|
+
`postgres_export`, `postgres_backup`, `postgres_schema_snapshot`, `postgres_copy` (export direction),
|
|
489
|
+
and the audit log (`PG_MCP_AUDIT_PATH`) — writes inside it, and nothing outside `PG_MCP_ALLOWED_DIRS` is
|
|
490
|
+
reachable at all.
|
|
491
|
+
|
|
492
|
+
**Path rule:** a relative path resolves against the profile's first allowed directory (so `dumps/x.dump`
|
|
493
|
+
means `/workspace/dumps/x.dump`); an absolute path is normalized and its parent directory must still land
|
|
494
|
+
inside an allowed directory. Files are written as `<name>.partial` and renamed on success, so a killed
|
|
495
|
+
`pg_dump`/export never leaves a plausible-looking truncated file under the real name.
|
|
496
|
+
|
|
497
|
+
**Linux ownership note.** The container always runs as uid `10001` (never root — see the `Dockerfile`).
|
|
498
|
+
On Docker Desktop (Windows/macOS) the bind-mount boundary maps every uid to full access, so this is
|
|
499
|
+
cosmetic. **On native Linux it is not cosmetic — it is a real `EACCES` risk.** `./pg-workspace` ends up
|
|
500
|
+
owned by whichever uid created it first: your own, if you ran `mkdir -p pg-workspace` first (Quick
|
|
501
|
+
start step 3); `root:root` at mode `0755`, if you skipped that and let the Docker daemon auto-create
|
|
502
|
+
the missing bind-mount source. Either way, uid `10001` is neither the owner nor (normally) in the
|
|
503
|
+
owning group, and the default `0755` grants no write to anyone else — so the very first
|
|
504
|
+
`postgres_export`/`postgres_backup`/`postgres_schema_snapshot` call fails with `EACCES` writing into a
|
|
505
|
+
directory that, from the host side, looks perfectly normal and owned by you. The fix is the same one
|
|
506
|
+
that solves the ownership question above — run the container as your own uid/gid instead of `10001`:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
docker run --rm -i --init --user "$(id -u):$(id -g)" \
|
|
510
|
+
--add-host=host.docker.internal:host-gateway \
|
|
511
|
+
--env-file /absolute/path/to/.env \
|
|
512
|
+
-v /absolute/path/to/pg-workspace:/workspace \
|
|
513
|
+
mrrainx/pg-admin-mcp:pg17
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
For the Compose form (`pg_mcp.yml`, see [Using it from several projects](#using-it-from-several-projects)),
|
|
517
|
+
the equivalent is a `user:` key on the `pg-mcp` service — plain `$UID`/`$GID` are bash builtins and are
|
|
518
|
+
**not** exported to `docker compose` by default, so export them under different names first:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
export DOCKER_UID="$(id -u)" DOCKER_GID="$(id -g)"
|
|
522
|
+
docker compose -f pg_mcp.yml run --rm -T pg-mcp
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```yaml
|
|
526
|
+
services:
|
|
527
|
+
pg-mcp:
|
|
528
|
+
image: mrrainx/pg-admin-mcp:pg17
|
|
529
|
+
user: "${DOCKER_UID:-10001}:${DOCKER_GID:-10001}"
|
|
530
|
+
# ... rest unchanged
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Not applied by default in [`examples/per-project-docker/pg_mcp.yml`](examples/per-project-docker/pg_mcp.yml) — the
|
|
534
|
+
key is a no-op on Docker Desktop and would need every Linux user to remember to export
|
|
535
|
+
`DOCKER_UID`/`DOCKER_GID` first, so it stays opt-in text here rather than a template default that fails
|
|
536
|
+
open to `10001:10001` for anyone who forgets.
|
|
537
|
+
|
|
538
|
+
## Troubleshooting
|
|
539
|
+
|
|
540
|
+
- **`PROFILE_UNREACHABLE` pointing at `localhost`.** You configured `PG_<ALIAS>_URL` with `localhost` or
|
|
541
|
+
`127.0.0.1` — inside the container that's the container itself. See [Networking](#networking).
|
|
542
|
+
- **A password with `#` gets silently truncated.** Docker's `--env-file` does no quote stripping and no
|
|
543
|
+
interpolation — a value is taken literally from the first `=` to end of line, and a `#` anywhere starts a
|
|
544
|
+
comment. Percent-encode special characters in the connection URL (`p@ss#1` → `p%40ss%231`).
|
|
545
|
+
- **Never pass `-t` to `docker run`.** A TTY echoes input back onto stdout and corrupts the JSON-RPC frame
|
|
546
|
+
stream this server relies on. Use `-i` only.
|
|
547
|
+
- **`BINARY_MISSING`.** `postgres_backup`/`postgres_restore` need `pg_dump`/`pg_restore` on `PATH` (or
|
|
548
|
+
`PG_MCP_PG_BIN_DIR`). Resolution is lazy — the other 16 tools work with no client binaries installed at
|
|
549
|
+
all; this error only ever comes from the two backup/restore tools. In host mode install a client
|
|
550
|
+
matching your server's major (Windows `winget install PostgreSQL.PostgreSQL.17`, Debian/Ubuntu
|
|
551
|
+
`sudo apt install postgresql-client`, macOS `brew install libpq`) and point `PG_MCP_PG_BIN_DIR` at its
|
|
552
|
+
`bin` if it is not on `PATH` (Windows: `C:\Program Files\PostgreSQL\17\bin`).
|
|
553
|
+
- **npx server exits immediately with `PG_MCP_ENV_FILE points to …`.** The `.env` it names does not
|
|
554
|
+
exist at that path. A relative path resolves against the directory the client was launched from —
|
|
555
|
+
start `claude` from the project root, or make the path absolute.
|
|
556
|
+
- **`pg_dump`/`pg_restore` major-version mismatch.** Archives are forward-only: a client older than the
|
|
557
|
+
server it dumped from refuses to run, and an archive produced by a newer `pg_dump` cannot be restored by
|
|
558
|
+
an older `pg_restore`. Use the image tag matching your PostgreSQL major (`:pg17`, `:pg16`, …) —
|
|
559
|
+
`postgres_restore` checks this before spawning anything and names the required tag in the refusal.
|
|
560
|
+
|
|
561
|
+
## Known limitations
|
|
562
|
+
|
|
563
|
+
- `postgres_restore` only accepts single-file archives (`pg_dump --format=custom` or `--format=tar`).
|
|
564
|
+
**Directory-format archives are not supported** — they cannot be identity-checked as one file.
|
|
565
|
+
- `postgres_restore` cannot restore **plain-format (SQL text) dumps** — those aren't `pg_restore` archives;
|
|
566
|
+
feed them to `psql` instead.
|
|
567
|
+
- `keyChecksum` in the dry-run/confirm protocol pins *which* rows a statement touches (by primary key), not
|
|
568
|
+
their contents — see [Safety model](#safety-model) for the exact case it does and doesn't catch.
|
|
569
|
+
|
|
570
|
+
## Development
|
|
571
|
+
|
|
572
|
+
Package manager is **pnpm** — never npm.
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
pnpm install
|
|
576
|
+
pnpm check # lint + typecheck + unit tests + build + stdio smoke check
|
|
577
|
+
pnpm test # unit tests only (no Docker)
|
|
578
|
+
pnpm test:int # integration tests against a real postgres:17 via testcontainers (needs Docker)
|
|
579
|
+
RUN_CONTAINER_SMOKE=1 pnpm test:smoke # builds the image and drives it over stdio (needs Docker)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Three Vitest projects: `unit` needs nothing but Node; `integration` needs Docker (testcontainers starts
|
|
583
|
+
`postgres:17` on a random port); `smoke` needs Docker and is opt-in (`RUN_CONTAINER_SMOKE=1`, ~300s) because
|
|
584
|
+
it builds the image from scratch. **The suite runs on the host, never inside the MCP image** — testing
|
|
585
|
+
in-image would need Docker-in-Docker for testcontainers for no gain.
|
|
586
|
+
|
|
587
|
+
### Building the image locally
|
|
588
|
+
|
|
589
|
+
The published image comes from Docker Hub (see [Quick start (Docker, alternative)](#quick-start-docker-alternative)) — build locally only when
|
|
590
|
+
testing a change before it ships. Client binaries are pinned to a PostgreSQL major; pass the `PG_MAJOR`
|
|
591
|
+
matching your server and tag accordingly:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
docker build --build-arg PG_MAJOR=17 -t pg-admin-mcp:pg17 .
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
See [docs/releasing.md](docs/releasing.md) for how a tagged commit turns this into the published
|
|
598
|
+
`mrrainx/pg-admin-mcp:pg17`/`:pg16` images (`linux/amd64` only).
|
|
599
|
+
|
|
600
|
+
## Status
|
|
601
|
+
|
|
602
|
+
Personal project, version `0.1.0`. Distributed on npm as `pg-admin-mcp` (`npx -y pg-admin-mcp`) — if
|
|
603
|
+
`npx` reports `404 Not Found`, no npm release has been cut yet; until then run it from a clone
|
|
604
|
+
(`pnpm install && pnpm build`, then `node /absolute/path/to/dist/index.js` as the client `command`). Docker images are
|
|
605
|
+
built and pushed to Docker Hub as `mrrainx/pg-admin-mcp` (`:pg17`, `:pg16`, `linux/amd64` only) by the
|
|
606
|
+
release workflow described in [docs/releasing.md](docs/releasing.md), triggered by pushing a `v*` tag —
|
|
607
|
+
if no such tag has been pushed yet, `docker pull` returns `manifest unknown` and
|
|
608
|
+
[Building the image locally](#building-the-image-locally) is the only way to get the image. No license
|
|
609
|
+
file yet.
|