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
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadProfiles } from '../config/profiles.js';
|
|
3
|
+
import { leadingKeyword, obviousWriteKeyword, planStatements } from '../core/classifier.js';
|
|
4
|
+
import { PgMcpError, envPrefix, toToolResult } from '../core/errors.js';
|
|
5
|
+
import { formatOutcome } from '../core/format.js';
|
|
6
|
+
import { assertRawWhere, CONFIRM_PROTOCOL_DESCRIPTION, runGuardedMutation } from '../core/guards.js';
|
|
7
|
+
import { resolveLimits } from '../core/limits.js';
|
|
8
|
+
import { requireCapability } from '../core/policy.js';
|
|
9
|
+
import { withTx } from '../db/session.js';
|
|
10
|
+
import { confirmFragment, toExpectToken } from './_batch_shared.js';
|
|
11
|
+
import { defineTool, profileParam } from './_shared.js';
|
|
12
|
+
/**
|
|
13
|
+
* `postgres_execute_sql` — the arbitrary-SQL escape hatch.
|
|
14
|
+
*
|
|
15
|
+
* This is the one tool where the project's usual discipline (`SafeIdent`
|
|
16
|
+
* identifiers resolved through the catalog, values only ever bound as `$N`)
|
|
17
|
+
* does not apply, because the caller supplies the SQL text. Its safety story
|
|
18
|
+
* therefore comes from somewhere else entirely:
|
|
19
|
+
*
|
|
20
|
+
* 1. **PostgreSQL's own read-only transaction.** Every statement that is not
|
|
21
|
+
* routed to the write path runs inside `withTx({ readOnly: true })`, so the
|
|
22
|
+
* server itself rejects any write with SQLSTATE `25006`, which
|
|
23
|
+
* `core/errors.ts#fromPgError` maps to `POLICY_READ_ONLY` with a hint naming
|
|
24
|
+
* `PG_<ALIAS>_ALLOW_WRITE`. That, not the lexical pre-check below, is what
|
|
25
|
+
* catches `WITH x AS (INSERT … RETURNING) SELECT * FROM x`,
|
|
26
|
+
* `SELECT nextval('s')`, and a VOLATILE function that writes. See
|
|
27
|
+
* `core/classifier.ts`.
|
|
28
|
+
*
|
|
29
|
+
* **This still holds on a write-capable profile**, and it is the single most
|
|
30
|
+
* important consequence of the routing rule below: the write path is entered
|
|
31
|
+
* only by a statement whose *leading keyword* says it writes. A statement
|
|
32
|
+
* that hides its write (`WITH … INSERT … RETURNING`, `SELECT nextval()`) is
|
|
33
|
+
* therefore still run read-only and still refused with 25006 — even when the
|
|
34
|
+
* profile has `ALLOW_WRITE`. Rewrite it as a plain `INSERT`/`UPDATE`/`DELETE`
|
|
35
|
+
* to get the dry-run/confirm protocol. Refusing is the safe direction: the
|
|
36
|
+
* alternative is a statement that writes without ever being previewed.
|
|
37
|
+
* 2. **Multi-statement rejection** (design.md risk 7), via the scanner in
|
|
38
|
+
* `core/classifier.ts#splitStatements`. `pg` runs a multi-statement string
|
|
39
|
+
* in simple-query mode as one implicit transaction *and silently ignores
|
|
40
|
+
* parameters*; both halves of that are refused here.
|
|
41
|
+
* 3. **The extended query protocol, forced explicitly.** Every query this tool
|
|
42
|
+
* issues goes through `{ text, values, queryMode: 'extended' }`, where
|
|
43
|
+
* PostgreSQL accepts exactly one statement per Parse and answers a second
|
|
44
|
+
* one with `42601 cannot insert multiple commands into a prepared
|
|
45
|
+
* statement`. That makes the scanner in (2) a fast, readable first refusal
|
|
46
|
+
* rather than the sole fence.
|
|
47
|
+
*
|
|
48
|
+
* **`queryMode` is not decoration.** The previous version of this comment
|
|
49
|
+
* claimed an always-present `params` array was enough — it is not.
|
|
50
|
+
* `pg@8.22`'s `Query.requiresPreparation()` ends `return this.values.length
|
|
51
|
+
* > 0`, so on the common no-params call `client.query(sql, [])` runs in
|
|
52
|
+
* *simple* mode and `SELECT 1; INSERT …` executes both commands. Measured,
|
|
53
|
+
* not assumed. Do not remove `queryMode` from any call site in this file.
|
|
54
|
+
*
|
|
55
|
+
* **Since the write path shipped, `queryMode` is the ONLY structural
|
|
56
|
+
* multi-statement fence left on this tool.** The read-only transaction used
|
|
57
|
+
* to be a blanket second answer to `SELECT 1; INSERT …` — a write-capable
|
|
58
|
+
* profile no longer has it. `core/guards.ts#runMutation` sets `queryMode`
|
|
59
|
+
* for the same reason, so both paths keep the fence. There is an integration
|
|
60
|
+
* test that submits exactly that string to a write-capable profile and
|
|
61
|
+
* asserts `42601`; if it ever goes green for the wrong reason, this is why
|
|
62
|
+
* it exists.
|
|
63
|
+
*
|
|
64
|
+
* ## Routing: which statements take the write path
|
|
65
|
+
*
|
|
66
|
+
* One table, {@link WRITE_PATH_CAPABILITY}, keyed by the statement's leading
|
|
67
|
+
* keyword (`core/classifier.ts#leadingKeyword`), mapping to the capability that
|
|
68
|
+
* statement needs. Absent from the table means the read path.
|
|
69
|
+
*
|
|
70
|
+
* - DML (`INSERT`/`UPDATE`/`DELETE`/`MERGE`) needs `write`.
|
|
71
|
+
* - DDL (`CREATE`/`ALTER`/`DROP`/`TRUNCATE`) needs `ddl` **on top of** `write`.
|
|
72
|
+
* `ALLOW_WRITE` is the master switch for this tool's write path; `ALLOW_DDL`
|
|
73
|
+
* is the second key for the statements that can destroy a schema rather than
|
|
74
|
+
* rows.
|
|
75
|
+
* - `GRANT`/`REVOKE` need `admin`, again on top of `write`. Changing who may
|
|
76
|
+
* read a table is not DDL and is not an ordinary write.
|
|
77
|
+
* - **Transaction control** (`BEGIN`, `COMMIT`, `ROLLBACK`, `END`, `ABORT`,
|
|
78
|
+
* `SAVEPOINT`, `RELEASE`, `START`) is routed to the write path deliberately,
|
|
79
|
+
* even though it writes nothing. It is what *attacks* the guard: a bare
|
|
80
|
+
* `COMMIT` ends the transaction `withTx` owns, and on the write path the
|
|
81
|
+
* `rollbackAlways` savepoint fence detects that and raises
|
|
82
|
+
* `GUARD_DRY_RUN_ESCAPED` with a human-readable hint. Routed to the read path
|
|
83
|
+
* it would instead succeed silently as a no-op. Nothing is lost by refusing
|
|
84
|
+
* it — this server always owns the transaction.
|
|
85
|
+
*
|
|
86
|
+
* **Bounded fetch, not just a bounded response.** The page is read through a
|
|
87
|
+
* server-side `DECLARE … NO SCROLL CURSOR` + one `FETCH FORWARD n+1` inside
|
|
88
|
+
* the same transaction, so `limit: 1` against a 400k-row table transfers one
|
|
89
|
+
* row instead of buffering all 400k in the driver and slicing in JS (measured:
|
|
90
|
+
* a 168.9 MB heap delta to return a single row). Statements that cannot back a
|
|
91
|
+
* cursor at all — `EXPLAIN`, `SHOW`, … — fall back to a direct query; those
|
|
92
|
+
* return a handful of rows, which is not the failure mode this guards.
|
|
93
|
+
*
|
|
94
|
+
* **No cursor is handed out.** design.md §`core/paginate.ts` is explicit:
|
|
95
|
+
* **keyset cursors only**, because successive calls land on different pooled
|
|
96
|
+
* backends (risk 4). Offset-into-a-re-run is not keyset — a plan change
|
|
97
|
+
* between pages overlaps or skips rows — so this tool caps the page and tells
|
|
98
|
+
* the caller to put `LIMIT`/`OFFSET` (or a keyset predicate) in their own SQL,
|
|
99
|
+
* where they can see it. `postgres_select` is the tool with real pagination.
|
|
100
|
+
*
|
|
101
|
+
* Read-only is not a sandbox (design.md risk 8): it does not stop
|
|
102
|
+
* `COPY … TO PROGRAM`, `lo_export()`, or advisory locks. The documented
|
|
103
|
+
* production posture is a PostgreSQL role that is itself read-only.
|
|
104
|
+
*
|
|
105
|
+
* ## What the write path cannot offer, and why
|
|
106
|
+
*
|
|
107
|
+
* - **No `keyChecksum`, no preview.** The guard's key preview comes from
|
|
108
|
+
* catalog-resolved primary key columns wrapped around the statement as
|
|
109
|
+
* `WITH … RETURNING pk`. Raw SQL has no catalog-resolved target relation —
|
|
110
|
+
* deriving one would mean parsing the statement, which this project
|
|
111
|
+
* deliberately refuses to do (`core/classifier.ts`). So `keyColumns` is empty
|
|
112
|
+
* here and `rowCount` is the whole receipt.
|
|
113
|
+
* - **Therefore raw SQL cannot write on a `production` profile at all.**
|
|
114
|
+
* `core/policy.ts` makes `expect.keyChecksum` mandatory there, and
|
|
115
|
+
* `runGuardedMutation` refuses up front rather than after a dry run nobody
|
|
116
|
+
* could confirm. That is the right outcome, not a gap: on production, use
|
|
117
|
+
* `postgres_update_batch`/`postgres_delete_batch`/`postgres_insert_batch`,
|
|
118
|
+
* which resolve a real primary key and can pin *which* rows they touch.
|
|
119
|
+
* - **No `allowMultiple`.** Refused outright on the write path. N statements in
|
|
120
|
+
* one transaction have no per-statement dry run, no `rowCount` worth hashing
|
|
121
|
+
* and no `expect` token that could mean anything about the middle of the list.
|
|
122
|
+
* - **No rows come back.** A confirmed write reports a row count; a `RETURNING`
|
|
123
|
+
* list is executed but not rendered. `limit` applies to the read path only.
|
|
124
|
+
*/
|
|
125
|
+
const DEFAULT_PAGE_SIZE = 200;
|
|
126
|
+
const MAX_PAGE_SIZE = 2000;
|
|
127
|
+
/** Named rather than random so it is greppable in a server log; closed before the transaction ends, so it never outlives the checkout. */
|
|
128
|
+
const CURSOR_NAME = 'pg_admin_mcp_exec_cursor';
|
|
129
|
+
/** Fences the `DECLARE` attempt: a statement a cursor cannot wrap aborts the transaction, and this is what lets the direct-query fallback run anyway. */
|
|
130
|
+
const CURSOR_SAVEPOINT = 'pg_admin_mcp_exec_probe';
|
|
131
|
+
const inputSchema = z.object({
|
|
132
|
+
...profileParam,
|
|
133
|
+
sql: z.string().min(1).describe('The SQL to run. One statement unless allowMultiple is set. Never string-interpolate values into this — use params.'),
|
|
134
|
+
params: z
|
|
135
|
+
.array(z.unknown())
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('Bound parameters for $1, $2, … — always sent over the extended query protocol, so a value is data, never SQL.'),
|
|
138
|
+
allowMultiple: z
|
|
139
|
+
.boolean()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe('Permit several ;-separated statements. Refused by default; impossible together with params.'),
|
|
142
|
+
limit: z
|
|
143
|
+
.number()
|
|
144
|
+
.int()
|
|
145
|
+
.min(1)
|
|
146
|
+
.max(MAX_PAGE_SIZE)
|
|
147
|
+
.optional()
|
|
148
|
+
.describe(`Rows fetched (max ${MAX_PAGE_SIZE}), further capped by the profile row limit. Bounds the fetch, not just the response — put LIMIT/OFFSET in your own SQL to read past it.`),
|
|
149
|
+
timeoutMs: z
|
|
150
|
+
.number()
|
|
151
|
+
.int()
|
|
152
|
+
.min(1)
|
|
153
|
+
.optional()
|
|
154
|
+
.describe('Per-call statement timeout. Can only tighten the profile\'s PG_<ALIAS>_STATEMENT_TIMEOUT_MS, never loosen it.'),
|
|
155
|
+
...confirmFragment,
|
|
156
|
+
});
|
|
157
|
+
/**
|
|
158
|
+
* The cheap fast-fail for a profile that cannot write at all. Never the
|
|
159
|
+
* guarantee — see `core/classifier.ts`. Exported so `tools/export.ts` reuses it
|
|
160
|
+
* for its `sql`-source path (always read-only) instead of a second copy.
|
|
161
|
+
*
|
|
162
|
+
* Still here, and still branched on `profile.capabilities.write` rather than
|
|
163
|
+
* deleted, because on a read-only profile it is the fastest and most readable
|
|
164
|
+
* refusal there is: it names the keyword and the env var without a round trip.
|
|
165
|
+
*/
|
|
166
|
+
export function assertNoObviousWrite(statements, alias) {
|
|
167
|
+
for (const statement of statements) {
|
|
168
|
+
const keyword = obviousWriteKeyword(statement);
|
|
169
|
+
if (keyword === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
throw new PgMcpError({
|
|
172
|
+
code: 'POLICY_READ_ONLY',
|
|
173
|
+
message: `this statement starts with ${keyword}, and this profile runs every statement inside a read-only transaction`,
|
|
174
|
+
hint: `set ${envPrefix(alias)}_ALLOW_WRITE=true on this profile and restart the server to unlock ` +
|
|
175
|
+
'postgres_execute_sql\'s write path (a two-call dry run + confirm); DDL additionally needs ' +
|
|
176
|
+
`${envPrefix(alias)}_ALLOW_DDL=true`,
|
|
177
|
+
details: { profile: alias, keyword },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Every spelling PostgreSQL accepts for "end or fence the transaction". See the file header for why they take the write path. */
|
|
182
|
+
const TX_CONTROL_KEYWORDS = ['BEGIN', 'START', 'COMMIT', 'END', 'ROLLBACK', 'ABORT', 'SAVEPOINT', 'RELEASE'];
|
|
183
|
+
const TX_CONTROL = new Set(TX_CONTROL_KEYWORDS);
|
|
184
|
+
/** The keywords whose dry run genuinely reports a row count. */
|
|
185
|
+
const DML_KEYWORDS = new Set(['INSERT', 'UPDATE', 'DELETE', 'MERGE']);
|
|
186
|
+
/** Leading keyword -> the capability that statement needs. Absent = read path. */
|
|
187
|
+
const WRITE_PATH_CAPABILITY = {
|
|
188
|
+
INSERT: 'write',
|
|
189
|
+
UPDATE: 'write',
|
|
190
|
+
DELETE: 'write',
|
|
191
|
+
MERGE: 'write',
|
|
192
|
+
CREATE: 'ddl',
|
|
193
|
+
ALTER: 'ddl',
|
|
194
|
+
DROP: 'ddl',
|
|
195
|
+
TRUNCATE: 'ddl',
|
|
196
|
+
GRANT: 'admin',
|
|
197
|
+
REVOKE: 'admin',
|
|
198
|
+
...Object.fromEntries(TX_CONTROL_KEYWORDS.map((keyword) => [keyword, 'write'])),
|
|
199
|
+
};
|
|
200
|
+
/** The first statement that would take the write path, or `undefined` for a pure read. */
|
|
201
|
+
function planWrite(statements) {
|
|
202
|
+
for (const statement of statements) {
|
|
203
|
+
const keyword = leadingKeyword(statement);
|
|
204
|
+
const capability = keyword === undefined ? undefined : WRITE_PATH_CAPABILITY[keyword];
|
|
205
|
+
if (keyword !== undefined && capability !== undefined)
|
|
206
|
+
return { keyword, capability, statement };
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* What a DDL dry run actually establishes, stated so that nobody reads
|
|
212
|
+
* "0 row(s) would be affected" as "nothing will happen".
|
|
213
|
+
*
|
|
214
|
+
* PostgreSQL returns no row count for a utility statement, so the 0 is the
|
|
215
|
+
* absence of a number, not a measurement. The dry run really did run the
|
|
216
|
+
* statement against the real catalog inside a transaction that was rolled back
|
|
217
|
+
* — which proves parse, resolution and privilege, and proves nothing at all
|
|
218
|
+
* about blast radius.
|
|
219
|
+
*/
|
|
220
|
+
function ddlNote(keyword) {
|
|
221
|
+
return (`DDL — READ THIS BEFORE CONFIRMING. PostgreSQL reports no row count for ${keyword}, so the 0 above means ` +
|
|
222
|
+
'"not applicable", NOT "nothing will happen". What this dry run DID prove: the statement parsed, ran to ' +
|
|
223
|
+
'completion against the real catalog inside a transaction that was rolled back, and your role holds the ' +
|
|
224
|
+
'privileges for it. What it CANNOT tell you: how many rows a DROP or TRUNCATE will destroy, which views, ' +
|
|
225
|
+
'foreign keys, sequences or functions depend on the object, or how long an ALTER that rewrites the table will ' +
|
|
226
|
+
`hold its lock. Confirming runs the same ${keyword} for real. Call postgres_describe_object on the target first.`);
|
|
227
|
+
}
|
|
228
|
+
/** The other half of the honesty: raw DML has a real row count but no key identity behind it. */
|
|
229
|
+
const RAW_DML_NOTE = 'The row count above is real — it is what the statement actually affected inside the rolled-back transaction. ' +
|
|
230
|
+
'There is no key preview and no keyChecksum: raw SQL has no catalog-resolved primary key, so the confirming call ' +
|
|
231
|
+
'can only re-verify the COUNT, not that the same rows are still the ones being hit. If the identity of the rows ' +
|
|
232
|
+
'matters, use postgres_update_batch / postgres_delete_batch instead.';
|
|
233
|
+
/** Only reachable for BEGIN/START/SAVEPOINT — the rest end the transaction and are caught by the dry-run fence first. */
|
|
234
|
+
const TX_CONTROL_NOTE = 'This statement only manipulates the transaction that this server owns and opens for you. It changed no data, and ' +
|
|
235
|
+
'confirming it would change none either. Delete it from your SQL: use confirm:true to commit a write, not COMMIT.';
|
|
236
|
+
/** `runGuardedMutation` opens the transaction, so this refusal has to happen before it — and before any round trip. */
|
|
237
|
+
function refuseMultiStatementWrite(keyword, alias) {
|
|
238
|
+
throw new PgMcpError({
|
|
239
|
+
code: 'PG_ERROR',
|
|
240
|
+
message: `allowMultiple cannot be combined with a write (this call starts with ${keyword}) — a list of statements has ` +
|
|
241
|
+
'no per-statement dry run, no row count worth hashing, and no expect token that would mean anything about the ' +
|
|
242
|
+
'middle of the list',
|
|
243
|
+
hint: 'send one write per call so each gets its own dry run and its own expect token, and re-run',
|
|
244
|
+
details: { profile: alias, keyword },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The guarded write path. Split out of the handler so the read path above stays
|
|
249
|
+
* exactly as it was — the two share only `planStatements` and the profile.
|
|
250
|
+
*/
|
|
251
|
+
async function runWrite(profile, write, args, params, timeoutMs) {
|
|
252
|
+
if (!profile.capabilities.write) {
|
|
253
|
+
// The fast, readable refusal, kept and branched rather than deleted.
|
|
254
|
+
assertNoObviousWrite([write.statement], profile.alias);
|
|
255
|
+
// Transaction control is not an "obvious write", so it reaches here instead.
|
|
256
|
+
requireCapability('write', profile);
|
|
257
|
+
}
|
|
258
|
+
if (args.allowMultiple === true)
|
|
259
|
+
refuseMultiStatementWrite(write.keyword, profile.alias);
|
|
260
|
+
if (write.keyword === 'UPDATE' || write.keyword === 'DELETE') {
|
|
261
|
+
// The WHERE guard for raw SQL. Cheap and easily fooled — the row count the
|
|
262
|
+
// dry run reports is the guard that matters. See `core/guards.ts`.
|
|
263
|
+
assertRawWhere(write.statement, { profile: profile.alias, operation: 'postgres_execute_sql' });
|
|
264
|
+
}
|
|
265
|
+
const outcome = await runGuardedMutation({
|
|
266
|
+
profile,
|
|
267
|
+
// `write` was verified above; this is the SECOND key for DDL/GRANT.
|
|
268
|
+
capability: write.capability,
|
|
269
|
+
operation: 'execute_sql',
|
|
270
|
+
sql: write.statement,
|
|
271
|
+
params,
|
|
272
|
+
// Raw SQL has no catalog-resolved relation, so there is no primary key to
|
|
273
|
+
// preview or checksum — and nothing here computes a checksum over
|
|
274
|
+
// server-generated keys, which a rolled-back dry run would invalidate
|
|
275
|
+
// anyway (sequences are not transactional; see `tools/insert_batch.ts`).
|
|
276
|
+
keyColumns: [],
|
|
277
|
+
confirm: args.confirm,
|
|
278
|
+
expect: toExpectToken(args.expect),
|
|
279
|
+
allowRowDrift: args.allowRowDrift,
|
|
280
|
+
...(timeoutMs === undefined ? {} : { statementTimeoutMs: timeoutMs }),
|
|
281
|
+
});
|
|
282
|
+
if (outcome.kind !== 'dryRun')
|
|
283
|
+
return formatOutcome(outcome);
|
|
284
|
+
const note = DML_KEYWORDS.has(write.keyword)
|
|
285
|
+
? RAW_DML_NOTE
|
|
286
|
+
: TX_CONTROL.has(write.keyword)
|
|
287
|
+
? TX_CONTROL_NOTE
|
|
288
|
+
: ddlNote(write.keyword);
|
|
289
|
+
return formatOutcome({ ...outcome, note });
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The only way caller-supplied SQL reaches PostgreSQL in this file.
|
|
293
|
+
* `queryMode: 'extended'` is the whole point — see the file header.
|
|
294
|
+
*/
|
|
295
|
+
async function runExtended(client, text, values) {
|
|
296
|
+
return client.query({ text, values: [...values], queryMode: 'extended' });
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Fetches at most `pageLimit + 1` rows (the extra row is what proves there are
|
|
300
|
+
* more) without ever materializing the full result in the driver.
|
|
301
|
+
*/
|
|
302
|
+
async function fetchBounded(client, sql, params, pageLimit) {
|
|
303
|
+
await client.query(`SAVEPOINT ${CURSOR_SAVEPOINT}`);
|
|
304
|
+
try {
|
|
305
|
+
await runExtended(client, `DECLARE ${CURSOR_NAME} NO SCROLL CURSOR FOR ${sql}`, params);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
// `DECLARE … CURSOR FOR` only accepts a SELECT/VALUES/TABLE/WITH query, so
|
|
309
|
+
// `EXPLAIN`, `SHOW`, `TABLE`-adjacent utility statements and friends land
|
|
310
|
+
// here. The failed DECLARE aborted the transaction; rewind and run the
|
|
311
|
+
// statement directly — still extended-mode, still one command, just
|
|
312
|
+
// unbounded, which is fine for statements that return a handful of rows.
|
|
313
|
+
// A genuine error in the caller's SQL surfaces identically from the direct
|
|
314
|
+
// run, so nothing is swallowed.
|
|
315
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${CURSOR_SAVEPOINT}`);
|
|
316
|
+
void err;
|
|
317
|
+
return runExtended(client, sql, params);
|
|
318
|
+
}
|
|
319
|
+
const page = await client.query(`FETCH FORWARD ${pageLimit + 1} FROM ${CURSOR_NAME}`);
|
|
320
|
+
await client.query(`CLOSE ${CURSOR_NAME}`);
|
|
321
|
+
return page;
|
|
322
|
+
}
|
|
323
|
+
export const executeSqlTool = defineTool('postgres_execute_sql', {
|
|
324
|
+
title: 'Run arbitrary SQL (reads, and guarded writes)',
|
|
325
|
+
description: 'The arbitrary-SQL escape hatch — for anything postgres_select cannot express (joins, CTEs, window functions, ' +
|
|
326
|
+
'set operations, catalog queries). Values belong in params ($1, $2, …), which are always bound, never ' +
|
|
327
|
+
'interpolated. Multi-statement strings are refused unless allowMultiple is set, and are impossible when params ' +
|
|
328
|
+
'are supplied. READS run inside a PostgreSQL READ ONLY transaction and return up to `limit` rows; this tool ' +
|
|
329
|
+
'hands out no cursor, so put LIMIT/OFFSET (with an ORDER BY) in your own SQL to read past the cap. WRITES: a ' +
|
|
330
|
+
'statement whose leading keyword is INSERT/UPDATE/DELETE/MERGE takes the guarded write path when the profile ' +
|
|
331
|
+
'sets PG_<ALIAS>_ALLOW_WRITE=true; UPDATE and DELETE must carry a WHERE clause that references a real column. ' +
|
|
332
|
+
'CREATE/ALTER/DROP/TRUNCATE additionally need PG_<ALIAS>_ALLOW_DDL=true, and GRANT/REVOKE need ' +
|
|
333
|
+
'PG_<ALIAS>_ALLOW_ADMIN=true. A DDL DRY RUN CANNOT REPORT A ROW COUNT — PostgreSQL returns none for DDL, so a ' +
|
|
334
|
+
'dry-run "0 rows" on a DROP TABLE means "not applicable", never "nothing will happen"; the dry run proves only ' +
|
|
335
|
+
'that the statement parses, resolves and is permitted, and says nothing about how many rows it destroys or ' +
|
|
336
|
+
'what depends on the object. allowMultiple is refused on the write path. No rows are returned by a write, and ' +
|
|
337
|
+
'raw SQL has no catalog-resolved primary key, so there is no key preview and no keyChecksum — which means a ' +
|
|
338
|
+
'production profile refuses raw-SQL writes outright (use postgres_update_batch / postgres_delete_batch / ' +
|
|
339
|
+
'postgres_insert_batch there). A write that no keyword check can see (WITH … INSERT … RETURNING, nextval()) ' +
|
|
340
|
+
`still runs read-only and is still refused by the server; rewrite it as a plain INSERT. ${CONFIRM_PROTOCOL_DESCRIPTION}`,
|
|
341
|
+
inputSchema,
|
|
342
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
343
|
+
}, async (args) => {
|
|
344
|
+
try {
|
|
345
|
+
const registry = loadProfiles();
|
|
346
|
+
const profile = registry.get(args.profile);
|
|
347
|
+
const params = args.params ?? [];
|
|
348
|
+
const statements = planStatements(args.sql, {
|
|
349
|
+
allowMultiple: args.allowMultiple === true,
|
|
350
|
+
hasParams: params.length > 0,
|
|
351
|
+
});
|
|
352
|
+
// Only tighten: a timeoutMs above the profile's ceiling is capped, never applied.
|
|
353
|
+
const timeoutMs = args.timeoutMs === undefined ? undefined : Math.min(args.timeoutMs, profile.statementTimeoutMs);
|
|
354
|
+
const write = planWrite(statements);
|
|
355
|
+
if (write !== undefined)
|
|
356
|
+
return await runWrite(profile, write, args, params, timeoutMs);
|
|
357
|
+
const limits = resolveLimits(profile.maxRows, args.limit === undefined ? {} : { maxRows: args.limit }, process.env);
|
|
358
|
+
const pageLimit = Math.min(args.limit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, limits.maxRows);
|
|
359
|
+
const result = await withTx(profile, async (client) => {
|
|
360
|
+
// Under `allowMultiple` only the last statement's rows come back, so
|
|
361
|
+
// the leading ones are run for their effect and only the last one is
|
|
362
|
+
// worth bounding. Each is its own round trip — never one
|
|
363
|
+
// simple-query batch.
|
|
364
|
+
for (const statement of statements.slice(0, -1))
|
|
365
|
+
await runExtended(client, statement, []);
|
|
366
|
+
const last = statements[statements.length - 1] ?? '';
|
|
367
|
+
return fetchBounded(client, last, params, pageLimit);
|
|
368
|
+
}, {
|
|
369
|
+
readOnly: true,
|
|
370
|
+
operation: 'execute_sql',
|
|
371
|
+
...(timeoutMs === undefined ? {} : { statementTimeoutMs: timeoutMs }),
|
|
372
|
+
});
|
|
373
|
+
const columns = result.fields.map((f) => ({ name: f.name, dataTypeId: f.dataTypeID }));
|
|
374
|
+
const fetched = result.rows;
|
|
375
|
+
const hasMore = fetched.length > pageLimit;
|
|
376
|
+
const rows = hasMore ? fetched.slice(0, pageLimit) : fetched;
|
|
377
|
+
return formatOutcome({
|
|
378
|
+
kind: 'rows',
|
|
379
|
+
columns,
|
|
380
|
+
rows,
|
|
381
|
+
rowCount: rows.length,
|
|
382
|
+
truncated: hasMore,
|
|
383
|
+
...(hasMore
|
|
384
|
+
? {
|
|
385
|
+
rowCapLabel: `row cap ${pageLimit}; this tool hands out no cursor (offset paging over a re-run query can ` +
|
|
386
|
+
'overlap or skip rows) — add ORDER BY plus LIMIT/OFFSET to the statement to read the rest',
|
|
387
|
+
}
|
|
388
|
+
: {}),
|
|
389
|
+
}, { limits });
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
return toToolResult(err);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
//# sourceMappingURL=execute_sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute_sql.js","sourceRoot":"","sources":["../../src/tools/execute_sql.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadProfiles } from '../config/profiles.js';
|
|
3
|
+
import { planStatements } from '../core/classifier.js';
|
|
4
|
+
import { envPrefix, PgMcpError, toToolResult } from '../core/errors.js';
|
|
5
|
+
import { formatOutcome } from '../core/format.js';
|
|
6
|
+
import { withTx } from '../db/session.js';
|
|
7
|
+
import { defineTool, profileParam } from './_shared.js';
|
|
8
|
+
/**
|
|
9
|
+
* `postgres_explain` — EXPLAIN a statement, with format/flag options.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `postgres_select`, this takes a raw `statement: string` — a
|
|
12
|
+
* deliberate, scoped exception to "no arbitrary SQL in Phase 1": EXPLAIN
|
|
13
|
+
* itself never executes anything (ANALYZE is the one thing that does), and
|
|
14
|
+
* the phase brief specifically requires explaining an INSERT/UPDATE/DELETE
|
|
15
|
+
* under ANALYZE, which a structured shape like `postgres_select`'s cannot
|
|
16
|
+
* express.
|
|
17
|
+
*
|
|
18
|
+
* **Multi-statement rejection, the same two layers every other tool has.**
|
|
19
|
+
* `statement` goes through `core/classifier.ts#planStatements` (one statement,
|
|
20
|
+
* `;`-lists refused) and the EXPLAIN is issued with an explicit
|
|
21
|
+
* `queryMode: 'extended'`. Both are load-bearing. The original version of this
|
|
22
|
+
* comment claimed "a (possibly empty) params array forces the extended query
|
|
23
|
+
* protocol" — it does not: `pg@8.22`'s `Query.requiresPreparation()` ends
|
|
24
|
+
* `return this.values.length > 0`, so on the common no-params call this ran in
|
|
25
|
+
* *simple* mode, where `EXPLAIN … SELECT 1; DROP TABLE t; COMMIT` executed all
|
|
26
|
+
* three commands — the trailing `COMMIT` escaping both `rollbackAlways` and the
|
|
27
|
+
* savepoint fence — on a profile holding neither `write` nor `ddl`, and came
|
|
28
|
+
* back as a `TypeError` about `result.fields` so the damage was invisible in
|
|
29
|
+
* the response. Measured, not assumed. Do not remove either layer.
|
|
30
|
+
*
|
|
31
|
+
* `analyze: true` actually executes `statement` (design.md risk 3), so it is
|
|
32
|
+
* gated twice:
|
|
33
|
+
* 1. the profile's `explainAnalyze` capability must be on (env var named in
|
|
34
|
+
* the `POLICY_ANALYZE_DENIED` hint) — checked before anything runs;
|
|
35
|
+
* 2. even when permitted, the whole EXPLAIN runs inside
|
|
36
|
+
* `withTx({ rollbackAlways: !confirm })` — a real transaction that is
|
|
37
|
+
* rolled back unless the caller passes `confirm: true`, fenced by a
|
|
38
|
+
* SAVEPOINT (`db/session.ts#fenceDryRun`) that raises
|
|
39
|
+
* `GUARD_DRY_RUN_ESCAPED` if `statement` ends the transaction itself.
|
|
40
|
+
*
|
|
41
|
+
* Plain EXPLAIN (no ANALYZE) never executes `statement`, so it just runs
|
|
42
|
+
* inside `withTx({ readOnly: true })` like every other Phase 1 tool.
|
|
43
|
+
*/
|
|
44
|
+
const FORMATS = ['TEXT', 'JSON', 'XML', 'YAML'];
|
|
45
|
+
const inputSchema = z.object({
|
|
46
|
+
...profileParam,
|
|
47
|
+
statement: z.string().min(1).describe('The SQL statement to explain — SELECT, INSERT, UPDATE, DELETE, etc. Exactly one statement.'),
|
|
48
|
+
params: z.array(z.unknown()).optional().describe('Bound parameters ($1, $2, ...) referenced by statement.'),
|
|
49
|
+
analyze: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('EXPLAIN ANALYZE actually executes statement. Requires the profile\'s explainAnalyze capability.'),
|
|
53
|
+
format: z.enum(FORMATS).optional().describe('Plan output format. Defaults to TEXT.'),
|
|
54
|
+
costs: z.boolean().optional(),
|
|
55
|
+
buffers: z.boolean().optional(),
|
|
56
|
+
verbose: z.boolean().optional(),
|
|
57
|
+
settings: z.boolean().optional(),
|
|
58
|
+
confirm: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Only meaningful with analyze:true. Without it the transaction is always rolled back (dry run) even though ' +
|
|
62
|
+
'ANALYZE really executed statement. With it, the transaction commits normally.'),
|
|
63
|
+
});
|
|
64
|
+
function explainOptionsClause(args) {
|
|
65
|
+
const parts = [`ANALYZE ${args.analyze === true}`, `FORMAT ${args.format ?? 'TEXT'}`];
|
|
66
|
+
if (args.costs !== undefined)
|
|
67
|
+
parts.push(`COSTS ${args.costs}`);
|
|
68
|
+
if (args.buffers !== undefined)
|
|
69
|
+
parts.push(`BUFFERS ${args.buffers}`);
|
|
70
|
+
if (args.verbose !== undefined)
|
|
71
|
+
parts.push(`VERBOSE ${args.verbose}`);
|
|
72
|
+
if (args.settings !== undefined)
|
|
73
|
+
parts.push(`SETTINGS ${args.settings}`);
|
|
74
|
+
return parts.join(', ');
|
|
75
|
+
}
|
|
76
|
+
export const explainTool = defineTool('postgres_explain', {
|
|
77
|
+
title: 'Explain a PostgreSQL statement',
|
|
78
|
+
description: 'EXPLAIN a statement (any SQL — SELECT/INSERT/UPDATE/DELETE/etc.) with format (TEXT/JSON/XML/YAML) and flag ' +
|
|
79
|
+
'(costs, buffers, verbose, settings) options. analyze:true actually executes the statement to get real ' +
|
|
80
|
+
'timings — requires the profile\'s explainAnalyze capability, and is rolled back unless confirm:true is also ' +
|
|
81
|
+
'passed. Without analyze, nothing is ever executed.',
|
|
82
|
+
inputSchema,
|
|
83
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
84
|
+
}, async (args) => {
|
|
85
|
+
try {
|
|
86
|
+
const registry = loadProfiles();
|
|
87
|
+
const profile = registry.get(args.profile);
|
|
88
|
+
const analyze = args.analyze === true;
|
|
89
|
+
if (analyze && !profile.capabilities.explainAnalyze) {
|
|
90
|
+
throw new PgMcpError({
|
|
91
|
+
code: 'POLICY_ANALYZE_DENIED',
|
|
92
|
+
message: `profile "${profile.alias}" does not allow EXPLAIN ANALYZE`,
|
|
93
|
+
hint: `set ${envPrefix(profile.alias)}_ALLOW_EXPLAIN_ANALYZE=true for this profile and re-run`,
|
|
94
|
+
details: { profile: profile.alias },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const params = args.params ?? [];
|
|
98
|
+
// Exactly one statement, refused before any round trip. `allowMultiple` is
|
|
99
|
+
// not offered here at all: an EXPLAIN of a `;`-list is meaningless, and it
|
|
100
|
+
// was the way a `COMMIT` got past the dry-run fence.
|
|
101
|
+
const [statement = ''] = planStatements(args.statement, {
|
|
102
|
+
allowMultiple: false,
|
|
103
|
+
hasParams: params.length > 0,
|
|
104
|
+
});
|
|
105
|
+
const sql = `EXPLAIN (${explainOptionsClause(args)}) ${statement}`;
|
|
106
|
+
const format = args.format ?? 'TEXT';
|
|
107
|
+
const plan = await withTx(profile, async (client) => {
|
|
108
|
+
const result = await client.query({ text: sql, values: [...params], queryMode: 'extended' });
|
|
109
|
+
// `result.fields?.` and not `result.fields[0]?.`: a shape surprise
|
|
110
|
+
// (an array of results, which is what a multi-statement string used to
|
|
111
|
+
// produce here) must not surface as a TypeError blamed on the caller.
|
|
112
|
+
const col = result.fields?.[0]?.name;
|
|
113
|
+
if (col === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
if (format === 'JSON')
|
|
116
|
+
return result.rows[0]?.[col] ?? null;
|
|
117
|
+
return result.rows.map((r) => String(r[col])).join('\n');
|
|
118
|
+
}, analyze ? { rollbackAlways: args.confirm !== true, operation: 'explain' } : { readOnly: true, operation: 'explain' });
|
|
119
|
+
return formatOutcome({
|
|
120
|
+
kind: 'report',
|
|
121
|
+
data: {
|
|
122
|
+
profile: profile.alias,
|
|
123
|
+
analyze,
|
|
124
|
+
format,
|
|
125
|
+
rolledBack: analyze ? args.confirm !== true : true,
|
|
126
|
+
plan,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return toToolResult(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=explain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/tools/explain.ts"],"names":[],"mappings":"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"}
|