@pilat/mcp-datalink 1.1.0 → 1.2.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 +19 -97
- package/dist/adapters/factory.d.ts +0 -4
- package/dist/adapters/factory.d.ts.map +1 -1
- package/dist/adapters/factory.js +3 -8
- package/dist/adapters/factory.js.map +1 -1
- package/dist/adapters/mysql/adapter.d.ts +2 -31
- package/dist/adapters/mysql/adapter.d.ts.map +1 -1
- package/dist/adapters/mysql/adapter.js +27 -283
- package/dist/adapters/mysql/adapter.js.map +1 -1
- package/dist/adapters/postgresql/adapter.d.ts +2 -22
- package/dist/adapters/postgresql/adapter.d.ts.map +1 -1
- package/dist/adapters/postgresql/adapter.js +18 -175
- package/dist/adapters/postgresql/adapter.js.map +1 -1
- package/dist/adapters/sqlite/adapter.d.ts +3 -25
- package/dist/adapters/sqlite/adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite/adapter.js +65 -364
- package/dist/adapters/sqlite/adapter.js.map +1 -1
- package/dist/adapters/sqlite/pragma-check.d.ts +19 -0
- package/dist/adapters/sqlite/pragma-check.d.ts.map +1 -0
- package/dist/adapters/sqlite/pragma-check.js +25 -0
- package/dist/adapters/sqlite/pragma-check.js.map +1 -0
- package/dist/adapters/sqlite/url-parser.d.ts +34 -0
- package/dist/adapters/sqlite/url-parser.d.ts.map +1 -0
- package/dist/adapters/sqlite/url-parser.js +73 -0
- package/dist/adapters/sqlite/url-parser.js.map +1 -0
- package/dist/adapters/types.d.ts +10 -113
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +31 -20
- package/dist/server.js.map +1 -1
- package/dist/tools/describe-table.d.ts +5 -6
- package/dist/tools/describe-table.d.ts.map +1 -1
- package/dist/tools/describe-table.js +65 -9
- package/dist/tools/describe-table.js.map +1 -1
- package/dist/tools/execute.d.ts +5 -6
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/execute.js +13 -11
- package/dist/tools/execute.js.map +1 -1
- package/dist/tools/explain.d.ts +5 -10
- package/dist/tools/explain.d.ts.map +1 -1
- package/dist/tools/explain.js +25 -26
- package/dist/tools/explain.js.map +1 -1
- package/dist/tools/list-databases.d.ts +5 -7
- package/dist/tools/list-databases.d.ts.map +1 -1
- package/dist/tools/list-databases.js +31 -0
- package/dist/tools/list-databases.js.map +1 -1
- package/dist/tools/list-tables.d.ts +5 -10
- package/dist/tools/list-tables.d.ts.map +1 -1
- package/dist/tools/list-tables.js +27 -9
- package/dist/tools/list-tables.js.map +1 -1
- package/dist/tools/query.d.ts +1 -6
- package/dist/tools/query.d.ts.map +1 -1
- package/dist/tools/query.js +7 -27
- package/dist/tools/query.js.map +1 -1
- package/dist/types.d.ts +39 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/formatter.d.ts +7 -4
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +20 -10
- package/dist/utils/formatter.js.map +1 -1
- package/dist/utils/sql-parser.d.ts +51 -0
- package/dist/utils/sql-parser.d.ts.map +1 -0
- package/dist/utils/sql-parser.js +310 -0
- package/dist/utils/sql-parser.js.map +1 -0
- package/dist/utils/truncate.d.ts +4 -13
- package/dist/utils/truncate.d.ts.map +1 -1
- package/dist/utils/truncate.js +38 -18
- package/dist/utils/truncate.js.map +1 -1
- package/dist/utils/validation.d.ts +35 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +89 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +10 -11
package/dist/utils/formatter.js
CHANGED
|
@@ -2,9 +2,22 @@
|
|
|
2
2
|
* Formatting utilities for query results
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* JSON replacer function that handles BigInt values by converting them to strings.
|
|
6
|
+
* This prevents "TypeError: Do not know how to serialize a BigInt" when using JSON.stringify.
|
|
7
7
|
*/
|
|
8
|
+
export function bigIntReplacer(_key, value) {
|
|
9
|
+
if (typeof value === 'bigint') {
|
|
10
|
+
return value.toString();
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Safely stringify a value to JSON, handling BigInt values.
|
|
16
|
+
*/
|
|
17
|
+
export function safeJsonStringify(value) {
|
|
18
|
+
return JSON.stringify(value, bigIntReplacer);
|
|
19
|
+
}
|
|
20
|
+
/** Format a value for display (handles null, dates, objects, primitives) */
|
|
8
21
|
export function formatValue(value) {
|
|
9
22
|
if (value === null || value === undefined) {
|
|
10
23
|
return 'NULL';
|
|
@@ -19,19 +32,16 @@ export function formatValue(value) {
|
|
|
19
32
|
return value.toISOString();
|
|
20
33
|
}
|
|
21
34
|
if (typeof value === 'object') {
|
|
22
|
-
return
|
|
35
|
+
return safeJsonStringify(value);
|
|
23
36
|
}
|
|
24
37
|
return String(value);
|
|
25
38
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Escape pipe characters in cell content for Markdown tables
|
|
28
|
-
*/
|
|
29
39
|
function escapeForMarkdown(value) {
|
|
30
|
-
return value
|
|
40
|
+
return value
|
|
41
|
+
.replace(/\|/g, '\\|')
|
|
42
|
+
.replace(/\r\n|\r|\n/g, ' ')
|
|
43
|
+
.replace(/`/g, '\\`');
|
|
31
44
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Format query results as a Markdown table
|
|
34
|
-
*/
|
|
35
45
|
export function formatAsMarkdownTable(columns, rows) {
|
|
36
46
|
if (columns.length === 0) {
|
|
37
47
|
return '';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/utils/formatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/utils/formatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAc;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;AAC/C,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC5D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK;SACT,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,OAAiB,EACjB,IAAyB;IAEzB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,aAAa;IACb,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAElD,gBAAgB;IAChB,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IAChD,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAErD,YAAY;IACZ,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5C,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified SQL Parser Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides SQL parsing and validation for PostgreSQL, MySQL, and SQLite
|
|
5
|
+
* using node-sql-parser library.
|
|
6
|
+
*/
|
|
7
|
+
import type { QueryType } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Supported SQL dialects
|
|
10
|
+
*/
|
|
11
|
+
export type SqlDialect = 'PostgreSQL' | 'MySQL' | 'SQLite';
|
|
12
|
+
/**
|
|
13
|
+
* Inject a LIMIT clause into a SELECT query if it doesn't have one
|
|
14
|
+
*
|
|
15
|
+
* @param sql - SQL query
|
|
16
|
+
* @param limit - Maximum number of rows
|
|
17
|
+
* @param dialect - SQL dialect
|
|
18
|
+
* @returns SQL with LIMIT clause
|
|
19
|
+
*/
|
|
20
|
+
export declare function injectLimit(sql: string, limit: number, dialect: SqlDialect): string;
|
|
21
|
+
/**
|
|
22
|
+
* Result of parsing a SQL query
|
|
23
|
+
*/
|
|
24
|
+
export interface ParseResult {
|
|
25
|
+
type: QueryType;
|
|
26
|
+
hasLimit: boolean;
|
|
27
|
+
isDangerous: boolean;
|
|
28
|
+
dangerousReason?: string;
|
|
29
|
+
sql: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse and validate a SQL query
|
|
33
|
+
*
|
|
34
|
+
* @param sql - SQL query to parse
|
|
35
|
+
* @param dialect - SQL dialect
|
|
36
|
+
* @returns Parsed query information
|
|
37
|
+
* @throws DbMcpError if query is invalid or contains multiple statements
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseQuery(sql: string, dialect: SqlDialect): ParseResult;
|
|
40
|
+
/**
|
|
41
|
+
* Validate that a parsed SQL query is appropriate for a specific tool
|
|
42
|
+
*
|
|
43
|
+
* This function contains the shared validation logic used by all database adapters.
|
|
44
|
+
* Adapters may perform additional adapter-specific checks before calling this.
|
|
45
|
+
*
|
|
46
|
+
* @param parsed - Parsed query result from parseQuery
|
|
47
|
+
* @param tool - Tool being used ('query' for SELECT, 'execute' for INSERT/UPDATE/DELETE)
|
|
48
|
+
* @throws DbMcpError if the query is not appropriate for the specified tool
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateQueryForTool(parsed: ParseResult, tool: 'query' | 'execute'): void;
|
|
51
|
+
//# sourceMappingURL=sql-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-parser.d.ts","sourceRoot":"","sources":["../../src/utils/sql-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AA6J3D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,CAyCnF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;CACb;AA6CD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,WAAW,CAwExE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,OAAO,GAAG,SAAS,GACxB,IAAI,CA6CN"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified SQL Parser Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides SQL parsing and validation for PostgreSQL, MySQL, and SQLite
|
|
5
|
+
* using node-sql-parser library.
|
|
6
|
+
*/
|
|
7
|
+
import { Parser } from 'node-sql-parser';
|
|
8
|
+
import { DbMcpError, ErrorCode } from './errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Prefixes that indicate dangerous SQL operations
|
|
11
|
+
*/
|
|
12
|
+
const DANGEROUS_PREFIXES = [
|
|
13
|
+
'drop',
|
|
14
|
+
'truncate',
|
|
15
|
+
'alter',
|
|
16
|
+
'create',
|
|
17
|
+
'grant',
|
|
18
|
+
'revoke',
|
|
19
|
+
'rename',
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* SQLite-specific dangerous operations
|
|
23
|
+
*/
|
|
24
|
+
const SQLITE_DANGEROUS_OPERATIONS = [
|
|
25
|
+
'attach',
|
|
26
|
+
'detach',
|
|
27
|
+
'vacuum',
|
|
28
|
+
'reindex',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* SQLite-specific operations that node-sql-parser cannot parse.
|
|
32
|
+
* These need to be detected before parsing to throw QUERY_BLOCKED instead of INVALID_SQL.
|
|
33
|
+
* Note: ATTACH is parseable by node-sql-parser, so it's excluded from this list.
|
|
34
|
+
*/
|
|
35
|
+
const SQLITE_UNPARSEABLE_OPERATIONS = [
|
|
36
|
+
'detach',
|
|
37
|
+
'vacuum',
|
|
38
|
+
'reindex',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Check if SQL starts with a SQLite-specific dangerous operation
|
|
42
|
+
* that node-sql-parser cannot parse
|
|
43
|
+
*
|
|
44
|
+
* @param sql - SQL query to check
|
|
45
|
+
* @returns Object with isDangerous flag and optional reason
|
|
46
|
+
*/
|
|
47
|
+
function checkSqliteUnparseableDangerousPrefix(sql) {
|
|
48
|
+
const normalized = sql.trim().toLowerCase();
|
|
49
|
+
for (const op of SQLITE_UNPARSEABLE_OPERATIONS) {
|
|
50
|
+
if (normalized.startsWith(op)) {
|
|
51
|
+
return {
|
|
52
|
+
isDangerous: true,
|
|
53
|
+
reason: `${op.toUpperCase()} statements are not allowed`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { isDangerous: false };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a parser instance for the specified dialect
|
|
61
|
+
*/
|
|
62
|
+
function createParser() {
|
|
63
|
+
return new Parser();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse SQL and return AST
|
|
67
|
+
*
|
|
68
|
+
* @param sql - SQL query to parse
|
|
69
|
+
* @param dialect - SQL dialect (PostgreSQL, MySQL, SQLite)
|
|
70
|
+
* @returns Parsed AST
|
|
71
|
+
* @throws DbMcpError if parsing fails or SQL contains dangerous operations
|
|
72
|
+
*/
|
|
73
|
+
function parseSQL(sql, dialect) {
|
|
74
|
+
// For SQLite, check for dangerous unparseable operations before parsing
|
|
75
|
+
// since node-sql-parser may not recognize these statements
|
|
76
|
+
if (dialect === 'SQLite') {
|
|
77
|
+
const { isDangerous, reason } = checkSqliteUnparseableDangerousPrefix(sql);
|
|
78
|
+
if (isDangerous) {
|
|
79
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, reason ?? 'Statement is not allowed', { sql });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const parser = createParser();
|
|
83
|
+
try {
|
|
84
|
+
return parser.astify(sql, { database: dialect });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : 'Unknown parse error';
|
|
88
|
+
throw new DbMcpError(ErrorCode.INVALID_SQL, 'Failed to parse SQL: ' + message, { sql });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Convert AST back to SQL string
|
|
93
|
+
*
|
|
94
|
+
* @param ast - AST to convert
|
|
95
|
+
* @param dialect - SQL dialect
|
|
96
|
+
* @returns SQL string
|
|
97
|
+
*/
|
|
98
|
+
function astToSQL(ast, dialect) {
|
|
99
|
+
const parser = createParser();
|
|
100
|
+
return parser.sqlify(ast, { database: dialect });
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a SQL statement type is dangerous
|
|
104
|
+
*
|
|
105
|
+
* @param stmtType - Statement type from AST
|
|
106
|
+
* @param dialect - SQL dialect
|
|
107
|
+
* @returns Object with isDangerous flag and optional reason
|
|
108
|
+
*/
|
|
109
|
+
function checkDangerousType(stmtType, dialect) {
|
|
110
|
+
const normalizedType = stmtType.toLowerCase();
|
|
111
|
+
// Check common dangerous prefixes
|
|
112
|
+
for (const prefix of DANGEROUS_PREFIXES) {
|
|
113
|
+
if (normalizedType === prefix || normalizedType.startsWith(prefix)) {
|
|
114
|
+
return {
|
|
115
|
+
isDangerous: true,
|
|
116
|
+
reason: `${prefix.toUpperCase()} statements are not allowed`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check SQLite-specific dangerous operations
|
|
121
|
+
if (dialect === 'SQLite') {
|
|
122
|
+
for (const op of SQLITE_DANGEROUS_OPERATIONS) {
|
|
123
|
+
if (normalizedType === op || normalizedType.startsWith(op)) {
|
|
124
|
+
return {
|
|
125
|
+
isDangerous: true,
|
|
126
|
+
reason: `${op.toUpperCase()} statements are not allowed`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { isDangerous: false };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Regular expression to check for LIMIT clause in SQL
|
|
135
|
+
* Matches LIMIT followed by a number, handling whitespace
|
|
136
|
+
*/
|
|
137
|
+
const LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
|
|
138
|
+
/**
|
|
139
|
+
* Inject a LIMIT clause into a SELECT query if it doesn't have one
|
|
140
|
+
*
|
|
141
|
+
* @param sql - SQL query
|
|
142
|
+
* @param limit - Maximum number of rows
|
|
143
|
+
* @param dialect - SQL dialect
|
|
144
|
+
* @returns SQL with LIMIT clause
|
|
145
|
+
*/
|
|
146
|
+
export function injectLimit(sql, limit, dialect) {
|
|
147
|
+
const ast = parseSQL(sql, dialect);
|
|
148
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
149
|
+
if (statements.length !== 1) {
|
|
150
|
+
return sql;
|
|
151
|
+
}
|
|
152
|
+
const stmt = statements[0];
|
|
153
|
+
if (stmt.type !== 'select') {
|
|
154
|
+
return sql;
|
|
155
|
+
}
|
|
156
|
+
const selectStmt = stmt;
|
|
157
|
+
// Check if already has limit (value array must have entries)
|
|
158
|
+
if (selectStmt.limit && Array.isArray(selectStmt.limit.value) && selectStmt.limit.value.length > 0) {
|
|
159
|
+
return sql;
|
|
160
|
+
}
|
|
161
|
+
// Add limit clause
|
|
162
|
+
selectStmt.limit = {
|
|
163
|
+
seperator: '',
|
|
164
|
+
value: [{ type: 'number', value: limit }],
|
|
165
|
+
};
|
|
166
|
+
const result = astToSQL(stmt, dialect);
|
|
167
|
+
// Verify that LIMIT was actually added to the result
|
|
168
|
+
if (!LIMIT_PATTERN.test(result)) {
|
|
169
|
+
// LIMIT injection failed - fall back to original SQL
|
|
170
|
+
// This is a safety measure in case astToSQL doesn't properly serialize the limit
|
|
171
|
+
console.warn(`[mcp-datalink] LIMIT injection verification failed: astToSQL did not produce LIMIT clause. ` +
|
|
172
|
+
`Returning original SQL. Dialect: ${dialect}`);
|
|
173
|
+
return sql;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Allowed query types for the execute tool
|
|
179
|
+
*/
|
|
180
|
+
const EXECUTE_ALLOWED_TYPES = ['insert', 'update', 'delete'];
|
|
181
|
+
/**
|
|
182
|
+
* Data-modifying statement types that should not be in CTEs for query tool
|
|
183
|
+
*/
|
|
184
|
+
const DATA_MODIFYING_TYPES = ['insert', 'update', 'delete'];
|
|
185
|
+
/**
|
|
186
|
+
* Check if a CTE contains data-modifying statements
|
|
187
|
+
*
|
|
188
|
+
* PostgreSQL supports data-modifying CTEs like:
|
|
189
|
+
* WITH inserted AS (INSERT ... RETURNING *) SELECT * FROM inserted
|
|
190
|
+
*
|
|
191
|
+
* These modify data even though the outer query is a SELECT.
|
|
192
|
+
*
|
|
193
|
+
* @param ast - Parsed AST
|
|
194
|
+
* @returns Object with hasDataModifyingCTE flag and optional reason
|
|
195
|
+
*/
|
|
196
|
+
function checkDataModifyingCTE(ast) {
|
|
197
|
+
const withClause = ast.with;
|
|
198
|
+
if (!Array.isArray(withClause)) {
|
|
199
|
+
return { hasDataModifyingCTE: false };
|
|
200
|
+
}
|
|
201
|
+
for (const cte of withClause) {
|
|
202
|
+
const stmtType = cte.stmt?.type?.toLowerCase();
|
|
203
|
+
if (stmtType && DATA_MODIFYING_TYPES.includes(stmtType)) {
|
|
204
|
+
return {
|
|
205
|
+
hasDataModifyingCTE: true,
|
|
206
|
+
reason: `CTE contains data-modifying ${stmtType.toUpperCase()} statement`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { hasDataModifyingCTE: false };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Parse and validate a SQL query
|
|
214
|
+
*
|
|
215
|
+
* @param sql - SQL query to parse
|
|
216
|
+
* @param dialect - SQL dialect
|
|
217
|
+
* @returns Parsed query information
|
|
218
|
+
* @throws DbMcpError if query is invalid or contains multiple statements
|
|
219
|
+
*/
|
|
220
|
+
export function parseQuery(sql, dialect) {
|
|
221
|
+
const ast = parseSQL(sql, dialect);
|
|
222
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
223
|
+
// Filter out null/undefined entries
|
|
224
|
+
const validStatements = statements.filter((s) => s !== null && s !== undefined);
|
|
225
|
+
if (validStatements.length === 0) {
|
|
226
|
+
throw new DbMcpError(ErrorCode.INVALID_SQL, 'No valid SQL statement found', { sql });
|
|
227
|
+
}
|
|
228
|
+
if (validStatements.length > 1) {
|
|
229
|
+
throw new DbMcpError(ErrorCode.MULTI_STATEMENT, 'Multiple SQL statements are not allowed. Please provide a single statement.', { sql, statementCount: validStatements.length });
|
|
230
|
+
}
|
|
231
|
+
const stmt = validStatements[0];
|
|
232
|
+
const stmtType = (stmt.type ?? '').toLowerCase();
|
|
233
|
+
// Determine query type
|
|
234
|
+
let queryType;
|
|
235
|
+
switch (stmtType) {
|
|
236
|
+
case 'select':
|
|
237
|
+
queryType = 'select';
|
|
238
|
+
break;
|
|
239
|
+
case 'insert':
|
|
240
|
+
queryType = 'insert';
|
|
241
|
+
break;
|
|
242
|
+
case 'update':
|
|
243
|
+
queryType = 'update';
|
|
244
|
+
break;
|
|
245
|
+
case 'delete':
|
|
246
|
+
queryType = 'delete';
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
queryType = 'other';
|
|
250
|
+
}
|
|
251
|
+
// Check for dangerous operations
|
|
252
|
+
let { isDangerous, reason } = checkDangerousType(stmtType, dialect);
|
|
253
|
+
// Check for data-modifying CTEs (e.g. WITH x AS (INSERT ...) SELECT ...)
|
|
254
|
+
if (!isDangerous) {
|
|
255
|
+
const cteCheck = checkDataModifyingCTE(stmt);
|
|
256
|
+
if (cteCheck.hasDataModifyingCTE) {
|
|
257
|
+
isDangerous = true;
|
|
258
|
+
reason = cteCheck.reason;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Check for LIMIT clause
|
|
262
|
+
let hasLimit = false;
|
|
263
|
+
if (queryType === 'select') {
|
|
264
|
+
const selectStmt = stmt;
|
|
265
|
+
// node-sql-parser returns limit: { value: [] } even when no LIMIT is present
|
|
266
|
+
hasLimit = !!(selectStmt.limit && Array.isArray(selectStmt.limit.value) && selectStmt.limit.value.length > 0);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
type: queryType,
|
|
270
|
+
hasLimit,
|
|
271
|
+
isDangerous,
|
|
272
|
+
dangerousReason: reason,
|
|
273
|
+
sql,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Validate that a parsed SQL query is appropriate for a specific tool
|
|
278
|
+
*
|
|
279
|
+
* This function contains the shared validation logic used by all database adapters.
|
|
280
|
+
* Adapters may perform additional adapter-specific checks before calling this.
|
|
281
|
+
*
|
|
282
|
+
* @param parsed - Parsed query result from parseQuery
|
|
283
|
+
* @param tool - Tool being used ('query' for SELECT, 'execute' for INSERT/UPDATE/DELETE)
|
|
284
|
+
* @throws DbMcpError if the query is not appropriate for the specified tool
|
|
285
|
+
*/
|
|
286
|
+
export function validateQueryForTool(parsed, tool) {
|
|
287
|
+
if (tool === 'query') {
|
|
288
|
+
if (parsed.type !== 'select') {
|
|
289
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, 'The query tool only accepts SELECT statements. Use the execute tool for ' +
|
|
290
|
+
parsed.type.toUpperCase() +
|
|
291
|
+
' statements.', { sql: parsed.sql, queryType: parsed.type, tool });
|
|
292
|
+
}
|
|
293
|
+
// Block data-modifying CTEs in query tool
|
|
294
|
+
if (parsed.isDangerous) {
|
|
295
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, parsed.dangerousReason ?? 'This operation is not allowed', { sql: parsed.sql, queryType: parsed.type, tool });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else if (tool === 'execute') {
|
|
299
|
+
if (parsed.type === 'select') {
|
|
300
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, 'The execute tool does not accept SELECT statements. Use the query tool instead.', { sql: parsed.sql, queryType: parsed.type, tool });
|
|
301
|
+
}
|
|
302
|
+
if (parsed.isDangerous) {
|
|
303
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, parsed.dangerousReason ?? 'This operation is not allowed', { sql: parsed.sql, queryType: parsed.type, tool });
|
|
304
|
+
}
|
|
305
|
+
if (!EXECUTE_ALLOWED_TYPES.includes(parsed.type)) {
|
|
306
|
+
throw new DbMcpError(ErrorCode.QUERY_BLOCKED, 'The execute tool only accepts INSERT, UPDATE, or DELETE statements.', { sql: parsed.sql, queryType: parsed.type, tool });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=sql-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-parser.js","sourceRoot":"","sources":["../../src/utils/sql-parser.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/truncate.d.ts
CHANGED
|
@@ -8,30 +8,21 @@ export interface TruncationInfo {
|
|
|
8
8
|
returned?: number;
|
|
9
9
|
hint?: string;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
* Truncate array of rows to maxRows limit
|
|
13
|
-
*/
|
|
14
11
|
export declare function truncateRows(rows: unknown[][], maxRows: number, totalAvailable?: number): {
|
|
15
12
|
rows: unknown[][];
|
|
16
13
|
info: TruncationInfo;
|
|
17
14
|
};
|
|
18
|
-
/**
|
|
19
|
-
* Truncate individual cell value to maxLength
|
|
20
|
-
*/
|
|
21
15
|
export declare function truncateCell(value: unknown, maxLength: number): {
|
|
22
16
|
value: string;
|
|
23
17
|
truncated: boolean;
|
|
24
18
|
originalLength?: number;
|
|
25
19
|
};
|
|
26
|
-
/**
|
|
27
|
-
* Truncate columns array to maxColumns limit
|
|
28
|
-
*/
|
|
29
20
|
export declare function truncateColumns(columns: string[], maxColumns: number): {
|
|
30
21
|
columns: string[];
|
|
31
22
|
truncated: boolean;
|
|
32
23
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
export declare function checkTotalSize(data: string, maxSize: number): {
|
|
25
|
+
data: string;
|
|
26
|
+
info: TruncationInfo;
|
|
27
|
+
};
|
|
37
28
|
//# sourceMappingURL=truncate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,cAAc,GAAG,YAAY,CAAC;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EAAE,EAAE,EACjB,OAAO,EAAE,MAAM,EACf,cAAc,CAAC,EAAE,MAAM,GACtB;IAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,CAoB7C;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CA4ChE;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,GACjB;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAY3C;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,CAmCxC"}
|
package/dist/utils/truncate.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Truncation utilities for limiting MCP response sizes
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
* Truncate array of rows to maxRows limit
|
|
6
|
-
*/
|
|
4
|
+
import { safeJsonStringify } from './formatter.js';
|
|
7
5
|
export function truncateRows(rows, maxRows, totalAvailable) {
|
|
8
6
|
const available = totalAvailable ?? rows.length;
|
|
9
7
|
if (rows.length <= maxRows) {
|
|
@@ -23,9 +21,6 @@ export function truncateRows(rows, maxRows, totalAvailable) {
|
|
|
23
21
|
},
|
|
24
22
|
};
|
|
25
23
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Truncate individual cell value to maxLength
|
|
28
|
-
*/
|
|
29
24
|
export function truncateCell(value, maxLength) {
|
|
30
25
|
// Convert value to string
|
|
31
26
|
let stringValue;
|
|
@@ -42,7 +37,7 @@ export function truncateCell(value, maxLength) {
|
|
|
42
37
|
stringValue = value.toISOString();
|
|
43
38
|
}
|
|
44
39
|
else if (typeof value === 'object') {
|
|
45
|
-
stringValue =
|
|
40
|
+
stringValue = safeJsonStringify(value);
|
|
46
41
|
}
|
|
47
42
|
else {
|
|
48
43
|
stringValue = String(value);
|
|
@@ -53,15 +48,23 @@ export function truncateCell(value, maxLength) {
|
|
|
53
48
|
truncated: false,
|
|
54
49
|
};
|
|
55
50
|
}
|
|
51
|
+
// Account for ellipsis length (3 chars) when truncating
|
|
52
|
+
const ellipsis = '...';
|
|
53
|
+
// Edge case: if maxLength is too small for ellipsis, just truncate without it
|
|
54
|
+
if (maxLength < ellipsis.length) {
|
|
55
|
+
return {
|
|
56
|
+
value: stringValue.slice(0, maxLength),
|
|
57
|
+
truncated: true,
|
|
58
|
+
originalLength: stringValue.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const truncateAt = maxLength - ellipsis.length;
|
|
56
62
|
return {
|
|
57
|
-
value: stringValue.slice(0,
|
|
63
|
+
value: stringValue.slice(0, truncateAt) + ellipsis,
|
|
58
64
|
truncated: true,
|
|
59
65
|
originalLength: stringValue.length,
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Truncate columns array to maxColumns limit
|
|
64
|
-
*/
|
|
65
68
|
export function truncateColumns(columns, maxColumns) {
|
|
66
69
|
if (columns.length <= maxColumns) {
|
|
67
70
|
return {
|
|
@@ -74,18 +77,35 @@ export function truncateColumns(columns, maxColumns) {
|
|
|
74
77
|
truncated: true,
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Check if total response size exceeds maxSize
|
|
79
|
-
*/
|
|
80
80
|
export function checkTotalSize(data, maxSize) {
|
|
81
81
|
const byteLength = Buffer.byteLength(data, 'utf8');
|
|
82
82
|
if (byteLength <= maxSize) {
|
|
83
|
-
return { truncated: false };
|
|
83
|
+
return { data, info: { truncated: false } };
|
|
84
84
|
}
|
|
85
|
+
// Truncate to fit within maxSize bytes
|
|
86
|
+
// We need to be careful with multi-byte UTF-8 characters
|
|
87
|
+
// Binary search for the right character position
|
|
88
|
+
let low = 0;
|
|
89
|
+
let high = data.length;
|
|
90
|
+
while (low < high) {
|
|
91
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
92
|
+
const slice = data.slice(0, mid);
|
|
93
|
+
const sliceBytes = Buffer.byteLength(slice, 'utf8');
|
|
94
|
+
if (sliceBytes <= maxSize) {
|
|
95
|
+
low = mid;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
high = mid - 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const truncatedData = data.slice(0, low);
|
|
85
102
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
data: truncatedData,
|
|
104
|
+
info: {
|
|
105
|
+
truncated: true,
|
|
106
|
+
truncationReason: 'maxTotalSize',
|
|
107
|
+
hint: 'Use LIMIT/OFFSET or WHERE clause to paginate',
|
|
108
|
+
},
|
|
89
109
|
};
|
|
90
110
|
}
|
|
91
111
|
//# sourceMappingURL=truncate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/utils/truncate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for database operations
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, DatabaseConfig } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Get a validated database configuration, throwing if not found
|
|
7
|
+
*
|
|
8
|
+
* @param database - The database name to look up
|
|
9
|
+
* @param config - The application configuration
|
|
10
|
+
* @returns The database configuration
|
|
11
|
+
* @throws DbMcpError with DATABASE_NOT_FOUND if database not configured
|
|
12
|
+
*/
|
|
13
|
+
export declare function getValidatedDatabase(database: string, config: Config): DatabaseConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Count placeholder parameters in SQL query
|
|
16
|
+
*
|
|
17
|
+
* Counts PostgreSQL-style placeholders ($1, $2, etc.) in SQL.
|
|
18
|
+
* Ignores placeholders inside string literals (single or double quoted).
|
|
19
|
+
*
|
|
20
|
+
* Note: LLM always uses $N style placeholders. For MySQL/SQLite,
|
|
21
|
+
* adapters convert $N to ? via convertPlaceholders() after validation.
|
|
22
|
+
*
|
|
23
|
+
* @param sql - SQL query to analyze
|
|
24
|
+
* @returns Number of unique placeholders found
|
|
25
|
+
*/
|
|
26
|
+
export declare function countPlaceholders(sql: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* Validate that the number of parameters matches the placeholder count in SQL
|
|
29
|
+
*
|
|
30
|
+
* @param sql - SQL query with $1, $2, etc. placeholders
|
|
31
|
+
* @param params - Array of parameter values
|
|
32
|
+
* @throws DbMcpError with INVALID_SQL if counts don't match
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateParamCount(sql: string, params: unknown[]): void;
|
|
35
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI1D;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAUrF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8CrD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAWvE"}
|