@nifrajs/mcp-db 2.2.0 → 2.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 +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +29 -15
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ because it cannot provide the same engine-level read-only guarantee.
|
|
|
22
22
|
|
|
23
23
|
## For AI agents
|
|
24
24
|
|
|
25
|
-
Start with [`LLM.md`](./LLM.md)
|
|
25
|
+
Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
|
|
26
26
|
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
27
27
|
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
28
28
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp-db`
|
|
2
|
+
* `@nifrajs/mcp-db` - serve a SQLite database as an MCP server, fail-closed.
|
|
3
3
|
*
|
|
4
4
|
* Out of the box only SCHEMA tools are exposed (`list_tables`, `describe_table`), and only for
|
|
5
5
|
* tables on the explicit `tables` allowlist. Query execution is OPT-IN and requires BOTH the
|
|
6
|
-
* allowlist and an `authorize` hook
|
|
6
|
+
* allowlist and an `authorize` hook - a database is PII by default, and an MCP client is an LLM;
|
|
7
7
|
* this package refuses to be mounted open rather than defaulting open.
|
|
8
8
|
*
|
|
9
9
|
* Read-only is enforced in layers, not promised: `PRAGMA query_only = ON` on the connection
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* capped by rows and bytes (an LLM does not need 100k rows; a transport does not want them) with
|
|
13
13
|
* an explicit `truncated` marker.
|
|
14
14
|
*
|
|
15
|
-
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally
|
|
15
|
+
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally - no driver dependency).
|
|
16
16
|
* There is deliberately NO D1 mode: D1 has no `query_only`, so read-only cannot be guaranteed.
|
|
17
17
|
*
|
|
18
18
|
* import { Database } from "bun:sqlite"
|
|
@@ -32,7 +32,7 @@ export interface SqliteDatabaseLike {
|
|
|
32
32
|
/** Execute a statement for its side effect (used only for `PRAGMA query_only`). */
|
|
33
33
|
run(sql: string): unknown;
|
|
34
34
|
}
|
|
35
|
-
/** Context forwarded to `authorize`
|
|
35
|
+
/** Context forwarded to `authorize` - the inbound HTTP Request carrying the `run_query` call. */
|
|
36
36
|
export interface McpDbAuthorizeContext {
|
|
37
37
|
readonly toolName: "run_query";
|
|
38
38
|
readonly request: Request;
|
|
@@ -41,8 +41,8 @@ export interface RunQueryOptions {
|
|
|
41
41
|
/**
|
|
42
42
|
* REQUIRED. Authorize each `run_query` call at the transport boundary (inspect the inbound
|
|
43
43
|
* Request's auth). Return false to reject with a JSON-RPC error. There is no unauthenticated
|
|
44
|
-
* mode
|
|
45
|
-
* direct `handle()` dispatch rejects it (no Request to authorize
|
|
44
|
+
* mode - schema tools are the anonymous surface. Note: `run_query` is therefore HTTP-only;
|
|
45
|
+
* direct `handle()` dispatch rejects it (no Request to authorize - fails closed).
|
|
46
46
|
*/
|
|
47
47
|
readonly authorize: (context: McpDbAuthorizeContext) => boolean | Promise<boolean>;
|
|
48
48
|
/** Max rows returned per query (default 100). */
|
|
@@ -64,9 +64,9 @@ export interface ServeDatabaseAsMcpOptions {
|
|
|
64
64
|
readonly version?: string;
|
|
65
65
|
/**
|
|
66
66
|
* Set `PRAGMA query_only = ON` on the connection at construction (default true). Disable ONLY
|
|
67
|
-
* when the same connection must also serve writes elsewhere in the app
|
|
67
|
+
* when the same connection must also serve writes elsewhere in the app - with it disabled,
|
|
68
68
|
* `run_query` still gates on SELECT/WITH + plan verification, but the engine-level guarantee is
|
|
69
|
-
* yours to provide (e.g. open a second, read-only connection for the MCP mount
|
|
69
|
+
* yours to provide (e.g. open a second, read-only connection for the MCP mount - preferred).
|
|
70
70
|
*/
|
|
71
71
|
readonly enforceQueryOnly?: boolean;
|
|
72
72
|
}
|
|
@@ -75,7 +75,7 @@ export declare class McpDbConfigError extends Error {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* Serve `db` as a mountable MCP server (`mcp.fetch` at `POST /mcp`). See module docs for the
|
|
78
|
-
* security model. Throws {@link McpDbConfigError} on any unsafe configuration
|
|
78
|
+
* security model. Throws {@link McpDbConfigError} on any unsafe configuration - always at
|
|
79
79
|
* construction (boot), never at request time.
|
|
80
80
|
*/
|
|
81
81
|
export declare function serveDatabaseAsMcp(db: SqliteDatabaseLike, options: ServeDatabaseAsMcpOptions): McpServer;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAEnG,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;KAAE,CAAA;IAC9D,mFAAmF;IACnF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC1B;AAED,iGAAiG;AACjG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClF,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAA;IACnC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACpC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAEnG,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;KAAE,CAAA;IAC9D,mFAAmF;IACnF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC1B;AAED,iGAAiG;AACjG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClF,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAA;IACnC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACpC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAoCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,yBAAyB,GACjC,SAAS,CAkMX"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp-db`
|
|
2
|
+
* `@nifrajs/mcp-db` - serve a SQLite database as an MCP server, fail-closed.
|
|
3
3
|
*
|
|
4
4
|
* Out of the box only SCHEMA tools are exposed (`list_tables`, `describe_table`), and only for
|
|
5
5
|
* tables on the explicit `tables` allowlist. Query execution is OPT-IN and requires BOTH the
|
|
6
|
-
* allowlist and an `authorize` hook
|
|
6
|
+
* allowlist and an `authorize` hook - a database is PII by default, and an MCP client is an LLM;
|
|
7
7
|
* this package refuses to be mounted open rather than defaulting open.
|
|
8
8
|
*
|
|
9
9
|
* Read-only is enforced in layers, not promised: `PRAGMA query_only = ON` on the connection
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* capped by rows and bytes (an LLM does not need 100k rows; a transport does not want them) with
|
|
13
13
|
* an explicit `truncated` marker.
|
|
14
14
|
*
|
|
15
|
-
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally
|
|
15
|
+
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally - no driver dependency).
|
|
16
16
|
* There is deliberately NO D1 mode: D1 has no `query_only`, so read-only cannot be guaranteed.
|
|
17
17
|
*
|
|
18
18
|
* import { Database } from "bun:sqlite"
|
|
@@ -29,8 +29,19 @@ export class McpDbConfigError extends Error {
|
|
|
29
29
|
this.name = "McpDbConfigError";
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
/** A safe SQL identifier
|
|
32
|
+
/** A safe SQL identifier - quoting is not enough for PRAGMA args, so reject instead. */
|
|
33
33
|
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
|
+
/** Build SQL containing identifiers only after validating every substitution at this boundary. */
|
|
35
|
+
function sqlIdentifiers(strings, ...identifiers) {
|
|
36
|
+
let sql = strings[0] ?? "";
|
|
37
|
+
for (const [index, identifier] of identifiers.entries()) {
|
|
38
|
+
if (!SAFE_IDENTIFIER.test(identifier)) {
|
|
39
|
+
throw new McpDbConfigError(`unsafe SQL identifier ${JSON.stringify(identifier)}`);
|
|
40
|
+
}
|
|
41
|
+
sql += identifier + (strings[index + 1] ?? "");
|
|
42
|
+
}
|
|
43
|
+
return sql;
|
|
44
|
+
}
|
|
34
45
|
const stripSqlNoise = (sql) => sql
|
|
35
46
|
// Strings first so comment markers inside literals don't count.
|
|
36
47
|
.replace(/'(?:[^']|'')*'/g, "''")
|
|
@@ -48,12 +59,12 @@ const isReadStatement = (sql) => {
|
|
|
48
59
|
};
|
|
49
60
|
/**
|
|
50
61
|
* Serve `db` as a mountable MCP server (`mcp.fetch` at `POST /mcp`). See module docs for the
|
|
51
|
-
* security model. Throws {@link McpDbConfigError} on any unsafe configuration
|
|
62
|
+
* security model. Throws {@link McpDbConfigError} on any unsafe configuration - always at
|
|
52
63
|
* construction (boot), never at request time.
|
|
53
64
|
*/
|
|
54
65
|
export function serveDatabaseAsMcp(db, options) {
|
|
55
66
|
if (options.tables.length === 0) {
|
|
56
|
-
throw new McpDbConfigError("an explicit non-empty `tables` allowlist is required
|
|
67
|
+
throw new McpDbConfigError("an explicit non-empty `tables` allowlist is required - this package never defaults to exposing a whole database");
|
|
57
68
|
}
|
|
58
69
|
for (const table of options.tables) {
|
|
59
70
|
if (!SAFE_IDENTIFIER.test(table)) {
|
|
@@ -66,7 +77,7 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
66
77
|
db.run("PRAGMA query_only = ON");
|
|
67
78
|
}
|
|
68
79
|
catch (error) {
|
|
69
|
-
throw new McpDbConfigError(`could not set PRAGMA query_only on the connection (${String(error)})
|
|
80
|
+
throw new McpDbConfigError(`could not set PRAGMA query_only on the connection (${String(error)}) - pass a bun:sqlite-shaped database or set enforceQueryOnly: false with a read-only connection`);
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
const maxRows = options.runQuery?.maxRows ?? 100;
|
|
@@ -76,7 +87,9 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
76
87
|
description: "List the tables this server exposes, with row counts.",
|
|
77
88
|
handler: () => {
|
|
78
89
|
const rows = options.tables.map((table) => {
|
|
79
|
-
const [count] = db
|
|
90
|
+
const [count] = db
|
|
91
|
+
.prepare(sqlIdentifiers `SELECT count(*) AS n FROM "${table}"`)
|
|
92
|
+
.all();
|
|
80
93
|
return { table, rows: count?.n ?? 0 };
|
|
81
94
|
});
|
|
82
95
|
return { text: JSON.stringify(rows), structuredContent: { tables: rows } };
|
|
@@ -97,7 +110,7 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
97
110
|
}
|
|
98
111
|
// Identifier is allowlist-verified (and the allowlist is identifier-checked at boot),
|
|
99
112
|
// so interpolation here cannot inject.
|
|
100
|
-
const columns = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
113
|
+
const columns = db.prepare(sqlIdentifiers `PRAGMA table_info("${table}")`).all();
|
|
101
114
|
return { text: JSON.stringify(columns), structuredContent: { table, columns } };
|
|
102
115
|
},
|
|
103
116
|
});
|
|
@@ -180,7 +193,7 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
180
193
|
if (options.runQuery === undefined)
|
|
181
194
|
return server;
|
|
182
195
|
// `run_query` is authorized at the TRANSPORT boundary, where the inbound Request's credentials
|
|
183
|
-
// are visible
|
|
196
|
+
// are visible - the handler itself performs no auth, so every path to it must pass this gate:
|
|
184
197
|
// the wrapped fetch authorizes, and direct handle() dispatch fails closed (no Request → no auth).
|
|
185
198
|
const { authorize } = options.runQuery;
|
|
186
199
|
const rpcId = (id) => typeof id === "string" || typeof id === "number" ? id : null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/mcp-db",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Serve a SQLite database as a fail-closed MCP server
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Serve a SQLite database as a fail-closed MCP server - allowlisted schema tools by default, opt-in read-only queries with plan verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"typescript",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@nifrajs/mcp": "2.
|
|
38
|
+
"@nifrajs/mcp": "2.3.0"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/mcp-db`
|
|
2
|
+
* `@nifrajs/mcp-db` - serve a SQLite database as an MCP server, fail-closed.
|
|
3
3
|
*
|
|
4
4
|
* Out of the box only SCHEMA tools are exposed (`list_tables`, `describe_table`), and only for
|
|
5
5
|
* tables on the explicit `tables` allowlist. Query execution is OPT-IN and requires BOTH the
|
|
6
|
-
* allowlist and an `authorize` hook
|
|
6
|
+
* allowlist and an `authorize` hook - a database is PII by default, and an MCP client is an LLM;
|
|
7
7
|
* this package refuses to be mounted open rather than defaulting open.
|
|
8
8
|
*
|
|
9
9
|
* Read-only is enforced in layers, not promised: `PRAGMA query_only = ON` on the connection
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* capped by rows and bytes (an LLM does not need 100k rows; a transport does not want them) with
|
|
13
13
|
* an explicit `truncated` marker.
|
|
14
14
|
*
|
|
15
|
-
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally
|
|
15
|
+
* Bun/Node SQLite only (anything `bun:sqlite`-shaped, bound structurally - no driver dependency).
|
|
16
16
|
* There is deliberately NO D1 mode: D1 has no `query_only`, so read-only cannot be guaranteed.
|
|
17
17
|
*
|
|
18
18
|
* import { Database } from "bun:sqlite"
|
|
@@ -33,7 +33,7 @@ export interface SqliteDatabaseLike {
|
|
|
33
33
|
run(sql: string): unknown
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** Context forwarded to `authorize`
|
|
36
|
+
/** Context forwarded to `authorize` - the inbound HTTP Request carrying the `run_query` call. */
|
|
37
37
|
export interface McpDbAuthorizeContext {
|
|
38
38
|
readonly toolName: "run_query"
|
|
39
39
|
readonly request: Request
|
|
@@ -43,8 +43,8 @@ export interface RunQueryOptions {
|
|
|
43
43
|
/**
|
|
44
44
|
* REQUIRED. Authorize each `run_query` call at the transport boundary (inspect the inbound
|
|
45
45
|
* Request's auth). Return false to reject with a JSON-RPC error. There is no unauthenticated
|
|
46
|
-
* mode
|
|
47
|
-
* direct `handle()` dispatch rejects it (no Request to authorize
|
|
46
|
+
* mode - schema tools are the anonymous surface. Note: `run_query` is therefore HTTP-only;
|
|
47
|
+
* direct `handle()` dispatch rejects it (no Request to authorize - fails closed).
|
|
48
48
|
*/
|
|
49
49
|
readonly authorize: (context: McpDbAuthorizeContext) => boolean | Promise<boolean>
|
|
50
50
|
/** Max rows returned per query (default 100). */
|
|
@@ -67,9 +67,9 @@ export interface ServeDatabaseAsMcpOptions {
|
|
|
67
67
|
readonly version?: string
|
|
68
68
|
/**
|
|
69
69
|
* Set `PRAGMA query_only = ON` on the connection at construction (default true). Disable ONLY
|
|
70
|
-
* when the same connection must also serve writes elsewhere in the app
|
|
70
|
+
* when the same connection must also serve writes elsewhere in the app - with it disabled,
|
|
71
71
|
* `run_query` still gates on SELECT/WITH + plan verification, but the engine-level guarantee is
|
|
72
|
-
* yours to provide (e.g. open a second, read-only connection for the MCP mount
|
|
72
|
+
* yours to provide (e.g. open a second, read-only connection for the MCP mount - preferred).
|
|
73
73
|
*/
|
|
74
74
|
readonly enforceQueryOnly?: boolean
|
|
75
75
|
}
|
|
@@ -81,9 +81,21 @@ export class McpDbConfigError extends Error {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** A safe SQL identifier
|
|
84
|
+
/** A safe SQL identifier - quoting is not enough for PRAGMA args, so reject instead. */
|
|
85
85
|
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
86
86
|
|
|
87
|
+
/** Build SQL containing identifiers only after validating every substitution at this boundary. */
|
|
88
|
+
function sqlIdentifiers(strings: TemplateStringsArray, ...identifiers: readonly string[]): string {
|
|
89
|
+
let sql = strings[0] ?? ""
|
|
90
|
+
for (const [index, identifier] of identifiers.entries()) {
|
|
91
|
+
if (!SAFE_IDENTIFIER.test(identifier)) {
|
|
92
|
+
throw new McpDbConfigError(`unsafe SQL identifier ${JSON.stringify(identifier)}`)
|
|
93
|
+
}
|
|
94
|
+
sql += identifier + (strings[index + 1] ?? "")
|
|
95
|
+
}
|
|
96
|
+
return sql
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
const stripSqlNoise = (sql: string): string =>
|
|
88
100
|
sql
|
|
89
101
|
// Strings first so comment markers inside literals don't count.
|
|
@@ -105,7 +117,7 @@ const isReadStatement = (sql: string): boolean => {
|
|
|
105
117
|
|
|
106
118
|
/**
|
|
107
119
|
* Serve `db` as a mountable MCP server (`mcp.fetch` at `POST /mcp`). See module docs for the
|
|
108
|
-
* security model. Throws {@link McpDbConfigError} on any unsafe configuration
|
|
120
|
+
* security model. Throws {@link McpDbConfigError} on any unsafe configuration - always at
|
|
109
121
|
* construction (boot), never at request time.
|
|
110
122
|
*/
|
|
111
123
|
export function serveDatabaseAsMcp(
|
|
@@ -114,7 +126,7 @@ export function serveDatabaseAsMcp(
|
|
|
114
126
|
): McpServer {
|
|
115
127
|
if (options.tables.length === 0) {
|
|
116
128
|
throw new McpDbConfigError(
|
|
117
|
-
"an explicit non-empty `tables` allowlist is required
|
|
129
|
+
"an explicit non-empty `tables` allowlist is required - this package never defaults to exposing a whole database",
|
|
118
130
|
)
|
|
119
131
|
}
|
|
120
132
|
for (const table of options.tables) {
|
|
@@ -131,7 +143,7 @@ export function serveDatabaseAsMcp(
|
|
|
131
143
|
db.run("PRAGMA query_only = ON")
|
|
132
144
|
} catch (error) {
|
|
133
145
|
throw new McpDbConfigError(
|
|
134
|
-
`could not set PRAGMA query_only on the connection (${String(error)})
|
|
146
|
+
`could not set PRAGMA query_only on the connection (${String(error)}) - pass a bun:sqlite-shaped database or set enforceQueryOnly: false with a read-only connection`,
|
|
135
147
|
)
|
|
136
148
|
}
|
|
137
149
|
}
|
|
@@ -144,7 +156,9 @@ export function serveDatabaseAsMcp(
|
|
|
144
156
|
description: "List the tables this server exposes, with row counts.",
|
|
145
157
|
handler: () => {
|
|
146
158
|
const rows = options.tables.map((table) => {
|
|
147
|
-
const [count] = db
|
|
159
|
+
const [count] = db
|
|
160
|
+
.prepare(sqlIdentifiers`SELECT count(*) AS n FROM "${table}"`)
|
|
161
|
+
.all() as Array<{
|
|
148
162
|
n: number
|
|
149
163
|
}>
|
|
150
164
|
return { table, rows: count?.n ?? 0 }
|
|
@@ -168,7 +182,7 @@ export function serveDatabaseAsMcp(
|
|
|
168
182
|
}
|
|
169
183
|
// Identifier is allowlist-verified (and the allowlist is identifier-checked at boot),
|
|
170
184
|
// so interpolation here cannot inject.
|
|
171
|
-
const columns = db.prepare(`PRAGMA table_info("${table}")`).all()
|
|
185
|
+
const columns = db.prepare(sqlIdentifiers`PRAGMA table_info("${table}")`).all()
|
|
172
186
|
return { text: JSON.stringify(columns), structuredContent: { table, columns } }
|
|
173
187
|
},
|
|
174
188
|
})
|
|
@@ -256,7 +270,7 @@ export function serveDatabaseAsMcp(
|
|
|
256
270
|
if (options.runQuery === undefined) return server
|
|
257
271
|
|
|
258
272
|
// `run_query` is authorized at the TRANSPORT boundary, where the inbound Request's credentials
|
|
259
|
-
// are visible
|
|
273
|
+
// are visible - the handler itself performs no auth, so every path to it must pass this gate:
|
|
260
274
|
// the wrapped fetch authorizes, and direct handle() dispatch fails closed (no Request → no auth).
|
|
261
275
|
const { authorize } = options.runQuery
|
|
262
276
|
|