@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,248 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ /**
3
+ * `.contextflo/context.md` — the whole context layer.
4
+ *
5
+ * No database, no embeddings, no index: a markdown file a human edits and this server
6
+ * hands to the model. The catalog can say a column is `numeric(12,2)` named `revenue`;
7
+ * only a person can say it is gross rather than net, or that `fct_orders_v2` is the one
8
+ * the team actually uses.
9
+ *
10
+ * Parsing is deliberately forgiving. This file's job is to be edited by hand, so anything
11
+ * it does not recognise is left alone rather than treated as an error — a file that
12
+ * rejects your notes is a file you stop updating.
13
+ */
14
+ export const DEFAULT_CONTEXT_DIRECTORY = '.contextflo';
15
+ export const DEFAULT_CONTEXT_FILE = `${DEFAULT_CONTEXT_DIRECTORY}/context.md`;
16
+ /** Everything above this heading is passed to the model verbatim. */
17
+ const TABLES_HEADING = /^##\s+tables\s*$/i;
18
+ const TABLE_HEADING = /^###\s+(.+?)\s*$/;
19
+ /** `- \`col\` — meaning`, `- col: meaning`, `- col - meaning`. */
20
+ const COLUMN_LINE = /^[-*]\s+`?([A-Za-z_][\w$]*)`?\s*(?:[—–:-])\s*(.+)$/;
21
+ export function emptyContextDocument() {
22
+ return { preamble: '', tables: new Map() };
23
+ }
24
+ /** Returns null when the file does not exist — the server runs fine without one. */
25
+ export async function loadContextFile(path) {
26
+ let raw;
27
+ try {
28
+ raw = await readFile(path, 'utf8');
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT')
32
+ return null;
33
+ throw error;
34
+ }
35
+ return parseContextFile(raw);
36
+ }
37
+ export function parseContextFile(raw) {
38
+ const document = emptyContextDocument();
39
+ const lines = raw.split(/\r?\n/);
40
+ let index = 0;
41
+ const preamble = [];
42
+ while (index < lines.length && !TABLES_HEADING.test(lines[index])) {
43
+ preamble.push(lines[index]);
44
+ index++;
45
+ }
46
+ // HTML comments are notes to the human editing the file (init writes one), not to the model.
47
+ document.preamble = preamble.join('\n').replace(/<!--[\s\S]*?-->/g, '').replace(/\n{3,}/g, '\n\n').trim();
48
+ index++; // step past "## Tables"
49
+ let current;
50
+ let descriptionLines = [];
51
+ const flush = () => {
52
+ if (current) {
53
+ const description = descriptionLines.join(' ').trim();
54
+ if (description)
55
+ current.description = description;
56
+ }
57
+ descriptionLines = [];
58
+ };
59
+ for (; index < lines.length; index++) {
60
+ const line = lines[index];
61
+ const heading = TABLE_HEADING.exec(line);
62
+ if (heading) {
63
+ flush();
64
+ current = { columns: new Map() };
65
+ document.tables.set(heading[1].trim().toLowerCase(), current);
66
+ continue;
67
+ }
68
+ if (!current)
69
+ continue;
70
+ const column = COLUMN_LINE.exec(line);
71
+ if (column) {
72
+ flush();
73
+ current.columns.set(column[1].toLowerCase(), column[2].trim());
74
+ continue;
75
+ }
76
+ // Prose between the heading and the first bullet is the table's description.
77
+ if (current.columns.size === 0 && line.trim() !== '' && !line.startsWith('#')) {
78
+ descriptionLines.push(line.trim());
79
+ }
80
+ }
81
+ flush();
82
+ return document;
83
+ }
84
+ export function notesForTable(document, fullyQualifiedName) {
85
+ const notes = document.tables.get(fullyQualifiedName.toLowerCase());
86
+ if (notes)
87
+ return notes;
88
+ // Tolerate an unqualified heading like "### orders" for a single-schema database.
89
+ const bare = fullyQualifiedName.split('.').pop();
90
+ return bare ? document.tables.get(bare.toLowerCase()) : undefined;
91
+ }
92
+ /**
93
+ * The curated file wins over the database comment. `init` seeds the file from comments,
94
+ * so the two agree until somebody edits — and an edit is exactly the signal to prefer it.
95
+ */
96
+ export function preferCuratedDescription(curated, fromCatalog) {
97
+ return curated ?? fromCatalog;
98
+ }
99
+ /**
100
+ * Adds notes to the file's text, touching nothing else — the file belongs to the humans
101
+ * editing it, so formatting, comments, and ordering all survive.
102
+ *
103
+ * Append-only by design. A note never replaces what is there: when the table or column
104
+ * already has a description, the note is added after it. Anything an agent writes can be
105
+ * reviewed in a diff and deleted, and nothing a person wrote is ever lost to it.
106
+ *
107
+ * `fullyQualifiedName` is the resolved `schema.table`. An existing section headed with
108
+ * the bare table name is reused rather than duplicated.
109
+ */
110
+ export function addTableNotes(raw, fullyQualifiedName, notes) {
111
+ const lines = (raw.trim() === '' ? CONTEXT_FILE_HEADER : raw).replace(/\r\n/g, '\n').split('\n');
112
+ if (!lines.some((line) => TABLES_HEADING.test(line))) {
113
+ trimTrailingBlankLines(lines);
114
+ lines.push('', '## Tables', '');
115
+ }
116
+ // A leading bullet would make the parser read the note as a column line.
117
+ const tableNote = notes.note ? singleLine(notes.note).replace(/^[-*]\s+/, '') : undefined;
118
+ const columnNotes = Object.entries(notes.columns ?? {})
119
+ .map(([column, note]) => [column, singleLine(note)])
120
+ .filter(([, note]) => note !== '');
121
+ const section = findTableSection(lines, fullyQualifiedName);
122
+ if (!section) {
123
+ trimTrailingBlankLines(lines);
124
+ lines.push('', `### ${fullyQualifiedName}`);
125
+ if (tableNote)
126
+ lines.push(tableNote);
127
+ if (columnNotes.length > 0) {
128
+ lines.push('');
129
+ for (const [column, note] of columnNotes)
130
+ lines.push(`- ${column} — ${note}`);
131
+ }
132
+ return `${lines.join('\n')}\n`;
133
+ }
134
+ let end = section.end;
135
+ for (const [column, note] of columnNotes) {
136
+ const existing = findColumnLine(lines, section.start, end, column);
137
+ if (existing !== -1) {
138
+ lines[existing] = appendSentence(lines[existing], note);
139
+ continue;
140
+ }
141
+ // After the section's last column line; or, for the first one, after the prose with a
142
+ // blank line between, which is how the parser tells a description from a column list.
143
+ const lastColumn = findLastColumnLine(lines, section.start, end);
144
+ const inserted = lastColumn === -1 ? ['', `- ${column} — ${note}`] : [`- ${column} — ${note}`];
145
+ const at = lastColumn === -1 ? lastContentLine(lines, section.start, end) + 1 : lastColumn + 1;
146
+ lines.splice(at, 0, ...inserted);
147
+ end += inserted.length;
148
+ }
149
+ if (tableNote && !sectionText(lines, section.start, end).includes(tableNote)) {
150
+ // The parser reads prose between the heading and the first column line as the
151
+ // description, so the note goes at the end of that prose.
152
+ const firstColumn = findFirstColumnLine(lines, section.start, end);
153
+ const proseEnd = lastContentLine(lines, section.start, firstColumn === -1 ? end : firstColumn);
154
+ lines.splice(proseEnd + 1, 0, tableNote);
155
+ }
156
+ return `${lines.join('\n').replace(/\n*$/, '')}\n`;
157
+ }
158
+ /** Section bounds as [start, end): `start` is the heading line. */
159
+ function findTableSection(lines, fullyQualifiedName) {
160
+ const tablesAt = lines.findIndex((line) => TABLES_HEADING.test(line));
161
+ const wanted = fullyQualifiedName.toLowerCase();
162
+ const bare = wanted.split('.').pop();
163
+ const headings = lines
164
+ .map((line, index) => ({ index, name: TABLE_HEADING.exec(line)?.[1]?.trim().toLowerCase() }))
165
+ .filter((heading) => heading.index > tablesAt && heading.name !== undefined);
166
+ const match = headings.find((heading) => heading.name === wanted) ?? headings.find((heading) => heading.name === bare);
167
+ if (!match)
168
+ return undefined;
169
+ let end = match.index + 1;
170
+ while (end < lines.length && !/^#{1,3}\s/.test(lines[end]))
171
+ end++;
172
+ return { start: match.index, end };
173
+ }
174
+ function findColumnLine(lines, start, end, column) {
175
+ for (let index = start + 1; index < end; index++) {
176
+ if (COLUMN_LINE.exec(lines[index])?.[1]?.toLowerCase() === column.toLowerCase())
177
+ return index;
178
+ }
179
+ return -1;
180
+ }
181
+ function findFirstColumnLine(lines, start, end) {
182
+ for (let index = start + 1; index < end; index++) {
183
+ if (COLUMN_LINE.test(lines[index]))
184
+ return index;
185
+ }
186
+ return -1;
187
+ }
188
+ function findLastColumnLine(lines, start, end) {
189
+ for (let index = end - 1; index > start; index--) {
190
+ if (COLUMN_LINE.test(lines[index]))
191
+ return index;
192
+ }
193
+ return -1;
194
+ }
195
+ /** The last non-blank line in (start, end), or `start` itself when there is none. */
196
+ function lastContentLine(lines, start, end) {
197
+ for (let index = end - 1; index > start; index--) {
198
+ if (lines[index].trim() !== '')
199
+ return index;
200
+ }
201
+ return start;
202
+ }
203
+ function sectionText(lines, start, end) {
204
+ return lines.slice(start, end).join('\n');
205
+ }
206
+ function appendSentence(line, note) {
207
+ if (line.includes(note))
208
+ return line;
209
+ const trimmed = line.trimEnd();
210
+ return /[.!?]$/.test(trimmed) ? `${trimmed} ${note}` : `${trimmed}. ${note}`;
211
+ }
212
+ /** A note is one line: a newline would end the column line or split the description. */
213
+ function singleLine(text) {
214
+ return text.replace(/\s+/g, ' ').trim();
215
+ }
216
+ function trimTrailingBlankLines(lines) {
217
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '')
218
+ lines.pop();
219
+ }
220
+ export const CONTEXT_FILE_HEADER = `# Database context
221
+
222
+ <!--
223
+ postgres-mcp reads this file and hands it to the model every session. It is the whole
224
+ context layer — no database, no index, just this file.
225
+
226
+ Everything above "## Tables" is passed through verbatim. Put the things your team
227
+ argues about here: whether revenue is gross or net, what counts as an active customer,
228
+ which table is the source of truth and which one nobody got around to dropping.
229
+
230
+ Under "## Tables", a "###" heading names a table, the prose beneath it describes the
231
+ table, and each "- column — meaning" line describes a column. Seeded from your
232
+ database's COMMENT ON values; anything you write wins over those.
233
+
234
+ Add a line for any column you like — only already-commented ones were seeded, to keep
235
+ this file small enough that you will actually edit it.
236
+
237
+ The agent can add notes here too, with the add_table_context tool, when it finds
238
+ something about the data the next person would get wrong. It only ever appends, so
239
+ review those additions in a diff like any other change.
240
+ -->
241
+
242
+ ## Business definitions
243
+
244
+ _Replace this with the definitions a new analyst would get wrong on their first day._
245
+
246
+ ## Tables
247
+ `;
248
+ //# sourceMappingURL=context-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-file.js","sourceRoot":"","sources":["../../src/context/context-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Database } from '../db/pool.js';
2
+ export interface InitResult {
3
+ path: string;
4
+ tableCount: number;
5
+ seededColumnCount: number;
6
+ truncated: boolean;
7
+ schemas: string[];
8
+ }
9
+ export declare class ContextFileExists extends Error {
10
+ constructor(path: string);
11
+ }
12
+ export declare function runInit(database: Database, contextFile: string): Promise<InitResult>;
13
+ /** The setup that makes read-only true regardless of any bug in this server. */
14
+ export declare function readOnlyRoleSnippet(databaseName: string, schemas: string[]): string;
@@ -0,0 +1,71 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { getTableContext, listTables } from '../db/introspection.js';
4
+ import { CONTEXT_FILE_HEADER } from './context-file.js';
5
+ /**
6
+ * `init` — scan the schema and write a context file worth editing.
7
+ *
8
+ * The temptation is to emit a placeholder line for every column of every table. On a
9
+ * real warehouse that is thousands of blank lines, and a file nobody edits is a file
10
+ * that does nothing. So: every table gets a heading, but only columns that already carry
11
+ * a COMMENT are seeded. The file stays proportional to what is already documented, and
12
+ * adding a line for an undocumented column is one line of typing.
13
+ */
14
+ const MAX_TABLES = 1000;
15
+ export class ContextFileExists extends Error {
16
+ constructor(path) {
17
+ super(`${path} already exists. Delete it to regenerate, or pass --context-file to write elsewhere — ` +
18
+ 'refusing to overwrite notes you may have written by hand.');
19
+ }
20
+ }
21
+ export async function runInit(database, contextFile) {
22
+ const { tables, totalMatches } = await listTables(database, { limit: MAX_TABLES });
23
+ const described = await getTableContext(database, tables.map((table) => table.fullyQualifiedName));
24
+ const markdown = renderContextFile(described);
25
+ await mkdir(dirname(contextFile), { recursive: true });
26
+ // 'wx' fails if the file exists, so a hand-edited context file is never clobbered.
27
+ try {
28
+ await writeFile(contextFile, markdown, { encoding: 'utf8', flag: 'wx' });
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'EEXIST')
32
+ throw new ContextFileExists(contextFile);
33
+ throw error;
34
+ }
35
+ return {
36
+ path: contextFile,
37
+ tableCount: described.length,
38
+ seededColumnCount: described.reduce((total, table) => total + table.columns.filter((column) => column.description).length, 0),
39
+ truncated: totalMatches > tables.length,
40
+ schemas: [...new Set(described.map((table) => table.fullyQualifiedName.split('.')[0]))].sort(),
41
+ };
42
+ }
43
+ function renderContextFile(tables) {
44
+ const sections = tables.map((table) => {
45
+ const lines = [`### ${table.fullyQualifiedName}`];
46
+ lines.push(table.description ?? '');
47
+ const documented = table.columns.filter((column) => column.description);
48
+ if (documented.length > 0) {
49
+ lines.push('');
50
+ for (const column of documented) {
51
+ lines.push(`- ${column.name} — ${column.description}`);
52
+ }
53
+ }
54
+ return lines.join('\n');
55
+ });
56
+ return `${CONTEXT_FILE_HEADER}\n${sections.join('\n\n')}\n`;
57
+ }
58
+ /** The setup that makes read-only true regardless of any bug in this server. */
59
+ export function readOnlyRoleSnippet(databaseName, schemas) {
60
+ const grants = schemas
61
+ .flatMap((schema) => [
62
+ `GRANT USAGE ON SCHEMA ${schema} TO mcp_readonly;`,
63
+ `GRANT SELECT ON ALL TABLES IN SCHEMA ${schema} TO mcp_readonly;`,
64
+ `ALTER DEFAULT PRIVILEGES IN SCHEMA ${schema} GRANT SELECT ON TABLES TO mcp_readonly;`,
65
+ ])
66
+ .join('\n');
67
+ return `CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
68
+ GRANT CONNECT ON DATABASE ${databaseName} TO mcp_readonly;
69
+ ${grants}`;
70
+ }
71
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/context/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type ContextDocument, type NewTableNotes } from './context-file.js';
2
+ /**
3
+ * The context file as the tools see it: re-read whenever it changes on disk, so an edit
4
+ * someone makes mid-session — or a note the agent just added — shows up on the next tool
5
+ * call without restarting the server.
6
+ *
7
+ * Only the preamble is fixed at startup, because MCP delivers server instructions once,
8
+ * at initialisation.
9
+ */
10
+ export declare class ContextStore {
11
+ readonly path: string | null;
12
+ /** False when context writes were turned off, or there is no file to write to. */
13
+ readonly writable: boolean;
14
+ private document;
15
+ private loadedMtimeMs;
16
+ /** Writes are chained so two notes added at once cannot clobber each other. */
17
+ private pendingWrite;
18
+ private constructor();
19
+ static open(path: string, options: {
20
+ writable: boolean;
21
+ }): Promise<ContextStore>;
22
+ /** A fixed document with no file behind it. */
23
+ static inMemory(document?: ContextDocument): ContextStore;
24
+ /** The document as last loaded. */
25
+ get current(): ContextDocument;
26
+ /**
27
+ * Re-reads the file if it changed since the last load; returns the current document.
28
+ * `force` skips the mtime check, for after our own writes: two writes inside one clock
29
+ * tick can leave the mtime unchanged.
30
+ */
31
+ refresh(force?: boolean): Promise<ContextDocument>;
32
+ /** Appends notes to the file, creating it if needed. See {@link addTableNotes}. */
33
+ addNotes(fullyQualifiedName: string, notes: NewTableNotes): Promise<void>;
34
+ }
@@ -0,0 +1,87 @@
1
+ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { addTableNotes, emptyContextDocument, parseContextFile, } from './context-file.js';
4
+ /**
5
+ * The context file as the tools see it: re-read whenever it changes on disk, so an edit
6
+ * someone makes mid-session — or a note the agent just added — shows up on the next tool
7
+ * call without restarting the server.
8
+ *
9
+ * Only the preamble is fixed at startup, because MCP delivers server instructions once,
10
+ * at initialisation.
11
+ */
12
+ export class ContextStore {
13
+ path;
14
+ /** False when context writes were turned off, or there is no file to write to. */
15
+ writable;
16
+ document;
17
+ loadedMtimeMs;
18
+ /** Writes are chained so two notes added at once cannot clobber each other. */
19
+ pendingWrite = Promise.resolve();
20
+ constructor(path, writable, document, mtimeMs) {
21
+ this.path = path;
22
+ this.writable = writable;
23
+ this.document = document;
24
+ this.loadedMtimeMs = mtimeMs;
25
+ }
26
+ static async open(path, options) {
27
+ const store = new ContextStore(path, options.writable, emptyContextDocument(), null);
28
+ await store.refresh();
29
+ return store;
30
+ }
31
+ /** A fixed document with no file behind it. */
32
+ static inMemory(document = emptyContextDocument()) {
33
+ return new ContextStore(null, false, document, null);
34
+ }
35
+ /** The document as last loaded. */
36
+ get current() {
37
+ return this.document;
38
+ }
39
+ /**
40
+ * Re-reads the file if it changed since the last load; returns the current document.
41
+ * `force` skips the mtime check, for after our own writes: two writes inside one clock
42
+ * tick can leave the mtime unchanged.
43
+ */
44
+ async refresh(force = false) {
45
+ if (this.path === null)
46
+ return this.document;
47
+ let mtimeMs;
48
+ try {
49
+ mtimeMs = (await stat(this.path)).mtimeMs;
50
+ }
51
+ catch (error) {
52
+ if (error.code !== 'ENOENT')
53
+ throw error;
54
+ mtimeMs = null;
55
+ }
56
+ if (!force && mtimeMs === this.loadedMtimeMs)
57
+ return this.document;
58
+ this.document = mtimeMs === null ? emptyContextDocument() : parseContextFile(await readFile(this.path, 'utf8'));
59
+ this.loadedMtimeMs = mtimeMs;
60
+ return this.document;
61
+ }
62
+ /** Appends notes to the file, creating it if needed. See {@link addTableNotes}. */
63
+ async addNotes(fullyQualifiedName, notes) {
64
+ const path = this.path;
65
+ if (path === null || !this.writable)
66
+ throw new Error('Context writes are disabled on this server.');
67
+ const write = this.pendingWrite.then(async () => {
68
+ let raw = '';
69
+ try {
70
+ raw = await readFile(path, 'utf8');
71
+ }
72
+ catch (error) {
73
+ if (error.code !== 'ENOENT')
74
+ throw error;
75
+ }
76
+ await mkdir(dirname(path), { recursive: true });
77
+ // Write-then-rename, so an editor or a crash never sees a half-written file.
78
+ const temporary = `${path}.${process.pid}.tmp`;
79
+ await writeFile(temporary, addTableNotes(raw, fullyQualifiedName, notes), 'utf8');
80
+ await rename(temporary, path);
81
+ await this.refresh(true);
82
+ });
83
+ this.pendingWrite = write.catch(() => { });
84
+ await write;
85
+ }
86
+ }
87
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/context/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,gBAAgB,GAGjB,MAAM,mBAAmB,CAAA;AAE1B;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IACd,IAAI,CAAe;IAC5B,kFAAkF;IACzE,QAAQ,CAAS;IAClB,QAAQ,CAAiB;IACzB,aAAa,CAAe;IACpC,+EAA+E;IACvE,YAAY,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAA;IAE1D,YAAoB,IAAmB,EAAE,QAAiB,EAAE,QAAyB,EAAE,OAAsB;QAC3G,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,aAAa,GAAG,OAAO,CAAA;IAC9B,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,OAA8B;QAC5D,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,CAAC,CAAA;QACpF,MAAM,KAAK,CAAC,OAAO,EAAE,CAAA;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,+CAA+C;IAC/C,MAAM,CAAC,QAAQ,CAAC,WAA4B,oBAAoB,EAAE;QAChE,OAAO,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtD,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAA;QAE5C,IAAI,OAAsB,CAAA;QAC1B,IAAI,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAA;YACnE,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;QAED,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAA;QAElE,IAAI,CAAC,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;QAC/G,IAAI,CAAC,aAAa,GAAG,OAAO,CAAA;QAC5B,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED,mFAAmF;IACnF,KAAK,CAAC,QAAQ,CAAC,kBAA0B,EAAE,KAAoB;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;QAEnG,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAC9C,IAAI,GAAG,GAAG,EAAE,CAAA;YACZ,IAAI,CAAC;gBACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YACpC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,MAAM,KAAK,CAAA;YACrE,CAAC;YAED,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC/C,6EAA6E;YAC7E,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,MAAM,CAAA;YAC9C,MAAM,SAAS,CAAC,SAAS,EAAE,aAAa,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;YACjF,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;YAE7B,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC1B,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACzC,MAAM,KAAK,CAAA;IACb,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Turns Postgres driver errors into messages a model can act on, without echoing more of
3
+ * the database's internals than the caller already knew.
4
+ *
5
+ * Ported from the ContextFlo connector's `executeQuery` error handling and extended with
6
+ * the cases this server's safety layers produce.
7
+ */
8
+ export declare function describeQueryError(error: unknown): string;
9
+ export declare function describeConnectionError(error: unknown): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Turns Postgres driver errors into messages a model can act on, without echoing more of
3
+ * the database's internals than the caller already knew.
4
+ *
5
+ * Ported from the ContextFlo connector's `executeQuery` error handling and extended with
6
+ * the cases this server's safety layers produce.
7
+ */
8
+ /** Postgres SQLSTATE 25006 — attempted a write inside a read-only transaction. */
9
+ const READ_ONLY_TRANSACTION = '25006';
10
+ export function describeQueryError(error) {
11
+ const pgError = error;
12
+ const message = pgError?.message ?? 'Unknown database error';
13
+ if (pgError?.code === READ_ONLY_TRANSACTION) {
14
+ return `Rejected by the database: this connection is read-only. (${message})`;
15
+ }
16
+ // Postgres refuses multi-statement input over the extended query protocol. Reaching
17
+ // this means the parser-level check was bypassed — the wire protocol caught it anyway.
18
+ if (message.includes('cannot insert multiple commands into a prepared statement')) {
19
+ return 'Only one statement per call is allowed. Send each statement as a separate query call.';
20
+ }
21
+ if (message.includes('canceling statement due to statement timeout')) {
22
+ return 'Query exceeded the statement timeout. Narrow the query, or raise the limit with --statement-timeout.';
23
+ }
24
+ if (message.includes('syntax error')) {
25
+ const position = pgError?.position ? ` (at position ${pgError.position})` : '';
26
+ return `SQL syntax error: ${message}${position}`;
27
+ }
28
+ if (message.includes('does not exist')) {
29
+ return `Object not found: ${message}`;
30
+ }
31
+ if (message.includes('permission denied')) {
32
+ return `Permission denied: ${message}`;
33
+ }
34
+ return message;
35
+ }
36
+ export function describeConnectionError(error) {
37
+ const message = error?.message ?? 'Unknown connection error';
38
+ if (message.includes('ECONNREFUSED')) {
39
+ return 'Connection refused. Check that the Postgres server is running and the host/port are correct.';
40
+ }
41
+ if (message.includes('ENOTFOUND')) {
42
+ return 'Host not found. Check the hostname or IP address in the connection string.';
43
+ }
44
+ if (message.includes('password authentication failed')) {
45
+ return 'Authentication failed. Check the username and password in the connection string.';
46
+ }
47
+ if (message.includes('database') && message.includes('does not exist')) {
48
+ return 'Database does not exist. Check the database name in the connection string.';
49
+ }
50
+ if (message.includes('SSL') || message.includes('ssl')) {
51
+ return `SSL connection error: ${message}. Try adding ?sslmode=require to the connection string.`;
52
+ }
53
+ if (message.includes('timeout')) {
54
+ return 'Connection timed out. Check network connectivity to the database host.';
55
+ }
56
+ return message;
57
+ }
58
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/db/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,kFAAkF;AAClF,MAAM,qBAAqB,GAAG,OAAO,CAAA;AAErC,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,MAAM,OAAO,GAAG,KAAoB,CAAA;IACpC,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,wBAAwB,CAAA;IAE5D,IAAI,OAAO,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;QAC5C,OAAO,4DAA4D,OAAO,GAAG,CAAA;IAC/E,CAAC;IAED,oFAAoF;IACpF,uFAAuF;IACvF,IAAI,OAAO,CAAC,QAAQ,CAAC,2DAA2D,CAAC,EAAE,CAAC;QAClF,OAAO,uFAAuF,CAAA;IAChG,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,8CAA8C,CAAC,EAAE,CAAC;QACrE,OAAO,sGAAsG,CAAA;IAC/G,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;QAC9E,OAAO,qBAAqB,OAAO,GAAG,QAAQ,EAAE,CAAA;IAClD,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACvC,OAAO,qBAAqB,OAAO,EAAE,CAAA;IACvC,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC1C,OAAO,sBAAsB,OAAO,EAAE,CAAA;IACxC,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,MAAM,OAAO,GAAI,KAAqB,EAAE,OAAO,IAAI,0BAA0B,CAAA;IAE7E,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACrC,OAAO,8FAA8F,CAAA;IACvG,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAClC,OAAO,4EAA4E,CAAA;IACrF,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,gCAAgC,CAAC,EAAE,CAAC;QACvD,OAAO,kFAAkF,CAAA;IAC3F,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACvE,OAAO,4EAA4E,CAAA;IACrF,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,yBAAyB,OAAO,yDAAyD,CAAA;IAClG,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAChC,OAAO,wEAAwE,CAAA;IACjF,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { Database } from './pool.js';
2
+ export interface TableSummary {
3
+ schema: string;
4
+ name: string;
5
+ /** `schema.name`, the identifier every other tool accepts. */
6
+ fullyQualifiedName: string;
7
+ kind: string;
8
+ description: string | null;
9
+ }
10
+ export interface ListTablesResult {
11
+ tables: TableSummary[];
12
+ /** Total matches before the limit was applied, so truncation can be stated rather than hidden. */
13
+ totalMatches: number;
14
+ }
15
+ export interface ColumnContext {
16
+ name: string;
17
+ dataType: string;
18
+ isNullable: boolean;
19
+ defaultValue: string | null;
20
+ description: string | null;
21
+ isPrimaryKey: boolean;
22
+ /** `schema.table.column` this column references, when it is a foreign key. */
23
+ references: string | null;
24
+ /** Labels in sort order, when the column's type is an enum. */
25
+ enumValues: string[] | null;
26
+ }
27
+ export interface TableContext {
28
+ fullyQualifiedName: string;
29
+ kind: string;
30
+ description: string | null;
31
+ /** Planner estimate from `reltuples`, not an exact count — cheap on large tables. */
32
+ approximateRows: number | null;
33
+ columns: ColumnContext[];
34
+ }
35
+ /**
36
+ * Lists tables, optionally narrowed by a case-insensitive substring.
37
+ *
38
+ * The pattern matches anywhere in the table name, the qualified name, or the table's
39
+ * comment — the comment included because a table named `fct_orders` may be the one
40
+ * someone means by "revenue", and a name-only match would report nothing and send the
41
+ * model away empty-handed.
42
+ */
43
+ export declare function listTables(database: Database, options: {
44
+ pattern?: string | undefined;
45
+ schema?: string | undefined;
46
+ limit: number;
47
+ }): Promise<ListTablesResult>;
48
+ /**
49
+ * Describes several tables in one round trip — the model usually has two or three
50
+ * candidates after a search and should not need a call each to choose between them.
51
+ *
52
+ * Names are matched case-insensitively, and an unqualified name resolves against any
53
+ * visible schema, because a model that read `orders` in a list will ask for `orders`.
54
+ */
55
+ export declare function getTableContext(database: Database, fullyQualifiedNames: string[]): Promise<TableContext[]>;