@apono-io/mcp-mysql 0.2.0 → 0.3.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 +30 -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 +53 -0
- package/dist/__tests__/execute.test.js.map +1 -0
- package/dist/__tests__/helpers/database.d.ts +8 -0
- package/dist/__tests__/helpers/database.d.ts.map +1 -0
- package/dist/__tests__/helpers/database.js +16 -0
- package/dist/__tests__/helpers/database.js.map +1 -0
- package/dist/__tests__/tools.test.js +9 -1
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +7 -3
- package/dist/db.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +4 -42
- 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,16 +6,43 @@ Apono MCP server for MySQL 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 a SQL statement. Returns rows for a statement that produces a result set (`SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`, ...), and the affected row count for one that changes data or schema |
|
|
11
10
|
| `list_databases` | List all databases on the server |
|
|
12
11
|
| `list_tables` | List all tables in a database |
|
|
13
12
|
| `describe_table` | Get column names, types, constraints, and indexes for a table |
|
|
14
13
|
| `get_table_indexes` | List indexes on a specific table |
|
|
15
|
-
| `explain_query` | Get the execution plan for a SQL query |
|
|
16
14
|
|
|
17
15
|
All tools (except `list_databases`) accept a `database` parameter.
|
|
18
16
|
|
|
17
|
+
## How SQL is authorized
|
|
18
|
+
|
|
19
|
+
`execute` is the only tool that runs caller-supplied SQL. The server does not
|
|
20
|
+
classify that SQL itself. The Apono Agentic Gateway reads the `sql` argument
|
|
21
|
+
and matches it against the keyword rules declared for the tool in
|
|
22
|
+
`managed-mcp-configs/configs/mysql.json`, then applies the policy for the
|
|
23
|
+
resulting category (`read`, `create`, `update`, `delete` or `admin`). Rules are
|
|
24
|
+
evaluated from the most restrictive category to the least, so the first match
|
|
25
|
+
decides. A statement matching no rule takes the declared default, `update`.
|
|
26
|
+
|
|
27
|
+
The rules match the SQL text, so a read whose text contains a write keyword, as in
|
|
28
|
+
`WHERE op = 'delete'`, grades at that keyword's category. It errs toward asking for
|
|
29
|
+
approval rather than allowing.
|
|
30
|
+
|
|
31
|
+
Earlier versions also exposed `query` and `explain_query`, graded `read` and so
|
|
32
|
+
runnable with no approval, which relied on the server to enforce read-only.
|
|
33
|
+
That enforcement was not sound: MySQL commits any open transaction by itself
|
|
34
|
+
before DDL, so `START TRANSACTION READ ONLY` did not stop `CREATE`, `ALTER`,
|
|
35
|
+
`DROP`, `TRUNCATE` or `RENAME TABLE`. Both tools were removed in 0.3.0
|
|
36
|
+
(DVL-12107) so that all SQL is graded through one mechanism. Send the same SQL
|
|
37
|
+
to `execute` instead.
|
|
38
|
+
|
|
39
|
+
The `list_*` and `describe_table` tools stay safe to grade `read` because they
|
|
40
|
+
run fixed `information_schema` queries with parameter placeholders; a caller
|
|
41
|
+
supplies only a database or table name, never the statement.
|
|
42
|
+
|
|
43
|
+
A call carries one statement. `multipleStatements` is off by default in the
|
|
44
|
+
driver, so a call holding several is rejected.
|
|
45
|
+
|
|
19
46
|
## Environment Variables
|
|
20
47
|
|
|
21
48
|
| Variable | Description |
|
|
@@ -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, end: vi.fn(async () => undefined) };
|
|
4
|
+
}
|
|
5
|
+
let created = [];
|
|
6
|
+
const createPool = 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('mysql2/promise', () => ({ default: { createPool } }));
|
|
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
|
+
createPool.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 reset'));
|
|
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 reset'));
|
|
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 reset'));
|
|
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 reset');
|
|
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,53 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import { connect, useTestCredentials } from './helpers/database.js';
|
|
3
|
+
const DATABASE = 'mcp_mysql_execute_test';
|
|
4
|
+
describe('execute', () => {
|
|
5
|
+
let admin;
|
|
6
|
+
let tools;
|
|
7
|
+
let closeAllPools;
|
|
8
|
+
async function rowCount() {
|
|
9
|
+
const [rows] = await admin.query('SELECT COUNT(*) AS n FROM users');
|
|
10
|
+
return Number(rows[0].n);
|
|
11
|
+
}
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
admin = await connect();
|
|
14
|
+
await admin.query(`DROP DATABASE IF EXISTS \`${DATABASE}\``);
|
|
15
|
+
await admin.query(`CREATE DATABASE \`${DATABASE}\``);
|
|
16
|
+
await admin.changeUser({ database: DATABASE });
|
|
17
|
+
useTestCredentials();
|
|
18
|
+
const toolsModule = await import('../tools.js');
|
|
19
|
+
closeAllPools = (await import('../db.js')).closeAllPools;
|
|
20
|
+
tools = new Map();
|
|
21
|
+
toolsModule.registerTools({
|
|
22
|
+
tool: (name, _d, _s, handler) => {
|
|
23
|
+
tools.set(name, handler);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await closeAllPools?.();
|
|
29
|
+
await admin?.query(`DROP DATABASE IF EXISTS \`${DATABASE}\``);
|
|
30
|
+
await admin?.end();
|
|
31
|
+
});
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
await admin.query('DROP TABLE IF EXISTS users');
|
|
34
|
+
await admin.query('CREATE TABLE users (id INT PRIMARY KEY)');
|
|
35
|
+
await admin.query('INSERT INTO users VALUES (1)');
|
|
36
|
+
});
|
|
37
|
+
it('leaves execute outside the read-only transaction', async () => {
|
|
38
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'DELETE FROM users' });
|
|
39
|
+
expect(result.isError).toBeUndefined();
|
|
40
|
+
expect(await rowCount()).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
it('returns the rows of a statement that produces a result set', async () => {
|
|
43
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'SELECT id FROM users' });
|
|
44
|
+
expect(result.isError).toBeUndefined();
|
|
45
|
+
expect(result.content[0].text).not.toContain('Rows affected');
|
|
46
|
+
expect(result.content[0].text).toContain('1');
|
|
47
|
+
});
|
|
48
|
+
it('reports the affected row count of a statement that changes data', async () => {
|
|
49
|
+
const result = await tools.get('execute')({ database: DATABASE, sql: 'DELETE FROM users' });
|
|
50
|
+
expect(result.content[0].text).toContain('Rows affected: 1');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
//# 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,8 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
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 function connect(): Promise<mysql.Connection>;
|
|
7
|
+
export declare function useTestCredentials(): void;
|
|
8
|
+
//# 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,KAAK,MAAM,gBAAgB,CAAC;AAEnC,eAAO,MAAM,IAAI,QAA6C,CAAC;AAC/D,eAAO,MAAM,IAAI,QAA8C,CAAC;AAChE,eAAO,MAAM,IAAI,QAAwC,CAAC;AAC1D,eAAO,MAAM,QAAQ,QAAkD,CAAC;AAExE,wBAAgB,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAEnD;AAGD,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
export const HOST = process.env.MYSQL_TEST_HOST ?? '127.0.0.1';
|
|
3
|
+
export const PORT = Number(process.env.MYSQL_TEST_PORT ?? 3306);
|
|
4
|
+
export const USER = process.env.MYSQL_TEST_USER ?? 'root';
|
|
5
|
+
export const PASSWORD = process.env.MYSQL_TEST_PASSWORD ?? 'apono-test';
|
|
6
|
+
export function connect() {
|
|
7
|
+
return mysql.createConnection({ host: HOST, port: PORT, user: USER, password: PASSWORD });
|
|
8
|
+
}
|
|
9
|
+
// The server builds its pools from these, so they are set before it is imported.
|
|
10
|
+
export function useTestCredentials() {
|
|
11
|
+
process.env.MYSQL_HOST = HOST;
|
|
12
|
+
process.env.MYSQL_PORT = String(PORT);
|
|
13
|
+
process.env.MYSQL_USER = USER;
|
|
14
|
+
process.env.MYSQL_PASSWORD = PASSWORD;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/__tests__/helpers/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AAEnC,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,WAAW,CAAC;AAC/D,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,MAAM,CAAC;AAC1D,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,YAAY,CAAC;AAExE,MAAM,UAAU,OAAO;IACrB,OAAO,KAAK,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC5F,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,kBAAkB;IAChC,OAAO,CAAC,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,QAAQ,CAAC;AACxC,CAAC"}
|
|
@@ -1,10 +1,18 @@
|
|
|
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 5 mysql 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-12107)').toEqual(['describe_table', 'execute', 'get_table_indexes', 'list_databases', 'list_tables']);
|
|
16
|
+
});
|
|
9
17
|
});
|
|
10
18
|
//# 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,2CAA2C,EAAE,GAAG,EAAE;QACnD,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,2CAA2C,EAAE,GAAG,EAAE;QACnD,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,CAAC,gBAAgB,EAAE,SAAS,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAC,CAAC;IACjG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
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,KAAK,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AAgBnC,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,IAAI,CAOpD;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAOnD"}
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import mysql from 'mysql2/promise';
|
|
2
|
+
import { log } from './logger.js';
|
|
2
3
|
const pools = new Map();
|
|
3
4
|
function getPoolConfig() {
|
|
4
5
|
return {
|
|
@@ -19,9 +20,12 @@ export function getPool(database) {
|
|
|
19
20
|
return pool;
|
|
20
21
|
}
|
|
21
22
|
export async function closeAllPools() {
|
|
22
|
-
|
|
23
|
-
await pool.end();
|
|
24
|
-
}
|
|
23
|
+
const open = [...pools.values()];
|
|
25
24
|
pools.clear();
|
|
25
|
+
const results = await Promise.allSettled(open.map((pool) => pool.end()));
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
if (result.status === 'rejected')
|
|
28
|
+
log('Failed to close pool:', result.reason);
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
//# 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,KAAK,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE5C,SAAS,aAAa;IACpB,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;QAC5B,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;QAC1E,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;QAC5B,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;QACpC,kBAAkB,EAAE,IAAI;QACxB,eAAe,EAAE,CAAC;KACnB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,QAAgB;IACtC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,EAAE,GAAG,aAAa,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACjC,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACzE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU;YAAE,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAChF,CAAC;AACH,CAAC"}
|
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;AAsBpE,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAyGrD"}
|
package/dist/tools.js
CHANGED
|
@@ -15,39 +15,15 @@ function formatRows(rows, emptyHint) {
|
|
|
15
15
|
}
|
|
16
16
|
return JSON.stringify(rows, null, 2);
|
|
17
17
|
}
|
|
18
|
-
// Runs sql inside a read-only transaction so MySQL itself rejects any write,
|
|
19
|
-
// including writes reached through EXPLAIN ANALYZE.
|
|
20
|
-
async function queryReadOnly(database, sql) {
|
|
21
|
-
const connection = await getPool(database).getConnection();
|
|
22
|
-
try {
|
|
23
|
-
await connection.query('START TRANSACTION READ ONLY');
|
|
24
|
-
const [rows] = await connection.query(sql);
|
|
25
|
-
await connection.query('COMMIT');
|
|
26
|
-
return rows;
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
await connection.query('ROLLBACK').catch(() => undefined);
|
|
30
|
-
throw err;
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
connection.release();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
18
|
export function registerTools(server) {
|
|
37
|
-
server.tool('
|
|
38
|
-
log(`Tool called: query on ${database}`);
|
|
39
|
-
try {
|
|
40
|
-
return textContent(formatRows(await queryReadOnly(database, sql)));
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
return errorContent('query', err);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
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 }) => {
|
|
19
|
+
server.tool('execute', 'Execute a SQL statement. Returns the rows for a statement that produces a result set, such as SELECT, SHOW, DESCRIBE or EXPLAIN, and the affected row count for a statement that changes data or schema.', { database: z.string().describe('The database to execute against'), sql: z.string().describe('The SQL statement to execute') }, async ({ database, sql }) => {
|
|
47
20
|
log(`Tool called: execute on ${database}`);
|
|
48
21
|
try {
|
|
49
22
|
const pool = getPool(database);
|
|
50
23
|
const [result] = await pool.query(sql);
|
|
24
|
+
if (Array.isArray(result)) {
|
|
25
|
+
return textContent(formatRows(result));
|
|
26
|
+
}
|
|
51
27
|
const affectedRows = result.affectedRows ?? 0;
|
|
52
28
|
return textContent(`Statement executed successfully. Rows affected: ${affectedRows}`);
|
|
53
29
|
}
|
|
@@ -112,19 +88,5 @@ export function registerTools(server) {
|
|
|
112
88
|
return errorContent('get_table_indexes', err);
|
|
113
89
|
}
|
|
114
90
|
});
|
|
115
|
-
server.tool('explain_query', 'Get the execution plan for a SQL query.', {
|
|
116
|
-
database: z.string().describe('The database to explain the query against'),
|
|
117
|
-
sql: z.string().describe('The SQL query to explain'),
|
|
118
|
-
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.'),
|
|
119
|
-
}, async ({ database, sql, analyze }) => {
|
|
120
|
-
log(`Tool called: explain_query on ${database}`);
|
|
121
|
-
try {
|
|
122
|
-
const explainSql = analyze ? `EXPLAIN ANALYZE ${sql}` : `EXPLAIN ${sql}`;
|
|
123
|
-
return textContent(formatRows(await queryReadOnly(database, explainSql)));
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
return errorContent('explain_query', err);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
91
|
}
|
|
130
92
|
//# 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":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB,EAAE,GAAY;IAClD,GAAG,CAAC,YAAY,QAAQ,GAAG,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,UAAU,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAa,EAAE,CAAC;AACzG,CAAC;AAED,SAAS,UAAU,CAAC,IAA+B,EAAE,SAAkB;IACrE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,SAAS,IAAI,mBAAmB,CAAC;QAC9C,OAAO,GAAG,IAAI,0FAA0F,CAAC;IAC3G,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,
|
|
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 connection = {
|
|
3
|
-
query: vi.fn(),
|
|
4
|
-
release: vi.fn(),
|
|
5
|
-
};
|
|
6
|
-
const pool = {
|
|
7
|
-
getConnection: vi.fn(async () => connection),
|
|
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
|
-
connection.query.mockReset();
|
|
24
|
-
connection.release.mockReset();
|
|
25
|
-
pool.getConnection.mockClear();
|
|
26
|
-
});
|
|
27
|
-
it('wraps query in a read-only transaction and commits', async () => {
|
|
28
|
-
connection.query.mockResolvedValue([[{ id: 1 }]]);
|
|
29
|
-
const result = await collectTools().get('query')({ database: 'apono', sql: 'SELECT 1' });
|
|
30
|
-
const issued = connection.query.mock.calls.map((call) => call[0]);
|
|
31
|
-
expect(issued[0]).toBe('START TRANSACTION READ ONLY');
|
|
32
|
-
expect(issued).toContain('SELECT 1');
|
|
33
|
-
expect(issued).toContain('COMMIT');
|
|
34
|
-
expect(result.isError).toBeUndefined();
|
|
35
|
-
expect(connection.release).toHaveBeenCalledOnce();
|
|
36
|
-
});
|
|
37
|
-
it('surfaces the database rejection when a write is attempted and rolls back', async () => {
|
|
38
|
-
connection.query.mockImplementation(async (sql) => {
|
|
39
|
-
if (sql === 'START TRANSACTION READ ONLY')
|
|
40
|
-
return [[]];
|
|
41
|
-
if (sql === 'ROLLBACK')
|
|
42
|
-
return [[]];
|
|
43
|
-
throw new Error('Cannot execute statement 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(connection.query.mock.calls.map((call) => call[0])).toContain('ROLLBACK');
|
|
49
|
-
expect(connection.release).toHaveBeenCalledOnce();
|
|
50
|
-
});
|
|
51
|
-
it('runs explain_query with analyze inside the read-only transaction', async () => {
|
|
52
|
-
connection.query.mockResolvedValue([[{ EXPLAIN: 'scan' }]]);
|
|
53
|
-
await collectTools().get('explain_query')({ database: 'apono', sql: 'SELECT 1', analyze: true });
|
|
54
|
-
const issued = connection.query.mock.calls.map((call) => call[0]);
|
|
55
|
-
expect(issued[0]).toBe('START 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([{ affectedRows: 1 }]);
|
|
60
|
-
await collectTools().get('execute')({ database: 'apono', sql: 'DELETE FROM users' });
|
|
61
|
-
expect(pool.getConnection).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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"}
|