@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,138 @@
1
+ import { describeQueryError } from '../db/errors.js';
2
+ import { SafetyError } from '../safety/errors.js';
3
+ import { validateReadOnlySql } from '../safety/validate.js';
4
+ /**
5
+ * Name, argument, and result shape match the archived
6
+ * `@modelcontextprotocol/server-postgres` so existing prompts and configs keep working
7
+ * when someone swaps the package name.
8
+ */
9
+ export const QUERY_TOOL = {
10
+ name: 'query',
11
+ description: 'Run a read-only SQL query against the Postgres database. Accepts a single SELECT ' +
12
+ '(including WITH ... SELECT), EXPLAIN, or SHOW statement. Writes, DDL, multiple ' +
13
+ 'statements, and SET are rejected. information_schema and pg_catalog are readable, so ' +
14
+ 'schema questions this server has no tool for can be answered with plain SQL.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ sql: {
19
+ type: 'string',
20
+ description: 'A single read-only SQL statement.',
21
+ },
22
+ },
23
+ required: ['sql'],
24
+ },
25
+ };
26
+ export async function runQueryTool(context, args) {
27
+ const sql = args?.sql;
28
+ if (typeof sql !== 'string' || sql.trim() === '') {
29
+ return toolError('The "sql" argument is required and must be a non-empty string.');
30
+ }
31
+ try {
32
+ validateReadOnlySql(sql);
33
+ }
34
+ catch (error) {
35
+ if (error instanceof SafetyError) {
36
+ context.log.record({ sql, outcome: 'rejected', message: error.message });
37
+ return toolError(error.message);
38
+ }
39
+ throw error;
40
+ }
41
+ const startedAt = Date.now();
42
+ let result;
43
+ try {
44
+ result = await context.database.runReadOnly(sql, context.maxRows);
45
+ }
46
+ catch (error) {
47
+ const message = describeQueryError(error);
48
+ context.log.record({ sql, outcome: 'error', durationMs: Date.now() - startedAt, message });
49
+ return toolError(message);
50
+ }
51
+ context.log.record({
52
+ sql,
53
+ outcome: 'ok',
54
+ rowCount: result.rows.length,
55
+ durationMs: Date.now() - startedAt,
56
+ });
57
+ const rendered = renderRows(result.rows, context.maxOutputChars);
58
+ const content = [{ type: 'text', text: rendered.text }];
59
+ const notes = [];
60
+ if (rendered.shown < result.rows.length) {
61
+ notes.push(`Output truncated to ${rendered.shown} of ${result.rows.length} rows to stay under ` +
62
+ `${context.maxOutputChars.toLocaleString('en-US')} characters. Select fewer columns, add LIMIT, ` +
63
+ 'or aggregate. --max-output-chars raises the budget.');
64
+ }
65
+ if (result.truncated) {
66
+ notes.push(`Results truncated to ${context.maxRows} rows. Add LIMIT/aggregation to narrow the query, ` +
67
+ 'or start the server with --max-rows to raise the cap.');
68
+ }
69
+ if (rendered.cellsShortened) {
70
+ notes.push(`Values longer than ${MAX_CELL_CHARS.toLocaleString('en-US')} characters were shortened.`);
71
+ }
72
+ if (notes.length > 0)
73
+ content.push({ type: 'text', text: notes.join('\n') });
74
+ return { content, isError: false };
75
+ }
76
+ /**
77
+ * A JSON array with one row per line: still valid JSON for anything that parses it, but
78
+ * without the indentation that roughly doubled the token cost of every result.
79
+ *
80
+ * Rows are added until the character budget is spent — a `SELECT *` over a table with a
81
+ * wide jsonb column should not blow the model's context window on its first try.
82
+ */
83
+ function renderRows(rows, maxChars) {
84
+ const state = { cellsShortened: false };
85
+ const lines = [];
86
+ let used = 2;
87
+ for (const row of rows) {
88
+ const line = JSON.stringify(toJsonSafe(row, state));
89
+ // Always show at least one row, even if it alone is over budget.
90
+ if (lines.length > 0 && used + line.length + 2 > maxChars)
91
+ break;
92
+ lines.push(line);
93
+ used += line.length + 2;
94
+ }
95
+ const text = lines.length === 0 ? '[]' : `[\n${lines.join(',\n')}\n]`;
96
+ return { text, shown: lines.length, cellsShortened: state.cellsShortened };
97
+ }
98
+ function toolError(message) {
99
+ // Returned as a tool error rather than thrown so the model sees it and can correct the
100
+ // query itself, instead of the client surfacing a protocol-level failure.
101
+ return { content: [{ type: 'text', text: message }], isError: true };
102
+ }
103
+ /** Bytes rendered as `{"type":"Buffer","data":[…]}` are unreadable and expensive in tokens. */
104
+ const MAX_RENDERED_BYTES = 32;
105
+ /** One long text or jsonb value should not crowd every other row out of the budget. */
106
+ const MAX_CELL_CHARS = 2_000;
107
+ function toJsonSafe(value, state, isRow = true) {
108
+ if (Buffer.isBuffer(value)) {
109
+ return renderBytes(value);
110
+ }
111
+ if (typeof value === 'bigint') {
112
+ return value.toString();
113
+ }
114
+ if (typeof value === 'string') {
115
+ return shorten(value, value, state);
116
+ }
117
+ if (isRow && value !== null && typeof value === 'object') {
118
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJsonSafe(item, state, false)]));
119
+ }
120
+ if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
121
+ // An array or jsonb value is one cell: keep it structured when it fits, shorten its
122
+ // serialised form when it does not.
123
+ const serialised = JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? item.toString() : item);
124
+ return shorten(serialised, value, state);
125
+ }
126
+ return value;
127
+ }
128
+ function shorten(serialised, original, state) {
129
+ if (serialised.length <= MAX_CELL_CHARS)
130
+ return original;
131
+ state.cellsShortened = true;
132
+ return `${serialised.slice(0, MAX_CELL_CHARS)}… (${serialised.length.toLocaleString('en-US')} chars)`;
133
+ }
134
+ function renderBytes(value) {
135
+ const hex = value.subarray(0, MAX_RENDERED_BYTES).toString('hex');
136
+ return value.length > MAX_RENDERED_BYTES ? `\\x${hex}… (${value.length} bytes)` : `\\x${hex}`;
137
+ }
138
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/tools/query.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@contextflo/postgres-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Read-only Postgres MCP server: a drop-in replacement for the archived @modelcontextprotocol/server-postgres, with schema context for correct answers.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "postgres",
9
+ "postgresql",
10
+ "mcp-server",
11
+ "read-only",
12
+ "claude",
13
+ "cursor",
14
+ "text-to-sql"
15
+ ],
16
+ "homepage": "https://github.com/contextflo/postgres-mcp#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/contextflo/postgres-mcp.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/contextflo/postgres-mcp/issues"
23
+ },
24
+ "author": "ContextFlo (https://contextflo.com)",
25
+ "license": "MIT",
26
+ "mcpName": "io.github.contextflo/postgres-mcp",
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "bin": {
32
+ "postgres-mcp": "dist/index.js"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "db:up": "docker compose -f docker-compose.test.yml up -d --wait",
42
+ "db:down": "docker compose -f docker-compose.test.yml down -v",
43
+ "prepublishOnly": "npm run build && npm test"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.30.0",
47
+ "libpg-query": "^17.7.4",
48
+ "pg": "^8.16.3",
49
+ "pg-connection-string": "^2.9.1",
50
+ "pg-cursor": "^2.15.3"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.15.3",
54
+ "@types/pg": "^8.15.5",
55
+ "@types/pg-cursor": "^2.7.2",
56
+ "typescript": "^5.9.2",
57
+ "vitest": "^3.2.4"
58
+ }
59
+ }