@eamonboyle/mssql-mcp 1.0.0 → 1.1.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 +31 -26
- package/dist/__tests__/db.test.d.ts +2 -0
- package/dist/__tests__/db.test.d.ts.map +1 -0
- package/dist/__tests__/db.test.js +204 -0
- package/dist/__tests__/db.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +111 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +30 -7
- package/dist/db.js.map +1 -1
- package/dist/index.js +39 -22
- package/dist/index.js.map +1 -1
- package/dist/tools/CreateIndexTool.d.ts.map +1 -1
- package/dist/tools/CreateIndexTool.js +15 -9
- package/dist/tools/CreateIndexTool.js.map +1 -1
- package/dist/tools/CreateTableTool.d.ts.map +1 -1
- package/dist/tools/CreateTableTool.js +12 -7
- package/dist/tools/CreateTableTool.js.map +1 -1
- package/dist/tools/DescribeTableTool.d.ts.map +1 -1
- package/dist/tools/DescribeTableTool.js +5 -2
- package/dist/tools/DescribeTableTool.js.map +1 -1
- package/dist/tools/DropTableTool.d.ts.map +1 -1
- package/dist/tools/DropTableTool.js +3 -3
- package/dist/tools/DropTableTool.js.map +1 -1
- package/dist/tools/InsertDataTool.d.ts.map +1 -1
- package/dist/tools/InsertDataTool.js +8 -8
- package/dist/tools/InsertDataTool.js.map +1 -1
- package/dist/tools/ListTableTool.d.ts.map +1 -1
- package/dist/tools/ListTableTool.js +6 -4
- package/dist/tools/ListTableTool.js.map +1 -1
- package/dist/tools/ReadDataTool.d.ts +0 -8
- package/dist/tools/ReadDataTool.d.ts.map +1 -1
- package/dist/tools/ReadDataTool.js +15 -138
- package/dist/tools/ReadDataTool.js.map +1 -1
- package/dist/tools/UpdateDataTool.d.ts.map +1 -1
- package/dist/tools/UpdateDataTool.js +5 -5
- package/dist/tools/UpdateDataTool.js.map +1 -1
- package/dist/validation.d.ts +14 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +134 -0
- package/dist/validation.js.map +1 -0
- package/package.json +14 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSqlRequest } from "../db.js";
|
|
2
|
+
import { validateReadQuery } from "../validation.js";
|
|
2
3
|
export class ReadDataTool {
|
|
3
4
|
constructor() {
|
|
4
5
|
this.name = "read_data";
|
|
@@ -8,92 +9,16 @@ export class ReadDataTool {
|
|
|
8
9
|
properties: {
|
|
9
10
|
query: {
|
|
10
11
|
type: "string",
|
|
11
|
-
description: "SQL SELECT query to execute (must start with SELECT and cannot contain destructive operations). Example: SELECT * FROM movies WHERE genre = 'comedy'"
|
|
12
|
+
description: "SQL SELECT query to execute (must start with SELECT and cannot contain destructive operations). Example: SELECT * FROM movies WHERE genre = 'comedy'",
|
|
12
13
|
},
|
|
13
14
|
databaseName: {
|
|
14
15
|
type: "string",
|
|
15
|
-
description: "Name of the database to query (optional). Omit to use the default database."
|
|
16
|
+
description: "Name of the database to query (optional). Omit to use the default configured database.",
|
|
16
17
|
},
|
|
17
18
|
},
|
|
18
19
|
required: ["query"],
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Validates the SQL query for security issues
|
|
23
|
-
* @param query The SQL query to validate
|
|
24
|
-
* @returns Validation result with success flag and error message if invalid
|
|
25
|
-
*/
|
|
26
|
-
validateQuery(query) {
|
|
27
|
-
if (!query || typeof query !== 'string') {
|
|
28
|
-
return {
|
|
29
|
-
isValid: false,
|
|
30
|
-
error: 'Query must be a non-empty string'
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
// Remove comments and normalize whitespace for analysis
|
|
34
|
-
const cleanQuery = query
|
|
35
|
-
.replace(/--.*$/gm, '') // Remove line comments
|
|
36
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
37
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
38
|
-
.trim();
|
|
39
|
-
if (!cleanQuery) {
|
|
40
|
-
return {
|
|
41
|
-
isValid: false,
|
|
42
|
-
error: 'Query cannot be empty after removing comments'
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const upperQuery = cleanQuery.toUpperCase();
|
|
46
|
-
// Must start with SELECT
|
|
47
|
-
if (!upperQuery.startsWith('SELECT')) {
|
|
48
|
-
return {
|
|
49
|
-
isValid: false,
|
|
50
|
-
error: 'Query must start with SELECT for security reasons'
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
// Check for dangerous keywords in the cleaned query using word boundaries
|
|
54
|
-
for (const keyword of ReadDataTool.DANGEROUS_KEYWORDS) {
|
|
55
|
-
// Use word boundary regex to match only complete keywords, not parts of words
|
|
56
|
-
const keywordRegex = new RegExp(`(^|\\s|[^A-Za-z0-9_])${keyword}($|\\s|[^A-Za-z0-9_])`, 'i');
|
|
57
|
-
if (keywordRegex.test(upperQuery)) {
|
|
58
|
-
return {
|
|
59
|
-
isValid: false,
|
|
60
|
-
error: `Dangerous keyword '${keyword}' detected in query. Only SELECT operations are allowed.`
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Check for dangerous patterns using regex
|
|
65
|
-
for (const pattern of ReadDataTool.DANGEROUS_PATTERNS) {
|
|
66
|
-
if (pattern.test(query)) {
|
|
67
|
-
return {
|
|
68
|
-
isValid: false,
|
|
69
|
-
error: 'Potentially malicious SQL pattern detected. Only simple SELECT queries are allowed.'
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Additional validation: Check for multiple statements
|
|
74
|
-
const statements = cleanQuery.split(';').filter(stmt => stmt.trim().length > 0);
|
|
75
|
-
if (statements.length > 1) {
|
|
76
|
-
return {
|
|
77
|
-
isValid: false,
|
|
78
|
-
error: 'Multiple SQL statements are not allowed. Use only a single SELECT statement.'
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Check for suspicious string patterns that might indicate obfuscation
|
|
82
|
-
if (query.includes('CHAR(') || query.includes('NCHAR(') || query.includes('ASCII(')) {
|
|
83
|
-
return {
|
|
84
|
-
isValid: false,
|
|
85
|
-
error: 'Character conversion functions are not allowed as they may be used for obfuscation.'
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
// Limit query length to prevent potential DoS
|
|
89
|
-
if (query.length > 10000) {
|
|
90
|
-
return {
|
|
91
|
-
isValid: false,
|
|
92
|
-
error: 'Query is too long. Maximum allowed length is 10,000 characters.'
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return { isValid: true };
|
|
96
|
-
}
|
|
97
22
|
/**
|
|
98
23
|
* Sanitizes the query result to prevent any potential security issues
|
|
99
24
|
* @param data The query result data
|
|
@@ -109,12 +34,12 @@ export class ReadDataTool {
|
|
|
109
34
|
console.warn(`Query returned ${data.length} records, limiting to ${maxRecords}`);
|
|
110
35
|
return data.slice(0, maxRecords);
|
|
111
36
|
}
|
|
112
|
-
return data.map(record => {
|
|
113
|
-
if (typeof record ===
|
|
37
|
+
return data.map((record) => {
|
|
38
|
+
if (typeof record === "object" && record !== null) {
|
|
114
39
|
const sanitized = {};
|
|
115
40
|
for (const [key, value] of Object.entries(record)) {
|
|
116
41
|
// Sanitize column names (remove any suspicious characters)
|
|
117
|
-
const sanitizedKey = key.replace(/[^\w\s-_.]/g,
|
|
42
|
+
const sanitizedKey = key.replace(/[^\w\s-_.]/g, "");
|
|
118
43
|
if (sanitizedKey !== key) {
|
|
119
44
|
console.warn(`Column name sanitized: ${key} -> ${sanitizedKey}`);
|
|
120
45
|
}
|
|
@@ -138,17 +63,17 @@ export class ReadDataTool {
|
|
|
138
63
|
return { success: false, message: error, error: "INVALID_DATABASE" };
|
|
139
64
|
}
|
|
140
65
|
// Validate the query for security issues
|
|
141
|
-
const validation =
|
|
66
|
+
const validation = validateReadQuery(query);
|
|
142
67
|
if (!validation.isValid) {
|
|
143
68
|
console.warn(`Security validation failed for query: ${query.substring(0, 100)}...`);
|
|
144
69
|
return {
|
|
145
70
|
success: false,
|
|
146
71
|
message: `Security validation failed: ${validation.error}`,
|
|
147
|
-
error:
|
|
72
|
+
error: "SECURITY_VALIDATION_FAILED",
|
|
148
73
|
};
|
|
149
74
|
}
|
|
150
75
|
// Log the query for audit purposes (in production, consider more secure logging)
|
|
151
|
-
console.log(`Executing validated SELECT query: ${query.substring(0, 200)}${query.length > 200 ?
|
|
76
|
+
console.log(`Executing validated SELECT query: ${query.substring(0, 200)}${query.length > 200 ? "..." : ""}`);
|
|
152
77
|
// Execute the query
|
|
153
78
|
const result = await request.query(query);
|
|
154
79
|
// Sanitize the result
|
|
@@ -157,73 +82,25 @@ export class ReadDataTool {
|
|
|
157
82
|
success: true,
|
|
158
83
|
message: `Query executed successfully. Retrieved ${sanitizedData.length} record(s)${result.recordset.length !== sanitizedData.length
|
|
159
84
|
? ` (limited from ${result.recordset.length} total records)`
|
|
160
|
-
:
|
|
85
|
+
: ""}`,
|
|
161
86
|
data: sanitizedData,
|
|
162
87
|
recordCount: sanitizedData.length,
|
|
163
|
-
totalRecords: result.recordset.length
|
|
88
|
+
totalRecords: result.recordset.length,
|
|
164
89
|
};
|
|
165
90
|
}
|
|
166
91
|
catch (error) {
|
|
167
92
|
console.error("Error executing query:", error);
|
|
168
93
|
// Don't expose internal error details to prevent information leakage
|
|
169
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
170
|
-
const safeErrorMessage = errorMessage.includes(
|
|
94
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
95
|
+
const safeErrorMessage = errorMessage.includes("Invalid object name")
|
|
171
96
|
? errorMessage
|
|
172
|
-
:
|
|
97
|
+
: "Database query execution failed";
|
|
173
98
|
return {
|
|
174
99
|
success: false,
|
|
175
100
|
message: `Failed to execute query: ${safeErrorMessage}`,
|
|
176
|
-
error:
|
|
101
|
+
error: "QUERY_EXECUTION_FAILED",
|
|
177
102
|
};
|
|
178
103
|
}
|
|
179
104
|
}
|
|
180
105
|
}
|
|
181
|
-
// List of dangerous SQL keywords that should not be allowed
|
|
182
|
-
ReadDataTool.DANGEROUS_KEYWORDS = [
|
|
183
|
-
'DELETE', 'DROP', 'UPDATE', 'INSERT', 'ALTER', 'CREATE',
|
|
184
|
-
'TRUNCATE', 'EXEC', 'EXECUTE', 'MERGE', 'REPLACE',
|
|
185
|
-
'GRANT', 'REVOKE', 'COMMIT', 'ROLLBACK', 'TRANSACTION',
|
|
186
|
-
'BEGIN', 'DECLARE', 'SET', 'USE', 'BACKUP',
|
|
187
|
-
'RESTORE', 'KILL', 'SHUTDOWN', 'WAITFOR', 'OPENROWSET',
|
|
188
|
-
'OPENDATASOURCE', 'OPENQUERY', 'OPENXML', 'BULK', 'INTO'
|
|
189
|
-
];
|
|
190
|
-
// Regex patterns to detect common SQL injection techniques
|
|
191
|
-
ReadDataTool.DANGEROUS_PATTERNS = [
|
|
192
|
-
// SELECT INTO operations that create new tables
|
|
193
|
-
/SELECT\s+.*?\s+INTO\s+/i,
|
|
194
|
-
// Semicolon followed by dangerous keywords
|
|
195
|
-
/;\s*(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|MERGE|REPLACE|GRANT|REVOKE)/i,
|
|
196
|
-
// UNION injection attempts with dangerous keywords
|
|
197
|
-
/UNION\s+(?:ALL\s+)?SELECT.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
|
|
198
|
-
// Comment-based injection attempts
|
|
199
|
-
/--.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
|
|
200
|
-
/\/\*.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE).*?\*\//i,
|
|
201
|
-
// Stored procedure execution patterns
|
|
202
|
-
/EXEC\s*\(/i,
|
|
203
|
-
/EXECUTE\s*\(/i,
|
|
204
|
-
/sp_/i,
|
|
205
|
-
/xp_/i,
|
|
206
|
-
// Dynamic SQL construction
|
|
207
|
-
/EXEC\s*\(/i,
|
|
208
|
-
/EXECUTE\s*\(/i,
|
|
209
|
-
// Bulk operations
|
|
210
|
-
/BULK\s+INSERT/i,
|
|
211
|
-
/OPENROWSET/i,
|
|
212
|
-
/OPENDATASOURCE/i,
|
|
213
|
-
// System functions that could be dangerous
|
|
214
|
-
/@@/,
|
|
215
|
-
/SYSTEM_USER/i,
|
|
216
|
-
/USER_NAME/i,
|
|
217
|
-
/DB_NAME/i,
|
|
218
|
-
/HOST_NAME/i,
|
|
219
|
-
// Time delay attacks
|
|
220
|
-
/WAITFOR\s+DELAY/i,
|
|
221
|
-
/WAITFOR\s+TIME/i,
|
|
222
|
-
// Multiple statements (semicolon not at end)
|
|
223
|
-
/;\s*\w/,
|
|
224
|
-
// String concatenation that might hide malicious code
|
|
225
|
-
/\+\s*CHAR\s*\(/i,
|
|
226
|
-
/\+\s*NCHAR\s*\(/i,
|
|
227
|
-
/\+\s*ASCII\s*\(/i,
|
|
228
|
-
];
|
|
229
106
|
//# sourceMappingURL=ReadDataTool.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReadDataTool.js","sourceRoot":"","sources":["../../src/tools/ReadDataTool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReadDataTool.js","sourceRoot":"","sources":["../../src/tools/ReadDataTool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,OAAO,YAAY;IAAzB;QAEE,SAAI,GAAG,WAAW,CAAC;QACnB,gBAAW,GACT,8JAA8J,CAAC;QAEjK,gBAAW,GAAG;YACZ,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,KAAK,EAAE;oBACL,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,sJAAsJ;iBACzJ;gBACD,YAAY,EAAE;oBACZ,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,wFAAwF;iBAC3F;aACF;YACD,QAAQ,EAAE,CAAC,OAAO,CAAC;SACb,CAAC;IAwGX,CAAC;IAtGC;;;;OAIG;IACK,cAAc,CAAC,IAAW;QAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACxB,OAAO,EAAE,CAAC;SACX;QAED,gEAAgE;QAChE,MAAM,UAAU,GAAG,KAAK,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,UAAU,EAAE;YAC5B,OAAO,CAAC,IAAI,CACV,kBAAkB,IAAI,CAAC,MAAM,yBAAyB,UAAU,EAAE,CACnE,CAAC;YACF,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;SAClC;QAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACzB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE;gBACjD,MAAM,SAAS,GAAQ,EAAE,CAAC;gBAC1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;oBACjD,2DAA2D;oBAC3D,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;oBACpD,IAAI,YAAY,KAAK,GAAG,EAAE;wBACxB,OAAO,CAAC,IAAI,CAAC,0BAA0B,GAAG,OAAO,YAAY,EAAE,CAAC,CAAC;qBAClE;oBACD,SAAS,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC;iBACjC;gBACD,OAAO,SAAS,CAAC;aAClB;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAG,CAAC,MAAW;QACnB,IAAI;YACF,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;YAEvC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC,CAAC;YAC7D,IAAI,KAAK,EAAE;gBACT,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;aACtE;YAED,yCAAyC;YACzC,MAAM,UAAU,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC5C,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;gBACvB,OAAO,CAAC,IAAI,CACV,yCAAyC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CACtE,CAAC;gBACF,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,OAAO,EAAE,+BAA+B,UAAU,CAAC,KAAK,EAAE;oBAC1D,KAAK,EAAE,4BAA4B;iBACpC,CAAC;aACH;YAED,iFAAiF;YACjF,OAAO,CAAC,GAAG,CACT,qCAAqC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CACjG,CAAC;YAEF,oBAAoB;YACpB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAE1C,sBAAsB;YACtB,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAE5D,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,0CAA0C,aAAa,CAAC,MAAM,aACrE,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM;oBAC9C,CAAC,CAAC,kBAAkB,MAAM,CAAC,SAAS,CAAC,MAAM,iBAAiB;oBAC5D,CAAC,CAAC,EACN,EAAE;gBACF,IAAI,EAAE,aAAa;gBACnB,WAAW,EAAE,aAAa,CAAC,MAAM;gBACjC,YAAY,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM;aACtC,CAAC;SACH;QAAC,OAAO,KAAK,EAAE;YACd,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;YAE/C,qEAAqE;YACrE,MAAM,YAAY,GAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;YACpE,MAAM,gBAAgB,GAAG,YAAY,CAAC,QAAQ,CAAC,qBAAqB,CAAC;gBACnE,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,iCAAiC,CAAC;YAEtC,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,4BAA4B,gBAAgB,EAAE;gBACvD,KAAK,EAAE,wBAAwB;aAChC,CAAC;SACH;IACH,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateDataTool.d.ts","sourceRoot":"","sources":["../../src/tools/UpdateDataTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAG1D,qBAAa,cAAe,YAAW,IAAI;IACzC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IACnB,IAAI,SAAiB;IACrB,WAAW,
|
|
1
|
+
{"version":3,"file":"UpdateDataTool.d.ts","sourceRoot":"","sources":["../../src/tools/UpdateDataTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAG1D,qBAAa,cAAe,YAAW,IAAI;IACzC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IACnB,IAAI,SAAiB;IACrB,WAAW,SACuG;IAClH,WAAW,MAwBF;IAEH,GAAG,CAAC,MAAM,EAAE,GAAG;;;;;;;;;CAwCtB"}
|
|
@@ -8,7 +8,7 @@ export class UpdateDataTool {
|
|
|
8
8
|
properties: {
|
|
9
9
|
tableName: {
|
|
10
10
|
type: "string",
|
|
11
|
-
description: "Name of the table to update"
|
|
11
|
+
description: "Name of the table to update",
|
|
12
12
|
},
|
|
13
13
|
updates: {
|
|
14
14
|
type: "object",
|
|
@@ -16,11 +16,11 @@ export class UpdateDataTool {
|
|
|
16
16
|
},
|
|
17
17
|
whereClause: {
|
|
18
18
|
type: "string",
|
|
19
|
-
description: "WHERE clause to identify which records to update. Example: \"genre = 'comedy' AND created_date <= '2025-07-05'\""
|
|
19
|
+
description: "WHERE clause to identify which records to update. Example: \"genre = 'comedy' AND created_date <= '2025-07-05'\"",
|
|
20
20
|
},
|
|
21
21
|
databaseName: {
|
|
22
22
|
type: "string",
|
|
23
|
-
description: "Name of the database to use (optional). Omit to use the default database."
|
|
23
|
+
description: "Name of the database to use (optional). Omit to use the default configured database.",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
required: ["tableName", "updates", "whereClause"],
|
|
@@ -35,7 +35,7 @@ export class UpdateDataTool {
|
|
|
35
35
|
return { success: false, message: error };
|
|
36
36
|
}
|
|
37
37
|
// Basic validation: ensure whereClause is not empty
|
|
38
|
-
if (!whereClause || whereClause.trim() ===
|
|
38
|
+
if (!whereClause || whereClause.trim() === "") {
|
|
39
39
|
throw new Error("WHERE clause is required for security reasons");
|
|
40
40
|
}
|
|
41
41
|
// Build SET clause with parameterized queries for security
|
|
@@ -58,7 +58,7 @@ export class UpdateDataTool {
|
|
|
58
58
|
console.error("Error updating data:", error);
|
|
59
59
|
return {
|
|
60
60
|
success: false,
|
|
61
|
-
message: `Failed to update data ${query ? ` with '${query}'` :
|
|
61
|
+
message: `Failed to update data ${query ? ` with '${query}'` : ""}: ${error}`,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateDataTool.js","sourceRoot":"","sources":["../../src/tools/UpdateDataTool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,cAAc;IAA3B;QAEE,SAAI,GAAG,aAAa,CAAC;QACrB,gBAAW,
|
|
1
|
+
{"version":3,"file":"UpdateDataTool.js","sourceRoot":"","sources":["../../src/tools/UpdateDataTool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,cAAc;IAA3B;QAEE,SAAI,GAAG,aAAa,CAAC;QACrB,gBAAW,GACT,+GAA+G,CAAC;QAClH,gBAAW,GAAG;YACZ,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,SAAS,EAAE;oBACT,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,6BAA6B;iBAC3C;gBACD,OAAO,EAAE;oBACP,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,qGAAqG;iBACxG;gBACD,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,kHAAkH;iBACrH;gBACD,YAAY,EAAE;oBACZ,IAAI,EAAE,QAAQ;oBACd,WAAW,EACT,sFAAsF;iBACzF;aACF;YACD,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,aAAa,CAAC;SAC3C,CAAC;IA0CX,CAAC;IAxCC,KAAK,CAAC,GAAG,CAAC,MAAW;QACnB,IAAI,KAAyB,CAAC;QAC9B,IAAI;YACF,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;YAEjE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC,CAAC;YAC7D,IAAI,KAAK,EAAE;gBACT,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;aAC3C;YAED,oDAAoD;YACpD,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC7C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;aAClE;YAED,2DAA2D;YAC3D,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;iBACnC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;gBAClB,MAAM,SAAS,GAAG,UAAU,KAAK,EAAE,CAAC;gBACpC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvC,OAAO,IAAI,GAAG,QAAQ,SAAS,EAAE,CAAC;YACpC,CAAC,CAAC;iBACD,IAAI,CAAC,IAAI,CAAC,CAAC;YAEd,KAAK,GAAG,UAAU,SAAS,QAAQ,SAAS,UAAU,WAAW,EAAE,CAAC;YACpE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAE1C,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,kCAAkC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,kBAAkB;gBACnF,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;aACrC,CAAC;SACH;QAAC,OAAO,KAAK,EAAE;YACd,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,yBAAyB,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,EAAE;aAC9E,CAAC;SACH;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL query validation for read-only operations.
|
|
3
|
+
* Extracted for testability and reuse.
|
|
4
|
+
*/
|
|
5
|
+
export interface ValidationResult {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validates a SQL query for read-only (SELECT) operations.
|
|
11
|
+
* Rejects dangerous keywords, injection patterns, and multi-statement queries.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateReadQuery(query: string): ValidationResult;
|
|
14
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8DH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAoFjE"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL query validation for read-only operations.
|
|
3
|
+
* Extracted for testability and reuse.
|
|
4
|
+
*/
|
|
5
|
+
const DANGEROUS_KEYWORDS = [
|
|
6
|
+
"DELETE",
|
|
7
|
+
"DROP",
|
|
8
|
+
"UPDATE",
|
|
9
|
+
"INSERT",
|
|
10
|
+
"ALTER",
|
|
11
|
+
"CREATE",
|
|
12
|
+
"TRUNCATE",
|
|
13
|
+
"EXEC",
|
|
14
|
+
"EXECUTE",
|
|
15
|
+
"MERGE",
|
|
16
|
+
"REPLACE",
|
|
17
|
+
"GRANT",
|
|
18
|
+
"REVOKE",
|
|
19
|
+
"COMMIT",
|
|
20
|
+
"ROLLBACK",
|
|
21
|
+
"TRANSACTION",
|
|
22
|
+
"BEGIN",
|
|
23
|
+
"DECLARE",
|
|
24
|
+
"SET",
|
|
25
|
+
"USE",
|
|
26
|
+
"BACKUP",
|
|
27
|
+
"RESTORE",
|
|
28
|
+
"KILL",
|
|
29
|
+
"SHUTDOWN",
|
|
30
|
+
"WAITFOR",
|
|
31
|
+
"OPENROWSET",
|
|
32
|
+
"OPENDATASOURCE",
|
|
33
|
+
"OPENQUERY",
|
|
34
|
+
"OPENXML",
|
|
35
|
+
"BULK",
|
|
36
|
+
"INTO",
|
|
37
|
+
];
|
|
38
|
+
const DANGEROUS_PATTERNS = [
|
|
39
|
+
/SELECT\s+.*?\s+INTO\s+/i,
|
|
40
|
+
/;\s*(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|MERGE|REPLACE|GRANT|REVOKE)/i,
|
|
41
|
+
/UNION\s+(?:ALL\s+)?SELECT.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
|
|
42
|
+
/--.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE)/i,
|
|
43
|
+
/\/\*.*?(DELETE|DROP|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE).*?\*\//i,
|
|
44
|
+
/EXEC\s*\(/i,
|
|
45
|
+
/EXECUTE\s*\(/i,
|
|
46
|
+
/sp_/i,
|
|
47
|
+
/xp_/i,
|
|
48
|
+
/BULK\s+INSERT/i,
|
|
49
|
+
/OPENROWSET/i,
|
|
50
|
+
/OPENDATASOURCE/i,
|
|
51
|
+
/@@/,
|
|
52
|
+
/SYSTEM_USER/i,
|
|
53
|
+
/USER_NAME/i,
|
|
54
|
+
/DB_NAME/i,
|
|
55
|
+
/HOST_NAME/i,
|
|
56
|
+
/WAITFOR\s+DELAY/i,
|
|
57
|
+
/WAITFOR\s+TIME/i,
|
|
58
|
+
/;\s*\w/,
|
|
59
|
+
/\+\s*CHAR\s*\(/i,
|
|
60
|
+
/\+\s*NCHAR\s*\(/i,
|
|
61
|
+
/\+\s*ASCII\s*\(/i,
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Validates a SQL query for read-only (SELECT) operations.
|
|
65
|
+
* Rejects dangerous keywords, injection patterns, and multi-statement queries.
|
|
66
|
+
*/
|
|
67
|
+
export function validateReadQuery(query) {
|
|
68
|
+
if (!query || typeof query !== "string") {
|
|
69
|
+
return {
|
|
70
|
+
isValid: false,
|
|
71
|
+
error: "Query must be a non-empty string",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const cleanQuery = query
|
|
75
|
+
.replace(/--.*$/gm, "")
|
|
76
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
77
|
+
.replace(/\s+/g, " ")
|
|
78
|
+
.trim();
|
|
79
|
+
if (!cleanQuery) {
|
|
80
|
+
return {
|
|
81
|
+
isValid: false,
|
|
82
|
+
error: "Query cannot be empty after removing comments",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const upperQuery = cleanQuery.toUpperCase();
|
|
86
|
+
if (!upperQuery.startsWith("SELECT")) {
|
|
87
|
+
return {
|
|
88
|
+
isValid: false,
|
|
89
|
+
error: "Query must start with SELECT for security reasons",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
for (const keyword of DANGEROUS_KEYWORDS) {
|
|
93
|
+
const keywordRegex = new RegExp(`(^|\\s|[^A-Za-z0-9_])${keyword}($|\\s|[^A-Za-z0-9_])`, "i");
|
|
94
|
+
if (keywordRegex.test(upperQuery)) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: false,
|
|
97
|
+
error: `Dangerous keyword '${keyword}' detected in query. Only SELECT operations are allowed.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
102
|
+
if (pattern.test(query)) {
|
|
103
|
+
return {
|
|
104
|
+
isValid: false,
|
|
105
|
+
error: "Potentially malicious SQL pattern detected. Only simple SELECT queries are allowed.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const statements = cleanQuery
|
|
110
|
+
.split(";")
|
|
111
|
+
.filter((stmt) => stmt.trim().length > 0);
|
|
112
|
+
if (statements.length > 1) {
|
|
113
|
+
return {
|
|
114
|
+
isValid: false,
|
|
115
|
+
error: "Multiple SQL statements are not allowed. Use only a single SELECT statement.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (query.includes("CHAR(") ||
|
|
119
|
+
query.includes("NCHAR(") ||
|
|
120
|
+
query.includes("ASCII(")) {
|
|
121
|
+
return {
|
|
122
|
+
isValid: false,
|
|
123
|
+
error: "Character conversion functions are not allowed as they may be used for obfuscation.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (query.length > 10000) {
|
|
127
|
+
return {
|
|
128
|
+
isValid: false,
|
|
129
|
+
error: "Query is too long. Maximum allowed length is 10,000 characters.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { isValid: true };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,kBAAkB,GAAG;IACzB,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,UAAU;IACV,MAAM;IACN,SAAS;IACT,OAAO;IACP,SAAS;IACT,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,aAAa;IACb,OAAO;IACP,SAAS;IACT,KAAK;IACL,KAAK;IACL,QAAQ;IACR,SAAS;IACT,MAAM;IACN,UAAU;IACV,SAAS;IACT,YAAY;IACZ,gBAAgB;IAChB,WAAW;IACX,SAAS;IACT,MAAM;IACN,MAAM;CACP,CAAC;AAEF,MAAM,kBAAkB,GAAG;IACzB,yBAAyB;IACzB,gGAAgG;IAChG,6FAA6F;IAC7F,sEAAsE;IACtE,+EAA+E;IAC/E,YAAY;IACZ,eAAe;IACf,MAAM;IACN,MAAM;IACN,gBAAgB;IAChB,aAAa;IACb,iBAAiB;IACjB,IAAI;IACJ,cAAc;IACd,YAAY;IACZ,UAAU;IACV,YAAY;IACZ,kBAAkB;IAClB,iBAAiB;IACjB,QAAQ;IACR,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;CACnB,CAAC;AAOF;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACvC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,kCAAkC;SAC1C,CAAC;KACH;IAED,MAAM,UAAU,GAAG,KAAK;SACrB,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;SACtB,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IAEV,IAAI,CAAC,UAAU,EAAE;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,+CAA+C;SACvD,CAAC;KACH;IAED,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;IAE5C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QACpC,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,mDAAmD;SAC3D,CAAC;KACH;IAED,KAAK,MAAM,OAAO,IAAI,kBAAkB,EAAE;QACxC,MAAM,YAAY,GAAG,IAAI,MAAM,CAC7B,wBAAwB,OAAO,uBAAuB,EACtD,GAAG,CACJ,CAAC;QACF,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE;YACjC,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,sBAAsB,OAAO,0DAA0D;aAC/F,CAAC;SACH;KACF;IAED,KAAK,MAAM,OAAO,IAAI,kBAAkB,EAAE;QACxC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;YACvB,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,KAAK,EACH,qFAAqF;aACxF,CAAC;SACH;KACF;IAED,MAAM,UAAU,GAAG,UAAU;SAC1B,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC5C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE;QACzB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EACH,8EAA8E;SACjF,CAAC;KACH;IAED,IACE,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;QACvB,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACxB,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EACxB;QACA,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EACH,qFAAqF;SACxF,CAAC;KACH;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,EAAE;QACxB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,iEAAiE;SACzE,CAAC;KACH;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eamonboyle/mssql-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for Microsoft SQL Server - query and manage your database with natural language in Cursor, Claude, and VS Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
30
30
|
"prepare": "npm run build",
|
|
31
31
|
"watch": "tsc --watch",
|
|
32
|
-
"start": "node dist/index.js"
|
|
32
|
+
"start": "node dist/index.js",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"lint": "eslint src",
|
|
36
|
+
"format": "prettier --write src"
|
|
33
37
|
},
|
|
34
38
|
"files": [
|
|
35
39
|
"dist"
|
|
@@ -38,10 +42,16 @@
|
|
|
38
42
|
"node": ">=18.0.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
45
|
+
"@eslint/js": "^9.39.2",
|
|
41
46
|
"@types/mssql": "^9.1.7",
|
|
42
|
-
"@types/node": "^
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"eslint": "^9.39.2",
|
|
49
|
+
"eslint-config-prettier": "^10.1.8",
|
|
50
|
+
"prettier": "^3.8.1",
|
|
43
51
|
"shx": "^0.3.4",
|
|
44
|
-
"typescript": "^4.9.5"
|
|
52
|
+
"typescript": "^4.9.5",
|
|
53
|
+
"typescript-eslint": "^8.55.0",
|
|
54
|
+
"vitest": "^2.1.0"
|
|
45
55
|
},
|
|
46
56
|
"dependencies": {
|
|
47
57
|
"@modelcontextprotocol/sdk": "^1.0.0",
|