@nifrajs/mcp-db 2.11.0 → 2.12.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/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +393 -102
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +435 -105
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface SqliteDatabaseLike {
|
|
|
31
31
|
};
|
|
32
32
|
/** Execute a statement for its side effect (used only for `PRAGMA query_only`). */
|
|
33
33
|
run(sql: string): unknown;
|
|
34
|
+
/** Optional path this database was opened from; a file-backed one can be reopened read-only in a
|
|
35
|
+
* worker so `run_query` runs off the serving connection. `:memory:` and the empty anonymous
|
|
36
|
+
* database cannot, and run in process. */
|
|
37
|
+
readonly filename?: string;
|
|
34
38
|
}
|
|
35
39
|
/** Context forwarded to `authorize` - the inbound HTTP Request carrying the `run_query` call. */
|
|
36
40
|
export interface McpDbAuthorizeContext {
|
|
@@ -49,6 +53,19 @@ export interface RunQueryOptions {
|
|
|
49
53
|
readonly maxRows?: number;
|
|
50
54
|
/** Max serialized result size in bytes (default 100 KB). */
|
|
51
55
|
readonly maxResultBytes?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Maximum wall-clock time for planning, execution, and optional counting (default 5 seconds).
|
|
58
|
+
* The deadline always bounds the RESPONSE. Whether it also stops the WORK depends on where the
|
|
59
|
+
* query runs: a file-backed database runs it in a reusable read-only worker that is terminated
|
|
60
|
+
* on expiry, while an in-memory database (or a shim with no `filename`) runs it on this thread,
|
|
61
|
+
* where a synchronous statement cannot be preempted and the overrun is only reported after it
|
|
62
|
+
* returns.
|
|
63
|
+
*/
|
|
64
|
+
readonly queryTimeoutMs?: number;
|
|
65
|
+
/** Return an exact total after row truncation. Off by default because it re-executes the query. */
|
|
66
|
+
readonly exactTotal?: boolean;
|
|
67
|
+
/** Maximum simultaneous run_query calls on this database connection (default 1). */
|
|
68
|
+
readonly maxConcurrentQueries?: number;
|
|
52
69
|
}
|
|
53
70
|
export interface ServeDatabaseAsMcpOptions {
|
|
54
71
|
/**
|
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;
|
|
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;IACzB;;8CAE0C;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;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;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,mGAAmG;IACnG,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;CACvC;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;AAqaD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,yBAAyB,GACjC,SAAS,CA6PX"}
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,130 @@ const stripSqlNoise = (sql) => sql
|
|
|
48
48
|
.replace(/"(?:[^"]|"")*"/g, '""')
|
|
49
49
|
.replace(/--[^\n]*/g, " ")
|
|
50
50
|
.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
51
|
+
// SQLite's bare-identifier alphabet. Non-ASCII is included because SQLite treats any byte >= 0x80 as
|
|
52
|
+
// an identifier character; leaving it out would end a word early and split one table name into two.
|
|
53
|
+
const WORD_START = /[A-Za-z_\u0080-\uffff]/;
|
|
54
|
+
const WORD_PART = /[A-Za-z0-9_$\u0080-\uffff]/;
|
|
55
|
+
/**
|
|
56
|
+
* Tokenize far enough to name every relation. A regex cannot do this job: SQLite needs no separator
|
|
57
|
+
* between a keyword and a quoted identifier (`FROM"users"` is legal), and it accepts four identifier
|
|
58
|
+
* quotings - `"x"`, `[x]`, `` `x` ``, and bare. A `\s+`-anchored pattern silently reads none of
|
|
59
|
+
* those as a table reference, which is exactly how an aliased `FROM"users"AS habits` slipped past
|
|
60
|
+
* the allowlist and returned a non-exposed table's rows.
|
|
61
|
+
*
|
|
62
|
+
* String literals and comments become nothing, so a `FROM` inside attacker-controlled text is inert.
|
|
63
|
+
*/
|
|
64
|
+
function tokenizeSql(sql) {
|
|
65
|
+
const tokens = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
/** Read a quoted identifier, collapsing the doubled-delimiter escape SQLite uses for `"` and `` ` ``. */
|
|
68
|
+
const readQuoted = (close, escapable) => {
|
|
69
|
+
let value = "";
|
|
70
|
+
i++; // past the opening delimiter
|
|
71
|
+
while (i < sql.length) {
|
|
72
|
+
if (sql[i] === close) {
|
|
73
|
+
if (escapable && sql[i + 1] === close) {
|
|
74
|
+
value += close;
|
|
75
|
+
i += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
i++;
|
|
79
|
+
tokens.push({ kind: "word", value, bare: false });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
value += sql[i];
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
// Unterminated - emit what we have. The caller's single-statement and plan checks still run.
|
|
86
|
+
tokens.push({ kind: "word", value, bare: false });
|
|
87
|
+
};
|
|
88
|
+
while (i < sql.length) {
|
|
89
|
+
const char = sql[i];
|
|
90
|
+
if (char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f") {
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
else if (char === "-" && sql[i + 1] === "-") {
|
|
94
|
+
const end = sql.indexOf("\n", i);
|
|
95
|
+
i = end === -1 ? sql.length : end + 1;
|
|
96
|
+
}
|
|
97
|
+
else if (char === "/" && sql[i + 1] === "*") {
|
|
98
|
+
const end = sql.indexOf("*/", i + 2);
|
|
99
|
+
i = end === -1 ? sql.length : end + 2;
|
|
100
|
+
}
|
|
101
|
+
else if (char === "'") {
|
|
102
|
+
// Literal: consumed and dropped, so its contents can never be read as SQL.
|
|
103
|
+
i++;
|
|
104
|
+
while (i < sql.length) {
|
|
105
|
+
if (sql[i] === "'") {
|
|
106
|
+
if (sql[i + 1] === "'") {
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
i++;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (char === '"') {
|
|
117
|
+
readQuoted('"', true);
|
|
118
|
+
}
|
|
119
|
+
else if (char === "`") {
|
|
120
|
+
readQuoted("`", true);
|
|
121
|
+
}
|
|
122
|
+
else if (char === "[") {
|
|
123
|
+
readQuoted("]", false); // MSSQL-style bracket quoting: no escape form
|
|
124
|
+
}
|
|
125
|
+
else if (WORD_START.test(char)) {
|
|
126
|
+
const start = i;
|
|
127
|
+
while (i < sql.length && WORD_PART.test(sql[i]))
|
|
128
|
+
i++;
|
|
129
|
+
tokens.push({ kind: "word", value: sql.slice(start, i), bare: true });
|
|
130
|
+
}
|
|
131
|
+
else if (char >= "0" && char <= "9") {
|
|
132
|
+
while (i < sql.length && /[0-9A-Za-z._]/.test(sql[i]))
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
tokens.push({ kind: "punct", value: char, bare: false });
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return tokens;
|
|
141
|
+
}
|
|
142
|
+
/** True when `token` is the unquoted keyword `word` (case-insensitive). */
|
|
143
|
+
const isKeyword = (token, word) => token?.bare === true && token.value.toLowerCase() === word;
|
|
144
|
+
/**
|
|
145
|
+
* Every relation the statement names after `FROM`/`JOIN`, lowercased, with CTE names excluded.
|
|
146
|
+
* A schema qualifier resolves to its table (`main.habits` -> `habits`); a subquery contributes
|
|
147
|
+
* nothing of its own because its inner `FROM` is tokenized alongside everything else.
|
|
148
|
+
*/
|
|
149
|
+
function relationNames(sql) {
|
|
150
|
+
const tokens = tokenizeSql(sql);
|
|
151
|
+
// `name AS (` only ever introduces a CTE - a column alias cannot be followed by a paren - so this
|
|
152
|
+
// needs no `WITH` tracking to be exact.
|
|
153
|
+
const ctes = new Set();
|
|
154
|
+
for (let i = 1; i < tokens.length - 1; i++) {
|
|
155
|
+
const name = tokens[i - 1];
|
|
156
|
+
if (isKeyword(tokens[i], "as") && tokens[i + 1]?.value === "(" && name.kind === "word") {
|
|
157
|
+
ctes.add(name.value.toLowerCase());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const names = [];
|
|
161
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
162
|
+
if (!isKeyword(tokens[i], "from") && !isKeyword(tokens[i], "join"))
|
|
163
|
+
continue;
|
|
164
|
+
const first = tokens[i + 1];
|
|
165
|
+
if (first === undefined || first.kind !== "word")
|
|
166
|
+
continue; // `FROM (subquery)` - nothing to name here
|
|
167
|
+
// A qualified reference names the table second; an unqualified one names it first.
|
|
168
|
+
const qualified = tokens[i + 2]?.value === "." && tokens[i + 3]?.kind === "word";
|
|
169
|
+
const table = qualified ? tokens[i + 3].value : first.value;
|
|
170
|
+
if (table !== "" && !ctes.has(table.toLowerCase()))
|
|
171
|
+
names.push(table.toLowerCase());
|
|
172
|
+
}
|
|
173
|
+
return names;
|
|
174
|
+
}
|
|
51
175
|
/** Reject multi-statement input: allow one terminator only when it is the final character. */
|
|
52
176
|
const isSingleStatement = (sql) => {
|
|
53
177
|
let quote = null;
|
|
@@ -162,6 +286,154 @@ function assertResultLimit(value, name) {
|
|
|
162
286
|
throw new McpDbConfigError(`${name} must be a finite positive safe integer`);
|
|
163
287
|
}
|
|
164
288
|
}
|
|
289
|
+
class QueryTimeoutError extends Error {
|
|
290
|
+
constructor() {
|
|
291
|
+
super("query exceeded the time limit");
|
|
292
|
+
this.name = "QueryTimeoutError";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The isolated lane. The worker reopens the SAME database FILE read-only and answers one statement
|
|
297
|
+
* at a time; it is spawned once and reused for the life of the server. It deliberately does not
|
|
298
|
+
* take a `serialize()` snapshot - that copies the whole database into memory on EVERY call, which
|
|
299
|
+
* is a larger availability problem than the slow query the deadline exists to bound.
|
|
300
|
+
*/
|
|
301
|
+
const SQLITE_WORKER_SOURCE = `
|
|
302
|
+
const { Database } = require("bun:sqlite")
|
|
303
|
+
let db
|
|
304
|
+
self.onmessage = (event) => {
|
|
305
|
+
const { id, filename, sql } = event.data
|
|
306
|
+
try {
|
|
307
|
+
if (db === undefined) {
|
|
308
|
+
db = new Database(filename, { readonly: true })
|
|
309
|
+
db.run("PRAGMA query_only = ON")
|
|
310
|
+
}
|
|
311
|
+
self.postMessage({ id, ok: true, rows: db.prepare(sql).all() })
|
|
312
|
+
} catch (error) {
|
|
313
|
+
self.postMessage({ id, ok: false, error: String(error) })
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
function isWorkerAvailable() {
|
|
318
|
+
return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined";
|
|
319
|
+
}
|
|
320
|
+
/** Only a file-backed database can be reopened in a worker. `:memory:` and the anonymous temp
|
|
321
|
+
* database live in this process alone, so they take the in-process lane instead. */
|
|
322
|
+
function reopenableFilename(db) {
|
|
323
|
+
const filename = db.filename;
|
|
324
|
+
if (typeof filename !== "string" || filename === "" || filename === ":memory:")
|
|
325
|
+
return undefined;
|
|
326
|
+
return filename.startsWith("file::memory:") ? undefined : filename;
|
|
327
|
+
}
|
|
328
|
+
function workerSession(filename) {
|
|
329
|
+
let worker;
|
|
330
|
+
let sourceUrl;
|
|
331
|
+
let nextId = 0;
|
|
332
|
+
const pending = new Map();
|
|
333
|
+
let lane = Promise.resolve();
|
|
334
|
+
/** Drop the worker and fail everything riding on it. The only bound this side can enforce over a
|
|
335
|
+
* statement already running inside native SQLite is to stop owning the connection it runs on. */
|
|
336
|
+
const discard = (error) => {
|
|
337
|
+
const waiters = [...pending.values()];
|
|
338
|
+
pending.clear();
|
|
339
|
+
worker?.terminate();
|
|
340
|
+
if (sourceUrl !== undefined)
|
|
341
|
+
URL.revokeObjectURL(sourceUrl);
|
|
342
|
+
worker = undefined;
|
|
343
|
+
sourceUrl = undefined;
|
|
344
|
+
for (const waiter of waiters)
|
|
345
|
+
waiter.reject(error);
|
|
346
|
+
};
|
|
347
|
+
const ensureWorker = () => {
|
|
348
|
+
if (worker !== undefined)
|
|
349
|
+
return worker;
|
|
350
|
+
sourceUrl = URL.createObjectURL(new Blob([SQLITE_WORKER_SOURCE], { type: "text/javascript" }));
|
|
351
|
+
const spawned = new Worker(sourceUrl);
|
|
352
|
+
spawned.onmessage = (event) => {
|
|
353
|
+
const { id, ok, rows, error } = event.data;
|
|
354
|
+
if (typeof id !== "number")
|
|
355
|
+
return;
|
|
356
|
+
const waiter = pending.get(id);
|
|
357
|
+
if (waiter === undefined)
|
|
358
|
+
return;
|
|
359
|
+
pending.delete(id);
|
|
360
|
+
if (ok === true)
|
|
361
|
+
waiter.resolve(rows ?? []);
|
|
362
|
+
else
|
|
363
|
+
waiter.reject(new Error(error ?? "query failed in worker"));
|
|
364
|
+
};
|
|
365
|
+
spawned.onerror = () => discard(new Error("query failed in worker"));
|
|
366
|
+
spawned.unref?.();
|
|
367
|
+
worker = spawned;
|
|
368
|
+
return spawned;
|
|
369
|
+
};
|
|
370
|
+
const dispatch = (sql, deadline) => {
|
|
371
|
+
const remaining = deadline - Date.now();
|
|
372
|
+
if (remaining <= 0)
|
|
373
|
+
return Promise.reject(new QueryTimeoutError());
|
|
374
|
+
const active = ensureWorker();
|
|
375
|
+
const id = ++nextId;
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const timer = setTimeout(() => discard(new QueryTimeoutError()), remaining);
|
|
378
|
+
pending.set(id, {
|
|
379
|
+
resolve: (rows) => {
|
|
380
|
+
clearTimeout(timer);
|
|
381
|
+
resolve(rows);
|
|
382
|
+
},
|
|
383
|
+
reject: (error) => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
reject(error);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
try {
|
|
389
|
+
active.postMessage({ id, filename, sql });
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
pending.delete(id);
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
run(sql, deadline) {
|
|
400
|
+
// One worker owns the connection, so overlapping calls queue behind each other rather than
|
|
401
|
+
// interleaving statements on it.
|
|
402
|
+
const result = lane.then(() => dispatch(sql, deadline));
|
|
403
|
+
lane = result.catch(() => undefined);
|
|
404
|
+
return result;
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The in-process lane, used when the database cannot be reopened elsewhere. The deadline is checked
|
|
410
|
+
* around the statement and nothing more: `prepare().all()` is synchronous, so it holds the only
|
|
411
|
+
* thread there is and no timer of ours can fire while it runs. That makes this lane BEST EFFORT -
|
|
412
|
+
* it reports an overrun, it does not cut one short.
|
|
413
|
+
*/
|
|
414
|
+
function inProcessSession(db) {
|
|
415
|
+
return {
|
|
416
|
+
async run(sql, deadline) {
|
|
417
|
+
if (Date.now() >= deadline)
|
|
418
|
+
throw new QueryTimeoutError();
|
|
419
|
+
const rows = db.prepare(sql).all();
|
|
420
|
+
if (Date.now() > deadline)
|
|
421
|
+
throw new QueryTimeoutError();
|
|
422
|
+
return rows;
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Pick the lane once, at construction. A file-backed database is isolated in a reusable read-only
|
|
428
|
+
* worker, which is the only arrangement that can actually stop a running statement. Anything else
|
|
429
|
+
* runs in process - every database keeps working, none is refused.
|
|
430
|
+
*/
|
|
431
|
+
function openQuerySession(db) {
|
|
432
|
+
const filename = reopenableFilename(db);
|
|
433
|
+
if (filename !== undefined && isWorkerAvailable())
|
|
434
|
+
return workerSession(filename);
|
|
435
|
+
return inProcessSession(db);
|
|
436
|
+
}
|
|
165
437
|
/**
|
|
166
438
|
* Serve `db` as a mountable MCP server (`mcp.fetch` at `POST /mcp`). See module docs for the
|
|
167
439
|
* security model. Throws {@link McpDbConfigError} on any unsafe configuration - always at
|
|
@@ -187,8 +459,13 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
187
459
|
}
|
|
188
460
|
const maxRows = options.runQuery?.maxRows ?? 100;
|
|
189
461
|
const maxResultBytes = options.runQuery?.maxResultBytes ?? 100 * 1024;
|
|
462
|
+
const queryTimeoutMs = options.runQuery?.queryTimeoutMs ?? 5_000;
|
|
463
|
+
const maxConcurrentQueries = options.runQuery?.maxConcurrentQueries ?? 1;
|
|
190
464
|
assertResultLimit(maxRows, "maxRows");
|
|
191
465
|
assertResultLimit(maxResultBytes, "maxResultBytes");
|
|
466
|
+
assertResultLimit(queryTimeoutMs, "queryTimeoutMs");
|
|
467
|
+
assertResultLimit(maxConcurrentQueries, "maxConcurrentQueries");
|
|
468
|
+
let activeQueries = 0;
|
|
192
469
|
const listTables = defineMcpTool({
|
|
193
470
|
name: "list_tables",
|
|
194
471
|
description: "List the tables this server exposes, with row counts.",
|
|
@@ -223,6 +500,8 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
223
500
|
});
|
|
224
501
|
const tools = [listTables, describeTable];
|
|
225
502
|
if (options.runQuery !== undefined) {
|
|
503
|
+
// One lane for the life of the server: picked once here, so the handler below always has it.
|
|
504
|
+
const session = openQuerySession(db);
|
|
226
505
|
const runQuery = defineMcpTool({
|
|
227
506
|
name: "run_query",
|
|
228
507
|
description: "Run one read-only SELECT against the exposed tables. Results are capped by rows and bytes.",
|
|
@@ -234,88 +513,124 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
234
513
|
required: ["sql"],
|
|
235
514
|
},
|
|
236
515
|
intent: "table",
|
|
237
|
-
handler: (args) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return { isError: true, text: "empty query" };
|
|
241
|
-
if (!isSingleStatement(sql)) {
|
|
242
|
-
return { isError: true, text: "only a single statement is allowed" };
|
|
243
|
-
}
|
|
244
|
-
if (!isReadStatement(sql)) {
|
|
245
|
-
return { isError: true, text: "only SELECT (or WITH…SELECT) statements are allowed" };
|
|
516
|
+
handler: async (args) => {
|
|
517
|
+
if (activeQueries >= maxConcurrentQueries) {
|
|
518
|
+
return { isError: true, text: "too many concurrent queries" };
|
|
246
519
|
}
|
|
247
|
-
|
|
248
|
-
// scanned/searched relation in EXPLAIN QUERY PLAN detail rows.
|
|
249
|
-
const query = executableSql(sql);
|
|
250
|
-
let planRows;
|
|
520
|
+
activeQueries += 1;
|
|
251
521
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// constant-only SELECT. Only treat a scan/search target as a relation
|
|
261
|
-
// when it is not one of those planner pseudo-nodes.
|
|
262
|
-
if (/^SCAN\s+CONSTANT\s+ROW$/i.test(detail.trim()) ||
|
|
263
|
-
/^SCAN\s+SUBQUERY\s+\d+$/i.test(detail.trim())) {
|
|
264
|
-
continue;
|
|
522
|
+
const sql = String(args.sql ?? "").trim();
|
|
523
|
+
if (sql === "")
|
|
524
|
+
return { isError: true, text: "empty query" };
|
|
525
|
+
if (!isSingleStatement(sql)) {
|
|
526
|
+
return { isError: true, text: "only a single statement is allowed" };
|
|
527
|
+
}
|
|
528
|
+
if (!isReadStatement(sql)) {
|
|
529
|
+
return { isError: true, text: "only SELECT (or WITH…SELECT) statements are allowed" };
|
|
265
530
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
531
|
+
const deadline = Date.now() + queryTimeoutMs;
|
|
532
|
+
// Verify SQL relation tokens before the plan check. SQLite's plan output uses aliases as
|
|
533
|
+
// scan targets (e.g. `users AS habits` becomes `SCAN habits`), so plan-only validation can
|
|
534
|
+
// mistake an attacker-controlled alias for an allowlisted table.
|
|
535
|
+
const executable = executableSql(sql);
|
|
536
|
+
for (const relation of relationNames(executable)) {
|
|
269
537
|
if (!allowlist.has(relation)) {
|
|
270
538
|
return {
|
|
271
539
|
isError: true,
|
|
272
|
-
text: `query touches ${JSON.stringify(
|
|
540
|
+
text: `query touches ${JSON.stringify(relation)}, which is not exposed`,
|
|
273
541
|
};
|
|
274
542
|
}
|
|
275
543
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// materialization bounded even when the caller submits `SELECT * FROM a_huge_table`.
|
|
281
|
-
limitedRows = db
|
|
282
|
-
.prepare(`SELECT * FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`)
|
|
283
|
-
.all();
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
return { isError: true, text: `query failed: ${String(error)}` };
|
|
287
|
-
}
|
|
288
|
-
const wasLimited = limitedRows.length > maxRows;
|
|
289
|
-
let total = limitedRows.length;
|
|
290
|
-
if (wasLimited) {
|
|
544
|
+
// Verify via the query plan that only allowlisted tables are touched. SQLite names every
|
|
545
|
+
// scanned/searched relation in EXPLAIN QUERY PLAN detail rows.
|
|
546
|
+
const query = executable;
|
|
547
|
+
let planRows;
|
|
291
548
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
549
|
+
planRows = (await session.run(`EXPLAIN QUERY PLAN ${query}`, deadline));
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
if (error instanceof QueryTimeoutError)
|
|
553
|
+
return { isError: true, text: error.message };
|
|
554
|
+
return { isError: true, text: `query failed to plan: ${String(error)}` };
|
|
555
|
+
}
|
|
556
|
+
for (const row of planRows) {
|
|
557
|
+
const detail = typeof row.detail === "string" ? row.detail : "";
|
|
558
|
+
// SQLite also emits non-table nodes such as `SCAN CONSTANT ROW` for a
|
|
559
|
+
// constant-only SELECT. Only treat a scan/search target as a relation
|
|
560
|
+
// when it is not one of those planner pseudo-nodes.
|
|
561
|
+
if (/^SCAN\s+CONSTANT\s+ROW$/i.test(detail.trim()) ||
|
|
562
|
+
/^SCAN\s+SUBQUERY\s+\d+$/i.test(detail.trim())) {
|
|
563
|
+
continue;
|
|
298
564
|
}
|
|
299
|
-
|
|
565
|
+
// A schema-qualified reference plans as `SCAN main.habits`. The table is the last dotted
|
|
566
|
+
// segment; reading the first would reject `main.habits` as a table literally named "main".
|
|
567
|
+
const match = /(?:SCAN|SEARCH)\s+(?:TABLE\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(detail);
|
|
568
|
+
if (match !== null) {
|
|
569
|
+
const relation = match[1]?.toLowerCase() ?? "";
|
|
570
|
+
if (!allowlist.has(relation)) {
|
|
571
|
+
return {
|
|
572
|
+
isError: true,
|
|
573
|
+
text: `query touches ${JSON.stringify(match[1])}, which is not exposed`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
let limitedRows;
|
|
579
|
+
try {
|
|
580
|
+
// Fetch at most one row beyond the advertised cap. This keeps the database driver's
|
|
581
|
+
// materialization bounded even when the caller submits `SELECT * FROM a_huge_table`.
|
|
582
|
+
limitedRows = await session.run(`SELECT * FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`, deadline);
|
|
300
583
|
}
|
|
301
584
|
catch (error) {
|
|
302
|
-
|
|
585
|
+
if (error instanceof QueryTimeoutError)
|
|
586
|
+
return { isError: true, text: error.message };
|
|
587
|
+
return { isError: true, text: `query failed: ${String(error)}` };
|
|
588
|
+
}
|
|
589
|
+
const wasLimited = limitedRows.length > maxRows;
|
|
590
|
+
let total = limitedRows.length;
|
|
591
|
+
if (wasLimited && options.runQuery?.exactTotal === true) {
|
|
592
|
+
try {
|
|
593
|
+
const countRows = (await session.run(`SELECT count(*) AS "__nifra_total" FROM (${query}) AS "__nifra_count"`, deadline));
|
|
594
|
+
const count = countRows[0]?.__nifra_total;
|
|
595
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < maxRows) {
|
|
596
|
+
return { isError: true, text: "query failed: invalid row count" };
|
|
597
|
+
}
|
|
598
|
+
total = count;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
if (error instanceof QueryTimeoutError)
|
|
602
|
+
return { isError: true, text: error.message };
|
|
603
|
+
return { isError: true, text: `query failed to count results: ${String(error)}` };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else if (wasLimited) {
|
|
607
|
+
total = null;
|
|
608
|
+
}
|
|
609
|
+
let shown = Math.min(limitedRows.length, maxRows);
|
|
610
|
+
let payload = limitedRows.slice(0, shown);
|
|
611
|
+
let result = {
|
|
612
|
+
rows: payload,
|
|
613
|
+
...(wasLimited || shown < limitedRows.length ? { truncated: true, total } : {}),
|
|
614
|
+
};
|
|
615
|
+
let serialized = JSON.stringify(result);
|
|
616
|
+
const encoded = new TextEncoder();
|
|
617
|
+
while (encoded.encode(serialized).byteLength > maxResultBytes && shown > 0) {
|
|
618
|
+
shown = Math.max(0, Math.floor(shown / 2));
|
|
619
|
+
payload = limitedRows.slice(0, shown);
|
|
620
|
+
result = {
|
|
621
|
+
rows: payload,
|
|
622
|
+
...(wasLimited || shown < limitedRows.length ? { truncated: true, total } : {}),
|
|
623
|
+
};
|
|
624
|
+
serialized = JSON.stringify(result);
|
|
625
|
+
}
|
|
626
|
+
if (encoded.encode(serialized).byteLength > maxResultBytes) {
|
|
627
|
+
return { isError: true, text: "query result exceeds maxResultBytes" };
|
|
303
628
|
}
|
|
629
|
+
return { text: serialized, structuredContent: result };
|
|
304
630
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let serialized = JSON.stringify(payload);
|
|
308
|
-
while (serialized.length > maxResultBytes && shown > 0) {
|
|
309
|
-
shown = Math.max(0, Math.floor(shown / 2));
|
|
310
|
-
payload = limitedRows.slice(0, shown);
|
|
311
|
-
serialized = JSON.stringify(payload);
|
|
631
|
+
finally {
|
|
632
|
+
activeQueries -= 1;
|
|
312
633
|
}
|
|
313
|
-
const truncated = shown < total;
|
|
314
|
-
const result = {
|
|
315
|
-
rows: payload,
|
|
316
|
-
...(truncated ? { truncated: { shown, total } } : {}),
|
|
317
|
-
};
|
|
318
|
-
return { text: JSON.stringify(result), structuredContent: result };
|
|
319
634
|
},
|
|
320
635
|
});
|
|
321
636
|
tools.push(runQuery);
|
|
@@ -331,45 +646,21 @@ export function serveDatabaseAsMcp(db, options) {
|
|
|
331
646
|
// are visible - the handler itself performs no auth, so every path to it must pass this gate:
|
|
332
647
|
// the wrapped fetch authorizes, and direct handle() dispatch fails closed (no Request → no auth).
|
|
333
648
|
const { authorize } = options.runQuery;
|
|
334
|
-
const rpcId = (id) => typeof id === "string" || typeof id === "number" ? id : null;
|
|
335
|
-
const unauthorizedResult = (id) => ({
|
|
336
|
-
jsonrpc: "2.0",
|
|
337
|
-
id: rpcId(id),
|
|
338
|
-
result: {
|
|
339
|
-
content: [{ type: "text", text: "unauthorized" }],
|
|
340
|
-
isError: true,
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
const isRunQueryCall = (message) => {
|
|
344
|
-
if (typeof message !== "object" || message === null)
|
|
345
|
-
return false;
|
|
346
|
-
const { method, params } = message;
|
|
347
|
-
return method === "tools/call" && params?.name === "run_query";
|
|
348
|
-
};
|
|
349
649
|
return {
|
|
350
650
|
...server,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return server.fetch(request); // malformed JSON → the protocol layer's error handling
|
|
359
|
-
}
|
|
360
|
-
const messages = Array.isArray(body) ? body : [body];
|
|
361
|
-
const queryCall = messages.find(isRunQueryCall);
|
|
362
|
-
if (queryCall !== undefined) {
|
|
363
|
-
const ok = await authorize({ toolName: "run_query", request });
|
|
364
|
-
if (!ok)
|
|
365
|
-
return Response.json(unauthorizedResult(queryCall.id));
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return server.fetch(request);
|
|
369
|
-
},
|
|
651
|
+
fetch: (request) => server.fetch(request, {
|
|
652
|
+
authorizeMessage: async (message, inbound) => message.method !== "tools/call" || message.params?.name !== "run_query"
|
|
653
|
+
? true
|
|
654
|
+
: authorize({ toolName: "run_query", request: inbound }),
|
|
655
|
+
}),
|
|
370
656
|
async handle(message) {
|
|
371
|
-
if (
|
|
372
|
-
return
|
|
657
|
+
if (message.method === "tools/call" && message.params?.name === "run_query") {
|
|
658
|
+
return {
|
|
659
|
+
jsonrpc: "2.0",
|
|
660
|
+
id: typeof message.id === "string" || typeof message.id === "number" ? message.id : null,
|
|
661
|
+
result: { content: [{ type: "text", text: "unauthorized" }], isError: true },
|
|
662
|
+
};
|
|
663
|
+
}
|
|
373
664
|
return server.handle(message);
|
|
374
665
|
},
|
|
375
666
|
};
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/mcp-db",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@nifrajs/mcp": "2.
|
|
38
|
+
"@nifrajs/mcp": "2.12.0"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,10 @@ export interface SqliteDatabaseLike {
|
|
|
31
31
|
prepare(sql: string): { all(...params: unknown[]): unknown[] }
|
|
32
32
|
/** Execute a statement for its side effect (used only for `PRAGMA query_only`). */
|
|
33
33
|
run(sql: string): unknown
|
|
34
|
+
/** Optional path this database was opened from; a file-backed one can be reopened read-only in a
|
|
35
|
+
* worker so `run_query` runs off the serving connection. `:memory:` and the empty anonymous
|
|
36
|
+
* database cannot, and run in process. */
|
|
37
|
+
readonly filename?: string
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** Context forwarded to `authorize` - the inbound HTTP Request carrying the `run_query` call. */
|
|
@@ -51,6 +55,19 @@ export interface RunQueryOptions {
|
|
|
51
55
|
readonly maxRows?: number
|
|
52
56
|
/** Max serialized result size in bytes (default 100 KB). */
|
|
53
57
|
readonly maxResultBytes?: number
|
|
58
|
+
/**
|
|
59
|
+
* Maximum wall-clock time for planning, execution, and optional counting (default 5 seconds).
|
|
60
|
+
* The deadline always bounds the RESPONSE. Whether it also stops the WORK depends on where the
|
|
61
|
+
* query runs: a file-backed database runs it in a reusable read-only worker that is terminated
|
|
62
|
+
* on expiry, while an in-memory database (or a shim with no `filename`) runs it on this thread,
|
|
63
|
+
* where a synchronous statement cannot be preempted and the overrun is only reported after it
|
|
64
|
+
* returns.
|
|
65
|
+
*/
|
|
66
|
+
readonly queryTimeoutMs?: number
|
|
67
|
+
/** Return an exact total after row truncation. Off by default because it re-executes the query. */
|
|
68
|
+
readonly exactTotal?: boolean
|
|
69
|
+
/** Maximum simultaneous run_query calls on this database connection (default 1). */
|
|
70
|
+
readonly maxConcurrentQueries?: number
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
export interface ServeDatabaseAsMcpOptions {
|
|
@@ -104,6 +121,131 @@ const stripSqlNoise = (sql: string): string =>
|
|
|
104
121
|
.replace(/--[^\n]*/g, " ")
|
|
105
122
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
106
123
|
|
|
124
|
+
// SQLite's bare-identifier alphabet. Non-ASCII is included because SQLite treats any byte >= 0x80 as
|
|
125
|
+
// an identifier character; leaving it out would end a word early and split one table name into two.
|
|
126
|
+
const WORD_START = /[A-Za-z_\u0080-\uffff]/
|
|
127
|
+
const WORD_PART = /[A-Za-z0-9_$\u0080-\uffff]/
|
|
128
|
+
|
|
129
|
+
/** One lexical unit of SQL. `bare` distinguishes a keyword-capable word from a quoted identifier, so
|
|
130
|
+
* `FROM "from"` reads the second token as a table name and not as another clause. */
|
|
131
|
+
interface SqlToken {
|
|
132
|
+
readonly kind: "word" | "punct"
|
|
133
|
+
/** Identifier text with quoting removed and escapes collapsed; the raw character for punctuation. */
|
|
134
|
+
readonly value: string
|
|
135
|
+
/** True for an unquoted word - only these can be SQL keywords. */
|
|
136
|
+
readonly bare: boolean
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Tokenize far enough to name every relation. A regex cannot do this job: SQLite needs no separator
|
|
141
|
+
* between a keyword and a quoted identifier (`FROM"users"` is legal), and it accepts four identifier
|
|
142
|
+
* quotings - `"x"`, `[x]`, `` `x` ``, and bare. A `\s+`-anchored pattern silently reads none of
|
|
143
|
+
* those as a table reference, which is exactly how an aliased `FROM"users"AS habits` slipped past
|
|
144
|
+
* the allowlist and returned a non-exposed table's rows.
|
|
145
|
+
*
|
|
146
|
+
* String literals and comments become nothing, so a `FROM` inside attacker-controlled text is inert.
|
|
147
|
+
*/
|
|
148
|
+
function tokenizeSql(sql: string): SqlToken[] {
|
|
149
|
+
const tokens: SqlToken[] = []
|
|
150
|
+
let i = 0
|
|
151
|
+
/** Read a quoted identifier, collapsing the doubled-delimiter escape SQLite uses for `"` and `` ` ``. */
|
|
152
|
+
const readQuoted = (close: string, escapable: boolean): void => {
|
|
153
|
+
let value = ""
|
|
154
|
+
i++ // past the opening delimiter
|
|
155
|
+
while (i < sql.length) {
|
|
156
|
+
if (sql[i] === close) {
|
|
157
|
+
if (escapable && sql[i + 1] === close) {
|
|
158
|
+
value += close
|
|
159
|
+
i += 2
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
i++
|
|
163
|
+
tokens.push({ kind: "word", value, bare: false })
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
value += sql[i]
|
|
167
|
+
i++
|
|
168
|
+
}
|
|
169
|
+
// Unterminated - emit what we have. The caller's single-statement and plan checks still run.
|
|
170
|
+
tokens.push({ kind: "word", value, bare: false })
|
|
171
|
+
}
|
|
172
|
+
while (i < sql.length) {
|
|
173
|
+
const char = sql[i] as string
|
|
174
|
+
if (char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f") {
|
|
175
|
+
i++
|
|
176
|
+
} else if (char === "-" && sql[i + 1] === "-") {
|
|
177
|
+
const end = sql.indexOf("\n", i)
|
|
178
|
+
i = end === -1 ? sql.length : end + 1
|
|
179
|
+
} else if (char === "/" && sql[i + 1] === "*") {
|
|
180
|
+
const end = sql.indexOf("*/", i + 2)
|
|
181
|
+
i = end === -1 ? sql.length : end + 2
|
|
182
|
+
} else if (char === "'") {
|
|
183
|
+
// Literal: consumed and dropped, so its contents can never be read as SQL.
|
|
184
|
+
i++
|
|
185
|
+
while (i < sql.length) {
|
|
186
|
+
if (sql[i] === "'") {
|
|
187
|
+
if (sql[i + 1] === "'") {
|
|
188
|
+
i += 2
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
i++
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
i++
|
|
195
|
+
}
|
|
196
|
+
} else if (char === '"') {
|
|
197
|
+
readQuoted('"', true)
|
|
198
|
+
} else if (char === "`") {
|
|
199
|
+
readQuoted("`", true)
|
|
200
|
+
} else if (char === "[") {
|
|
201
|
+
readQuoted("]", false) // MSSQL-style bracket quoting: no escape form
|
|
202
|
+
} else if (WORD_START.test(char)) {
|
|
203
|
+
const start = i
|
|
204
|
+
while (i < sql.length && WORD_PART.test(sql[i] as string)) i++
|
|
205
|
+
tokens.push({ kind: "word", value: sql.slice(start, i), bare: true })
|
|
206
|
+
} else if (char >= "0" && char <= "9") {
|
|
207
|
+
while (i < sql.length && /[0-9A-Za-z._]/.test(sql[i] as string)) i++
|
|
208
|
+
} else {
|
|
209
|
+
tokens.push({ kind: "punct", value: char, bare: false })
|
|
210
|
+
i++
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return tokens
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** True when `token` is the unquoted keyword `word` (case-insensitive). */
|
|
217
|
+
const isKeyword = (token: SqlToken | undefined, word: string): boolean =>
|
|
218
|
+
token?.bare === true && token.value.toLowerCase() === word
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Every relation the statement names after `FROM`/`JOIN`, lowercased, with CTE names excluded.
|
|
222
|
+
* A schema qualifier resolves to its table (`main.habits` -> `habits`); a subquery contributes
|
|
223
|
+
* nothing of its own because its inner `FROM` is tokenized alongside everything else.
|
|
224
|
+
*/
|
|
225
|
+
function relationNames(sql: string): string[] {
|
|
226
|
+
const tokens = tokenizeSql(sql)
|
|
227
|
+
// `name AS (` only ever introduces a CTE - a column alias cannot be followed by a paren - so this
|
|
228
|
+
// needs no `WITH` tracking to be exact.
|
|
229
|
+
const ctes = new Set<string>()
|
|
230
|
+
for (let i = 1; i < tokens.length - 1; i++) {
|
|
231
|
+
const name = tokens[i - 1] as SqlToken
|
|
232
|
+
if (isKeyword(tokens[i], "as") && tokens[i + 1]?.value === "(" && name.kind === "word") {
|
|
233
|
+
ctes.add(name.value.toLowerCase())
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const names: string[] = []
|
|
237
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
238
|
+
if (!isKeyword(tokens[i], "from") && !isKeyword(tokens[i], "join")) continue
|
|
239
|
+
const first = tokens[i + 1]
|
|
240
|
+
if (first === undefined || first.kind !== "word") continue // `FROM (subquery)` - nothing to name here
|
|
241
|
+
// A qualified reference names the table second; an unqualified one names it first.
|
|
242
|
+
const qualified = tokens[i + 2]?.value === "." && tokens[i + 3]?.kind === "word"
|
|
243
|
+
const table = qualified ? (tokens[i + 3] as SqlToken).value : first.value
|
|
244
|
+
if (table !== "" && !ctes.has(table.toLowerCase())) names.push(table.toLowerCase())
|
|
245
|
+
}
|
|
246
|
+
return names
|
|
247
|
+
}
|
|
248
|
+
|
|
107
249
|
/** Reject multi-statement input: allow one terminator only when it is the final character. */
|
|
108
250
|
const isSingleStatement = (sql: string): boolean => {
|
|
109
251
|
let quote: "'" | '"' | null = null
|
|
@@ -215,6 +357,166 @@ function assertResultLimit(value: number, name: string): void {
|
|
|
215
357
|
}
|
|
216
358
|
}
|
|
217
359
|
|
|
360
|
+
class QueryTimeoutError extends Error {
|
|
361
|
+
constructor() {
|
|
362
|
+
super("query exceeded the time limit")
|
|
363
|
+
this.name = "QueryTimeoutError"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** One execution lane for `run_query`: `run` resolves rows, or rejects once the deadline passes. */
|
|
368
|
+
interface QuerySession {
|
|
369
|
+
run(sql: string, deadline: number): Promise<unknown[]>
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The isolated lane. The worker reopens the SAME database FILE read-only and answers one statement
|
|
374
|
+
* at a time; it is spawned once and reused for the life of the server. It deliberately does not
|
|
375
|
+
* take a `serialize()` snapshot - that copies the whole database into memory on EVERY call, which
|
|
376
|
+
* is a larger availability problem than the slow query the deadline exists to bound.
|
|
377
|
+
*/
|
|
378
|
+
const SQLITE_WORKER_SOURCE = `
|
|
379
|
+
const { Database } = require("bun:sqlite")
|
|
380
|
+
let db
|
|
381
|
+
self.onmessage = (event) => {
|
|
382
|
+
const { id, filename, sql } = event.data
|
|
383
|
+
try {
|
|
384
|
+
if (db === undefined) {
|
|
385
|
+
db = new Database(filename, { readonly: true })
|
|
386
|
+
db.run("PRAGMA query_only = ON")
|
|
387
|
+
}
|
|
388
|
+
self.postMessage({ id, ok: true, rows: db.prepare(sql).all() })
|
|
389
|
+
} catch (error) {
|
|
390
|
+
self.postMessage({ id, ok: false, error: String(error) })
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
`
|
|
394
|
+
|
|
395
|
+
function isWorkerAvailable(): boolean {
|
|
396
|
+
return typeof Worker !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Only a file-backed database can be reopened in a worker. `:memory:` and the anonymous temp
|
|
400
|
+
* database live in this process alone, so they take the in-process lane instead. */
|
|
401
|
+
function reopenableFilename(db: SqliteDatabaseLike): string | undefined {
|
|
402
|
+
const filename = db.filename
|
|
403
|
+
if (typeof filename !== "string" || filename === "" || filename === ":memory:") return undefined
|
|
404
|
+
return filename.startsWith("file::memory:") ? undefined : filename
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
interface PendingQuery {
|
|
408
|
+
resolve(rows: unknown[]): void
|
|
409
|
+
reject(error: Error): void
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function workerSession(filename: string): QuerySession {
|
|
413
|
+
let worker: Worker | undefined
|
|
414
|
+
let sourceUrl: string | undefined
|
|
415
|
+
let nextId = 0
|
|
416
|
+
const pending = new Map<number, PendingQuery>()
|
|
417
|
+
let lane: Promise<unknown> = Promise.resolve()
|
|
418
|
+
|
|
419
|
+
/** Drop the worker and fail everything riding on it. The only bound this side can enforce over a
|
|
420
|
+
* statement already running inside native SQLite is to stop owning the connection it runs on. */
|
|
421
|
+
const discard = (error: Error): void => {
|
|
422
|
+
const waiters = [...pending.values()]
|
|
423
|
+
pending.clear()
|
|
424
|
+
worker?.terminate()
|
|
425
|
+
if (sourceUrl !== undefined) URL.revokeObjectURL(sourceUrl)
|
|
426
|
+
worker = undefined
|
|
427
|
+
sourceUrl = undefined
|
|
428
|
+
for (const waiter of waiters) waiter.reject(error)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const ensureWorker = (): Worker => {
|
|
432
|
+
if (worker !== undefined) return worker
|
|
433
|
+
sourceUrl = URL.createObjectURL(new Blob([SQLITE_WORKER_SOURCE], { type: "text/javascript" }))
|
|
434
|
+
const spawned = new Worker(sourceUrl)
|
|
435
|
+
spawned.onmessage = (
|
|
436
|
+
event: MessageEvent<{ id?: number; ok?: boolean; rows?: unknown[]; error?: string }>,
|
|
437
|
+
) => {
|
|
438
|
+
const { id, ok, rows, error } = event.data
|
|
439
|
+
if (typeof id !== "number") return
|
|
440
|
+
const waiter = pending.get(id)
|
|
441
|
+
if (waiter === undefined) return
|
|
442
|
+
pending.delete(id)
|
|
443
|
+
if (ok === true) waiter.resolve(rows ?? [])
|
|
444
|
+
else waiter.reject(new Error(error ?? "query failed in worker"))
|
|
445
|
+
}
|
|
446
|
+
spawned.onerror = () => discard(new Error("query failed in worker"))
|
|
447
|
+
// An in-flight query holds its own deadline timer on the loop, so an IDLE worker must not be
|
|
448
|
+
// what keeps the process alive.
|
|
449
|
+
;(spawned as { unref?: () => void }).unref?.()
|
|
450
|
+
worker = spawned
|
|
451
|
+
return spawned
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const dispatch = (sql: string, deadline: number): Promise<unknown[]> => {
|
|
455
|
+
const remaining = deadline - Date.now()
|
|
456
|
+
if (remaining <= 0) return Promise.reject(new QueryTimeoutError())
|
|
457
|
+
const active = ensureWorker()
|
|
458
|
+
const id = ++nextId
|
|
459
|
+
return new Promise<unknown[]>((resolve, reject) => {
|
|
460
|
+
const timer = setTimeout(() => discard(new QueryTimeoutError()), remaining)
|
|
461
|
+
pending.set(id, {
|
|
462
|
+
resolve: (rows) => {
|
|
463
|
+
clearTimeout(timer)
|
|
464
|
+
resolve(rows)
|
|
465
|
+
},
|
|
466
|
+
reject: (error) => {
|
|
467
|
+
clearTimeout(timer)
|
|
468
|
+
reject(error)
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
try {
|
|
472
|
+
active.postMessage({ id, filename, sql })
|
|
473
|
+
} catch (error) {
|
|
474
|
+
pending.delete(id)
|
|
475
|
+
clearTimeout(timer)
|
|
476
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
run(sql, deadline) {
|
|
483
|
+
// One worker owns the connection, so overlapping calls queue behind each other rather than
|
|
484
|
+
// interleaving statements on it.
|
|
485
|
+
const result = lane.then(() => dispatch(sql, deadline))
|
|
486
|
+
lane = result.catch(() => undefined)
|
|
487
|
+
return result
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The in-process lane, used when the database cannot be reopened elsewhere. The deadline is checked
|
|
494
|
+
* around the statement and nothing more: `prepare().all()` is synchronous, so it holds the only
|
|
495
|
+
* thread there is and no timer of ours can fire while it runs. That makes this lane BEST EFFORT -
|
|
496
|
+
* it reports an overrun, it does not cut one short.
|
|
497
|
+
*/
|
|
498
|
+
function inProcessSession(db: SqliteDatabaseLike): QuerySession {
|
|
499
|
+
return {
|
|
500
|
+
async run(sql, deadline) {
|
|
501
|
+
if (Date.now() >= deadline) throw new QueryTimeoutError()
|
|
502
|
+
const rows = db.prepare(sql).all()
|
|
503
|
+
if (Date.now() > deadline) throw new QueryTimeoutError()
|
|
504
|
+
return rows
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Pick the lane once, at construction. A file-backed database is isolated in a reusable read-only
|
|
511
|
+
* worker, which is the only arrangement that can actually stop a running statement. Anything else
|
|
512
|
+
* runs in process - every database keeps working, none is refused.
|
|
513
|
+
*/
|
|
514
|
+
function openQuerySession(db: SqliteDatabaseLike): QuerySession {
|
|
515
|
+
const filename = reopenableFilename(db)
|
|
516
|
+
if (filename !== undefined && isWorkerAvailable()) return workerSession(filename)
|
|
517
|
+
return inProcessSession(db)
|
|
518
|
+
}
|
|
519
|
+
|
|
218
520
|
/**
|
|
219
521
|
* Serve `db` as a mountable MCP server (`mcp.fetch` at `POST /mcp`). See module docs for the
|
|
220
522
|
* security model. Throws {@link McpDbConfigError} on any unsafe configuration - always at
|
|
@@ -250,8 +552,13 @@ export function serveDatabaseAsMcp(
|
|
|
250
552
|
|
|
251
553
|
const maxRows = options.runQuery?.maxRows ?? 100
|
|
252
554
|
const maxResultBytes = options.runQuery?.maxResultBytes ?? 100 * 1024
|
|
555
|
+
const queryTimeoutMs = options.runQuery?.queryTimeoutMs ?? 5_000
|
|
556
|
+
const maxConcurrentQueries = options.runQuery?.maxConcurrentQueries ?? 1
|
|
253
557
|
assertResultLimit(maxRows, "maxRows")
|
|
254
558
|
assertResultLimit(maxResultBytes, "maxResultBytes")
|
|
559
|
+
assertResultLimit(queryTimeoutMs, "queryTimeoutMs")
|
|
560
|
+
assertResultLimit(maxConcurrentQueries, "maxConcurrentQueries")
|
|
561
|
+
let activeQueries = 0
|
|
255
562
|
|
|
256
563
|
const listTables = defineMcpTool({
|
|
257
564
|
name: "list_tables",
|
|
@@ -292,6 +599,8 @@ export function serveDatabaseAsMcp(
|
|
|
292
599
|
const tools = [listTables, describeTable]
|
|
293
600
|
|
|
294
601
|
if (options.runQuery !== undefined) {
|
|
602
|
+
// One lane for the life of the server: picked once here, so the handler below always has it.
|
|
603
|
+
const session = openQuerySession(db)
|
|
295
604
|
const runQuery = defineMcpTool({
|
|
296
605
|
name: "run_query",
|
|
297
606
|
description:
|
|
@@ -304,89 +613,133 @@ export function serveDatabaseAsMcp(
|
|
|
304
613
|
required: ["sql"],
|
|
305
614
|
},
|
|
306
615
|
intent: "table",
|
|
307
|
-
handler: (args) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!isSingleStatement(sql)) {
|
|
311
|
-
return { isError: true, text: "only a single statement is allowed" }
|
|
616
|
+
handler: async (args) => {
|
|
617
|
+
if (activeQueries >= maxConcurrentQueries) {
|
|
618
|
+
return { isError: true, text: "too many concurrent queries" }
|
|
312
619
|
}
|
|
313
|
-
|
|
314
|
-
return { isError: true, text: "only SELECT (or WITH…SELECT) statements are allowed" }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Verify via the query plan that only allowlisted tables are touched. SQLite names every
|
|
318
|
-
// scanned/searched relation in EXPLAIN QUERY PLAN detail rows.
|
|
319
|
-
const query = executableSql(sql)
|
|
320
|
-
let planRows: Array<{ detail?: unknown }>
|
|
620
|
+
activeQueries += 1
|
|
321
621
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
for (const row of planRows) {
|
|
327
|
-
const detail = typeof row.detail === "string" ? row.detail : ""
|
|
328
|
-
// SQLite also emits non-table nodes such as `SCAN CONSTANT ROW` for a
|
|
329
|
-
// constant-only SELECT. Only treat a scan/search target as a relation
|
|
330
|
-
// when it is not one of those planner pseudo-nodes.
|
|
331
|
-
if (
|
|
332
|
-
/^SCAN\s+CONSTANT\s+ROW$/i.test(detail.trim()) ||
|
|
333
|
-
/^SCAN\s+SUBQUERY\s+\d+$/i.test(detail.trim())
|
|
334
|
-
) {
|
|
335
|
-
continue
|
|
622
|
+
const sql = String(args.sql ?? "").trim()
|
|
623
|
+
if (sql === "") return { isError: true, text: "empty query" }
|
|
624
|
+
if (!isSingleStatement(sql)) {
|
|
625
|
+
return { isError: true, text: "only a single statement is allowed" }
|
|
336
626
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
627
|
+
if (!isReadStatement(sql)) {
|
|
628
|
+
return { isError: true, text: "only SELECT (or WITH…SELECT) statements are allowed" }
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const deadline = Date.now() + queryTimeoutMs
|
|
632
|
+
|
|
633
|
+
// Verify SQL relation tokens before the plan check. SQLite's plan output uses aliases as
|
|
634
|
+
// scan targets (e.g. `users AS habits` becomes `SCAN habits`), so plan-only validation can
|
|
635
|
+
// mistake an attacker-controlled alias for an allowlisted table.
|
|
636
|
+
const executable = executableSql(sql)
|
|
637
|
+
for (const relation of relationNames(executable)) {
|
|
340
638
|
if (!allowlist.has(relation)) {
|
|
341
639
|
return {
|
|
342
640
|
isError: true,
|
|
343
|
-
text: `query touches ${JSON.stringify(
|
|
641
|
+
text: `query touches ${JSON.stringify(relation)}, which is not exposed`,
|
|
344
642
|
}
|
|
345
643
|
}
|
|
346
644
|
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
let limitedRows: unknown[]
|
|
350
|
-
try {
|
|
351
|
-
// Fetch at most one row beyond the advertised cap. This keeps the database driver's
|
|
352
|
-
// materialization bounded even when the caller submits `SELECT * FROM a_huge_table`.
|
|
353
|
-
limitedRows = db
|
|
354
|
-
.prepare(`SELECT * FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`)
|
|
355
|
-
.all()
|
|
356
|
-
} catch (error) {
|
|
357
|
-
return { isError: true, text: `query failed: ${String(error)}` }
|
|
358
|
-
}
|
|
359
645
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
646
|
+
// Verify via the query plan that only allowlisted tables are touched. SQLite names every
|
|
647
|
+
// scanned/searched relation in EXPLAIN QUERY PLAN detail rows.
|
|
648
|
+
const query = executable
|
|
649
|
+
let planRows: Array<{ detail?: unknown }>
|
|
363
650
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
651
|
+
planRows = (await session.run(`EXPLAIN QUERY PLAN ${query}`, deadline)) as Array<{
|
|
652
|
+
detail?: unknown
|
|
653
|
+
}>
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error instanceof QueryTimeoutError) return { isError: true, text: error.message }
|
|
656
|
+
return { isError: true, text: `query failed to plan: ${String(error)}` }
|
|
657
|
+
}
|
|
658
|
+
for (const row of planRows) {
|
|
659
|
+
const detail = typeof row.detail === "string" ? row.detail : ""
|
|
660
|
+
// SQLite also emits non-table nodes such as `SCAN CONSTANT ROW` for a
|
|
661
|
+
// constant-only SELECT. Only treat a scan/search target as a relation
|
|
662
|
+
// when it is not one of those planner pseudo-nodes.
|
|
663
|
+
if (
|
|
664
|
+
/^SCAN\s+CONSTANT\s+ROW$/i.test(detail.trim()) ||
|
|
665
|
+
/^SCAN\s+SUBQUERY\s+\d+$/i.test(detail.trim())
|
|
666
|
+
) {
|
|
667
|
+
continue
|
|
370
668
|
}
|
|
371
|
-
|
|
669
|
+
// A schema-qualified reference plans as `SCAN main.habits`. The table is the last dotted
|
|
670
|
+
// segment; reading the first would reject `main.habits` as a table literally named "main".
|
|
671
|
+
const match =
|
|
672
|
+
/(?:SCAN|SEARCH)\s+(?:TABLE\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(
|
|
673
|
+
detail,
|
|
674
|
+
)
|
|
675
|
+
if (match !== null) {
|
|
676
|
+
const relation = match[1]?.toLowerCase() ?? ""
|
|
677
|
+
if (!allowlist.has(relation)) {
|
|
678
|
+
return {
|
|
679
|
+
isError: true,
|
|
680
|
+
text: `query touches ${JSON.stringify(match[1])}, which is not exposed`,
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let limitedRows: unknown[]
|
|
687
|
+
try {
|
|
688
|
+
// Fetch at most one row beyond the advertised cap. This keeps the database driver's
|
|
689
|
+
// materialization bounded even when the caller submits `SELECT * FROM a_huge_table`.
|
|
690
|
+
limitedRows = await session.run(
|
|
691
|
+
`SELECT * FROM (${query}) AS "__nifra_result" LIMIT ${maxRows + 1}`,
|
|
692
|
+
deadline,
|
|
693
|
+
)
|
|
372
694
|
} catch (error) {
|
|
373
|
-
return { isError: true, text:
|
|
695
|
+
if (error instanceof QueryTimeoutError) return { isError: true, text: error.message }
|
|
696
|
+
return { isError: true, text: `query failed: ${String(error)}` }
|
|
374
697
|
}
|
|
698
|
+
|
|
699
|
+
const wasLimited = limitedRows.length > maxRows
|
|
700
|
+
let total: number | null = limitedRows.length
|
|
701
|
+
if (wasLimited && options.runQuery?.exactTotal === true) {
|
|
702
|
+
try {
|
|
703
|
+
const countRows = (await session.run(
|
|
704
|
+
`SELECT count(*) AS "__nifra_total" FROM (${query}) AS "__nifra_count"`,
|
|
705
|
+
deadline,
|
|
706
|
+
)) as Array<{ __nifra_total?: unknown }>
|
|
707
|
+
const count = countRows[0]?.__nifra_total
|
|
708
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < maxRows) {
|
|
709
|
+
return { isError: true, text: "query failed: invalid row count" }
|
|
710
|
+
}
|
|
711
|
+
total = count
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (error instanceof QueryTimeoutError) return { isError: true, text: error.message }
|
|
714
|
+
return { isError: true, text: `query failed to count results: ${String(error)}` }
|
|
715
|
+
}
|
|
716
|
+
} else if (wasLimited) {
|
|
717
|
+
total = null
|
|
718
|
+
}
|
|
719
|
+
let shown = Math.min(limitedRows.length, maxRows)
|
|
720
|
+
let payload = limitedRows.slice(0, shown)
|
|
721
|
+
let result: Record<string, unknown> = {
|
|
722
|
+
rows: payload,
|
|
723
|
+
...(wasLimited || shown < limitedRows.length ? { truncated: true, total } : {}),
|
|
724
|
+
}
|
|
725
|
+
let serialized = JSON.stringify(result)
|
|
726
|
+
const encoded = new TextEncoder()
|
|
727
|
+
while (encoded.encode(serialized).byteLength > maxResultBytes && shown > 0) {
|
|
728
|
+
shown = Math.max(0, Math.floor(shown / 2))
|
|
729
|
+
payload = limitedRows.slice(0, shown)
|
|
730
|
+
result = {
|
|
731
|
+
rows: payload,
|
|
732
|
+
...(wasLimited || shown < limitedRows.length ? { truncated: true, total } : {}),
|
|
733
|
+
}
|
|
734
|
+
serialized = JSON.stringify(result)
|
|
735
|
+
}
|
|
736
|
+
if (encoded.encode(serialized).byteLength > maxResultBytes) {
|
|
737
|
+
return { isError: true, text: "query result exceeds maxResultBytes" }
|
|
738
|
+
}
|
|
739
|
+
return { text: serialized, structuredContent: result }
|
|
740
|
+
} finally {
|
|
741
|
+
activeQueries -= 1
|
|
375
742
|
}
|
|
376
|
-
let shown = Math.min(limitedRows.length, maxRows)
|
|
377
|
-
let payload = limitedRows.slice(0, shown)
|
|
378
|
-
let serialized = JSON.stringify(payload)
|
|
379
|
-
while (serialized.length > maxResultBytes && shown > 0) {
|
|
380
|
-
shown = Math.max(0, Math.floor(shown / 2))
|
|
381
|
-
payload = limitedRows.slice(0, shown)
|
|
382
|
-
serialized = JSON.stringify(payload)
|
|
383
|
-
}
|
|
384
|
-
const truncated = shown < total
|
|
385
|
-
const result: Record<string, unknown> = {
|
|
386
|
-
rows: payload,
|
|
387
|
-
...(truncated ? { truncated: { shown, total } } : {}),
|
|
388
|
-
}
|
|
389
|
-
return { text: JSON.stringify(result), structuredContent: result }
|
|
390
743
|
},
|
|
391
744
|
})
|
|
392
745
|
tools.push(runQuery)
|
|
@@ -405,46 +758,23 @@ export function serveDatabaseAsMcp(
|
|
|
405
758
|
// the wrapped fetch authorizes, and direct handle() dispatch fails closed (no Request → no auth).
|
|
406
759
|
const { authorize } = options.runQuery
|
|
407
760
|
|
|
408
|
-
const rpcId = (id: unknown): string | number | null =>
|
|
409
|
-
typeof id === "string" || typeof id === "number" ? id : null
|
|
410
|
-
|
|
411
|
-
const unauthorizedResult = (id: unknown): JsonRpcResponse =>
|
|
412
|
-
({
|
|
413
|
-
jsonrpc: "2.0",
|
|
414
|
-
id: rpcId(id),
|
|
415
|
-
result: {
|
|
416
|
-
content: [{ type: "text", text: "unauthorized" }],
|
|
417
|
-
isError: true,
|
|
418
|
-
},
|
|
419
|
-
}) as JsonRpcResponse
|
|
420
|
-
|
|
421
|
-
const isRunQueryCall = (message: unknown): message is { id?: unknown } => {
|
|
422
|
-
if (typeof message !== "object" || message === null) return false
|
|
423
|
-
const { method, params } = message as { method?: unknown; params?: { name?: unknown } }
|
|
424
|
-
return method === "tools/call" && params?.name === "run_query"
|
|
425
|
-
}
|
|
426
|
-
|
|
427
761
|
return {
|
|
428
762
|
...server,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
const messages = Array.isArray(body) ? body : [body]
|
|
438
|
-
const queryCall = messages.find(isRunQueryCall)
|
|
439
|
-
if (queryCall !== undefined) {
|
|
440
|
-
const ok = await authorize({ toolName: "run_query", request })
|
|
441
|
-
if (!ok) return Response.json(unauthorizedResult(queryCall.id))
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return server.fetch(request)
|
|
445
|
-
},
|
|
763
|
+
fetch: (request: Request) =>
|
|
764
|
+
server.fetch(request, {
|
|
765
|
+
authorizeMessage: async (message, inbound) =>
|
|
766
|
+
message.method !== "tools/call" || message.params?.name !== "run_query"
|
|
767
|
+
? true
|
|
768
|
+
: authorize({ toolName: "run_query", request: inbound }),
|
|
769
|
+
}),
|
|
446
770
|
async handle(message) {
|
|
447
|
-
if (
|
|
771
|
+
if (message.method === "tools/call" && message.params?.name === "run_query") {
|
|
772
|
+
return {
|
|
773
|
+
jsonrpc: "2.0",
|
|
774
|
+
id: typeof message.id === "string" || typeof message.id === "number" ? message.id : null,
|
|
775
|
+
result: { content: [{ type: "text", text: "unauthorized" }], isError: true },
|
|
776
|
+
} as JsonRpcResponse
|
|
777
|
+
}
|
|
448
778
|
return server.handle(message)
|
|
449
779
|
},
|
|
450
780
|
}
|