@apono-io/mcp-postgres 0.2.0 → 0.4.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.
- package/README.md +17 -3
- package/dist/__tests__/db.test.d.ts +2 -0
- package/dist/__tests__/db.test.d.ts.map +1 -0
- package/dist/__tests__/db.test.js +65 -0
- package/dist/__tests__/db.test.js.map +1 -0
- package/dist/__tests__/execute.test.d.ts +2 -0
- package/dist/__tests__/execute.test.d.ts.map +1 -0
- package/dist/__tests__/execute.test.js +153 -0
- package/dist/__tests__/execute.test.js.map +1 -0
- package/dist/__tests__/helpers/database.d.ts +10 -0
- package/dist/__tests__/helpers/database.d.ts.map +1 -0
- package/dist/__tests__/helpers/database.js +28 -0
- package/dist/__tests__/helpers/database.js.map +1 -0
- package/dist/__tests__/pool-health.test.d.ts +2 -0
- package/dist/__tests__/pool-health.test.d.ts.map +1 -0
- package/dist/__tests__/pool-health.test.js +57 -0
- package/dist/__tests__/pool-health.test.js.map +1 -0
- package/dist/__tests__/timeout.test.d.ts +2 -0
- package/dist/__tests__/timeout.test.d.ts.map +1 -0
- package/dist/__tests__/timeout.test.js +121 -0
- package/dist/__tests__/timeout.test.js.map +1 -0
- package/dist/__tests__/tools.test.js +17 -1
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/db.d.ts +2 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +67 -3
- package/dist/db.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +24 -56
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/read-only.test.d.ts +0 -2
- package/dist/__tests__/read-only.test.d.ts.map +0 -1
- package/dist/__tests__/read-only.test.js +0 -65
- package/dist/__tests__/read-only.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,14 +6,12 @@ Apono MCP server for PostgreSQL databases with JIT credential support.
|
|
|
6
6
|
|
|
7
7
|
| Tool | Description |
|
|
8
8
|
|------|------------|
|
|
9
|
-
| `
|
|
10
|
-
| `execute` | Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE) |
|
|
9
|
+
| `execute` | Execute any SQL statement, including `SELECT` and `EXPLAIN`. Returns rows when the statement returns rows |
|
|
11
10
|
| `list_schemas` | List all schemas in the database |
|
|
12
11
|
| `list_tables` | List all tables in a schema |
|
|
13
12
|
| `describe_table` | Get column names, types, constraints, and indexes for a table |
|
|
14
13
|
| `list_extensions` | List installed PostgreSQL extensions |
|
|
15
14
|
| `get_table_indexes` | List indexes on a specific table |
|
|
16
|
-
| `explain_query` | Get the execution plan for a SQL query |
|
|
17
15
|
|
|
18
16
|
All tools accept a `database` parameter to specify which database to operate on.
|
|
19
17
|
|
|
@@ -25,3 +23,19 @@ All tools accept a `database` parameter to specify which database to operate on.
|
|
|
25
23
|
| `POSTGRES_PORT` | PostgreSQL server port (default: 5432) |
|
|
26
24
|
| `POSTGRES_USER` | Database username |
|
|
27
25
|
| `POSTGRES_PASSWORD` | Database password |
|
|
26
|
+
| `PGSSLMODE` | `disable` turns TLS off; otherwise TLS without certificate verification |
|
|
27
|
+
| `POSTGRES_STATEMENT_TIMEOUT_MS` | How long a statement may run, in milliseconds (default: 600000, `0` keeps the server's own setting) |
|
|
28
|
+
| `POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS` | How long a transaction may sit idle, in milliseconds (default: 60000, `0` keeps the server's own setting) |
|
|
29
|
+
|
|
30
|
+
## How long a statement may run
|
|
31
|
+
|
|
32
|
+
Every statement runs with `statement_timeout` and
|
|
33
|
+
`idle_in_transaction_session_timeout` applied. PostgreSQL enforces both itself, so a
|
|
34
|
+
statement that outlives its client still ends, and the snapshot it holds is released
|
|
35
|
+
rather than blocking vacuum until the query finishes on its own.
|
|
36
|
+
|
|
37
|
+
A pooled connection keeps whatever a caller `SET` on it, so both are reapplied on
|
|
38
|
+
each checkout. A caller can still raise either one for the duration of its own
|
|
39
|
+
statement — these bound an accidental runaway query, not a deliberate one — but that
|
|
40
|
+
change does not carry over to the next call.
|
|
41
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/db.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
function makePool(id) {
|
|
3
|
+
return { id, on: vi.fn(), end: vi.fn(async () => undefined) };
|
|
4
|
+
}
|
|
5
|
+
let created = [];
|
|
6
|
+
const Pool = vi.fn(() => {
|
|
7
|
+
const pool = makePool(created.length);
|
|
8
|
+
created.push(pool);
|
|
9
|
+
return pool;
|
|
10
|
+
});
|
|
11
|
+
const log = vi.fn();
|
|
12
|
+
vi.mock('../logger.js', () => ({ log }));
|
|
13
|
+
vi.mock('pg', () => ({ default: { Pool } }));
|
|
14
|
+
const db = await import('../db.js');
|
|
15
|
+
const getPool = (database) => db.getPool(database);
|
|
16
|
+
const closeAllPools = db.closeAllPools;
|
|
17
|
+
describe('closeAllPools', () => {
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await closeAllPools();
|
|
20
|
+
created = [];
|
|
21
|
+
Pool.mockClear();
|
|
22
|
+
log.mockClear();
|
|
23
|
+
});
|
|
24
|
+
it('closes every pool and empties the cache', async () => {
|
|
25
|
+
const first = getPool('apono');
|
|
26
|
+
const second = getPool('other');
|
|
27
|
+
await closeAllPools();
|
|
28
|
+
expect(first.end).toHaveBeenCalledOnce();
|
|
29
|
+
expect(second.end).toHaveBeenCalledOnce();
|
|
30
|
+
expect(getPool('apono')).not.toBe(first);
|
|
31
|
+
expect(getPool('other')).not.toBe(second);
|
|
32
|
+
});
|
|
33
|
+
it('closes the other pools when one fails to close', async () => {
|
|
34
|
+
const failing = getPool('apono');
|
|
35
|
+
failing.end.mockRejectedValueOnce(new Error('connection terminated'));
|
|
36
|
+
const second = getPool('other');
|
|
37
|
+
const third = getPool('third');
|
|
38
|
+
await closeAllPools();
|
|
39
|
+
expect(second.end).toHaveBeenCalledOnce();
|
|
40
|
+
expect(third.end).toHaveBeenCalledOnce();
|
|
41
|
+
});
|
|
42
|
+
it('resolves when a pool fails to close', async () => {
|
|
43
|
+
getPool('apono').end.mockRejectedValueOnce(new Error('connection terminated'));
|
|
44
|
+
await expect(closeAllPools()).resolves.toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
it('forgets a pool that failed to close, so the next call builds a new one', async () => {
|
|
47
|
+
const failing = getPool('apono');
|
|
48
|
+
failing.end.mockRejectedValueOnce(new Error('connection terminated'));
|
|
49
|
+
await closeAllPools();
|
|
50
|
+
expect(getPool('apono')).not.toBe(failing);
|
|
51
|
+
});
|
|
52
|
+
it('logs the reason a pool failed to close', async () => {
|
|
53
|
+
const reason = new Error('connection terminated');
|
|
54
|
+
getPool('apono').end.mockRejectedValueOnce(reason);
|
|
55
|
+
await closeAllPools();
|
|
56
|
+
expect(log).toHaveBeenCalledWith('Failed to close pool:', reason);
|
|
57
|
+
});
|
|
58
|
+
it('logs nothing when every pool closes', async () => {
|
|
59
|
+
getPool('apono');
|
|
60
|
+
getPool('other');
|
|
61
|
+
await closeAllPools();
|
|
62
|
+
expect(log).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=db.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.test.js","sourceRoot":"","sources":["../../src/__tests__/db.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/execute.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import { USER, PASSWORD, connection, useTestCredentials, withMaintenance } from './helpers/database.js';
|
|
4
|
+
const DATABASE = 'mcp_postgres_execute_test';
|
|
5
|
+
describe('execute', () => {
|
|
6
|
+
let admin;
|
|
7
|
+
let tools;
|
|
8
|
+
let closeAllPools;
|
|
9
|
+
async function rowCount() {
|
|
10
|
+
const result = await admin.query('SELECT COUNT(*)::int AS n FROM users');
|
|
11
|
+
return result.rows[0].n;
|
|
12
|
+
}
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
await withMaintenance(async (client) => {
|
|
15
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
16
|
+
await client.query(`CREATE DATABASE ${DATABASE}`);
|
|
17
|
+
});
|
|
18
|
+
admin = new pg.Pool(connection(DATABASE));
|
|
19
|
+
useTestCredentials();
|
|
20
|
+
const toolsModule = await import('../tools.js');
|
|
21
|
+
closeAllPools = (await import('../db.js')).closeAllPools;
|
|
22
|
+
tools = new Map();
|
|
23
|
+
toolsModule.registerTools({
|
|
24
|
+
tool: (name, _d, _s, handler) => {
|
|
25
|
+
tools.set(name, handler);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await closeAllPools?.();
|
|
31
|
+
await admin?.end();
|
|
32
|
+
await withMaintenance(async (client) => {
|
|
33
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
await admin.query('DROP TABLE IF EXISTS users');
|
|
38
|
+
await admin.query('CREATE TABLE users (id INT PRIMARY KEY)');
|
|
39
|
+
await admin.query('INSERT INTO users VALUES (1)');
|
|
40
|
+
});
|
|
41
|
+
it('leaves execute outside the read-only transaction', async () => {
|
|
42
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'DELETE FROM users' });
|
|
43
|
+
expect(result.isError).toBeUndefined();
|
|
44
|
+
expect(await rowCount()).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
it('returns rows for a row-returning statement', async () => {
|
|
47
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'SELECT id FROM users' });
|
|
48
|
+
expect(result.isError).toBeUndefined();
|
|
49
|
+
expect(result.content[0].text).not.toContain('Rows affected');
|
|
50
|
+
expect(result.content[0].text).toContain('1');
|
|
51
|
+
});
|
|
52
|
+
it('reports the permission hint for a SELECT that matches nothing', async () => {
|
|
53
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'SELECT id FROM users WHERE id = 999' });
|
|
54
|
+
expect(result.isError).toBeUndefined();
|
|
55
|
+
expect(result.content[0].text).toContain('permissions');
|
|
56
|
+
});
|
|
57
|
+
it('returns the plan for an EXPLAIN, as text rather than a row per line', async () => {
|
|
58
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'EXPLAIN SELECT * FROM users' });
|
|
59
|
+
expect(result.isError).toBeUndefined();
|
|
60
|
+
expect(result.content[0].text).toContain('Scan');
|
|
61
|
+
expect(result.content[0].text).not.toContain('QUERY PLAN');
|
|
62
|
+
});
|
|
63
|
+
it('reports an empty FETCH as an empty result, not as rows affected', async () => {
|
|
64
|
+
const result = await tools.get('execute')({
|
|
65
|
+
database: DATABASE,
|
|
66
|
+
sql: 'BEGIN; DECLARE c CURSOR FOR SELECT * FROM users WHERE false; FETCH ALL FROM c; COMMIT',
|
|
67
|
+
});
|
|
68
|
+
expect(result.isError).toBeUndefined();
|
|
69
|
+
expect(result.content[0].text).toContain('permissions');
|
|
70
|
+
});
|
|
71
|
+
it('reports a CREATE TABLE AS as a write, though postgres tags it SELECT', async () => {
|
|
72
|
+
const result = await tools.get('execute')({
|
|
73
|
+
database: DATABASE,
|
|
74
|
+
sql: 'CREATE TABLE users_copy AS SELECT * FROM users',
|
|
75
|
+
});
|
|
76
|
+
expect(result.isError).toBeUndefined();
|
|
77
|
+
expect(result.content[0].text).not.toContain('permissions');
|
|
78
|
+
expect(result.content[0].text).toContain('Rows affected: 1');
|
|
79
|
+
});
|
|
80
|
+
it('reports a SELECT INTO as a write, though postgres tags it SELECT', async () => {
|
|
81
|
+
const result = await tools.get('execute')({
|
|
82
|
+
database: DATABASE,
|
|
83
|
+
sql: 'SELECT * INTO users_into FROM users',
|
|
84
|
+
});
|
|
85
|
+
expect(result.isError).toBeUndefined();
|
|
86
|
+
expect(result.content[0].text).not.toContain('permissions');
|
|
87
|
+
expect(result.content[0].text).toContain('Rows affected: 1');
|
|
88
|
+
});
|
|
89
|
+
it('reports the affected count for a write', async () => {
|
|
90
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'DELETE FROM users' });
|
|
91
|
+
expect(result.content[0].text).toContain('Rows affected: 1');
|
|
92
|
+
});
|
|
93
|
+
it('reports one result per statement for a multi-statement string', async () => {
|
|
94
|
+
const result = await tools.get('execute')({
|
|
95
|
+
database: DATABASE,
|
|
96
|
+
sql: 'SELECT count(*) AS n FROM users; DELETE FROM users',
|
|
97
|
+
});
|
|
98
|
+
expect(result.isError).toBeUndefined();
|
|
99
|
+
expect(result.content[0].text).toContain('statement 1');
|
|
100
|
+
expect(result.content[0].text).toContain('statement 2');
|
|
101
|
+
expect(await rowCount()).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
describe('what a READ_ONLY grant reaches', () => {
|
|
104
|
+
const ROLE = 'mcp_read_only_grant_test';
|
|
105
|
+
const SEQUENCE = 'grant_test_seq';
|
|
106
|
+
beforeAll(async () => {
|
|
107
|
+
await admin.query(`CREATE SEQUENCE ${SEQUENCE}`);
|
|
108
|
+
await admin.query(`SELECT nextval('${SEQUENCE}')`);
|
|
109
|
+
await admin.query(`DROP ROLE IF EXISTS ${ROLE}`);
|
|
110
|
+
await admin.query(`CREATE ROLE ${ROLE} LOGIN PASSWORD '${PASSWORD}'`);
|
|
111
|
+
await admin.query(`GRANT CONNECT ON DATABASE ${DATABASE} TO ${ROLE}`);
|
|
112
|
+
await admin.query(`GRANT USAGE ON SCHEMA public TO ${ROLE}`);
|
|
113
|
+
await admin.query(`GRANT SELECT, USAGE ON SEQUENCE ${SEQUENCE} TO ${ROLE}`);
|
|
114
|
+
await closeAllPools();
|
|
115
|
+
process.env.POSTGRES_USER = ROLE;
|
|
116
|
+
});
|
|
117
|
+
afterAll(async () => {
|
|
118
|
+
await closeAllPools();
|
|
119
|
+
process.env.POSTGRES_USER = USER;
|
|
120
|
+
await admin.query(`DROP OWNED BY ${ROLE}`).catch(() => undefined);
|
|
121
|
+
await admin.query(`DROP ROLE IF EXISTS ${ROLE}`).catch(() => undefined);
|
|
122
|
+
await admin.query(`DROP SEQUENCE IF EXISTS ${SEQUENCE}`);
|
|
123
|
+
});
|
|
124
|
+
beforeEach(async () => {
|
|
125
|
+
await admin.query(`GRANT SELECT ON users TO ${ROLE}`);
|
|
126
|
+
});
|
|
127
|
+
it.each([
|
|
128
|
+
['INSERT INTO users VALUES (2)'],
|
|
129
|
+
['UPDATE users SET id = 2'],
|
|
130
|
+
['DELETE FROM users'],
|
|
131
|
+
['CREATE TABLE staging (id INT)'],
|
|
132
|
+
['DROP TABLE users'],
|
|
133
|
+
['TRUNCATE users'],
|
|
134
|
+
])('refuses %s, because the role holds no table write privilege', async (sql) => {
|
|
135
|
+
const result = await tools.get('execute')({ database: DATABASE, sql });
|
|
136
|
+
expect(result.isError, `"${sql}" was not refused`).toBe(true);
|
|
137
|
+
expect(await rowCount()).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
it('still serves a read', async () => {
|
|
140
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'SELECT id FROM users' });
|
|
141
|
+
expect(result.isError).toBeUndefined();
|
|
142
|
+
expect(result.content[0].text).toContain('1');
|
|
143
|
+
});
|
|
144
|
+
it('reaches nextval, which USAGE allows and the catalog grades a write', async () => {
|
|
145
|
+
const before = await admin.query(`SELECT last_value FROM ${SEQUENCE}`);
|
|
146
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: `SELECT nextval('${SEQUENCE}')` });
|
|
147
|
+
expect(result.isError).toBeUndefined();
|
|
148
|
+
const after = await admin.query(`SELECT last_value FROM ${SEQUENCE}`);
|
|
149
|
+
expect(Number(after.rows[0].last_value)).toBeGreaterThan(Number(before.rows[0].last_value));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=execute.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.test.js","sourceRoot":"","sources":["../../src/__tests__/execute.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export declare const HOST: string;
|
|
3
|
+
export declare const PORT: number;
|
|
4
|
+
export declare const USER: string;
|
|
5
|
+
export declare const PASSWORD: string;
|
|
6
|
+
export declare const MAINTENANCE_DB = "postgres";
|
|
7
|
+
export declare function connection(database: string): pg.ClientConfig;
|
|
8
|
+
export declare function withMaintenance(run: (client: pg.Client) => Promise<void>): Promise<void>;
|
|
9
|
+
export declare function useTestCredentials(): void;
|
|
10
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,eAAO,MAAM,IAAI,QAAgD,CAAC;AAClE,eAAO,MAAM,IAAI,QAAiD,CAAC;AACnE,eAAO,MAAM,IAAI,QAA+C,CAAC;AACjE,eAAO,MAAM,QAAQ,QAAmD,CAAC;AACzE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,CAAC,YAAY,CAE5D;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;AAGD,wBAAgB,kBAAkB,IAAI,IAAI,CAMzC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export const HOST = process.env.POSTGRES_TEST_HOST ?? '127.0.0.1';
|
|
3
|
+
export const PORT = Number(process.env.POSTGRES_TEST_PORT ?? 5432);
|
|
4
|
+
export const USER = process.env.POSTGRES_TEST_USER ?? 'postgres';
|
|
5
|
+
export const PASSWORD = process.env.POSTGRES_TEST_PASSWORD ?? 'postgres';
|
|
6
|
+
export const MAINTENANCE_DB = 'postgres';
|
|
7
|
+
export function connection(database) {
|
|
8
|
+
return { host: HOST, port: PORT, user: USER, password: PASSWORD, database, ssl: false };
|
|
9
|
+
}
|
|
10
|
+
export async function withMaintenance(run) {
|
|
11
|
+
const client = new pg.Client(connection(MAINTENANCE_DB));
|
|
12
|
+
await client.connect();
|
|
13
|
+
try {
|
|
14
|
+
await run(client);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await client.end();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// The server builds its pools from these, so they are set before it is imported.
|
|
21
|
+
export function useTestCredentials() {
|
|
22
|
+
process.env.POSTGRES_HOST = HOST;
|
|
23
|
+
process.env.POSTGRES_PORT = String(PORT);
|
|
24
|
+
process.env.POSTGRES_USER = USER;
|
|
25
|
+
process.env.POSTGRES_PASSWORD = PASSWORD;
|
|
26
|
+
process.env.PGSSLMODE = 'disable';
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/__tests__/helpers/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,WAAW,CAAC;AAClE,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,UAAU,CAAC;AACjE,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,UAAU,CAAC;AACzE,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAEzC,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAC1F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAyC;IAC7E,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC;IACzD,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;IACrB,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,kBAAkB;IAChC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,IAAI,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,IAAI,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,iBAAiB,GAAG,QAAQ,CAAC;IACzC,OAAO,CAAC,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-health.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pool-health.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { useTestCredentials, withMaintenance } from './helpers/database.js';
|
|
3
|
+
const DATABASE = 'mcp_postgres_pool_health_test';
|
|
4
|
+
describe('pool health', () => {
|
|
5
|
+
let tools;
|
|
6
|
+
let closeAllPools;
|
|
7
|
+
async function execute(sql) {
|
|
8
|
+
return tools.get('execute')({ database: DATABASE, sql });
|
|
9
|
+
}
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await withMaintenance(async (client) => {
|
|
12
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
13
|
+
await client.query(`CREATE DATABASE ${DATABASE}`);
|
|
14
|
+
});
|
|
15
|
+
useTestCredentials();
|
|
16
|
+
const toolsModule = await import('../tools.js');
|
|
17
|
+
closeAllPools = (await import('../db.js')).closeAllPools;
|
|
18
|
+
tools = new Map();
|
|
19
|
+
toolsModule.registerTools({
|
|
20
|
+
tool: (name, _d, _s, handler) => {
|
|
21
|
+
tools.set(name, handler);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await closeAllPools?.();
|
|
27
|
+
});
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
await closeAllPools?.();
|
|
30
|
+
await withMaintenance(async (client) => {
|
|
31
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('survives the server ending a client left idle in a transaction', async () => {
|
|
35
|
+
process.env.POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS = '300';
|
|
36
|
+
await closeAllPools();
|
|
37
|
+
try {
|
|
38
|
+
expect((await execute('BEGIN')).isError).toBeUndefined();
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 1_500));
|
|
40
|
+
const afterwards = await execute('SELECT 1 AS ok');
|
|
41
|
+
expect(afterwards.isError).toBeUndefined();
|
|
42
|
+
expect(afterwards.content[0].text).toContain('1');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
delete process.env.POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS;
|
|
46
|
+
await closeAllPools();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('does not hand a broken client to the next call', async () => {
|
|
50
|
+
const failed = await execute('BEGIN; SELECT 1/0');
|
|
51
|
+
expect(failed.isError).toBe(true);
|
|
52
|
+
const afterwards = await tools.get('list_tables')({ database: DATABASE, schema: 'public' });
|
|
53
|
+
expect(afterwards.isError).toBeUndefined();
|
|
54
|
+
expect(afterwards.content[0].text).not.toContain('aborted');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=pool-health.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-health.test.js","sourceRoot":"","sources":["../../src/__tests__/pool-health.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/timeout.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { useTestCredentials, withMaintenance } from './helpers/database.js';
|
|
3
|
+
const DATABASE = 'mcp_postgres_timeout_test';
|
|
4
|
+
describe('statement timeouts', () => {
|
|
5
|
+
let tools;
|
|
6
|
+
let closeAllPools;
|
|
7
|
+
async function execute(sql) {
|
|
8
|
+
return tools.get('execute')({ database: DATABASE, sql });
|
|
9
|
+
}
|
|
10
|
+
async function withTimeouts(variables, run) {
|
|
11
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
delete process.env[name];
|
|
14
|
+
else
|
|
15
|
+
process.env[name] = value;
|
|
16
|
+
}
|
|
17
|
+
await closeAllPools();
|
|
18
|
+
try {
|
|
19
|
+
await run();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
for (const name of Object.keys(variables))
|
|
23
|
+
delete process.env[name];
|
|
24
|
+
await closeAllPools();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
await withMaintenance(async (client) => {
|
|
29
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
30
|
+
await client.query(`CREATE DATABASE ${DATABASE}`);
|
|
31
|
+
});
|
|
32
|
+
useTestCredentials();
|
|
33
|
+
const toolsModule = await import('../tools.js');
|
|
34
|
+
closeAllPools = (await import('../db.js')).closeAllPools;
|
|
35
|
+
tools = new Map();
|
|
36
|
+
toolsModule.registerTools({
|
|
37
|
+
tool: (name, _d, _s, handler) => {
|
|
38
|
+
tools.set(name, handler);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await closeAllPools?.();
|
|
44
|
+
});
|
|
45
|
+
afterAll(async () => {
|
|
46
|
+
await closeAllPools?.();
|
|
47
|
+
await withMaintenance(async (client) => {
|
|
48
|
+
await client.query(`DROP DATABASE IF EXISTS ${DATABASE}`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it('carries a statement timeout by default', async () => {
|
|
52
|
+
const result = await execute('SHOW statement_timeout');
|
|
53
|
+
expect(result.isError).toBeUndefined();
|
|
54
|
+
expect(result.content[0].text).toContain('10min');
|
|
55
|
+
});
|
|
56
|
+
it('carries an idle-in-transaction timeout by default', async () => {
|
|
57
|
+
const result = await execute('SHOW idle_in_transaction_session_timeout');
|
|
58
|
+
expect(result.isError).toBeUndefined();
|
|
59
|
+
expect(result.content[0].text).toContain('1min');
|
|
60
|
+
});
|
|
61
|
+
it('lets the server abort a statement that outruns the timeout', async () => {
|
|
62
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '300' }, async () => {
|
|
63
|
+
const result = await execute('SELECT pg_sleep(5)');
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
expect(result.content[0].text).toContain('statement timeout');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it('aborts the statement without the client giving up first', async () => {
|
|
69
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '300' }, async () => {
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
await execute('SELECT pg_sleep(5)');
|
|
72
|
+
expect(Date.now() - started).toBeLessThan(4_000);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it('takes a timeout from the environment', async () => {
|
|
76
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '2000' }, async () => {
|
|
77
|
+
const result = await execute('SHOW statement_timeout');
|
|
78
|
+
expect(result.content[0].text).toContain('2s');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('leaves the server setting alone when the timeout is zero', async () => {
|
|
82
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '0' }, async () => {
|
|
83
|
+
const result = await execute('SHOW statement_timeout');
|
|
84
|
+
expect(result.content[0].text).toContain('"statement_timeout": "0"');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it('refuses a value past what the server accepts, rather than breaking the pool', async () => {
|
|
88
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '3000000000' }, async () => {
|
|
89
|
+
const result = await execute('SHOW statement_timeout');
|
|
90
|
+
expect(result.isError).toBeUndefined();
|
|
91
|
+
expect(result.content[0].text).toContain('10min');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('falls back to the default when the variable is only whitespace', async () => {
|
|
95
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: ' ' }, async () => {
|
|
96
|
+
const result = await execute('SHOW statement_timeout');
|
|
97
|
+
expect(result.content[0].text).toContain('10min');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it('restores the timeout for the next call after a caller clears it', async () => {
|
|
101
|
+
await execute('SET statement_timeout = 0');
|
|
102
|
+
const result = await execute('SHOW statement_timeout');
|
|
103
|
+
expect(result.content[0].text).toContain('10min');
|
|
104
|
+
});
|
|
105
|
+
it('still aborts a long statement after a caller clears the timeout', async () => {
|
|
106
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: '300' }, async () => {
|
|
107
|
+
await execute('SET statement_timeout = 0');
|
|
108
|
+
const started = Date.now();
|
|
109
|
+
const result = await execute('SELECT pg_sleep(5)');
|
|
110
|
+
expect(result.isError).toBe(true);
|
|
111
|
+
expect(Date.now() - started).toBeLessThan(4_000);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it('falls back to the default when the variable is not a timeout', async () => {
|
|
115
|
+
await withTimeouts({ POSTGRES_STATEMENT_TIMEOUT_MS: 'soon' }, async () => {
|
|
116
|
+
const result = await execute('SHOW statement_timeout');
|
|
117
|
+
expect(result.content[0].text).toContain('10min');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=timeout.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.test.js","sourceRoot":"","sources":["../../src/__tests__/timeout.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { registerTools } from '../tools.js';
|
|
4
|
+
function registeredToolNames() {
|
|
5
|
+
const names = [];
|
|
6
|
+
registerTools({ tool: (name) => names.push(name) });
|
|
7
|
+
return names.sort();
|
|
8
|
+
}
|
|
4
9
|
describe('registerTools', () => {
|
|
5
|
-
it('registers all
|
|
10
|
+
it('registers all 7 postgres tools without error', () => {
|
|
6
11
|
const server = new McpServer({ name: 'test', version: '0.0.1' });
|
|
7
12
|
expect(() => registerTools(server)).not.toThrow();
|
|
8
13
|
});
|
|
14
|
+
it('exposes execute as the only SQL tool', () => {
|
|
15
|
+
expect(registeredToolNames(), 'a SQL tool graded `read` in the managed MCP catalog runs with no approval (DVL-12108)').toEqual([
|
|
16
|
+
'describe_table',
|
|
17
|
+
'execute',
|
|
18
|
+
'get_table_indexes',
|
|
19
|
+
'list_databases',
|
|
20
|
+
'list_extensions',
|
|
21
|
+
'list_schemas',
|
|
22
|
+
'list_tables',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
9
25
|
});
|
|
10
26
|
//# sourceMappingURL=tools.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.test.js","sourceRoot":"","sources":["../../src/__tests__/tools.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"tools.test.js","sourceRoot":"","sources":["../../src/__tests__/tools.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,SAAS,mBAAmB;IAC1B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAW,CAAC,CAAC;IACrE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CACJ,mBAAmB,EAAE,EACrB,uFAAuF,CACxF,CAAC,OAAO,CAAC;YACR,gBAAgB;YAChB,SAAS;YACT,mBAAmB;YACnB,gBAAgB;YAChB,iBAAiB;YACjB,cAAc;YACd,aAAa;SACd,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/db.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
2
|
export declare function getPool(database: string): pg.Pool;
|
|
3
|
+
export declare function query(database: string, sql: string, values?: unknown[]): Promise<pg.QueryResult>;
|
|
4
|
+
export declare function executeStatement(database: string, sql: string): Promise<pg.QueryResult | pg.QueryResult[]>;
|
|
3
5
|
export declare function closeAllPools(): Promise<void>;
|
|
4
6
|
//# sourceMappingURL=db.d.ts.map
|
package/dist/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAuDpB,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI,CAQjD;AAiBD,wBAAsB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,CAEtG;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,WAAW,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAEhH;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAOnD"}
|
package/dist/db.js
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
|
+
import { log } from './logger.js';
|
|
2
3
|
const pools = new Map();
|
|
4
|
+
const DEFAULT_STATEMENT_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
5
|
+
const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS = 60 * 1_000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
7
|
+
function timeoutMillis(variable, fallback) {
|
|
8
|
+
const raw = process.env[variable];
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return fallback;
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
if (trimmed === '')
|
|
13
|
+
return fallback;
|
|
14
|
+
if (!/^\d+$/.test(trimmed) || Number(trimmed) > MAX_TIMEOUT_MS) {
|
|
15
|
+
log(`Ignoring ${variable}=${raw}, expected whole milliseconds up to ${MAX_TIMEOUT_MS}`);
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
return Number(trimmed);
|
|
19
|
+
}
|
|
20
|
+
function statementTimeout() {
|
|
21
|
+
return timeoutMillis('POSTGRES_STATEMENT_TIMEOUT_MS', DEFAULT_STATEMENT_TIMEOUT_MS);
|
|
22
|
+
}
|
|
23
|
+
function idleInTransactionTimeout() {
|
|
24
|
+
return timeoutMillis('POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS', DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS);
|
|
25
|
+
}
|
|
26
|
+
function timeoutSql() {
|
|
27
|
+
const statements = [];
|
|
28
|
+
const statement = statementTimeout();
|
|
29
|
+
const idle = idleInTransactionTimeout();
|
|
30
|
+
if (statement > 0)
|
|
31
|
+
statements.push(`SET statement_timeout = ${statement}`);
|
|
32
|
+
if (idle > 0)
|
|
33
|
+
statements.push(`SET idle_in_transaction_session_timeout = ${idle}`);
|
|
34
|
+
return statements.join('; ');
|
|
35
|
+
}
|
|
3
36
|
function getPoolConfig() {
|
|
37
|
+
const statement = statementTimeout();
|
|
38
|
+
const idle = idleInTransactionTimeout();
|
|
4
39
|
return {
|
|
5
40
|
host: process.env.POSTGRES_HOST,
|
|
6
41
|
port: process.env.POSTGRES_PORT ? parseInt(process.env.POSTGRES_PORT, 10) : 5432,
|
|
@@ -9,6 +44,8 @@ function getPoolConfig() {
|
|
|
9
44
|
ssl: process.env.PGSSLMODE === 'disable' ? false : { rejectUnauthorized: false },
|
|
10
45
|
max: 5,
|
|
11
46
|
idleTimeoutMillis: 30_000,
|
|
47
|
+
...(statement > 0 ? { statement_timeout: statement } : {}),
|
|
48
|
+
...(idle > 0 ? { idle_in_transaction_session_timeout: idle } : {}),
|
|
12
49
|
};
|
|
13
50
|
}
|
|
14
51
|
export function getPool(database) {
|
|
@@ -16,13 +53,40 @@ export function getPool(database) {
|
|
|
16
53
|
if (existing)
|
|
17
54
|
return existing;
|
|
18
55
|
const pool = new pg.Pool({ ...getPoolConfig(), database });
|
|
56
|
+
pool.on('error', (err) => log(`Idle client error on ${database}:`, err));
|
|
19
57
|
pools.set(database, pool);
|
|
20
58
|
return pool;
|
|
21
59
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
60
|
+
async function runBounded(database, sql, values) {
|
|
61
|
+
const client = await getPool(database).connect();
|
|
62
|
+
let failure;
|
|
63
|
+
try {
|
|
64
|
+
const timeouts = timeoutSql();
|
|
65
|
+
if (timeouts)
|
|
66
|
+
await client.query(timeouts);
|
|
67
|
+
return await client.query(sql, values);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
failure = err;
|
|
71
|
+
throw err;
|
|
25
72
|
}
|
|
73
|
+
finally {
|
|
74
|
+
client.release(failure);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function query(database, sql, values) {
|
|
78
|
+
return (await runBounded(database, sql, values));
|
|
79
|
+
}
|
|
80
|
+
export async function executeStatement(database, sql) {
|
|
81
|
+
return (await runBounded(database, sql));
|
|
82
|
+
}
|
|
83
|
+
export async function closeAllPools() {
|
|
84
|
+
const open = [...pools.values()];
|
|
26
85
|
pools.clear();
|
|
86
|
+
const results = await Promise.allSettled(open.map((pool) => pool.end()));
|
|
87
|
+
for (const result of results) {
|
|
88
|
+
if (result.status === 'rejected')
|
|
89
|
+
log('Failed to close pool:', result.reason);
|
|
90
|
+
}
|
|
27
91
|
}
|
|
28
92
|
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0CpE,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAiJrD"}
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { executeStatement, query } from './db.js';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
function textContent(text) {
|
|
5
5
|
return { content: [{ type: 'text', text }] };
|
|
@@ -15,30 +15,28 @@ function formatRows(rows, emptyHint) {
|
|
|
15
15
|
}
|
|
16
16
|
return JSON.stringify(rows, null, 2);
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const result = await client.query(sql);
|
|
25
|
-
await client.query('COMMIT');
|
|
26
|
-
return result.rows;
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
await client.query('ROLLBACK').catch(() => undefined);
|
|
30
|
-
throw err;
|
|
18
|
+
function formatPlan(rows) {
|
|
19
|
+
return rows.map((row) => String(row['QUERY PLAN'])).join('\n');
|
|
20
|
+
}
|
|
21
|
+
function formatStatementResult(result) {
|
|
22
|
+
if (result.command === 'EXPLAIN' && result.rows.length > 0) {
|
|
23
|
+
return formatPlan(result.rows);
|
|
31
24
|
}
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
if (result.fields.length > 0) {
|
|
26
|
+
return formatRows(result.rows);
|
|
34
27
|
}
|
|
28
|
+
return `Statement executed successfully. Rows affected: ${result.rowCount ?? 0}`;
|
|
29
|
+
}
|
|
30
|
+
function formatExecuteResult(result) {
|
|
31
|
+
if (!Array.isArray(result))
|
|
32
|
+
return formatStatementResult(result);
|
|
33
|
+
return result.map((one, index) => `-- statement ${index + 1}\n${formatStatementResult(one)}`).join('\n\n');
|
|
35
34
|
}
|
|
36
35
|
export function registerTools(server) {
|
|
37
36
|
server.tool('list_databases', 'List all databases on the PostgreSQL server.', {}, async () => {
|
|
38
37
|
log('Tool called: list_databases');
|
|
39
38
|
try {
|
|
40
|
-
const
|
|
41
|
-
const result = await pool.query(`SELECT d.datname AS name, pg_catalog.pg_get_userbyid(d.datdba) AS owner, pg_catalog.pg_encoding_to_char(d.encoding) AS encoding
|
|
39
|
+
const result = await query('postgres', `SELECT d.datname AS name, pg_catalog.pg_get_userbyid(d.datdba) AS owner, pg_catalog.pg_encoding_to_char(d.encoding) AS encoding
|
|
42
40
|
FROM pg_catalog.pg_database d
|
|
43
41
|
WHERE d.datistemplate = false
|
|
44
42
|
ORDER BY d.datname`);
|
|
@@ -48,21 +46,11 @@ export function registerTools(server) {
|
|
|
48
46
|
return errorContent('list_databases', err);
|
|
49
47
|
}
|
|
50
48
|
});
|
|
51
|
-
server.tool('
|
|
52
|
-
log(`Tool called: query on ${database}`);
|
|
53
|
-
try {
|
|
54
|
-
return textContent(formatRows(await queryReadOnly(database, sql)));
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
return errorContent('query', err);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
server.tool('execute', 'Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE).', { database: z.string().describe('The database to execute against'), sql: z.string().describe('The SQL statement to execute') }, async ({ database, sql }) => {
|
|
49
|
+
server.tool('execute', 'Execute a SQL statement. Use this for every statement, including SELECT.', { database: z.string().describe('The database to execute against'), sql: z.string().describe('The SQL statement to execute') }, async ({ database, sql }) => {
|
|
61
50
|
log(`Tool called: execute on ${database}`);
|
|
62
51
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
return textContent(`Statement executed successfully. Rows affected: ${result.rowCount ?? 0}`);
|
|
52
|
+
const result = await executeStatement(database, sql);
|
|
53
|
+
return textContent(formatExecuteResult(result));
|
|
66
54
|
}
|
|
67
55
|
catch (err) {
|
|
68
56
|
return errorContent('execute', err);
|
|
@@ -71,8 +59,7 @@ export function registerTools(server) {
|
|
|
71
59
|
server.tool('list_schemas', 'List all schemas in the database.', { database: z.string().describe('The database to inspect') }, async ({ database }) => {
|
|
72
60
|
log(`Tool called: list_schemas on ${database}`);
|
|
73
61
|
try {
|
|
74
|
-
const
|
|
75
|
-
const result = await pool.query(`SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name`);
|
|
62
|
+
const result = await query(database, `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schema_name`);
|
|
76
63
|
const schemas = result.rows.map((r) => r.schema_name);
|
|
77
64
|
if (schemas.length === 0) {
|
|
78
65
|
return textContent('No schemas found. Note: this may be due to insufficient permissions granted to the current database user.');
|
|
@@ -89,8 +76,7 @@ export function registerTools(server) {
|
|
|
89
76
|
}, async ({ database, schema }) => {
|
|
90
77
|
log(`Tool called: list_tables on ${database}.${schema}`);
|
|
91
78
|
try {
|
|
92
|
-
const
|
|
93
|
-
const result = await pool.query(`SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`, [schema]);
|
|
79
|
+
const result = await query(database, `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name`, [schema]);
|
|
94
80
|
return textContent(formatRows(result.rows));
|
|
95
81
|
}
|
|
96
82
|
catch (err) {
|
|
@@ -104,8 +90,7 @@ export function registerTools(server) {
|
|
|
104
90
|
}, async ({ database, table, schema }) => {
|
|
105
91
|
log(`Tool called: describe_table on ${database}.${schema}.${table}`);
|
|
106
92
|
try {
|
|
107
|
-
const
|
|
108
|
-
const result = await pool.query(`SELECT column_name, data_type, is_nullable, column_default, character_maximum_length
|
|
93
|
+
const result = await query(database, `SELECT column_name, data_type, is_nullable, column_default, character_maximum_length
|
|
109
94
|
FROM information_schema.columns
|
|
110
95
|
WHERE table_schema = $1 AND table_name = $2
|
|
111
96
|
ORDER BY ordinal_position`, [schema, table]);
|
|
@@ -118,8 +103,7 @@ export function registerTools(server) {
|
|
|
118
103
|
server.tool('list_extensions', 'List installed PostgreSQL extensions.', { database: z.string().describe('The database to inspect') }, async ({ database }) => {
|
|
119
104
|
log(`Tool called: list_extensions on ${database}`);
|
|
120
105
|
try {
|
|
121
|
-
const
|
|
122
|
-
const result = await pool.query(`SELECT extname, extversion FROM pg_extension ORDER BY extname`);
|
|
106
|
+
const result = await query(database, `SELECT extname, extversion FROM pg_extension ORDER BY extname`);
|
|
123
107
|
return textContent(formatRows(result.rows));
|
|
124
108
|
}
|
|
125
109
|
catch (err) {
|
|
@@ -133,8 +117,7 @@ export function registerTools(server) {
|
|
|
133
117
|
}, async ({ database, table, schema }) => {
|
|
134
118
|
log(`Tool called: get_table_indexes on ${database}.${schema}.${table}`);
|
|
135
119
|
try {
|
|
136
|
-
const
|
|
137
|
-
const result = await pool.query(`SELECT indexname, indexdef
|
|
120
|
+
const result = await query(database, `SELECT indexname, indexdef
|
|
138
121
|
FROM pg_indexes
|
|
139
122
|
WHERE schemaname = $1 AND tablename = $2
|
|
140
123
|
ORDER BY indexname`, [schema, table]);
|
|
@@ -144,20 +127,5 @@ export function registerTools(server) {
|
|
|
144
127
|
return errorContent('get_table_indexes', err);
|
|
145
128
|
}
|
|
146
129
|
});
|
|
147
|
-
server.tool('explain_query', 'Get the execution plan for a SQL query.', {
|
|
148
|
-
database: z.string().describe('The database to explain the query against'),
|
|
149
|
-
sql: z.string().describe('The SQL query to explain'),
|
|
150
|
-
analyze: z.boolean().default(false).describe('Whether to actually execute the query for real timing data. Writes stay rejected because the plan runs in a read-only transaction.'),
|
|
151
|
-
}, async ({ database, sql, analyze }) => {
|
|
152
|
-
log(`Tool called: explain_query on ${database}`);
|
|
153
|
-
try {
|
|
154
|
-
const explainSql = analyze ? `EXPLAIN ANALYZE ${sql}` : `EXPLAIN ${sql}`;
|
|
155
|
-
const rows = await queryReadOnly(database, explainSql);
|
|
156
|
-
return textContent(rows.map((row) => row['QUERY PLAN']).join('\n'));
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
return errorContent('explain_query', err);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
130
|
}
|
|
163
131
|
//# sourceMappingURL=tools.js.map
|
package/dist/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-only.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/read-only.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
const client = {
|
|
3
|
-
query: vi.fn(),
|
|
4
|
-
release: vi.fn(),
|
|
5
|
-
};
|
|
6
|
-
const pool = {
|
|
7
|
-
connect: vi.fn(async () => client),
|
|
8
|
-
query: vi.fn(),
|
|
9
|
-
};
|
|
10
|
-
vi.mock('../db.js', () => ({ getPool: () => pool }));
|
|
11
|
-
const { registerTools } = await import('../tools.js');
|
|
12
|
-
function collectTools() {
|
|
13
|
-
const handlers = new Map();
|
|
14
|
-
registerTools({
|
|
15
|
-
tool: (name, _description, _schema, handler) => {
|
|
16
|
-
handlers.set(name, handler);
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
return handlers;
|
|
20
|
-
}
|
|
21
|
-
describe('read-only transaction', () => {
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
client.query.mockReset();
|
|
24
|
-
client.release.mockReset();
|
|
25
|
-
pool.connect.mockClear();
|
|
26
|
-
});
|
|
27
|
-
it('wraps query in a read-only transaction and commits', async () => {
|
|
28
|
-
client.query.mockResolvedValue({ rows: [{ id: 1 }] });
|
|
29
|
-
const result = await collectTools().get('query')({ database: 'apono', sql: 'SELECT 1' });
|
|
30
|
-
const issued = client.query.mock.calls.map((call) => call[0]);
|
|
31
|
-
expect(issued[0]).toBe('BEGIN TRANSACTION READ ONLY');
|
|
32
|
-
expect(issued).toContain('SELECT 1');
|
|
33
|
-
expect(issued).toContain('COMMIT');
|
|
34
|
-
expect(result.isError).toBeUndefined();
|
|
35
|
-
expect(client.release).toHaveBeenCalledOnce();
|
|
36
|
-
});
|
|
37
|
-
it('surfaces the database rejection when a write is attempted and rolls back', async () => {
|
|
38
|
-
client.query.mockImplementation(async (sql) => {
|
|
39
|
-
if (sql === 'BEGIN TRANSACTION READ ONLY')
|
|
40
|
-
return { rows: [] };
|
|
41
|
-
if (sql === 'ROLLBACK')
|
|
42
|
-
return { rows: [] };
|
|
43
|
-
throw new Error('cannot execute DELETE in a read-only transaction');
|
|
44
|
-
});
|
|
45
|
-
const result = await collectTools().get('query')({ database: 'apono', sql: 'DELETE FROM users' });
|
|
46
|
-
expect(result.isError).toBe(true);
|
|
47
|
-
expect(result.content[0].text).toContain('read-only transaction');
|
|
48
|
-
expect(client.query.mock.calls.map((call) => call[0])).toContain('ROLLBACK');
|
|
49
|
-
expect(client.release).toHaveBeenCalledOnce();
|
|
50
|
-
});
|
|
51
|
-
it('runs explain_query with analyze inside the read-only transaction', async () => {
|
|
52
|
-
client.query.mockResolvedValue({ rows: [{ 'QUERY PLAN': 'Seq Scan on users' }] });
|
|
53
|
-
await collectTools().get('explain_query')({ database: 'apono', sql: 'SELECT 1', analyze: true });
|
|
54
|
-
const issued = client.query.mock.calls.map((call) => call[0]);
|
|
55
|
-
expect(issued[0]).toBe('BEGIN TRANSACTION READ ONLY');
|
|
56
|
-
expect(issued).toContain('EXPLAIN ANALYZE SELECT 1');
|
|
57
|
-
});
|
|
58
|
-
it('leaves execute outside the read-only transaction', async () => {
|
|
59
|
-
pool.query.mockResolvedValue({ rowCount: 1 });
|
|
60
|
-
await collectTools().get('execute')({ database: 'apono', sql: 'DELETE FROM users' });
|
|
61
|
-
expect(pool.connect).not.toHaveBeenCalled();
|
|
62
|
-
expect(pool.query).toHaveBeenCalledWith('DELETE FROM users');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
//# sourceMappingURL=read-only.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-only.test.js","sourceRoot":"","sources":["../../src/__tests__/read-only.test.ts"],"names":[],"mappings":"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"}
|