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.
Files changed (130) hide show
  1. package/README.md +608 -2
  2. package/dist/config/env.js +67 -0
  3. package/dist/config/env.js.map +1 -0
  4. package/dist/config/profiles.js +257 -0
  5. package/dist/config/profiles.js.map +1 -0
  6. package/dist/core/audit-context.js +21 -0
  7. package/dist/core/audit-context.js.map +1 -0
  8. package/dist/core/audit.js +91 -0
  9. package/dist/core/audit.js.map +1 -0
  10. package/dist/core/brands.js +24 -0
  11. package/dist/core/brands.js.map +1 -0
  12. package/dist/core/classifier.js +279 -0
  13. package/dist/core/classifier.js.map +1 -0
  14. package/dist/core/errors.js +197 -0
  15. package/dist/core/errors.js.map +1 -0
  16. package/dist/core/format.js +258 -0
  17. package/dist/core/format.js.map +1 -0
  18. package/dist/core/guards.js +513 -0
  19. package/dist/core/guards.js.map +1 -0
  20. package/dist/core/identifiers.js +431 -0
  21. package/dist/core/identifiers.js.map +1 -0
  22. package/dist/core/limits.js +79 -0
  23. package/dist/core/limits.js.map +1 -0
  24. package/dist/core/logger.js +34 -0
  25. package/dist/core/logger.js.map +1 -0
  26. package/dist/core/paginate.js +195 -0
  27. package/dist/core/paginate.js.map +1 -0
  28. package/dist/core/paths.js +392 -0
  29. package/dist/core/paths.js.map +1 -0
  30. package/dist/core/policy.js +98 -0
  31. package/dist/core/policy.js.map +1 -0
  32. package/dist/core/shutdown.js +10 -0
  33. package/dist/core/shutdown.js.map +1 -0
  34. package/dist/core/stdout-guard.js +25 -0
  35. package/dist/core/stdout-guard.js.map +1 -0
  36. package/dist/db/catalog.js +1006 -0
  37. package/dist/db/catalog.js.map +1 -0
  38. package/dist/db/pool.js +158 -0
  39. package/dist/db/pool.js.map +1 -0
  40. package/dist/db/session.js +258 -0
  41. package/dist/db/session.js.map +1 -0
  42. package/dist/db/types.js +2 -0
  43. package/dist/db/types.js.map +1 -0
  44. package/dist/index.js +91 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/resources/index.js +88 -0
  47. package/dist/resources/index.js.map +1 -0
  48. package/dist/runtime/binaries.js +356 -0
  49. package/dist/runtime/binaries.js.map +1 -0
  50. package/dist/runtime/env-file.js +62 -0
  51. package/dist/runtime/env-file.js.map +1 -0
  52. package/dist/runtime/environment.js +34 -0
  53. package/dist/runtime/environment.js.map +1 -0
  54. package/dist/server.js +106 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/snapshot/capture.js +189 -0
  57. package/dist/snapshot/capture.js.map +1 -0
  58. package/dist/snapshot/diff.js +254 -0
  59. package/dist/snapshot/diff.js.map +1 -0
  60. package/dist/snapshot/migrate.js +1596 -0
  61. package/dist/snapshot/migrate.js.map +1 -0
  62. package/dist/snapshot/model.js +80 -0
  63. package/dist/snapshot/model.js.map +1 -0
  64. package/dist/tools/_archive_meta.js +138 -0
  65. package/dist/tools/_archive_meta.js.map +1 -0
  66. package/dist/tools/_batch_shared.js +73 -0
  67. package/dist/tools/_batch_shared.js.map +1 -0
  68. package/dist/tools/_export_format.js +107 -0
  69. package/dist/tools/_export_format.js.map +1 -0
  70. package/dist/tools/_object_kinds.js +97 -0
  71. package/dist/tools/_object_kinds.js.map +1 -0
  72. package/dist/tools/_relation_detail.js +55 -0
  73. package/dist/tools/_relation_detail.js.map +1 -0
  74. package/dist/tools/_shared.js +17 -0
  75. package/dist/tools/_shared.js.map +1 -0
  76. package/dist/tools/_snapshot_io.js +238 -0
  77. package/dist/tools/_snapshot_io.js.map +1 -0
  78. package/dist/tools/admin/_shared.js +18 -0
  79. package/dist/tools/admin/_shared.js.map +1 -0
  80. package/dist/tools/admin/extensions.js +80 -0
  81. package/dist/tools/admin/extensions.js.map +1 -0
  82. package/dist/tools/admin/grants.js +207 -0
  83. package/dist/tools/admin/grants.js.map +1 -0
  84. package/dist/tools/admin/index.js +138 -0
  85. package/dist/tools/admin/index.js.map +1 -0
  86. package/dist/tools/admin/maintenance.js +106 -0
  87. package/dist/tools/admin/maintenance.js.map +1 -0
  88. package/dist/tools/admin/roles.js +144 -0
  89. package/dist/tools/admin/roles.js.map +1 -0
  90. package/dist/tools/admin/sessions.js +104 -0
  91. package/dist/tools/admin/sessions.js.map +1 -0
  92. package/dist/tools/admin/stats.js +63 -0
  93. package/dist/tools/admin/stats.js.map +1 -0
  94. package/dist/tools/backup.js +288 -0
  95. package/dist/tools/backup.js.map +1 -0
  96. package/dist/tools/connection_status.js +138 -0
  97. package/dist/tools/connection_status.js.map +1 -0
  98. package/dist/tools/copy.js +355 -0
  99. package/dist/tools/copy.js.map +1 -0
  100. package/dist/tools/delete_batch.js +73 -0
  101. package/dist/tools/delete_batch.js.map +1 -0
  102. package/dist/tools/describe_object.js +87 -0
  103. package/dist/tools/describe_object.js.map +1 -0
  104. package/dist/tools/execute_sql.js +395 -0
  105. package/dist/tools/execute_sql.js.map +1 -0
  106. package/dist/tools/explain.js +134 -0
  107. package/dist/tools/explain.js.map +1 -0
  108. package/dist/tools/export.js +215 -0
  109. package/dist/tools/export.js.map +1 -0
  110. package/dist/tools/get_schema.js +148 -0
  111. package/dist/tools/get_schema.js.map +1 -0
  112. package/dist/tools/index.js +40 -0
  113. package/dist/tools/index.js.map +1 -0
  114. package/dist/tools/insert_batch.js +212 -0
  115. package/dist/tools/insert_batch.js.map +1 -0
  116. package/dist/tools/list_objects.js +160 -0
  117. package/dist/tools/list_objects.js.map +1 -0
  118. package/dist/tools/prepare_migration.js +112 -0
  119. package/dist/tools/prepare_migration.js.map +1 -0
  120. package/dist/tools/restore.js +301 -0
  121. package/dist/tools/restore.js.map +1 -0
  122. package/dist/tools/schema_diff.js +79 -0
  123. package/dist/tools/schema_diff.js.map +1 -0
  124. package/dist/tools/schema_snapshot.js +57 -0
  125. package/dist/tools/schema_snapshot.js.map +1 -0
  126. package/dist/tools/select.js +162 -0
  127. package/dist/tools/select.js.map +1 -0
  128. package/dist/tools/update_batch.js +154 -0
  129. package/dist/tools/update_batch.js.map +1 -0
  130. package/package.json +47 -5
@@ -0,0 +1,301 @@
1
+ import process from 'node:process';
2
+ import { z } from 'zod';
3
+ import { readInt } from '../config/env.js';
4
+ import { loadProfiles } from '../config/profiles.js';
5
+ import { PgMcpError, toToolResult } from '../core/errors.js';
6
+ import { formatOutcome } from '../core/format.js';
7
+ import { resolveWorkspaceFileForRead } from '../core/paths.js';
8
+ import { capabilityEnvVar, requireCapability } from '../core/policy.js';
9
+ import { assertArchiveReadable, assertClientCoversServer, connectionArgs, parseMajor, requireBinary, runBinary, } from '../runtime/binaries.js';
10
+ import { readSidecar, serverVersionOf, sidecarPathFor } from './_archive_meta.js';
11
+ import { defineTool, profileParam } from './_shared.js';
12
+ /**
13
+ * `postgres_restore` — **the most destructive tool in this server.**
14
+ * `pg_restore` can drop and recreate every object in the target database, and
15
+ * with `clean: true` it does exactly that. There is no undo.
16
+ *
17
+ * ## It does NOT compose `runGuardedMutation`, and here is why
18
+ *
19
+ * Stated plainly because the alternative — a tool that *looks* guarded and is
20
+ * not — is worse than an unguarded one. Every other mutating tool in this
21
+ * server goes through `core/guards.ts#runGuardedMutation`, which previews a
22
+ * change by running it inside a transaction this server opened and then rolling
23
+ * back, and confirms it by re-running it and comparing a row count and a key
24
+ * checksum *inside the committing transaction*. Not one part of that is
25
+ * available here:
26
+ *
27
+ * - **There is no transaction we control.** `pg_restore` is a separate process
28
+ * that opens its own connections. `withTx`'s `BEGIN`/`ROLLBACK` are on a
29
+ * different backend entirely and could not roll back a single thing the child
30
+ * did. `pg_restore --single-transaction` is the child's own transaction,
31
+ * committed by the child before it exits — we cannot inspect it, let alone
32
+ * abort it.
33
+ * - **There is no row count.** `pg_restore` reports nothing comparable; a
34
+ * restore is a stream of DDL and COPY, not one statement with a result.
35
+ * - **There is no `sqlHash`.** The unit of work is an archive file, not SQL we
36
+ * authored, so there is no normalized statement to hash and nothing for
37
+ * `expect` to pin.
38
+ *
39
+ * So: same shape as `tools/admin/maintenance.ts`'s VACUUM — outside the guard,
40
+ * because the guard is *not possible* here, not because it was inconvenient.
41
+ * What replaces it, and what the tool actually enforces:
42
+ *
43
+ * 1. **`mode: "inspect"` is the dry run.** `pg_restore --list` reads the
44
+ * archive's table of contents and touches no database at all — it is not a
45
+ * weakened preview, it is a complete and truthful account of what the
46
+ * archive contains. It is also the default: an omitted `mode` inspects.
47
+ * 2. **`mode: "restore"` requires `confirm: true`**, in every environment
48
+ * class, with no `expect` token — because there is no honest token to
49
+ * issue. A restore without `confirm` changes nothing and returns the
50
+ * inspection instead.
51
+ * 3. **The `restore` capability** (`PG_<ALIAS>_ALLOW_RESTORE`) is off by
52
+ * default, like every other write capability.
53
+ *
54
+ * ## The archive is untrusted operator input
55
+ *
56
+ * It is a file path handed in by whoever is driving the agent, so it goes
57
+ * through `core/paths.ts#resolveWorkspaceFileForRead` — the same one policy
58
+ * `postgres_copy`'s import direction and the snapshot reader use: allow-list
59
+ * containment, a second resolve after `realpath` so a symlink cannot lead out
60
+ * of the workspace, open-once plus an `ino` check so the file cannot be swapped
61
+ * after it was approved, and a size cap (`PG_MCP_MAX_ARCHIVE_BYTES`) enforced
62
+ * from the open handle's `fstat`.
63
+ *
64
+ * *Documented residual gap:* `pg_restore` takes a **path**, not a file
65
+ * descriptor, so it re-opens the file by name. This module holds its own handle
66
+ * open for the whole restore, which keeps the approved inode alive, but a
67
+ * rename of the path between the check and the child's open would still send
68
+ * `pg_restore` at a different file. Closing that completely would mean piping
69
+ * the archive to the child's stdin, which `pg_restore` supports only for
70
+ * single-file custom archives and only without parallelism. The exposure is an
71
+ * operator racing their own workspace; the containment check is what stops a
72
+ * *planted* symlink, and that one is closed.
73
+ *
74
+ * ## Forward-only archives (design.md risk 10)
75
+ *
76
+ * `pg_dump 17` dumps a 16 server fine, but a custom-format archive written by
77
+ * 17 can never be read by `pg_restore 16`. The archive major comes from the
78
+ * sidecar `postgres_backup` wrote (checked before anything is spawned) or, for
79
+ * a hand-made archive, from the `Dumped by pg_dump version:` line in
80
+ * `pg_restore --list`'s own header. Either way the refusal names the image tag
81
+ * that *can* read it, instead of surfacing `unsupported version (1.16) in file
82
+ * header`.
83
+ */
84
+ /**
85
+ * 20 GiB. Unlike `PG_MCP_MAX_IMPORT_BYTES` this is not a throughput bound —
86
+ * nothing here reads the archive into this process at all, `pg_restore` does —
87
+ * it is a sanity bound on what an operator can point this tool at, and archives
88
+ * are legitimately much larger than a CSV import.
89
+ */
90
+ const DEFAULT_MAX_ARCHIVE_BYTES = 20 * 1024 * 1024 * 1024;
91
+ function maxArchiveBytes() {
92
+ return readInt(process.env, 'PG_MCP_MAX_ARCHIVE_BYTES', { default: DEFAULT_MAX_ARCHIVE_BYTES, min: 1 });
93
+ }
94
+ const inputSchema = z.object({
95
+ ...profileParam,
96
+ path: z
97
+ .string()
98
+ .min(1)
99
+ .describe('Archive written by pg_dump in custom or tar format. Relative paths resolve against the profile\'s first allowed directory; absolute paths must still land inside one.'),
100
+ mode: z
101
+ .enum(['inspect', 'restore'])
102
+ .optional()
103
+ .describe('inspect (default) — list the archive contents, touching no database. restore — apply it. THIS OVERWRITES DATA.'),
104
+ confirm: z
105
+ .boolean()
106
+ .optional()
107
+ .describe('Required (true) for mode:"restore". There is no dry-run/expect token here: pg_restore is a separate process with no transaction this server can roll back. Use mode:"inspect" to see what the archive holds first.'),
108
+ clean: z
109
+ .boolean()
110
+ .optional()
111
+ .describe('pg_restore --clean: DROP every object before recreating it. THE MOST DESTRUCTIVE OPTION IN THIS SERVER — existing data in the target objects is gone.'),
112
+ noOwner: z.boolean().optional().describe('Ignore ownership commands in the archive; objects end up owned by the connecting role.'),
113
+ singleTransaction: z
114
+ .boolean()
115
+ .optional()
116
+ .describe('pg_restore --single-transaction: all-or-nothing inside the CHILD process\'s own transaction. It reduces the blast radius of a partial failure; it is not a dry run and this server cannot roll it back.'),
117
+ schemas: z.array(z.string().min(1)).optional().describe('Restore only these schemas from the archive (pg_restore --schema).'),
118
+ tables: z.array(z.string().min(1)).optional().describe('Restore only these tables from the archive (pg_restore --table).'),
119
+ timeoutMs: z
120
+ .number()
121
+ .int()
122
+ .min(1)
123
+ .optional()
124
+ .describe('Wall-clock ceiling for the pg_restore child, clamped to PG_<ALIAS>_BACKUP_TIMEOUT_MAX_MS (1 h by default).'),
125
+ });
126
+ /** `; Dumped by pg_dump version: 17.5` — the archive's own header, the fallback when there is no sidecar. */
127
+ export function archiveMajorFromListing(listing) {
128
+ const line = /Dumped by pg_dump version:\s*(\S+)/.exec(listing);
129
+ return line?.[1] === undefined ? undefined : parseMajor(line[1]);
130
+ }
131
+ /**
132
+ * `pg_restore`'s own refusal when the archive is too new for it. Recognized so
133
+ * the operator gets the image-tag remedy even for a hand-made archive with no
134
+ * sidecar, where the major could not be read up front — the failure is the
135
+ * only place the truth surfaces.
136
+ */
137
+ function isUnsupportedVersion(err) {
138
+ return err instanceof PgMcpError && /unsupported version|not supported by this version/i.test(err.message);
139
+ }
140
+ function tooNewForLocalRestore(archivePath, restore, cause) {
141
+ return new PgMcpError({
142
+ code: 'BINARY_MISSING',
143
+ message: `"${archivePath}" was written by a newer pg_dump than the local pg_restore (${restore.major}), which cannot read it`,
144
+ hint: 'archives are forward-only: run the pg-admin-mcp image tag matching the pg_dump major that wrote this archive ' +
145
+ '(pg-admin-mcp:pg<N>), or point PG_MCP_PG_BIN_DIR at that postgresql-client version',
146
+ details: { archivePath, restoreMajor: restore.major },
147
+ cause,
148
+ });
149
+ }
150
+ function listArgs(archivePath) {
151
+ return ['--list', archivePath];
152
+ }
153
+ export function restoreArgs(profile, args, archivePath) {
154
+ return [
155
+ ...connectionArgs(profile),
156
+ `--dbname=${profile.database}`,
157
+ ...(args.clean === true ? ['--clean', '--if-exists'] : []),
158
+ ...(args.noOwner === true ? ['--no-owner'] : []),
159
+ ...(args.singleTransaction === true ? ['--single-transaction'] : []),
160
+ ...(args.schemas ?? []).map((s) => `--schema=${s}`),
161
+ ...(args.tables ?? []).map((t) => `--table=${t}`),
162
+ // Last, and always an absolute resolved path, so it can never be read as a flag.
163
+ archivePath,
164
+ ];
165
+ }
166
+ /** The archive's TOC, as `pg_restore --list` prints it. Touches no database. */
167
+ async function inspect(archivePath, restore, timeoutMs) {
168
+ try {
169
+ const { stdout } = await runBinary('pg_restore', restore.command, listArgs(archivePath), {
170
+ timeoutMs,
171
+ operation: 'restore inspect',
172
+ stdoutMaxBytes: 4 * 1024 * 1024,
173
+ });
174
+ return stdout;
175
+ }
176
+ catch (err) {
177
+ if (isUnsupportedVersion(err))
178
+ throw tooNewForLocalRestore(archivePath, restore, err);
179
+ throw err;
180
+ }
181
+ }
182
+ function confirmRequired(profile, archivePath, clean) {
183
+ return new PgMcpError({
184
+ code: 'GUARD_CONFIRM_REQUIRED',
185
+ message: `restoring "${archivePath}" into profile "${profile.alias}" (${profile.database} on ${profile.host}) would ` +
186
+ `${clean ? 'DROP and recreate every object in the archive' : 'overwrite the objects in the archive'}, and nothing was changed`,
187
+ hint: 'run this again with mode:"inspect" to see exactly what the archive contains, then re-run with confirm:true to ' +
188
+ 'apply it. There is no expect token for a restore: pg_restore runs in a separate process with no transaction ' +
189
+ 'this server can preview or roll back',
190
+ details: { profile: profile.alias, database: profile.database, archivePath, clean },
191
+ });
192
+ }
193
+ export const restoreTool = defineTool('postgres_restore', {
194
+ title: 'Inspect or restore a pg_dump archive (DESTRUCTIVE)',
195
+ description: 'THE MOST DESTRUCTIVE TOOL IN THIS SERVER. mode:"restore" runs pg_restore against the profile\'s database and can ' +
196
+ 'overwrite or, with clean:true, DROP AND RECREATE every object in the archive. There is no undo and no rollback: ' +
197
+ 'pg_restore is a separate process, so this tool is NOT wrapped in the dry-run/confirm transaction guard the other ' +
198
+ 'mutating tools use — there is no transaction this server can roll back, no row count to compare and no expect ' +
199
+ 'token to issue. What it has instead: mode:"inspect" (the default) lists the archive contents with ' +
200
+ 'pg_restore --list and touches no database, and mode:"restore" refuses without an explicit confirm:true. ' +
201
+ 'Requires PG_<ALIAS>_ALLOW_RESTORE=true. The archive must be inside an allowed directory, is size-capped by ' +
202
+ 'PG_MCP_MAX_ARCHIVE_BYTES, and is refused when it was written by a NEWER pg_dump than the local pg_restore ' +
203
+ '(archives are forward-only) with the image tag that can read it. Plain-format (SQL text) dumps are not ' +
204
+ 'pg_restore archives — feed those to psql.',
205
+ inputSchema,
206
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
207
+ }, async (args) => {
208
+ try {
209
+ const profile = loadProfiles().get(args.profile);
210
+ // Even inspect needs the capability: reading an archive's table of
211
+ // contents is the first step of a restore, and a profile that may never
212
+ // restore has no business being handed one.
213
+ requireCapability('restore', profile);
214
+ const cap = maxArchiveBytes();
215
+ const source = await resolveWorkspaceFileForRead(args.path, profile.allowedDirs, {
216
+ maxBytes: cap,
217
+ limitLabel: 'archive read',
218
+ limitHint: `raise PG_MCP_MAX_ARCHIVE_BYTES (currently ${cap}) and restart the server, or restore from a smaller archive`,
219
+ missingHint: 'a restore reads an existing archive — check the path, and that it is inside an allowed directory',
220
+ notFileHint: 'point path at a single-file archive (pg_dump --format=custom or tar). Directory-format archives are not supported here: ' +
221
+ 'they cannot be identity-checked as one file, so re-dump with --format=custom',
222
+ });
223
+ try {
224
+ const pgRestore = await requireBinary('pg_restore');
225
+ const timeoutMs = Math.min(args.timeoutMs ?? profile.backupTimeoutMaxMs, profile.backupTimeoutMaxMs);
226
+ // Before anything is spawned, when the sidecar is there to say so.
227
+ const sidecar = await readSidecar(source.path, profile.allowedDirs);
228
+ if (sidecar !== undefined)
229
+ assertArchiveReadable(sidecar.clientMajor, pgRestore, source.path);
230
+ const listing = await inspect(source.path, pgRestore, timeoutMs);
231
+ // Fallback for an archive with no sidecar: the TOC header carries the
232
+ // version that wrote it. Only reachable when --list itself succeeded,
233
+ // i.e. when the archive is readable but may still be too new to apply.
234
+ const listedMajor = archiveMajorFromListing(listing);
235
+ if (sidecar === undefined && listedMajor !== undefined) {
236
+ assertArchiveReadable(listedMajor, pgRestore, source.path);
237
+ }
238
+ const entries = listing.split('\n').filter((line) => line.trim() !== '' && !line.startsWith(';'));
239
+ if (args.mode !== 'restore') {
240
+ return formatOutcome({
241
+ kind: 'report',
242
+ data: {
243
+ mode: 'inspect',
244
+ path: source.path,
245
+ bytes: source.bytes,
246
+ archiveMajor: sidecar?.clientMajor ?? listedMajor ?? null,
247
+ sidecar: sidecar ?? null,
248
+ sidecarPath: sidecarPathFor(source.path),
249
+ entryCount: entries.length,
250
+ entries,
251
+ note: 'Nothing was changed — this is pg_restore --list, which reads the archive only. Re-run with ' +
252
+ 'mode:"restore" and confirm:true to apply it.',
253
+ },
254
+ });
255
+ }
256
+ if (args.confirm !== true)
257
+ throw confirmRequired(profile, source.path, args.clean === true);
258
+ // design.md risk 2, on the restoring side: a pg_restore older than the
259
+ // target server refuses to run, and the raw stderr says nothing useful.
260
+ const server = await serverVersionOf(profile, 'restore');
261
+ assertClientCoversServer(pgRestore, server.num, server.version, profile.alias);
262
+ const result = await runBinary('pg_restore', pgRestore.command, restoreArgs(profile, args, source.path), {
263
+ timeoutMs,
264
+ password: profile.password(),
265
+ operation: 'restore',
266
+ });
267
+ return formatOutcome({
268
+ kind: 'report',
269
+ data: {
270
+ mode: 'restore',
271
+ restored: true,
272
+ path: source.path,
273
+ database: profile.database,
274
+ host: `${profile.host}:${profile.port}`,
275
+ archiveMajor: sidecar?.clientMajor ?? listedMajor ?? null,
276
+ restoreMajor: pgRestore.major,
277
+ serverVersion: server.version,
278
+ entryCount: entries.length,
279
+ clean: args.clean === true,
280
+ singleTransaction: args.singleTransaction === true,
281
+ // pg_restore writes warnings here even on success (missing roles,
282
+ // already-existing objects under --clean --if-exists). Surfaced
283
+ // rather than swallowed: they are the only account of what did not
284
+ // apply cleanly.
285
+ warnings: result.stderr.trim() === '' ? [] : result.stderr.trim().split('\n').slice(-40),
286
+ capability: capabilityEnvVar('restore', profile.alias),
287
+ },
288
+ });
289
+ }
290
+ finally {
291
+ // Held open for the whole restore so the approved inode stays alive
292
+ // while pg_restore reads it — see the file header's note on the
293
+ // residual path-vs-fd gap.
294
+ await source.handle.close();
295
+ }
296
+ }
297
+ catch (err) {
298
+ return toToolResult(err);
299
+ }
300
+ });
301
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.js","sourceRoot":"","sources":["../../src/tools/restore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod';
2
+ import { loadProfiles } from '../config/profiles.js';
3
+ import { toToolResult } from '../core/errors.js';
4
+ import { formatOutcome } from '../core/format.js';
5
+ import { resolveLimits, truncateRows } from '../core/limits.js';
6
+ import { diffSnapshots } from '../snapshot/diff.js';
7
+ import { defineTool, profileParam } from './_shared.js';
8
+ import { resolveSnapshotSource, snapshotSourceSchema } from './_snapshot_io.js';
9
+ /**
10
+ * `postgres_schema_diff` — structural diff between two normalized schema
11
+ * snapshots (`snapshot/diff.ts#diffSnapshots`). Each side (`before`/`after`)
12
+ * is independently either a live database capture or a snapshot file written
13
+ * by `postgres_schema_snapshot` (`_snapshot_io.ts#resolveSnapshotSource`), so
14
+ * this covers all three shapes the phase asked for: two files, a file vs. a
15
+ * live database, or two live databases.
16
+ *
17
+ * A snapshot file is untrusted input — `_snapshot_io.ts#parseSnapshotFile`
18
+ * validates it against a `.strict()` zod mirror of `snapshot/model.ts` before
19
+ * it ever reaches `diffSnapshots`, and `diffSnapshots` itself refuses two
20
+ * snapshots of different `formatVersion`s (`SNAPSHOT_VERSION_MISMATCH`) rather
21
+ * than reporting every object as changed.
22
+ *
23
+ * Output is bounded the same way `postgres_get_schema` bounds its object
24
+ * list: `core/limits.ts#truncateRows` over the flat change list, byte-capped
25
+ * by the real serialized size of each entry, with an explicit count of what
26
+ * was kept vs. cut and how to narrow (`before`/`after` `schemas` scope).
27
+ */
28
+ const inputSchema = z.object({
29
+ ...profileParam,
30
+ before: snapshotSourceSchema.describe('The baseline side of the comparison.'),
31
+ after: snapshotSourceSchema.describe('The changed side of the comparison — the diff reads as "before -> after".'),
32
+ maxChanges: z
33
+ .number()
34
+ .int()
35
+ .positive()
36
+ .optional()
37
+ .describe('Caps the number of changes included before truncation. Defaults to the profile row limit.'),
38
+ });
39
+ /** An explicit, actionable truncation message: what was kept, what was cut, how to narrow. */
40
+ function truncationHint(kept, total, cutAt) {
41
+ const at = cutAt === undefined ? 'the cap' : `${cutAt.path} (${cutAt.objectType} ${cutAt.kind})`;
42
+ return `showing ${kept} of ${total} changes; truncated at ${at} — narrow before/after with a schemas scope, or raise maxChanges`;
43
+ }
44
+ export const schemaDiffTool = defineTool('postgres_schema_diff', {
45
+ title: 'Diff two PostgreSQL schema snapshots',
46
+ description: 'Structural diff between two schema snapshots. Each of before/after is independently either kind:"live" (a ' +
47
+ 'fresh capture) or kind:"file" (a file written earlier by postgres_schema_snapshot) — so this compares two ' +
48
+ 'files, a file against a live database, or two live databases. Read-only: never modifies either side. An ' +
49
+ 'oversized result is truncated with an explicit count and a hint for narrowing scope, never silently cut.',
50
+ inputSchema,
51
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
52
+ }, async (args) => {
53
+ try {
54
+ const registry = loadProfiles();
55
+ const profile = registry.get(args.profile);
56
+ const [before, after] = await Promise.all([
57
+ resolveSnapshotSource(args.before, args.profile),
58
+ resolveSnapshotSource(args.after, args.profile),
59
+ ]);
60
+ const changes = diffSnapshots(before, after);
61
+ const limits = resolveLimits(profile.maxRows, args.maxChanges === undefined ? {} : { maxRows: args.maxChanges }, process.env);
62
+ const { rows: kept, truncation } = truncateRows(changes, limits, (c) => JSON.stringify(c));
63
+ const truncated = truncation.reason !== null;
64
+ const data = {
65
+ before: { database: before.database },
66
+ after: { database: after.database },
67
+ changeCount: kept.length,
68
+ totalChanges: changes.length,
69
+ changes: kept,
70
+ truncated,
71
+ ...(truncated ? { truncationMessage: truncationHint(kept.length, changes.length, changes[kept.length]) } : {}),
72
+ };
73
+ return formatOutcome({ kind: 'report', data });
74
+ }
75
+ catch (err) {
76
+ return toToolResult(err);
77
+ }
78
+ });
79
+ //# sourceMappingURL=schema_diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema_diff.js","sourceRoot":"","sources":["../../src/tools/schema_diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { finished } from 'node:stream/promises';
2
+ import { z } from 'zod';
3
+ import { loadProfiles } from '../config/profiles.js';
4
+ import { toToolResult } from '../core/errors.js';
5
+ import { formatOutcome } from '../core/format.js';
6
+ import { renderHostPath, resolveWorkspacePath, writeFileAtomic } from '../core/paths.js';
7
+ import { captureSnapshot } from '../snapshot/capture.js';
8
+ import { defineTool, profileParam } from './_shared.js';
9
+ /**
10
+ * `postgres_schema_snapshot` — captures a normalized schema snapshot
11
+ * (`snapshot/capture.ts`) and persists it to a workspace file, so it can
12
+ * later be compared with `postgres_schema_diff` / `postgres_prepare_migration`
13
+ * without holding the source database open.
14
+ *
15
+ * Same path policy as `postgres_export`: `resolveWorkspacePath` (never a
16
+ * directory as the destination) then `writeFileAtomic` (a `.partial` file
17
+ * published only on success, refuses to clobber an existing file unless
18
+ * `overwrite: true`). The file is one `JSON.stringify` of the snapshot —
19
+ * `model.ts`'s own byte-stability guarantee means two captures of an
20
+ * unchanged database produce the identical file.
21
+ */
22
+ const inputSchema = z.object({
23
+ ...profileParam,
24
+ schemas: z
25
+ .array(z.string().min(1))
26
+ .optional()
27
+ .describe('Restrict the capture to these schemas. Omit for every schema in scope (Profile.schemas, or every non-system schema).'),
28
+ path: z.string().min(1).describe('Destination file path. Relative paths resolve against the profile\'s first allowed directory; absolute paths must still land inside an allowed directory.'),
29
+ overwrite: z.boolean().optional().describe('Required (true) to replace an existing file at path. Defaults to false.'),
30
+ });
31
+ export const schemaSnapshotTool = defineTool('postgres_schema_snapshot', {
32
+ title: 'Capture a normalized schema snapshot to a file',
33
+ description: 'Captures a normalized, byte-stable schema snapshot (tables, views, sequences, functions, enums, extensions ' +
34
+ '— see postgres_get_schema for a live equivalent) and writes it to a workspace file as JSON. Use the result ' +
35
+ 'with postgres_schema_diff or postgres_prepare_migration to compare it against another snapshot or a live ' +
36
+ 'database. Refuses to overwrite an existing file unless overwrite:true.',
37
+ inputSchema,
38
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
39
+ }, async (args) => {
40
+ try {
41
+ const registry = loadProfiles();
42
+ const profile = registry.get(args.profile);
43
+ const resolvedPath = await resolveWorkspacePath(args.path, profile.allowedDirs);
44
+ const snapshot = await captureSnapshot(profile, args.schemas === undefined ? {} : { schemas: args.schemas });
45
+ const json = JSON.stringify(snapshot);
46
+ const bytes = await writeFileAtomic(resolvedPath, async (dest) => {
47
+ dest.end(json);
48
+ await finished(dest);
49
+ }, { overwrite: args.overwrite === true });
50
+ const hostPath = renderHostPath(resolvedPath, profile.allowedDirs);
51
+ return formatOutcome({ kind: 'file', path: resolvedPath, bytes, ...(hostPath === undefined ? {} : { hostPath }) });
52
+ }
53
+ catch (err) {
54
+ return toToolResult(err);
55
+ }
56
+ });
57
+ //# sourceMappingURL=schema_snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema_snapshot.js","sourceRoot":"","sources":["../../src/tools/schema_snapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ import { z } from 'zod';
2
+ import { loadProfiles } from '../config/profiles.js';
3
+ import { toToolResult } from '../core/errors.js';
4
+ import { formatOutcome } from '../core/format.js';
5
+ import { columnIdentFromCatalog, resolveColumn, resolveRelation } from '../core/identifiers.js';
6
+ import { resolveLimits } from '../core/limits.js';
7
+ import { buildKeysetWhere, computeShapeHash, cursorValuesFromRow, decodeCursor, encodeCursor, orderByClause, withTiebreaker, } from '../core/paginate.js';
8
+ import { getColumns, getPrimaryKey } from '../db/catalog.js';
9
+ import { withTx } from '../db/session.js';
10
+ import { defineTool, profileParam } from './_shared.js';
11
+ /**
12
+ * `postgres_select` — structured, parameterized, paginated reads. Deliberately
13
+ * NOT an arbitrary-SQL escape hatch (that is `postgres_execute_sql`, Phase 2):
14
+ * every identifier (`columns`, `where[].column`, `orderBy[].column`) is
15
+ * validated against the relation's real columns via `core/identifiers.ts#resolveColumn`
16
+ * before it ever reaches SQL text, and every value is a bound `$N` parameter —
17
+ * never string-concatenated.
18
+ *
19
+ * Runs inside `withTx({ readOnly: true })`: PostgreSQL's own `READ ONLY`
20
+ * transaction (SQLSTATE 25006 -> `POLICY_READ_ONLY`) is the enforcement
21
+ * mechanism, not a lexical check — so a `where` value that calls a volatile
22
+ * writing function is rejected by PostgreSQL itself, not by this tool trying
23
+ * to parse SQL.
24
+ *
25
+ * Pagination is real SQL keyset pagination (unlike `list_objects`'s in-memory
26
+ * one): `core/paginate.ts#SortColumn`/`buildKeysetWhere`/`orderByClause` all
27
+ * resolve NULLS FIRST/LAST placement through the same `effectiveNulls`
28
+ * default, which is the entire mechanism that keeps the emitted `ORDER BY`
29
+ * and the keyset `WHERE` predicate from disagreeing on a nullable sort
30
+ * column — see that module's doc comment for the four-case breakdown.
31
+ */
32
+ const DEFAULT_PAGE_SIZE = 200;
33
+ const MAX_PAGE_SIZE = 2000;
34
+ /** Exported so `tools/export.ts`, `update_batch` and `delete_batch` reuse the exact same scalar shape instead of a second, later-diverging copy. */
35
+ export const scalarValue = z.union([z.string(), z.number(), z.boolean(), z.null()]);
36
+ /** Exported so `tools/export.ts`, `update_batch` and `delete_batch` reuse the exact same predicate shape and validation instead of a second, later-diverging copy. */
37
+ export const wherePredicate = z.discriminatedUnion('op', [
38
+ z.object({ column: z.string().min(1), op: z.literal('IS NULL') }),
39
+ z.object({ column: z.string().min(1), op: z.literal('IS NOT NULL') }),
40
+ z.object({
41
+ column: z.string().min(1),
42
+ op: z.enum(['=', '!=', '<', '<=', '>', '>=', 'LIKE', 'ILIKE']),
43
+ value: scalarValue,
44
+ }),
45
+ z.object({ column: z.string().min(1), op: z.enum(['IN', 'NOT IN']), value: z.array(scalarValue).min(1) }),
46
+ ]);
47
+ const orderByColumn = z.object({
48
+ column: z.string().min(1),
49
+ direction: z.enum(['ASC', 'DESC']).optional().describe('Defaults to ASC.'),
50
+ nulls: z.enum(['FIRST', 'LAST']).optional().describe('Defaults to the direction\'s own PostgreSQL default (ASC -> LAST, DESC -> FIRST).'),
51
+ });
52
+ const inputSchema = z.object({
53
+ ...profileParam,
54
+ schema: z.string().min(1).describe('Schema name, e.g. "app".'),
55
+ table: z.string().min(1).describe('Table/view name within the schema.'),
56
+ columns: z.array(z.string().min(1)).min(1).optional().describe('Columns to select, in this order. Omit for every column, in ordinal order.'),
57
+ where: z
58
+ .array(wherePredicate)
59
+ .optional()
60
+ .describe('Filter predicates, ANDed together. Every value is a bound parameter — never interpolated into SQL text.'),
61
+ orderBy: z
62
+ .array(orderByColumn)
63
+ .optional()
64
+ .describe('Sort columns in priority order. A unique tiebreaker (primary key, or ctid) is appended automatically.'),
65
+ limit: z.number().int().min(1).max(MAX_PAGE_SIZE).optional().describe(`Rows per page (max ${MAX_PAGE_SIZE}), further capped by the profile row limit.`),
66
+ cursor: z.string().optional().describe('Opaque pagination cursor — pass back the previous call\'s nextCursor verbatim to fetch the next page.'),
67
+ });
68
+ export const selectTool = defineTool('postgres_select', {
69
+ title: 'Select rows from a PostgreSQL table',
70
+ description: 'Structured, parameterized, paginated read of one table or view — the primary way to fetch actual row data. ' +
71
+ 'Filter with a structured `where` (=, !=, <, <=, >, >=, LIKE, ILIKE, IN, NOT IN, IS NULL, IS NOT NULL); sort ' +
72
+ 'with `orderBy` (direction and NULLS FIRST/LAST); page with `limit`/`cursor`. Every identifier is validated ' +
73
+ 'against the table\'s real columns (unknown column -> IDENTIFIER_UNKNOWN with suggestions); every value is a ' +
74
+ 'bound parameter. Runs inside a read-only transaction. Not an arbitrary-SQL tool.',
75
+ inputSchema,
76
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
77
+ }, async (args) => {
78
+ try {
79
+ const registry = loadProfiles();
80
+ const profile = registry.get(args.profile);
81
+ const rel = await resolveRelation(profile, args.schema, args.table);
82
+ const limits = resolveLimits(profile.maxRows, args.limit === undefined ? {} : { maxRows: args.limit }, process.env);
83
+ const pageLimit = Math.min(args.limit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, limits.maxRows);
84
+ const shapeHash = computeShapeHash({
85
+ profile: profile.alias,
86
+ schema: args.schema,
87
+ table: args.table,
88
+ columns: args.columns ?? null,
89
+ where: args.where ?? null,
90
+ orderBy: args.orderBy ?? null,
91
+ });
92
+ const result = await withTx(profile, async (client) => {
93
+ const columnInfos = await getColumns(client, rel);
94
+ const knownColumns = columnInfos.map((c) => c.name);
95
+ const selectColumns = args.columns === undefined
96
+ ? columnInfos.map((c) => ({ name: c.name, ident: columnIdentFromCatalog(c.name) }))
97
+ : args.columns.map((name) => ({ name, ident: resolveColumn(args.schema, args.table, name, knownColumns) }));
98
+ const primaryKey = await getPrimaryKey(client, rel);
99
+ const pkIdents = (primaryKey?.columns ?? []).map((name) => resolveColumn(args.schema, args.table, name, knownColumns));
100
+ const userSort = (args.orderBy ?? []).map((o) => ({
101
+ name: o.column,
102
+ ident: resolveColumn(args.schema, args.table, o.column, knownColumns),
103
+ direction: o.direction ?? 'ASC',
104
+ ...(o.nulls === undefined ? {} : { nulls: o.nulls }),
105
+ }));
106
+ const sortColumns = withTiebreaker(userSort, pkIdents);
107
+ let paramIndex = 1;
108
+ const whereParts = [];
109
+ const params = [];
110
+ for (const pred of args.where ?? []) {
111
+ const ident = resolveColumn(args.schema, args.table, pred.column, knownColumns);
112
+ if (pred.op === 'IS NULL' || pred.op === 'IS NOT NULL') {
113
+ whereParts.push(`${ident} ${pred.op}`);
114
+ }
115
+ else if (pred.op === 'IN' || pred.op === 'NOT IN') {
116
+ const placeholders = pred.value.map(() => `$${paramIndex++}`).join(', ');
117
+ whereParts.push(`${ident} ${pred.op} (${placeholders})`);
118
+ params.push(...pred.value);
119
+ }
120
+ else {
121
+ whereParts.push(`${ident} ${pred.op} $${paramIndex++}`);
122
+ params.push(pred.value);
123
+ }
124
+ }
125
+ const userWhere = whereParts.length === 0 ? 'TRUE' : whereParts.join(' AND ');
126
+ const cursorValues = args.cursor === undefined ? [] : decodeCursor(args.cursor, shapeHash);
127
+ const keyset = buildKeysetWhere(sortColumns, cursorValues, paramIndex);
128
+ const limitParamIndex = paramIndex + keyset.params.length;
129
+ const sql = `SELECT ${selectColumns.map((c) => c.ident).join(', ')} FROM ${rel.schema}.${rel.name} ` +
130
+ `WHERE (${userWhere}) AND (${keyset.clause}) ORDER BY ${orderByClause(sortColumns)} LIMIT $${limitParamIndex}`;
131
+ // `queryMode: 'extended'` explicitly, never "there are params so it
132
+ // must be extended": `pg` decides that from `values.length > 0`, which
133
+ // is a property of this statement today and not an invariant. Every
134
+ // identifier above is a catalog-resolved `SafeIdent`, so this is
135
+ // defence in depth rather than the fence — but it is the same one
136
+ // line every other call site in this server carries.
137
+ const queryResult = await client.query({
138
+ text: sql,
139
+ values: [...params, ...keyset.params, pageLimit + 1],
140
+ queryMode: 'extended',
141
+ });
142
+ const columns = queryResult.fields.map((f) => ({ name: f.name, dataTypeId: f.dataTypeID }));
143
+ const hasMore = queryResult.rows.length > pageLimit;
144
+ const rows = (hasMore ? queryResult.rows.slice(0, pageLimit) : queryResult.rows);
145
+ const last = rows[rows.length - 1];
146
+ const nextCursor = hasMore && last !== undefined ? encodeCursor(cursorValuesFromRow(last, sortColumns), shapeHash) : undefined;
147
+ return { columns, rows, hasMore, nextCursor };
148
+ }, { readOnly: true, operation: 'select' });
149
+ return formatOutcome({
150
+ kind: 'rows',
151
+ columns: result.columns,
152
+ rows: result.rows,
153
+ rowCount: result.rows.length,
154
+ truncated: result.hasMore,
155
+ ...(result.nextCursor === undefined ? {} : { nextCursor: result.nextCursor }),
156
+ }, { limits });
157
+ }
158
+ catch (err) {
159
+ return toToolResult(err);
160
+ }
161
+ });
162
+ //# sourceMappingURL=select.js.map