@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ContextFlo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @contextflo/postgres-mcp
2
+
3
+ The analytics MCP server for Postgres. Read-only by construction, with schema context that makes answers correct.
4
+
5
+ A drop-in replacement for the archived `@modelcontextprotocol/server-postgres`, which shipped with a
6
+ [SQL injection vulnerability](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/)
7
+ that let `COMMIT; DROP SCHEMA public CASCADE` walk straight out of its read-only transaction.
8
+
9
+ ```bash
10
+ npx @contextflo/postgres-mcp postgresql://localhost/mydb
11
+ ```
12
+
13
+ ## Why this one
14
+
15
+ **Read-only that holds up.** The archived server enforced read-only as a property of the SQL *string*. Here it is a
16
+ property of the connection, the role, and the wire protocol: four independent layers, each of which stops that
17
+ payload on its own. The exploit is a test case in this repo.
18
+
19
+ **Answers that make sense.** A model that does not know `fct_orders_v2` is the table your team actually uses, or that
20
+ `revenue` is gross rather than net, writes confident, wrong SQL. `.contextflo/context.md` is a markdown file you edit
21
+ and this server hands to the model. No database, no index, no service.
22
+
23
+ ## Setup
24
+
25
+ Add it to your MCP client:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "postgres": {
31
+ "command": "npx",
32
+ "args": ["-y", "@contextflo/postgres-mcp", "postgresql://localhost/mydb"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ <details>
39
+ <summary>Claude Code, Cursor, Claude Desktop, VS Code</summary>
40
+
41
+ **Claude Code**
42
+
43
+ ```bash
44
+ claude mcp add postgres -- npx -y @contextflo/postgres-mcp postgresql://localhost/mydb
45
+ ```
46
+
47
+ **Cursor** uses `.cursor/mcp.json`, same shape as above.
48
+
49
+ **Claude Desktop** uses `claude_desktop_config.json`
50
+ (macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`), same shape as above.
51
+
52
+ **VS Code** uses `.vscode/mcp.json`:
53
+
54
+ ```json
55
+ {
56
+ "servers": {
57
+ "postgres": {
58
+ "command": "npx",
59
+ "args": ["-y", "@contextflo/postgres-mcp", "postgresql://localhost/mydb"]
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ </details>
66
+
67
+ Then generate a context file:
68
+
69
+ ```bash
70
+ npx @contextflo/postgres-mcp init postgresql://localhost/mydb
71
+ ```
72
+
73
+ That writes `.contextflo/context.md`, seeded from your `COMMENT ON` values, and prints the `CREATE ROLE` snippet for
74
+ a read-only role. Editing the file is the point: the business definitions section is where the value is.
75
+
76
+ ## Tools
77
+
78
+ | Tool | What it does |
79
+ | --- | --- |
80
+ | `query` | Runs one read-only statement: `SELECT`, `WITH ... SELECT`, `EXPLAIN`, or `SHOW`. |
81
+ | `list_tables` | Lists readable tables with descriptions. `pattern` matches anywhere in the name or description. |
82
+ | `get_table_context` | Describes tables: columns, types, keys, foreign key targets, enum values, curated descriptions. |
83
+ | `add_table_context` | Lets the agent write down a gotcha it found (`amount` is in cents, `status` has an undocumented value) in the context file. |
84
+
85
+ There is no separate search tool, and that is deliberate. `information_schema` and `pg_catalog` are ordinary tables,
86
+ so anything more specific, like finding every column named like `%revenue%` or listing tables with no primary key,
87
+ is a query the model can write itself:
88
+
89
+ ```sql
90
+ SELECT table_schema, table_name, column_name
91
+ FROM information_schema.columns
92
+ WHERE column_name ILIKE '%revenue%';
93
+ ```
94
+
95
+ Table schemas are also exposed as `postgres://<host>/<table>/schema` resources, matching the archived server, for
96
+ anything pinned to those URIs. Most clients never fetch resources on their own, which is why discovery lives in the
97
+ tools.
98
+
99
+ ## How read-only is enforced
100
+
101
+ Four layers. Each one stops the archived server's exploit by itself.
102
+
103
+ **1. Extended query protocol.** User SQL goes through `pg-cursor`, which always issues Parse/Bind/Execute, so
104
+ Postgres itself rejects multi-statement input. The archived server called `client.query(sql)` with a bare string;
105
+ node-postgres only prepares a statement when there are bind values, so that took the *simple* protocol path, where
106
+ `;` separates statements. That is the whole bug.
107
+
108
+ **2. Connection-level read-only.** `default_transaction_read_only=on` is set in the startup packet, and every
109
+ statement runs inside an explicit `BEGIN READ ONLY` that always ends in `ROLLBACK`, never `COMMIT`. The rollback
110
+ also undoes any `SET` made inside the transaction, so a statement cannot leave a pooled connection weakened for
111
+ whoever gets it next.
112
+
113
+ **3. A statement allowlist on the real Postgres parser.** [`libpg-query`](https://github.com/launchql/libpg-query-node)
114
+ is the actual Postgres C parser compiled to WASM, not a JavaScript approximation of SQL. The whole parse tree is
115
+ walked rather than just the top-level node, which is what catches a data-modifying CTE:
116
+
117
+ ```sql
118
+ WITH x AS (INSERT INTO users VALUES (1) RETURNING *) SELECT * FROM x
119
+ ```
120
+
121
+ That parses as a `SelectStmt`. A validator checking only the statement type runs it. Unknown node types fail closed.
122
+
123
+ The same walk rejects built-in functions that act outside the transaction even inside a plain `SELECT`:
124
+ `dblink` (a second connection, which is not read-only), `query_to_xml` (runs a SQL string the parser never sees),
125
+ `pg_terminate_backend`, session-level advisory locks, `set_config`, and the server-filesystem functions.
126
+
127
+ **4. A read-only database role.** The layers above are code, and code has bugs. A role that cannot write is enforced
128
+ by Postgres regardless. `init` prints the snippet; this is the setup we recommend:
129
+
130
+ ```sql
131
+ CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
132
+ GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
133
+ GRANT USAGE ON SCHEMA public TO mcp_readonly;
134
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
135
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;
136
+ ```
137
+
138
+ The server warns on startup if you connect as a superuser.
139
+
140
+ **What this does not protect against.** The parser cannot see inside a user-defined function, so a volatile or
141
+ `SECURITY DEFINER` function called from an allowed `SELECT` can do anything its body does, including open its own
142
+ connection. The function denylist covers the built-in escapes, not yours. Layer 4 is what stops the rest, which is
143
+ why the read-only role is the recommended setup rather than an optional extra. Read-only is also not
144
+ confidentiality: anything the connected role can read, a model can read, so grant it only what you want an agent
145
+ to see.
146
+
147
+ ## Migrating from `@modelcontextprotocol/server-postgres`
148
+
149
+ Swap the package name. The tool is still called `query`, still takes `sql`, still returns JSON rows, and the
150
+ connection string is still the first argument.
151
+
152
+ Four deliberate differences:
153
+
154
+ 1. **Multi-statement SQL and `SET`/`RESET` are rejected** with a clear error. On the archived server these "worked",
155
+ and that was the vulnerability.
156
+ 2. **Results are capped** at 1000 rows and 50,000 characters by default, and single values over 2,000 characters are
157
+ shortened. Truncation is stated in the output, never silent. Rows come back one per line rather than
158
+ pretty-printed, which roughly halves their token cost; it is still a JSON array. Dates and timestamps are exactly
159
+ what Postgres sent, not re-rendered in the server's timezone.
160
+ 3. **Schema discovery is a tool, not just a resource.** Most clients do not auto-attach resources, which is why
161
+ models using the old server so often did not know the schema.
162
+ 4. **All non-system schemas are visible**, not only `public`, and column descriptions come through from
163
+ `COMMENT ON`.
164
+
165
+ ## Options
166
+
167
+ ```
168
+ --max-rows <n> Maximum rows returned per query (default: 1000)
169
+ --max-output-chars <n> Character budget for one query result (default: 50000)
170
+ --statement-timeout <ms> Server-side statement timeout (default: 30000)
171
+ --context-file <path> Curated schema context (default: .contextflo/context.md)
172
+ --no-context-writes Do not offer add_table_context; the context file is only read
173
+ --log-file <path> Query audit log (default: .contextflo/log.md once that directory exists)
174
+ --no-log Never write a query log
175
+ --http Serve over streamable HTTP instead of stdio
176
+ --port <n> HTTP port (default: 8080)
177
+ --host <addr> HTTP bind address (default: 127.0.0.1)
178
+ ```
179
+
180
+ `DATABASE_URL` supplies the connection string if you do not pass one. `AUTH_TOKEN`, with `--http`, requires that
181
+ value as a bearer token.
182
+
183
+ **Connection poolers.** PgBouncer (and so the pooled connection strings from Supabase, Neon, and others) refuses
184
+ the startup parameters this server normally sends. When that happens it reconnects without them and says so on
185
+ stderr. Nothing is weakened: every statement still runs in `BEGIN READ ONLY` with its own statement timeout, and CI
186
+ runs the read-only suite through PgBouncer in transaction mode.
187
+
188
+ ## The context file
189
+
190
+ ```markdown
191
+ # Database context
192
+
193
+ Revenue means gross, before refunds.
194
+ "Active customer" means an order in the last 90 days.
195
+
196
+ ## Tables
197
+
198
+ ### public.orders
199
+ One row per customer order. Source of truth for revenue, not `orders_legacy`.
200
+
201
+ - revenue_usd: Gross revenue, before refunds. Net lives in `order_refunds`.
202
+ - status: One of pending, paid, refunded.
203
+ ```
204
+
205
+ Everything above `## Tables` is handed to the model: as server instructions, and again at the top of `list_tables`,
206
+ because several clients never show the model server instructions. HTML comments are left out. Under `## Tables`, a
207
+ `###` heading names a table, the prose beneath describes it, and `- column: meaning` lines describe columns (a dash
208
+ separator works too). Your text wins over `COMMENT ON`. Edits take effect on the next tool call; no restart.
209
+
210
+ A relative `--context-file` resolves against the directory the client starts the server in. Claude Desktop starts
211
+ servers in `/`, so there it falls back to your home directory (`~/.contextflo/context.md`). For a file that lives in
212
+ a repo, pass an absolute path. The server prints the path it is using on startup.
213
+
214
+ **The agent adds to it.** When the model finds something the schema does not say (`amount` is in cents, `status`
215
+ also holds `'void'` on old rows, every query needs `deleted_at IS NULL`), `add_table_context` appends it to this file,
216
+ and every later session starts knowing it. Notes are only ever appended, never substituted for what you wrote, and
217
+ only for tables and columns that exist. Treat them like any other change: keep the file in git and read the diff. A
218
+ model can be wrong, and text inside your data can steer what it writes, so a note is a suggestion until someone has
219
+ looked at it. `--no-context-writes` turns the tool off.
220
+
221
+ `init` seeds this from existing comments, and only for columns that already have one. A file with a blank
222
+ placeholder for all 4000 columns is a file nobody edits.
223
+
224
+ Richer context generated from your code and docs, and shared across a team, is what
225
+ [contextflo.com](https://contextflo.com) does.
226
+
227
+ ## Query log
228
+
229
+ Once `.contextflo/` exists, every statement is appended to `.contextflo/log.md` with its outcome, row count, and
230
+ duration. It is written for you, not fed back to the model. `--no-log` turns it off.
231
+
232
+ ## Docker
233
+
234
+ ```bash
235
+ docker run -i --rm ghcr.io/contextflo/postgres-mcp postgresql://host.docker.internal/mydb
236
+ ```
237
+
238
+ For a remote endpoint, see [`docker-compose.example.yml`](docker-compose.example.yml). Note what you are doing
239
+ before you bind `0.0.0.0`: that is a live database connection on a port. Set `AUTH_TOKEN`, keep it inside a private
240
+ network, and terminate TLS in front of it, because the token is plaintext on the wire otherwise. The server prints a
241
+ warning when it is exposed without a token.
242
+
243
+ On the default loopback bind, requests whose `Host` or `Origin` is not local are refused. That is what stops a web
244
+ page from using DNS rebinding to reach a server on `127.0.0.1` through your browser.
245
+
246
+ ## Development
247
+
248
+ ```bash
249
+ npm install
250
+ npm run build
251
+ npm test
252
+
253
+ npm run db:up # throwaway Postgres for the integration suite (needs Docker)
254
+ TEST_DATABASE_URL=postgres://postgres:postgres@localhost:55432/postgres npm test
255
+ npm run db:down
256
+ ```
257
+
258
+ The integration suite deliberately bypasses the parser and drives the database directly, so it proves the wire and
259
+ transaction layers rather than the validator. It skips without `TEST_DATABASE_URL` and is required to run in CI.
260
+
261
+ ## Security
262
+
263
+ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md).
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,47 @@
1
+ export interface HttpConfig {
2
+ host: string;
3
+ port: number;
4
+ /** From the AUTH_TOKEN environment variable. Unset means no authentication. */
5
+ authToken: string | undefined;
6
+ }
7
+ export interface ServerConfig {
8
+ command: 'serve' | 'init';
9
+ connectionString: string;
10
+ maxRows: number;
11
+ maxOutputChars: number;
12
+ statementTimeoutMs: number;
13
+ /** Absolute. */
14
+ contextFile: string;
15
+ /** Whether the add_table_context tool is offered. */
16
+ contextWrites: boolean;
17
+ contextDirectory: string;
18
+ logFile: string | undefined;
19
+ logDisabled: boolean;
20
+ /** Undefined means stdio. */
21
+ http: HttpConfig | undefined;
22
+ }
23
+ export declare const DEFAULT_MAX_ROWS = 1000;
24
+ /** About 12k tokens: room for a real answer, not enough to crowd out the conversation. */
25
+ export declare const DEFAULT_MAX_OUTPUT_CHARS = 50000;
26
+ export declare const DEFAULT_STATEMENT_TIMEOUT_MS = 30000;
27
+ export declare const DEFAULT_HTTP_PORT = 8080;
28
+ /** Loopback by default: exposing a database to the network should take a deliberate flag. */
29
+ export declare const DEFAULT_HTTP_HOST = "127.0.0.1";
30
+ export declare class ConfigError extends Error {
31
+ }
32
+ /** Not a failure — `--help` is a successful invocation that prints and exits 0. */
33
+ export declare class HelpRequested extends Error {
34
+ constructor();
35
+ }
36
+ export declare const USAGE = "postgres-mcp \u2014 the analytics MCP server for Postgres (read-only)\n\nUsage:\n npx @contextflo/postgres-mcp <connection-string> [options]\n npx @contextflo/postgres-mcp init <connection-string>\n\nThe connection string may also be supplied via the DATABASE_URL environment variable.\n\nCommands:\n init Scan the schema and write .contextflo/context.md for you to edit\n\nOptions:\n --max-rows <n> Maximum rows returned per query (default: 1000)\n --max-output-chars <n> Character budget for one query result (default: 50000)\n --statement-timeout <ms> Server-side statement timeout in milliseconds (default: 30000)\n --context-file <path> Curated schema context served to the model (default: .contextflo/context.md)\n --no-context-writes Do not offer add_table_context; the context file is only read\n --log-file <path> Append a query audit log here (default: .contextflo/log.md once that directory exists)\n --no-log Never write a query log\n --http Serve over streamable HTTP instead of stdio\n --port <n> HTTP port (default: 8080)\n --host <addr> HTTP bind address (default: 127.0.0.1; use 0.0.0.0 to expose)\n -h, --help Show this message\n\nEnvironment:\n DATABASE_URL Connection string, if not given as an argument\n AUTH_TOKEN If set with --http, require this as a bearer token";
37
+ /**
38
+ * The connection URL is the first positional argument, matching the archived
39
+ * `@modelcontextprotocol/server-postgres` so migrating is a package-name swap.
40
+ */
41
+ export declare function parseArgs(argv: string[], env?: NodeJS.ProcessEnv, cwd?: string): ServerConfig;
42
+ /**
43
+ * Relative paths resolve against the working directory — except when that is the
44
+ * filesystem root, which is where Claude Desktop and some other clients start servers.
45
+ * `/.contextflo/context.md` is never what anyone meant, so fall back to the home directory.
46
+ */
47
+ export declare function resolveContextFile(path: string, cwd: string, home?: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,159 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, isAbsolute, resolve } from 'node:path';
3
+ import { DEFAULT_CONTEXT_DIRECTORY, DEFAULT_CONTEXT_FILE } from './context/context-file.js';
4
+ export const DEFAULT_MAX_ROWS = 1000;
5
+ /** About 12k tokens: room for a real answer, not enough to crowd out the conversation. */
6
+ export const DEFAULT_MAX_OUTPUT_CHARS = 50_000;
7
+ export const DEFAULT_STATEMENT_TIMEOUT_MS = 30_000;
8
+ export const DEFAULT_HTTP_PORT = 8080;
9
+ /** Loopback by default: exposing a database to the network should take a deliberate flag. */
10
+ export const DEFAULT_HTTP_HOST = '127.0.0.1';
11
+ export class ConfigError extends Error {
12
+ }
13
+ /** Not a failure — `--help` is a successful invocation that prints and exits 0. */
14
+ export class HelpRequested extends Error {
15
+ constructor() {
16
+ super(USAGE);
17
+ }
18
+ }
19
+ export const USAGE = `postgres-mcp — the analytics MCP server for Postgres (read-only)
20
+
21
+ Usage:
22
+ npx @contextflo/postgres-mcp <connection-string> [options]
23
+ npx @contextflo/postgres-mcp init <connection-string>
24
+
25
+ The connection string may also be supplied via the DATABASE_URL environment variable.
26
+
27
+ Commands:
28
+ init Scan the schema and write ${DEFAULT_CONTEXT_FILE} for you to edit
29
+
30
+ Options:
31
+ --max-rows <n> Maximum rows returned per query (default: ${DEFAULT_MAX_ROWS})
32
+ --max-output-chars <n> Character budget for one query result (default: ${DEFAULT_MAX_OUTPUT_CHARS})
33
+ --statement-timeout <ms> Server-side statement timeout in milliseconds (default: ${DEFAULT_STATEMENT_TIMEOUT_MS})
34
+ --context-file <path> Curated schema context served to the model (default: ${DEFAULT_CONTEXT_FILE})
35
+ --no-context-writes Do not offer add_table_context; the context file is only read
36
+ --log-file <path> Append a query audit log here (default: ${DEFAULT_CONTEXT_DIRECTORY}/log.md once that directory exists)
37
+ --no-log Never write a query log
38
+ --http Serve over streamable HTTP instead of stdio
39
+ --port <n> HTTP port (default: ${DEFAULT_HTTP_PORT})
40
+ --host <addr> HTTP bind address (default: ${DEFAULT_HTTP_HOST}; use 0.0.0.0 to expose)
41
+ -h, --help Show this message
42
+
43
+ Environment:
44
+ DATABASE_URL Connection string, if not given as an argument
45
+ AUTH_TOKEN If set with --http, require this as a bearer token`;
46
+ /**
47
+ * The connection URL is the first positional argument, matching the archived
48
+ * `@modelcontextprotocol/server-postgres` so migrating is a package-name swap.
49
+ */
50
+ export function parseArgs(argv, env = process.env, cwd = process.cwd()) {
51
+ let command = 'serve';
52
+ let connectionString;
53
+ let maxRows = DEFAULT_MAX_ROWS;
54
+ let maxOutputChars = DEFAULT_MAX_OUTPUT_CHARS;
55
+ let contextWrites = true;
56
+ let statementTimeoutMs = DEFAULT_STATEMENT_TIMEOUT_MS;
57
+ let contextFile = DEFAULT_CONTEXT_FILE;
58
+ let logFile;
59
+ let logDisabled = false;
60
+ let http = false;
61
+ let port = DEFAULT_HTTP_PORT;
62
+ let host = DEFAULT_HTTP_HOST;
63
+ for (let index = 0; index < argv.length; index++) {
64
+ const arg = argv[index];
65
+ switch (arg) {
66
+ case 'init':
67
+ if (index !== 0)
68
+ throw new ConfigError(`"init" must come first.\n\n${USAGE}`);
69
+ command = 'init';
70
+ break;
71
+ case '--max-rows':
72
+ maxRows = requirePositiveInteger(arg, argv[++index]);
73
+ break;
74
+ case '--max-output-chars':
75
+ maxOutputChars = requirePositiveInteger(arg, argv[++index]);
76
+ break;
77
+ case '--no-context-writes':
78
+ contextWrites = false;
79
+ break;
80
+ case '--statement-timeout':
81
+ statementTimeoutMs = requirePositiveInteger(arg, argv[++index]);
82
+ break;
83
+ case '--context-file':
84
+ contextFile = requireValue(arg, argv[++index]);
85
+ break;
86
+ case '--log-file':
87
+ logFile = requireValue(arg, argv[++index]);
88
+ break;
89
+ case '--no-log':
90
+ logDisabled = true;
91
+ break;
92
+ case '--http':
93
+ http = true;
94
+ break;
95
+ case '--port':
96
+ port = requirePositiveInteger(arg, argv[++index]);
97
+ break;
98
+ case '--host':
99
+ host = requireValue(arg, argv[++index]);
100
+ break;
101
+ case '-h':
102
+ case '--help':
103
+ throw new HelpRequested();
104
+ default:
105
+ if (arg.startsWith('-')) {
106
+ throw new ConfigError(`Unknown option: ${arg}\n\n${USAGE}`);
107
+ }
108
+ if (connectionString !== undefined) {
109
+ throw new ConfigError(`Unexpected argument: ${arg}\n\n${USAGE}`);
110
+ }
111
+ connectionString = arg;
112
+ }
113
+ }
114
+ connectionString ??= env.DATABASE_URL;
115
+ if (!connectionString) {
116
+ throw new ConfigError(`A Postgres connection string is required.\n\n${USAGE}`);
117
+ }
118
+ if (logFile && logDisabled) {
119
+ throw new ConfigError('--log-file and --no-log contradict each other.');
120
+ }
121
+ const resolvedContextFile = resolveContextFile(contextFile, cwd);
122
+ return {
123
+ command,
124
+ connectionString,
125
+ maxRows,
126
+ maxOutputChars,
127
+ statementTimeoutMs,
128
+ contextFile: resolvedContextFile,
129
+ contextWrites,
130
+ contextDirectory: dirname(resolvedContextFile),
131
+ logFile,
132
+ logDisabled,
133
+ http: http ? { host, port, authToken: env.AUTH_TOKEN || undefined } : undefined,
134
+ };
135
+ }
136
+ /**
137
+ * Relative paths resolve against the working directory — except when that is the
138
+ * filesystem root, which is where Claude Desktop and some other clients start servers.
139
+ * `/.contextflo/context.md` is never what anyone meant, so fall back to the home directory.
140
+ */
141
+ export function resolveContextFile(path, cwd, home = homedir()) {
142
+ if (isAbsolute(path))
143
+ return path;
144
+ return resolve(cwd === '/' || /^[A-Za-z]:\\?$/.test(cwd) ? home : cwd, path);
145
+ }
146
+ function requireValue(flag, raw) {
147
+ if (!raw || raw.startsWith('-')) {
148
+ throw new ConfigError(`${flag} expects a value, received: ${raw ?? '(nothing)'}`);
149
+ }
150
+ return raw;
151
+ }
152
+ function requirePositiveInteger(flag, raw) {
153
+ const value = Number(raw);
154
+ if (!raw || !Number.isInteger(value) || value <= 0) {
155
+ throw new ConfigError(`${flag} expects a positive integer, received: ${raw ?? '(nothing)'}`);
156
+ }
157
+ return value;
158
+ }
159
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `.contextflo/context.md` — the whole context layer.
3
+ *
4
+ * No database, no embeddings, no index: a markdown file a human edits and this server
5
+ * hands to the model. The catalog can say a column is `numeric(12,2)` named `revenue`;
6
+ * only a person can say it is gross rather than net, or that `fct_orders_v2` is the one
7
+ * the team actually uses.
8
+ *
9
+ * Parsing is deliberately forgiving. This file's job is to be edited by hand, so anything
10
+ * it does not recognise is left alone rather than treated as an error — a file that
11
+ * rejects your notes is a file you stop updating.
12
+ */
13
+ export declare const DEFAULT_CONTEXT_DIRECTORY = ".contextflo";
14
+ export declare const DEFAULT_CONTEXT_FILE = ".contextflo/context.md";
15
+ export interface TableNotes {
16
+ description?: string;
17
+ columns: Map<string, string>;
18
+ }
19
+ export interface ContextDocument {
20
+ /** Free text above `## Tables`: business definitions, caveats, which tables to prefer. */
21
+ preamble: string;
22
+ /** Keyed by lower-cased `schema.table`. */
23
+ tables: Map<string, TableNotes>;
24
+ }
25
+ export declare function emptyContextDocument(): ContextDocument;
26
+ /** Returns null when the file does not exist — the server runs fine without one. */
27
+ export declare function loadContextFile(path: string): Promise<ContextDocument | null>;
28
+ export declare function parseContextFile(raw: string): ContextDocument;
29
+ export declare function notesForTable(document: ContextDocument, fullyQualifiedName: string): TableNotes | undefined;
30
+ /**
31
+ * The curated file wins over the database comment. `init` seeds the file from comments,
32
+ * so the two agree until somebody edits — and an edit is exactly the signal to prefer it.
33
+ */
34
+ export declare function preferCuratedDescription(curated: string | undefined, fromCatalog: string | null): string | null;
35
+ export interface NewTableNotes {
36
+ /** Appended to the table's description. */
37
+ note?: string | undefined;
38
+ /** Column name → note, appended to that column's line or added as a new one. */
39
+ columns?: Record<string, string> | undefined;
40
+ }
41
+ /**
42
+ * Adds notes to the file's text, touching nothing else — the file belongs to the humans
43
+ * editing it, so formatting, comments, and ordering all survive.
44
+ *
45
+ * Append-only by design. A note never replaces what is there: when the table or column
46
+ * already has a description, the note is added after it. Anything an agent writes can be
47
+ * reviewed in a diff and deleted, and nothing a person wrote is ever lost to it.
48
+ *
49
+ * `fullyQualifiedName` is the resolved `schema.table`. An existing section headed with
50
+ * the bare table name is reused rather than duplicated.
51
+ */
52
+ export declare function addTableNotes(raw: string, fullyQualifiedName: string, notes: NewTableNotes): string;
53
+ export declare const CONTEXT_FILE_HEADER = "# Database context\n\n<!--\npostgres-mcp reads this file and hands it to the model every session. It is the whole\ncontext layer \u2014 no database, no index, just this file.\n\nEverything above \"## Tables\" is passed through verbatim. Put the things your team\nargues about here: whether revenue is gross or net, what counts as an active customer,\nwhich table is the source of truth and which one nobody got around to dropping.\n\nUnder \"## Tables\", a \"###\" heading names a table, the prose beneath it describes the\ntable, and each \"- column \u2014 meaning\" line describes a column. Seeded from your\ndatabase's COMMENT ON values; anything you write wins over those.\n\nAdd a line for any column you like \u2014 only already-commented ones were seeded, to keep\nthis file small enough that you will actually edit it.\n\nThe agent can add notes here too, with the add_table_context tool, when it finds\nsomething about the data the next person would get wrong. It only ever appends, so\nreview those additions in a diff like any other change.\n-->\n\n## Business definitions\n\n_Replace this with the definitions a new analyst would get wrong on their first day._\n\n## Tables\n";