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,88 @@
1
+ import { ProtocolError, ProtocolErrorCode, ResourceTemplate } from '@modelcontextprotocol/server';
2
+ import { loadProfiles } from '../config/profiles.js';
3
+ import { fromPgError, PgMcpError } from '../core/errors.js';
4
+ import { assertSchemaAllowed, resolveRelation } from '../core/identifiers.js';
5
+ import { listSchemas } from '../db/catalog.js';
6
+ import { withSession } from '../db/session.js';
7
+ import { gatherObjects } from '../tools/_object_kinds.js';
8
+ import { buildRelationDetail } from '../tools/_relation_detail.js';
9
+ /**
10
+ * Three read-only resources over the same `db/catalog.ts` listings the tools
11
+ * already use — no new catalog SQL.
12
+ *
13
+ * **Resources throw, tools return.** A tool handler's throw is swallowed by
14
+ * the SDK's `tools/call` seam and rewritten to `{ isError: true }` (see
15
+ * `core/errors.ts`'s file header and `toToolResult` — that is why every tool
16
+ * ends in `catch (err) { return toToolResult(err); }`). A resource handler
17
+ * has no such seam: `resources/read` propagates a thrown `ProtocolError`
18
+ * straight onto the wire as a real JSON-RPC error response. So the error path
19
+ * here is the exact opposite of a tool's — `throwResourceError` always
20
+ * throws, it never returns a result — and it must not be "fixed" to look like
21
+ * `toToolResult`.
22
+ */
23
+ function throwResourceError(err) {
24
+ const pgErr = err instanceof PgMcpError ? err : fromPgError(err);
25
+ const code = pgErr.code === 'PROFILE_UNKNOWN' || pgErr.code === 'IDENTIFIER_UNKNOWN' ? ProtocolErrorCode.InvalidParams : ProtocolErrorCode.InternalError;
26
+ throw new ProtocolError(code, pgErr.toString(), { code: pgErr.code, hint: pgErr.hint, details: pgErr.details });
27
+ }
28
+ /** A URI-template variable is a `string | string[]` per the SDK; ours are always single path segments. Malformed/missing -> InvalidParams. */
29
+ export function requireVariable(variables, name) {
30
+ const value = variables[name];
31
+ if (typeof value !== 'string' || value.length === 0) {
32
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `resource URI is missing a "${name}" segment`);
33
+ }
34
+ return value;
35
+ }
36
+ function jsonContents(uri, data) {
37
+ return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: 'application/json' }] };
38
+ }
39
+ /** `postgres://<profile>/schemas` — every schema in scope for the profile. */
40
+ export async function readSchemasResource(uri, variables) {
41
+ try {
42
+ const profileAlias = requireVariable(variables, 'profile');
43
+ const registry = loadProfiles();
44
+ const profile = registry.get(profileAlias);
45
+ const schemas = await withSession(profile, (client) => listSchemas(client, profile.schemas), { operation: 'resource:schemas' });
46
+ return jsonContents(uri, { profile: profile.alias, schemas });
47
+ }
48
+ catch (err) {
49
+ return throwResourceError(err);
50
+ }
51
+ }
52
+ /** `postgres://<profile>/schema/<schema>` — every object in one schema. */
53
+ export async function readSchemaResource(uri, variables) {
54
+ try {
55
+ const profileAlias = requireVariable(variables, 'profile');
56
+ const schema = requireVariable(variables, 'schema');
57
+ const registry = loadProfiles();
58
+ const profile = registry.get(profileAlias);
59
+ assertSchemaAllowed(profile, schema);
60
+ const objects = await withSession(profile, (client) => gatherObjects(client, [schema]), { operation: 'resource:schema' });
61
+ return jsonContents(uri, { profile: profile.alias, schema, objects });
62
+ }
63
+ catch (err) {
64
+ return throwResourceError(err);
65
+ }
66
+ }
67
+ /** `postgres://<profile>/table/<schema>/<table>` — full detail for one table-like relation. */
68
+ export async function readTableResource(uri, variables) {
69
+ try {
70
+ const profileAlias = requireVariable(variables, 'profile');
71
+ const schema = requireVariable(variables, 'schema');
72
+ const table = requireVariable(variables, 'table');
73
+ const registry = loadProfiles();
74
+ const profile = registry.get(profileAlias);
75
+ const rel = await resolveRelation(profile, schema, table);
76
+ const detail = await withSession(profile, (client) => buildRelationDetail(client, rel), { operation: 'resource:table' });
77
+ return jsonContents(uri, { profile: profile.alias, schema, table, kind: rel.kind, detail });
78
+ }
79
+ catch (err) {
80
+ return throwResourceError(err);
81
+ }
82
+ }
83
+ export function registerResources(server) {
84
+ server.registerResource('schemas', new ResourceTemplate('postgres://{profile}/schemas', { list: undefined }), { title: 'Schemas in scope for a profile', mimeType: 'application/json' }, readSchemasResource);
85
+ server.registerResource('schema', new ResourceTemplate('postgres://{profile}/schema/{schema}', { list: undefined }), { title: 'Objects in one schema', mimeType: 'application/json' }, readSchemaResource);
86
+ server.registerResource('table', new ResourceTemplate('postgres://{profile}/table/{schema}/{table}', { list: undefined }), { title: 'Full detail for one table-like relation', mimeType: 'application/json' }, readTableResource);
87
+ }
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,356 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { PgMcpError } from '../core/errors.js';
5
+ import { logger } from '../core/logger.js';
6
+ import { onShutdown } from '../core/shutdown.js';
7
+ /**
8
+ * Locating, versioning and running the PostgreSQL client binaries —
9
+ * `pg_dump`, `pg_restore`, `psql`. The only module in this project that
10
+ * spawns a child process.
11
+ *
12
+ * **Lazy, never at startup.** The server must boot and serve the other 16
13
+ * tools on a machine where no client binary exists at all — `pnpm dev` on a
14
+ * laptop, a container built without the PGDG packages. Nothing here runs until
15
+ * a tool actually needs a binary; {@link requireBinary} is what fails, at the
16
+ * tool boundary, with `BINARY_MISSING` naming the binary it could not find and
17
+ * the image that ships it. Resolution is cached per resolved command path, so
18
+ * the `--version` probe costs one spawn per process, not one per call.
19
+ *
20
+ * **Spawn discipline — argv arrays, never a shell.** Every child is spawned
21
+ * with an argv array and `shell: false` (Node's default), so nothing in an
22
+ * operator-supplied path, schema name or table pattern can be a shell
23
+ * metacharacter. Every option is passed in `--opt=value` form rather than as
24
+ * two argv entries, so a value that begins with `-` is a value, not a flag.
25
+ *
26
+ * **The password goes on the child's `env` as `PGPASSWORD`, never in argv.**
27
+ * argv is world-readable through `ps` on every platform this server runs on;
28
+ * `/proc/<pid>/environ` is not (it is readable only by the process owner and
29
+ * root). `Profile.password()` is a closure — the secret is never an enumerable
30
+ * property of the profile — and this module is the only other caller of it
31
+ * besides `db/pool.ts`. {@link buildSpawnEnv} is the one place that reads it,
32
+ * and `test/unit/binaries.test.ts` asserts the sentinel appears in no argv
33
+ * entry of any command this module builds.
34
+ *
35
+ * **Nothing may reach stdout.** stdout is the MCP transport. A child is always
36
+ * spawned with `stdio: ['ignore', 'pipe', 'pipe']` — never `'inherit'`, which
37
+ * would splice `pg_dump`'s output straight into the JSON-RPC frame stream.
38
+ * `pg_dump` writes its archive through its own `--file=`, and the only stdout
39
+ * this module ever reads is `pg_restore --list`'s table of contents, captured
40
+ * into a capped buffer.
41
+ *
42
+ * **Child hygiene.** Every child gets a timeout and is killed when it fires; a
43
+ * `pg_dump` left running holds a server connection open. Live children are
44
+ * tracked and killed by a `core/shutdown.ts#onShutdown` hook, so a container
45
+ * stop does not orphan one.
46
+ */
47
+ export const PG_BINARIES = ['pg_dump', 'pg_restore', 'psql'];
48
+ /** The image tag suggestion every `BINARY_MISSING` hint ends with. */
49
+ const IMAGE_HINT = 'the published image ships pg_dump/pg_restore/psql — run pg-admin-mcp:pg<major> (e.g. pg-admin-mcp:pg17), or install ' +
50
+ 'postgresql-client and point PG_MCP_PG_BIN_DIR at the directory containing them';
51
+ /**
52
+ * `PG_MCP_PG_BIN_DIR` first, `PATH` second — exactly the design.md contract.
53
+ * A configured directory wins outright rather than being appended to `PATH`:
54
+ * an operator who set it did so to pin a specific client major, and silently
55
+ * falling back to a different one on `PATH` is how a version mismatch becomes
56
+ * a mystery.
57
+ */
58
+ export function binaryCommand(name, env = process.env) {
59
+ const dir = env['PG_MCP_PG_BIN_DIR']?.trim();
60
+ return dir === undefined || dir === '' ? name : path.join(dir, name);
61
+ }
62
+ /** `pg_dump (PostgreSQL) 17.5 (Debian …)` -> `17`. Also copes with `18beta1` and with a bare `17.5`. */
63
+ export function parseMajor(versionLine) {
64
+ const qualified = /\(PostgreSQL\)\s+(\d+)/.exec(versionLine);
65
+ const bare = qualified ?? /(\d+)/.exec(versionLine);
66
+ const major = bare?.[1];
67
+ return major === undefined ? undefined : Number.parseInt(major, 10);
68
+ }
69
+ /** `170005` -> `17`. PostgreSQL's `server_version_num` is `major * 10000 + minor` for every version this server supports. */
70
+ export function serverMajorFromNum(serverVersionNum) {
71
+ return Math.trunc(serverVersionNum / 10000);
72
+ }
73
+ function binaryMissing(name, command, cause) {
74
+ return new PgMcpError({
75
+ code: 'BINARY_MISSING',
76
+ message: `${name} was not found (tried "${command}")`,
77
+ hint: IMAGE_HINT,
78
+ details: { binary: name, command },
79
+ cause,
80
+ });
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Child process plumbing
84
+ // ---------------------------------------------------------------------------
85
+ const live = new Set();
86
+ /** SIGTERM first so `pg_dump` can close its connection; SIGKILL if it is still there after this. */
87
+ const KILL_GRACE_MS = 5000;
88
+ function killChild(child) {
89
+ if (child.exitCode !== null || child.signalCode !== null)
90
+ return;
91
+ child.kill('SIGTERM');
92
+ const hard = setTimeout(() => {
93
+ if (child.exitCode === null && child.signalCode === null)
94
+ child.kill('SIGKILL');
95
+ }, KILL_GRACE_MS);
96
+ // Never let the grace timer hold the event loop open on its own.
97
+ hard.unref();
98
+ }
99
+ // A pg_dump still running when the container stops holds a server connection.
100
+ onShutdown(() => {
101
+ for (const child of live) {
102
+ logger.warn(`killing a still-running client binary (pid ${child.pid ?? -1}) during shutdown`);
103
+ killChild(child);
104
+ }
105
+ });
106
+ const DEFAULT_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
107
+ /** Enough stderr to diagnose; a `pg_restore` that fails on every one of 10 000 objects must not build a 10 MB error message. */
108
+ const STDERR_MAX_BYTES = 64 * 1024;
109
+ /**
110
+ * The child's environment. **The only place `Profile.password()` is read in
111
+ * this module**, and it lands on `PGPASSWORD` — never on argv.
112
+ */
113
+ export function buildSpawnEnv(password, base = process.env) {
114
+ if (password === undefined)
115
+ return { ...base };
116
+ return { ...base, PGPASSWORD: password };
117
+ }
118
+ function clip(text, max) {
119
+ return text.length <= max ? text : `${text.slice(0, max)}…[truncated]`;
120
+ }
121
+ /** The last few lines of stderr — what a human actually needs, without the progress noise above it. */
122
+ function stderrTail(stderr, lines = 6) {
123
+ const kept = stderr.trimEnd().split('\n').slice(-lines).join('\n').trim();
124
+ return kept === '' ? '(no stderr output)' : kept;
125
+ }
126
+ /**
127
+ * Spawns one client binary and waits for it. Resolves with the captured
128
+ * streams on exit code 0; throws `PgMcpError` otherwise — `BINARY_MISSING` for
129
+ * an `ENOENT`, `TIMEOUT` for a child killed by the deadline, and `PG_ERROR`
130
+ * carrying the stderr tail for anything else. The raw stderr never surfaces
131
+ * unexplained: every message says which binary, which operation, and what to
132
+ * do next.
133
+ */
134
+ export async function runBinary(name, command, args, opts) {
135
+ const child = spawn(command, [...args], {
136
+ // Never 'inherit' on stdout — that is the MCP transport. stdin is closed
137
+ // so a client binary that wants to prompt for a password fails fast
138
+ // instead of hanging forever on a tty that is not there.
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ env: buildSpawnEnv(opts.password),
141
+ shell: false,
142
+ windowsHide: true,
143
+ });
144
+ live.add(child);
145
+ const stdoutMax = opts.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES;
146
+ let stdout = '';
147
+ let stderr = '';
148
+ child.stderr.setEncoding('utf8');
149
+ child.stderr.on('data', (chunk) => {
150
+ if (stderr.length < STDERR_MAX_BYTES)
151
+ stderr += chunk;
152
+ });
153
+ if (opts.sink === undefined) {
154
+ child.stdout.setEncoding('utf8');
155
+ child.stdout.on('data', (chunk) => {
156
+ if (stdout.length < stdoutMax)
157
+ stdout += chunk;
158
+ });
159
+ }
160
+ let timedOut = false;
161
+ const timer = setTimeout(() => {
162
+ timedOut = true;
163
+ killChild(child);
164
+ }, opts.timeoutMs);
165
+ try {
166
+ const exited = new Promise((resolve) => {
167
+ child.once('error', resolve);
168
+ child.once('close', (code, signal) => resolve({ code, signal }));
169
+ });
170
+ // The sink is awaited BEFORE the exit is inspected: a destination write
171
+ // that failed is the real diagnosis, and a child still filling a pipe
172
+ // nobody drains would otherwise block forever.
173
+ if (opts.sink !== undefined) {
174
+ try {
175
+ await opts.sink(child.stdout);
176
+ }
177
+ catch (err) {
178
+ killChild(child);
179
+ await exited;
180
+ throw err;
181
+ }
182
+ }
183
+ const outcome = await exited;
184
+ if (outcome instanceof Error) {
185
+ if (outcome.code === 'ENOENT')
186
+ throw binaryMissing(name, command, outcome);
187
+ throw new PgMcpError({
188
+ code: 'PG_ERROR',
189
+ message: `${name} could not be started for ${opts.operation}: ${outcome.message}`,
190
+ hint: `check that "${command}" is executable by this process — ${IMAGE_HINT}`,
191
+ details: { binary: name, command, operation: opts.operation },
192
+ cause: outcome,
193
+ });
194
+ }
195
+ if (timedOut) {
196
+ throw new PgMcpError({
197
+ code: 'TIMEOUT',
198
+ message: `${name} exceeded the ${opts.timeoutMs} ms limit for ${opts.operation} and was killed`,
199
+ hint: 'raise timeoutMs on this call (bounded by PG_<ALIAS>_EXPORT_TIMEOUT_MAX_MS), narrow the scope, or run the ' +
200
+ 'dump closer to the server — the child was terminated and left nothing behind',
201
+ details: { binary: name, operation: opts.operation, timeoutMs: opts.timeoutMs },
202
+ });
203
+ }
204
+ if (outcome.code !== 0) {
205
+ throw new PgMcpError({
206
+ code: 'PG_ERROR',
207
+ message: `${name} failed during ${opts.operation} (exit ${outcome.code ?? `signal ${outcome.signal ?? 'unknown'}`}): ` +
208
+ stderrTail(stderr),
209
+ hint: 'fix the condition reported above and re-run — this is the client binary\'s own diagnosis, not a server refusal',
210
+ details: {
211
+ binary: name,
212
+ operation: opts.operation,
213
+ exitCode: outcome.code,
214
+ signal: outcome.signal,
215
+ stderr: clip(stderr, STDERR_MAX_BYTES),
216
+ },
217
+ });
218
+ }
219
+ return { stdout, stderr };
220
+ }
221
+ finally {
222
+ clearTimeout(timer);
223
+ live.delete(child);
224
+ }
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Resolution + version cache
228
+ // ---------------------------------------------------------------------------
229
+ const cache = new Map();
230
+ /** Test seam — the cache is keyed by resolved command, so a test that changes `PG_MCP_PG_BIN_DIR` needs this. */
231
+ export function resetBinaryCache() {
232
+ cache.clear();
233
+ }
234
+ async function probe(name, command) {
235
+ const { stdout } = await runBinary(name, command, ['--version'], {
236
+ timeoutMs: 10_000,
237
+ operation: `${name} --version`,
238
+ stdoutMaxBytes: 4096,
239
+ });
240
+ const version = stdout.trim().split('\n')[0]?.trim() ?? '';
241
+ const major = parseMajor(version);
242
+ if (major === undefined) {
243
+ throw new PgMcpError({
244
+ code: 'BINARY_MISSING',
245
+ message: `"${command}" answered --version with something unparseable: ${JSON.stringify(clip(version, 200))}`,
246
+ hint: `that is not a PostgreSQL client binary — ${IMAGE_HINT}`,
247
+ details: { binary: name, command },
248
+ });
249
+ }
250
+ return { name, command, version, major };
251
+ }
252
+ /**
253
+ * Resolves one binary and its version, once per process. Everything that needs
254
+ * a client binary goes through here, so `BINARY_MISSING` is raised at the tool
255
+ * boundary and never at import time.
256
+ *
257
+ * A failed probe is **not** cached: an operator who installs the package (or
258
+ * fixes `PG_MCP_PG_BIN_DIR`) while the server is running gets a working tool on
259
+ * the next call rather than a refusal frozen in for the process lifetime.
260
+ */
261
+ export async function requireBinary(name, env = process.env) {
262
+ const command = binaryCommand(name, env);
263
+ const key = `${name}${command}`;
264
+ const cached = cache.get(key);
265
+ if (cached !== undefined)
266
+ return cached;
267
+ const pending = probe(name, command);
268
+ cache.set(key, pending);
269
+ try {
270
+ return await pending;
271
+ }
272
+ catch (err) {
273
+ cache.delete(key);
274
+ throw err;
275
+ }
276
+ }
277
+ /** {@link requireBinary} for several at once, resolved in parallel. */
278
+ export async function requireBinaries(names, env = process.env) {
279
+ const resolved = await Promise.all(names.map(async (name) => requireBinary(name, env)));
280
+ return Object.fromEntries(resolved.map((bin) => [bin.name, bin]));
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Version compatibility (design.md risk 2 and risk 10)
284
+ // ---------------------------------------------------------------------------
285
+ /**
286
+ * design.md risk 2 — **a `pg_dump` older than the server refuses to run**, with
287
+ * a message about "server version mismatch" that says nothing about what to do.
288
+ * Caught here, at the tool boundary, before the child is spawned at all.
289
+ *
290
+ * The reverse (a newer client against an older server) is supported by
291
+ * PostgreSQL and is deliberately allowed.
292
+ *
293
+ * `BINARY_MISSING` is the code, not a new one: what is missing is a *usable*
294
+ * client for this server, and inventing a code here would put one in flight
295
+ * that no other module in the taxonomy knows.
296
+ */
297
+ export function assertClientCoversServer(client, serverVersionNum, serverVersion, profileAlias) {
298
+ const serverMajor = serverMajorFromNum(serverVersionNum);
299
+ if (client.major >= serverMajor)
300
+ return;
301
+ throw new PgMcpError({
302
+ code: 'BINARY_MISSING',
303
+ message: `${client.name} ${client.major} is older than profile "${profileAlias}"'s server (PostgreSQL ${serverVersion}), ` +
304
+ 'and PostgreSQL client binaries refuse to run against a newer server',
305
+ hint: `run the pg-admin-mcp:pg${serverMajor} image tag (or point PG_MCP_PG_BIN_DIR at a postgresql-client-${serverMajor} installation) and re-run`,
306
+ details: {
307
+ binary: client.name,
308
+ clientMajor: client.major,
309
+ serverMajor,
310
+ serverVersion,
311
+ profile: profileAlias,
312
+ },
313
+ });
314
+ }
315
+ /**
316
+ * design.md risk 10 — **archive format is forward-only.** `pg_dump 17` dumps a
317
+ * 16 server fine, but a custom-format archive produced by 17 cannot be read by
318
+ * `pg_restore 16`: the archive header carries a format version the older
319
+ * program does not know, and its own error ("unsupported version (1.16) in
320
+ * file header") names nothing an operator can act on.
321
+ */
322
+ export function assertArchiveReadable(archiveMajor, restore, archivePath) {
323
+ if (archiveMajor <= restore.major)
324
+ return;
325
+ throw new PgMcpError({
326
+ code: 'BINARY_MISSING',
327
+ message: `"${archivePath}" was produced by pg_dump ${archiveMajor}, and the local pg_restore is ${restore.major} — ` +
328
+ 'an archive can never be restored by an older pg_restore than the pg_dump that wrote it',
329
+ hint: `run the pg-admin-mcp:pg${archiveMajor} image tag (or point PG_MCP_PG_BIN_DIR at a postgresql-client-${archiveMajor} installation) and re-run`,
330
+ details: { archivePath, archiveMajor, restoreMajor: restore.major },
331
+ });
332
+ }
333
+ // ---------------------------------------------------------------------------
334
+ // Connection arguments
335
+ // ---------------------------------------------------------------------------
336
+ /**
337
+ * The connection half of every client-binary argv.
338
+ *
339
+ * `--opt=value` form throughout, so a database or user name beginning with `-`
340
+ * is a value and not a flag. `--no-password` guarantees the child never blocks
341
+ * on a password prompt — stdin is closed anyway, but a client binary that
342
+ * decides to prompt would otherwise fail with a confusing EOF error instead of
343
+ * a clear "no password supplied".
344
+ *
345
+ * **No password appears here.** It reaches the child through
346
+ * {@link buildSpawnEnv} only.
347
+ */
348
+ export function connectionArgs(profile) {
349
+ return [
350
+ `--host=${profile.host}`,
351
+ `--port=${profile.port}`,
352
+ `--username=${profile.user}`,
353
+ '--no-password',
354
+ ];
355
+ }
356
+ //# sourceMappingURL=binaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binaries.js","sourceRoot":"","sources":["../../src/runtime/binaries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { parseEnv } from 'node:util';
5
+ import { readString } from '../config/env.js';
6
+ import { PgMcpError } from '../core/errors.js';
7
+ /**
8
+ * Optional `.env` file loading for host mode (`npx pg-admin-mcp`).
9
+ *
10
+ * In the container, Compose / `docker run --env-file` already turn a `.env`
11
+ * into real environment variables. A host run has no such launcher, and MCP
12
+ * client configs (`.mcp.json`) are usually committed — so secrets need a place
13
+ * that is not that file. `PG_MCP_ENV_FILE` names it.
14
+ *
15
+ * **The real environment wins.** A key already present in `env` — even as an
16
+ * empty string — is never overwritten by the file. That makes the committed
17
+ * client config authoritative for every key it sets, and the file a fallback
18
+ * for whatever it leaves out (typically just the `PG_<ALIAS>_URL` lines that
19
+ * carry passwords). Same precedence as Node's own `--env-file`.
20
+ *
21
+ * A relative path resolves against `cwd` — for an MCP client that is the
22
+ * directory the client was launched from, not wherever its config file lives.
23
+ *
24
+ * Parsing is `util.parseEnv` (Node >= 20.12): stdlib, the same dotenv dialect
25
+ * Node's `--env-file` understands, no new dependency.
26
+ *
27
+ * Mutates `env` in place rather than returning a copy because the rest of the
28
+ * server reads `process.env` lazily, per call (`loadProfiles()` runs inside each
29
+ * tool handler) — the merge has to land where those reads look. Tests inject a
30
+ * plain object instead, so they never touch the real `process.env`.
31
+ *
32
+ * @returns the absolute path that was loaded, or `undefined` when
33
+ * `PG_MCP_ENV_FILE` is unset/blank (a no-op).
34
+ * @throws PgMcpError (`PROFILE_MISCONFIGURED`) when the file cannot be read.
35
+ * Deliberately fatal: a named-but-missing secrets file would otherwise boot a
36
+ * server whose every profile fails later with a far less obvious error.
37
+ */
38
+ export function loadEnvFile(env = process.env, cwd = process.cwd()) {
39
+ const raw = readString(env, 'PG_MCP_ENV_FILE')?.trim();
40
+ if (raw === undefined || raw === '')
41
+ return undefined;
42
+ const file = path.resolve(cwd, raw);
43
+ let contents;
44
+ try {
45
+ contents = readFileSync(file, 'utf8');
46
+ }
47
+ catch (err) {
48
+ throw new PgMcpError({
49
+ code: 'PROFILE_MISCONFIGURED',
50
+ message: `PG_MCP_ENV_FILE points to ${file}, which could not be read`,
51
+ hint: 'create the file (e.g. copy .env.example to .env), fix the path, or unset PG_MCP_ENV_FILE — ' +
52
+ 'a relative path resolves against the directory the MCP client was launched from',
53
+ cause: err,
54
+ });
55
+ }
56
+ for (const [key, value] of Object.entries(parseEnv(contents))) {
57
+ if (!Object.hasOwn(env, key))
58
+ env[key] = value;
59
+ }
60
+ return file;
61
+ }
62
+ //# sourceMappingURL=env-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/runtime/env-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,WAAW,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IAC3F,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,IAAI,EAAE,CAAC;IACvD,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAEtD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACpC,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,6BAA6B,IAAI,2BAA2B;YACrE,IAAI,EACF,6FAA6F;gBAC7F,iFAAiF;YACnF,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,34 @@
1
+ import process from 'node:process';
2
+ import { readPathList } from '../config/env.js';
3
+ /**
4
+ * Runtime facts about the process. Env is read through an injectable seam so unit
5
+ * tests never mutate the real `process.env`.
6
+ */
7
+ /**
8
+ * True when the server runs inside the published image.
9
+ *
10
+ * Reads `PG_MCP_IN_CONTAINER` (set to `1` by the Dockerfile) instead of probing
11
+ * `/.dockerenv`: deterministic, and unit-testable without a container.
12
+ */
13
+ export function inContainer(env = process.env) {
14
+ const raw = env['PG_MCP_IN_CONTAINER'];
15
+ return raw === '1' || raw?.toLowerCase() === 'true';
16
+ }
17
+ /**
18
+ * First entry of `PG_MCP_ALLOWED_DIRS`, which is where relative file arguments
19
+ * resolve. Full allow-list parsing and symlink-escape checks belong to
20
+ * `core/paths.ts`; this is only the "where do files land by default" answer.
21
+ */
22
+ export function workspaceRoot(env = process.env) {
23
+ const first = readPathList(env, 'PG_MCP_ALLOWED_DIRS')[0];
24
+ return first ?? (inContainer(env) ? '/workspace' : process.cwd());
25
+ }
26
+ export function runtimeReport(env = process.env) {
27
+ const major = Number.parseInt(env['PG_MCP_PG_MAJOR'] ?? '', 10);
28
+ return {
29
+ runtime: inContainer(env) ? 'container' : 'host',
30
+ node: process.versions.node,
31
+ ...(Number.isInteger(major) ? { pgMajor: major } : {}),
32
+ };
33
+ }
34
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/runtime/environment.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD;;;GAGG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC9D,MAAM,GAAG,GAAG,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACvC,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,KAAK,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AACpE,CAAC;AASD,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChE,OAAO;QACL,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM;QAChD,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI;QAC3B,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvD,CAAC;AACJ,CAAC"}