@contextflo/postgres-mcp 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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/config.d.ts +47 -0
  4. package/dist/config.js +159 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/context/context-file.d.ts +53 -0
  7. package/dist/context/context-file.js +248 -0
  8. package/dist/context/context-file.js.map +1 -0
  9. package/dist/context/init.d.ts +14 -0
  10. package/dist/context/init.js +71 -0
  11. package/dist/context/init.js.map +1 -0
  12. package/dist/context/store.d.ts +34 -0
  13. package/dist/context/store.js +87 -0
  14. package/dist/context/store.js.map +1 -0
  15. package/dist/db/errors.d.ts +9 -0
  16. package/dist/db/errors.js +58 -0
  17. package/dist/db/errors.js.map +1 -0
  18. package/dist/db/introspection.d.ts +55 -0
  19. package/dist/db/introspection.js +178 -0
  20. package/dist/db/introspection.js.map +1 -0
  21. package/dist/db/pool.d.ts +37 -0
  22. package/dist/db/pool.js +213 -0
  23. package/dist/db/pool.js.map +1 -0
  24. package/dist/http.d.ts +8 -0
  25. package/dist/http.js +137 -0
  26. package/dist/http.js.map +1 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +125 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/log.d.ts +46 -0
  31. package/dist/log.js +100 -0
  32. package/dist/log.js.map +1 -0
  33. package/dist/safety/errors.d.ts +11 -0
  34. package/dist/safety/errors.js +65 -0
  35. package/dist/safety/errors.js.map +1 -0
  36. package/dist/safety/validate.d.ts +12 -0
  37. package/dist/safety/validate.js +145 -0
  38. package/dist/safety/validate.js.map +1 -0
  39. package/dist/safety/walk.d.ts +26 -0
  40. package/dist/safety/walk.js +61 -0
  41. package/dist/safety/walk.js.map +1 -0
  42. package/dist/server.d.ts +9 -0
  43. package/dist/server.js +132 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/tools/add-table-context.d.ts +28 -0
  46. package/dist/tools/add-table-context.js +105 -0
  47. package/dist/tools/add-table-context.js.map +1 -0
  48. package/dist/tools/context.d.ts +12 -0
  49. package/dist/tools/context.js +2 -0
  50. package/dist/tools/context.js.map +1 -0
  51. package/dist/tools/get-table-context.d.ts +20 -0
  52. package/dist/tools/get-table-context.js +102 -0
  53. package/dist/tools/get-table-context.js.map +1 -0
  54. package/dist/tools/list-tables.d.ts +25 -0
  55. package/dist/tools/list-tables.js +78 -0
  56. package/dist/tools/list-tables.js.map +1 -0
  57. package/dist/tools/query.d.ts +22 -0
  58. package/dist/tools/query.js +138 -0
  59. package/dist/tools/query.js.map +1 -0
  60. package/package.json +59 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Schema introspection straight off the live catalog.
3
+ *
4
+ * Everything here filters on `has_table_privilege`, so the picture the model gets is
5
+ * scoped to what the connecting role can actually read. On the read-only role the README
6
+ * recommends, that is a useful property rather than an accident.
7
+ *
8
+ * These are deliberately thin. The catalog is queryable SQL through the `query` tool, so
9
+ * anything more specific than "list" and "describe" belongs there rather than in a tool
10
+ * signature the model has to learn.
11
+ */
12
+ /** Ordinary tables, views, materialized views, partitioned and foreign tables. */
13
+ const RELATION_KINDS = "('r', 'v', 'm', 'p', 'f')";
14
+ const VISIBLE_SCHEMAS = `
15
+ n.nspname NOT IN ('pg_catalog', 'information_schema')
16
+ AND n.nspname NOT LIKE 'pg_toast%'
17
+ AND n.nspname NOT LIKE 'pg_temp%'
18
+ `;
19
+ /**
20
+ * Lists tables, optionally narrowed by a case-insensitive substring.
21
+ *
22
+ * The pattern matches anywhere in the table name, the qualified name, or the table's
23
+ * comment — the comment included because a table named `fct_orders` may be the one
24
+ * someone means by "revenue", and a name-only match would report nothing and send the
25
+ * model away empty-handed.
26
+ */
27
+ export async function listTables(database, options) {
28
+ const pattern = options.pattern ?? null;
29
+ const schema = options.schema ?? null;
30
+ const rows = await database.internalQuery(`
31
+ SELECT n.nspname AS schema,
32
+ c.relname AS name,
33
+ CASE c.relkind
34
+ WHEN 'r' THEN 'table'
35
+ WHEN 'p' THEN 'partitioned table'
36
+ WHEN 'v' THEN 'view'
37
+ WHEN 'm' THEN 'materialized view'
38
+ WHEN 'f' THEN 'foreign table'
39
+ END AS kind,
40
+ obj_description(c.oid, 'pg_class') AS description,
41
+ count(*) OVER () AS total_matches
42
+ FROM pg_class c
43
+ JOIN pg_namespace n ON n.oid = c.relnamespace
44
+ WHERE c.relkind IN ${RELATION_KINDS}
45
+ AND ${VISIBLE_SCHEMAS}
46
+ AND has_table_privilege(c.oid, 'SELECT')
47
+ -- A table partitioned by day has hundreds of children; the parent is the one to query.
48
+ AND NOT c.relispartition
49
+ AND ($2::text IS NULL OR n.nspname = $2)
50
+ AND (
51
+ $1::text IS NULL
52
+ OR c.relname ILIKE '%' || $4::text || '%'
53
+ OR (n.nspname || '.' || c.relname) ILIKE '%' || $4::text || '%'
54
+ OR obj_description(c.oid, 'pg_class') ILIKE '%' || $4::text || '%'
55
+ )
56
+ ORDER BY
57
+ CASE
58
+ WHEN $1::text IS NULL THEN 0
59
+ WHEN lower(n.nspname || '.' || c.relname) = lower($1) THEN 0
60
+ WHEN lower(c.relname) = lower($1) THEN 1
61
+ WHEN c.relname ILIKE $4::text || '%' THEN 2
62
+ WHEN c.relname ILIKE '%' || $4::text || '%' THEN 3
63
+ ELSE 4
64
+ END,
65
+ n.nspname,
66
+ c.relname
67
+ LIMIT $3
68
+ `, [pattern, schema, options.limit, pattern === null ? null : escapeLike(pattern)]);
69
+ return {
70
+ tables: rows.map((row) => ({
71
+ schema: row.schema,
72
+ name: row.name,
73
+ fullyQualifiedName: `${row.schema}.${row.name}`,
74
+ kind: row.kind,
75
+ description: row.description,
76
+ })),
77
+ totalMatches: rows.length > 0 ? Number(rows[0].total_matches) : 0,
78
+ };
79
+ }
80
+ /** `order_items` should match that name, not every table with "order" + any char + "items". */
81
+ function escapeLike(pattern) {
82
+ return pattern.replace(/[\\%_]/g, (character) => `\\${character}`);
83
+ }
84
+ /**
85
+ * Describes several tables in one round trip — the model usually has two or three
86
+ * candidates after a search and should not need a call each to choose between them.
87
+ *
88
+ * Names are matched case-insensitively, and an unqualified name resolves against any
89
+ * visible schema, because a model that read `orders` in a list will ask for `orders`.
90
+ */
91
+ export async function getTableContext(database, fullyQualifiedNames) {
92
+ if (fullyQualifiedNames.length === 0)
93
+ return [];
94
+ const wanted = fullyQualifiedNames.map((name) => name.toLowerCase());
95
+ const rows = await database.internalQuery(`
96
+ SELECT n.nspname AS schema,
97
+ c.relname AS table,
98
+ CASE c.relkind
99
+ WHEN 'r' THEN 'table'
100
+ WHEN 'p' THEN 'partitioned table'
101
+ WHEN 'v' THEN 'view'
102
+ WHEN 'm' THEN 'materialized view'
103
+ WHEN 'f' THEN 'foreign table'
104
+ END AS kind,
105
+ obj_description(c.oid, 'pg_class') AS table_description,
106
+ NULLIF(c.reltuples, -1) AS approximate_rows,
107
+ a.attname AS column_name,
108
+ format_type(a.atttypid, a.atttypmod) AS data_type,
109
+ NOT a.attnotnull AS is_nullable,
110
+ pg_get_expr(d.adbin, d.adrelid) AS default_value,
111
+ col_description(c.oid, a.attnum) AS column_description,
112
+ EXISTS (
113
+ SELECT 1 FROM pg_index i
114
+ WHERE i.indrelid = c.oid AND i.indisprimary AND a.attnum = ANY (i.indkey)
115
+ ) AS is_primary_key,
116
+ (
117
+ SELECT tn.nspname || '.' || tc.relname || '.' || ta.attname
118
+ FROM pg_constraint con
119
+ JOIN pg_class tc ON tc.oid = con.confrelid
120
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
121
+ JOIN pg_attribute ta
122
+ ON ta.attrelid = con.confrelid
123
+ AND ta.attnum = con.confkey[array_position(con.conkey, a.attnum)]
124
+ WHERE con.conrelid = c.oid
125
+ AND con.contype = 'f'
126
+ AND a.attnum = ANY (con.conkey)
127
+ LIMIT 1
128
+ ) AS references,
129
+ (
130
+ SELECT array_agg(e.enumlabel::text ORDER BY e.enumsortorder)
131
+ FROM pg_enum e
132
+ WHERE e.enumtypid = a.atttypid
133
+ ) AS enum_values,
134
+ a.attnum AS ordinal
135
+ FROM pg_class c
136
+ JOIN pg_namespace n ON n.oid = c.relnamespace
137
+ LEFT JOIN pg_attribute a
138
+ ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
139
+ LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
140
+ WHERE c.relkind IN ${RELATION_KINDS}
141
+ AND ${VISIBLE_SCHEMAS}
142
+ AND has_table_privilege(c.oid, 'SELECT')
143
+ AND (
144
+ lower(n.nspname || '.' || c.relname) = ANY ($1::text[])
145
+ OR lower(c.relname) = ANY ($1::text[])
146
+ )
147
+ ORDER BY n.nspname, c.relname, a.attnum
148
+ `, [wanted]);
149
+ const byTable = new Map();
150
+ for (const row of rows) {
151
+ const fullyQualifiedName = `${row.schema}.${row.table}`;
152
+ let table = byTable.get(fullyQualifiedName);
153
+ if (!table) {
154
+ table = {
155
+ fullyQualifiedName,
156
+ kind: row.kind,
157
+ description: row.table_description,
158
+ approximateRows: row.approximate_rows === null ? null : Math.max(0, Math.round(Number(row.approximate_rows))),
159
+ columns: [],
160
+ };
161
+ byTable.set(fullyQualifiedName, table);
162
+ }
163
+ if (row.column_name !== null) {
164
+ table.columns.push({
165
+ name: row.column_name,
166
+ dataType: row.data_type ?? 'unknown',
167
+ isNullable: row.is_nullable ?? true,
168
+ defaultValue: row.default_value,
169
+ description: row.column_description,
170
+ isPrimaryKey: row.is_primary_key ?? false,
171
+ references: row.references,
172
+ enumValues: row.enum_values,
173
+ });
174
+ }
175
+ }
176
+ return [...byTable.values()];
177
+ }
178
+ //# sourceMappingURL=introspection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.js","sourceRoot":"","sources":["../../src/db/introspection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import pg from 'pg';
2
+ export interface DatabaseOptions {
3
+ connectionString: string;
4
+ statementTimeoutMs: number;
5
+ }
6
+ export interface ReadOnlyResult {
7
+ rows: Record<string, unknown>[];
8
+ /** True when the query produced more rows than the cap and the extras were dropped. */
9
+ truncated: boolean;
10
+ }
11
+ export declare class Database {
12
+ private readonly pool;
13
+ private readonly statementTimeoutMs;
14
+ /** True when startup options were refused and we connected without them. */
15
+ readonly behindPooler: boolean;
16
+ private constructor();
17
+ static connect(options: DatabaseOptions): Promise<Database>;
18
+ private static open;
19
+ /**
20
+ * Runs a single already-validated read-only statement, reading at most `maxRows`.
21
+ *
22
+ * Reads `maxRows + 1` so truncation is detectable rather than silent.
23
+ */
24
+ runReadOnly(sql: string, maxRows: number): Promise<ReadOnlyResult>;
25
+ /**
26
+ * For this server's own introspection queries — never for user input. `queryMode` is
27
+ * mandatory here for the reason described at the top of this file.
28
+ */
29
+ internalQuery<T extends pg.QueryResultRow>(text: string, values?: unknown[]): Promise<T[]>;
30
+ /**
31
+ * Layer 4 is a database role, not code — but connecting as a superuser silently
32
+ * defeats it, so say so. Also confirms the startup options actually landed, which a
33
+ * connection pooler in front of Postgres may prevent.
34
+ */
35
+ warnOnWeakSetup(): Promise<void>;
36
+ close(): Promise<void>;
37
+ }
@@ -0,0 +1,213 @@
1
+ import pg from 'pg';
2
+ import Cursor from 'pg-cursor';
3
+ import { parse as parseConnectionString } from 'pg-connection-string';
4
+ import { describeConnectionError } from './errors.js';
5
+ /**
6
+ * The single place user-supplied SQL reaches the database, and the home of safety layers
7
+ * 1 and 2.
8
+ *
9
+ * ## Layer 1 — extended query protocol
10
+ *
11
+ * The archived `@modelcontextprotocol/server-postgres` ran `client.query(sql)` with a
12
+ * bare string. node-postgres only prepares a statement when
13
+ * `Query.requiresPreparation()` is true, and with no values that is false — so the driver
14
+ * sends a *simple* Query message, which permits multiple statements. That is what let
15
+ * `COMMIT; DROP TABLE x; BEGIN;` escape their read-only transaction.
16
+ *
17
+ * So: user SQL goes through `pg-cursor`, which always issues Parse/Bind/Describe/Execute,
18
+ * and Postgres itself rejects multi-statement input at Parse time. Internal queries pass
19
+ * `queryMode: 'extended'` explicitly.
20
+ *
21
+ * Note that `values: []` does NOT force the extended protocol — `requiresPreparation()`
22
+ * checks `values.length > 0`. Anything added here must use a cursor or `queryMode`.
23
+ *
24
+ * ## Layer 2 — connection-level read-only
25
+ *
26
+ * `default_transaction_read_only=on` is set in the startup packet, and every statement
27
+ * additionally runs inside an explicit `BEGIN READ ONLY`. The explicit transaction is
28
+ * what makes this layer hold behind a pooler: PgBouncer refuses unknown startup
29
+ * parameters outright, so on that error we reconnect without them rather than fail. The
30
+ * statement timeout is set with `SET LOCAL` inside each transaction for the same reason.
31
+ * `SET`/`RESET` are rejected by the parser layer, so a session cannot turn either back off.
32
+ */
33
+ const DEFAULT_IDLE_TRANSACTION_TIMEOUT_MS = 60_000;
34
+ /** Client-side backstop in case the server never answers; the server-side timeout fires first. */
35
+ const CLIENT_TIMEOUT_GRACE_MS = 5_000;
36
+ // node-postgres turns date and timestamp values into JS Dates in the *MCP process's*
37
+ // timezone, and JSON renders those in UTC — so `2024-01-15` came out as
38
+ // `2024-01-14T18:30:00.000Z` for anyone east of Greenwich. Hand the model exactly what
39
+ // Postgres sent instead. Intervals too, which otherwise become `{ "days": 1 }` objects.
40
+ const DATE_LIKE_TYPES = [1082 /* date */, 1114 /* timestamp */, 1184 /* timestamptz */, 1186 /* interval */];
41
+ const DATE_LIKE_ARRAY_TYPES = [1182, 1115, 1185, 1187];
42
+ const TEXT_ARRAY_TYPE = 1009;
43
+ for (const oid of DATE_LIKE_TYPES)
44
+ pg.types.setTypeParser(oid, (value) => value);
45
+ for (const oid of DATE_LIKE_ARRAY_TYPES)
46
+ pg.types.setTypeParser(oid, pg.types.getTypeParser(TEXT_ARRAY_TYPE));
47
+ export class Database {
48
+ pool;
49
+ statementTimeoutMs;
50
+ /** True when startup options were refused and we connected without them. */
51
+ behindPooler;
52
+ constructor(pool, statementTimeoutMs, behindPooler) {
53
+ this.pool = pool;
54
+ this.statementTimeoutMs = statementTimeoutMs;
55
+ this.behindPooler = behindPooler;
56
+ }
57
+ static async connect(options) {
58
+ try {
59
+ return await Database.open(options, true);
60
+ }
61
+ catch (error) {
62
+ if (!isUnsupportedStartupParameter(error))
63
+ throw new Error(describeConnectionError(error));
64
+ }
65
+ console.error('[postgres-mcp] the server refused startup parameters, which usually means PgBouncer or another ' +
66
+ 'pooler. Reconnecting without them: every statement still runs in BEGIN READ ONLY with its ' +
67
+ 'own statement timeout.');
68
+ try {
69
+ return await Database.open(options, false);
70
+ }
71
+ catch (error) {
72
+ throw new Error(describeConnectionError(error));
73
+ }
74
+ }
75
+ static async open(options, withStartupOptions) {
76
+ const pool = new pg.Pool(buildPoolConfig(options, withStartupOptions));
77
+ // pg emits 'error' asynchronously when an idle connection is reaped server-side.
78
+ // Without a listener node re-throws it as an uncaught exception and kills the server.
79
+ pool.on('error', (error) => {
80
+ console.error(`[postgres-mcp] pool error: ${describeConnectionError(error)}`);
81
+ });
82
+ try {
83
+ const client = await pool.connect();
84
+ client.release();
85
+ }
86
+ catch (error) {
87
+ await pool.end().catch(() => { });
88
+ throw error;
89
+ }
90
+ return new Database(pool, options.statementTimeoutMs, !withStartupOptions);
91
+ }
92
+ /**
93
+ * Runs a single already-validated read-only statement, reading at most `maxRows`.
94
+ *
95
+ * Reads `maxRows + 1` so truncation is detectable rather than silent.
96
+ */
97
+ async runReadOnly(sql, maxRows) {
98
+ const client = await this.pool.connect();
99
+ try {
100
+ await client.query({ text: 'BEGIN READ ONLY', queryMode: 'extended' });
101
+ // SET LOCAL rather than a startup option, so the timeout holds behind a pooler too.
102
+ await client.query({
103
+ text: "SELECT set_config('statement_timeout', $1, true)",
104
+ values: [String(this.statementTimeoutMs)],
105
+ queryMode: 'extended',
106
+ });
107
+ const cursor = client.query(new Cursor(sql));
108
+ try {
109
+ const rows = (await cursor.read(maxRows + 1));
110
+ const truncated = rows.length > maxRows;
111
+ return { rows: truncated ? rows.slice(0, maxRows) : rows, truncated };
112
+ }
113
+ finally {
114
+ await cursor.close().catch(() => { });
115
+ }
116
+ }
117
+ finally {
118
+ // Always ROLLBACK, never COMMIT. Beyond discarding the transaction, this undoes any
119
+ // GUC change made inside it (SET is transactional), so a statement cannot leave a
120
+ // pooled connection in a weakened state for whoever gets it next.
121
+ await client.query({ text: 'ROLLBACK', queryMode: 'extended' }).catch((error) => {
122
+ console.error(`[postgres-mcp] could not roll back: ${describeConnectionError(error)}`);
123
+ });
124
+ client.release();
125
+ }
126
+ }
127
+ /**
128
+ * For this server's own introspection queries — never for user input. `queryMode` is
129
+ * mandatory here for the reason described at the top of this file.
130
+ */
131
+ async internalQuery(text, values = []) {
132
+ const result = await this.pool.query({ text, values, queryMode: 'extended' });
133
+ return result.rows;
134
+ }
135
+ /**
136
+ * Layer 4 is a database role, not code — but connecting as a superuser silently
137
+ * defeats it, so say so. Also confirms the startup options actually landed, which a
138
+ * connection pooler in front of Postgres may prevent.
139
+ */
140
+ async warnOnWeakSetup() {
141
+ try {
142
+ const rows = await this.internalQuery(`SELECT rolname AS role,
143
+ rolsuper AS is_superuser,
144
+ rolbypassrls AS bypasses_rls,
145
+ current_setting('default_transaction_read_only') AS default_read_only
146
+ FROM pg_roles
147
+ WHERE rolname = current_user`);
148
+ const info = rows[0];
149
+ if (!info)
150
+ return;
151
+ if (info.is_superuser || info.bypasses_rls) {
152
+ console.error(`[postgres-mcp] connected as "${info.role}", which is a superuser or bypasses RLS. ` +
153
+ 'Queries are still read-only, but the recommended setup is a dedicated read-only role ' +
154
+ 'so the database enforces it independently of this server. See the README.');
155
+ }
156
+ if (info.default_read_only !== 'on' && !this.behindPooler) {
157
+ console.error('[postgres-mcp] default_transaction_read_only did not take effect on this connection ' +
158
+ '(a connection pooler may be dropping startup options). Statements still run inside an ' +
159
+ 'explicit READ ONLY transaction, so writes remain blocked.');
160
+ }
161
+ }
162
+ catch (error) {
163
+ // A restricted role may not be able to read pg_roles. That is fine — it is a
164
+ // diagnostic, not a safety layer.
165
+ console.error(`[postgres-mcp] skipped setup check: ${describeConnectionError(error)}`);
166
+ }
167
+ }
168
+ async close() {
169
+ await this.pool.end();
170
+ }
171
+ }
172
+ /** PgBouncer: `unsupported startup parameter: options` (or `...in options: ...`). */
173
+ function isUnsupportedStartupParameter(error) {
174
+ return /unsupported startup parameter/i.test(error?.message ?? '');
175
+ }
176
+ function buildPoolConfig(options, withStartupOptions) {
177
+ // Parse here rather than handing `connectionString` to pg: pg re-parses it and
178
+ // Object.assigns the result over the rest of the config, which would drop our options.
179
+ const parsed = parseConnectionString(options.connectionString);
180
+ const hardening = [
181
+ '-c default_transaction_read_only=on',
182
+ `-c statement_timeout=${options.statementTimeoutMs}`,
183
+ `-c idle_in_transaction_session_timeout=${DEFAULT_IDLE_TRANSACTION_TIMEOUT_MS}`,
184
+ ].join(' ');
185
+ const config = {
186
+ max: 5,
187
+ idleTimeoutMillis: 30_000,
188
+ // Client-side only — never sent as a startup parameter, so safe behind a pooler.
189
+ query_timeout: options.statementTimeoutMs + CLIENT_TIMEOUT_GRACE_MS,
190
+ };
191
+ if (withStartupOptions) {
192
+ // Keep anything the user set (including their own `options`) and append ours last so
193
+ // the read-only settings win.
194
+ config.options = parsed.options ? `${parsed.options} ${hardening}` : hardening;
195
+ }
196
+ else if (parsed.options) {
197
+ config.options = parsed.options;
198
+ }
199
+ if (parsed.host)
200
+ config.host = parsed.host;
201
+ if (parsed.port)
202
+ config.port = Number(parsed.port);
203
+ if (parsed.database)
204
+ config.database = parsed.database;
205
+ if (parsed.user)
206
+ config.user = parsed.user;
207
+ if (parsed.password)
208
+ config.password = parsed.password;
209
+ if (parsed.ssl !== undefined)
210
+ config.ssl = parsed.ssl;
211
+ return config;
212
+ }
213
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/db/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,MAAM,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAE,KAAK,IAAI,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,mCAAmC,GAAG,MAAM,CAAA;AAClD,kGAAkG;AAClG,MAAM,uBAAuB,GAAG,KAAK,CAAA;AAErC,qFAAqF;AACrF,wEAAwE;AACxE,uFAAuF;AACvF,wFAAwF;AACxF,MAAM,eAAe,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;AAC5G,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AACtD,MAAM,eAAe,GAAW,IAAI,CAAA;AAEpC,KAAK,MAAM,GAAG,IAAI,eAAe;IAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,CAAA;AACxF,KAAK,MAAM,GAAG,IAAI,qBAAqB;IAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAA;AAa7G,MAAM,OAAO,QAAQ;IACF,IAAI,CAAS;IACb,kBAAkB,CAAQ;IAC3C,4EAA4E;IACnE,YAAY,CAAS;IAE9B,YAAoB,IAAa,EAAE,kBAA0B,EAAE,YAAqB;QAClF,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAA;QAC5C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;IAClC,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAwB;QAC3C,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,6BAA6B,CAAC,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAA;QAC5F,CAAC;QAED,OAAO,CAAC,KAAK,CACX,iGAAiG;YAC/F,4FAA4F;YAC5F,wBAAwB,CAC3B,CAAA;QAED,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAwB,EAAE,kBAA2B;QAC7E,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAA;QAEtE,iFAAiF;QACjF,sFAAsF;QACtF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACzB,OAAO,CAAC,KAAK,CAAC,8BAA8B,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC/E,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;YACnC,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAChC,MAAM,KAAK,CAAA;QACb,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,kBAAkB,EAAE,CAAC,kBAAkB,CAAC,CAAA;IAC5E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,WAAW,CAAC,GAAW,EAAE,OAAe;QAC5C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;QAExC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;YACtE,oFAAoF;YACpF,MAAM,MAAM,CAAC,KAAK,CAAC;gBACjB,IAAI,EAAE,kDAAkD;gBACxD,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;gBACzC,SAAS,EAAE,UAAU;aACtB,CAAC,CAAA;YAEF,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC5C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAA8B,CAAA;gBAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;gBAEvC,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAA;YACvE,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YACtC,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,oFAAoF;YACpF,kFAAkF;YAClF,kEAAkE;YAClE,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBACvF,OAAO,CAAC,KAAK,CAAC,uCAAuC,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YACxF,CAAC,CAAC,CAAA;YACF,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa,CAA8B,IAAY,EAAE,SAAoB,EAAE;QACnF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;QAChF,OAAO,MAAM,CAAC,IAAI,CAAA;IACpB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,aAAa,CAMnC;;;;;uCAK+B,CAChC,CAAA;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YACpB,IAAI,CAAC,IAAI;gBAAE,OAAM;YAEjB,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;gBAC3C,OAAO,CAAC,KAAK,CACX,gCAAgC,IAAI,CAAC,IAAI,2CAA2C;oBAClF,uFAAuF;oBACvF,2EAA2E,CAC9E,CAAA;YACH,CAAC;YAED,IAAI,IAAI,CAAC,iBAAiB,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;gBAC1D,OAAO,CAAC,KAAK,CACX,sFAAsF;oBACpF,wFAAwF;oBACxF,2DAA2D,CAC9D,CAAA;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,6EAA6E;YAC7E,kCAAkC;YAClC,OAAO,CAAC,KAAK,CAAC,uCAAuC,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACxF,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAA;IACvB,CAAC;CACF;AAED,qFAAqF;AACrF,SAAS,6BAA6B,CAAC,KAAc;IACnD,OAAO,gCAAgC,CAAC,IAAI,CAAE,KAA8B,EAAE,OAAO,IAAI,EAAE,CAAC,CAAA;AAC9F,CAAC;AAED,SAAS,eAAe,CAAC,OAAwB,EAAE,kBAA2B;IAC5E,+EAA+E;IAC/E,uFAAuF;IACvF,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAE9D,MAAM,SAAS,GAAG;QAChB,qCAAqC;QACrC,wBAAwB,OAAO,CAAC,kBAAkB,EAAE;QACpD,0CAA0C,mCAAmC,EAAE;KAChF,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEX,MAAM,MAAM,GAAkB;QAC5B,GAAG,EAAE,CAAC;QACN,iBAAiB,EAAE,MAAM;QACzB,iFAAiF;QACjF,aAAa,EAAE,OAAO,CAAC,kBAAkB,GAAG,uBAAuB;KACpE,CAAA;IAED,IAAI,kBAAkB,EAAE,CAAC;QACvB,qFAAqF;QACrF,8BAA8B;QAC9B,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,CAAC;SAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC1B,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;IACjC,CAAC;IAED,IAAI,MAAM,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IAC1C,IAAI,MAAM,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAClD,IAAI,MAAM,CAAC,QAAQ;QAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IACtD,IAAI,MAAM,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IAC1C,IAAI,MAAM,CAAC,QAAQ;QAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IACtD,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAAE,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,GAA2B,CAAA;IAE7E,OAAO,MAAM,CAAA;AACf,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { type Server as HttpServer } from 'node:http';
2
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import type { HttpConfig } from './config.js';
4
+ export interface HttpServerDeps {
5
+ config: HttpConfig;
6
+ createMcpServer: () => Server;
7
+ }
8
+ export declare function startHttpServer({ config, createMcpServer }: HttpServerDeps): Promise<HttpServer>;
package/dist/http.js ADDED
@@ -0,0 +1,137 @@
1
+ import { createServer as createHttpServer } from 'node:http';
2
+ import { timingSafeEqual } from 'node:crypto';
3
+ import { StreamableHTTPServerTransport, } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ /**
5
+ * Streamable HTTP transport, for running this on a box and pointing a remote client at it.
6
+ *
7
+ * Stateless: a fresh Server and transport per request, all sharing one connection pool.
8
+ * There is no session state worth keeping between calls, and no session map to leak.
9
+ *
10
+ * On authentication — an MCP endpoint on a reachable port is a live database connection
11
+ * for anyone who can open a socket to it. The read-only layers hold, so the exposure is
12
+ * reading everything rather than breaking anything, which is still the whole database.
13
+ * Hence: loopback unless a flag says otherwise, an optional bearer token, and a warning
14
+ * loud enough to be uncomfortable when it is exposed without one.
15
+ */
16
+ const MCP_PATH = '/mcp';
17
+ export async function startHttpServer({ config, createMcpServer }) {
18
+ warnAboutExposure(config);
19
+ const server = createHttpServer((request, response) => {
20
+ void handleRequest(request, response, config, createMcpServer);
21
+ });
22
+ await new Promise((resolve, reject) => {
23
+ server.once('error', reject);
24
+ server.listen(config.port, config.host, () => {
25
+ server.removeListener('error', reject);
26
+ resolve();
27
+ });
28
+ });
29
+ // Report the port actually bound, which differs from the requested one when it was 0.
30
+ const address = server.address();
31
+ const port = typeof address === 'object' && address !== null ? address.port : config.port;
32
+ console.error(`[postgres-mcp] listening on http://${config.host}:${port}${MCP_PATH}`);
33
+ return server;
34
+ }
35
+ async function handleRequest(request, response, config, createMcpServer) {
36
+ const path = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`).pathname;
37
+ if (path === '/health') {
38
+ respondJson(response, 200, { status: 'ok' });
39
+ return;
40
+ }
41
+ if (path !== MCP_PATH) {
42
+ respondJson(response, 404, { error: `Not found. The MCP endpoint is ${MCP_PATH}.` });
43
+ return;
44
+ }
45
+ if (!isLoopbackRequestAllowed(request, config.host)) {
46
+ respondJson(response, 403, {
47
+ error: 'Host or Origin is not local. This server is bound to loopback and only answers local requests.',
48
+ });
49
+ return;
50
+ }
51
+ if (!isAuthorized(request, config.authToken)) {
52
+ response.setHeader('WWW-Authenticate', 'Bearer');
53
+ respondJson(response, 401, { error: 'Missing or invalid bearer token.' });
54
+ return;
55
+ }
56
+ const mcpServer = createMcpServer();
57
+ // `sessionIdGenerator: undefined` is the SDK's documented way to ask for stateless mode,
58
+ // but its own types are not written for exactOptionalPropertyTypes. The two casts here
59
+ // are types-only; keeping the flag on is worth more in the safety layer than it costs
60
+ // at this boundary.
61
+ const transport = new StreamableHTTPServerTransport({
62
+ sessionIdGenerator: undefined,
63
+ });
64
+ response.on('close', () => {
65
+ void transport.close();
66
+ void mcpServer.close();
67
+ });
68
+ try {
69
+ await mcpServer.connect(transport);
70
+ await transport.handleRequest(request, response);
71
+ }
72
+ catch (error) {
73
+ console.error(`[postgres-mcp] request failed: ${error instanceof Error ? error.message : String(error)}`);
74
+ if (!response.headersSent) {
75
+ respondJson(response, 500, { error: 'Internal server error.' });
76
+ }
77
+ }
78
+ }
79
+ function isAuthorized(request, authToken) {
80
+ if (!authToken)
81
+ return true;
82
+ const header = request.headers.authorization;
83
+ if (!header?.startsWith('Bearer '))
84
+ return false;
85
+ const presented = Buffer.from(header.slice('Bearer '.length));
86
+ const expected = Buffer.from(authToken);
87
+ // timingSafeEqual throws on length mismatch, and the length itself is not a secret.
88
+ return presented.length === expected.length && timingSafeEqual(presented, expected);
89
+ }
90
+ function respondJson(response, status, body) {
91
+ response.writeHead(status, { 'content-type': 'application/json' });
92
+ response.end(JSON.stringify(body));
93
+ }
94
+ const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
95
+ /** As `URL.hostname` spells them, which brackets IPv6. */
96
+ const LOOPBACK_HOSTNAMES = new Set(['127.0.0.1', '[::1]', 'localhost']);
97
+ /**
98
+ * DNS rebinding: a web page on evil.example re-points its own hostname at 127.0.0.1, and
99
+ * the browser, seeing the same origin, lets its script POST to this server. Loopback plus
100
+ * no token is the default setup, so that script could read the database. The Host header
101
+ * still says evil.example, which is how it is caught. The MCP spec requires this check.
102
+ *
103
+ * Only for loopback binds: behind 0.0.0.0 the Host is whatever the proxy in front sends,
104
+ * and the bearer token is the control.
105
+ */
106
+ function isLoopbackRequestAllowed(request, boundHost) {
107
+ if (!LOOPBACK.has(boundHost))
108
+ return true;
109
+ const host = request.headers.host;
110
+ if (!host || !LOOPBACK_HOSTNAMES.has(hostnameOf(`http://${host}`)))
111
+ return false;
112
+ // Browsers send Origin; local tools such as the MCP Inspector are served from localhost.
113
+ const origin = request.headers.origin;
114
+ return origin === undefined || LOOPBACK_HOSTNAMES.has(hostnameOf(origin));
115
+ }
116
+ function hostnameOf(url) {
117
+ try {
118
+ return new URL(url).hostname;
119
+ }
120
+ catch {
121
+ return '';
122
+ }
123
+ }
124
+ function warnAboutExposure(config) {
125
+ if (LOOPBACK.has(config.host))
126
+ return;
127
+ if (config.authToken) {
128
+ console.error(`[postgres-mcp] bound to ${config.host} with bearer-token auth. Terminate TLS in front of it — ` +
129
+ 'the token crosses the wire in plaintext otherwise.');
130
+ return;
131
+ }
132
+ console.error(`[postgres-mcp] WARNING: bound to ${config.host} with NO AUTHENTICATION. Anyone who can reach ` +
133
+ `${config.host}:${config.port} can read every table this connection can see. Queries stay ` +
134
+ 'read-only, so this is data exposure rather than damage — but it is the whole database. ' +
135
+ 'Set AUTH_TOKEN, keep it inside a private network, or bind 127.0.0.1.');
136
+ }
137
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};