@berthojoris/mcp-mysql-server 1.12.0 → 1.14.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/CHANGELOG.md +56 -40
- package/DOCUMENTATIONS.md +94 -12
- package/README.md +12 -9
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/mcp-server.js +22 -31
- package/dist/optimization/explainAnalyzer.d.ts +21 -0
- package/dist/optimization/explainAnalyzer.js +147 -0
- package/dist/security/maskingLayer.d.ts +37 -0
- package/dist/security/maskingLayer.js +131 -0
- package/dist/security/securityLayer.d.ts +2 -0
- package/dist/security/securityLayer.js +4 -0
- package/dist/tools/aiTools.d.ts +22 -0
- package/dist/tools/aiTools.js +80 -0
- package/dist/tools/crudTools.js +2 -2
- package/dist/tools/queryTools.js +2 -1
- package/manifest.json +312 -82
- package/package.json +2 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare enum MaskingStrategy {
|
|
2
|
+
REDACT = "redact",// Replace with [REDACTED]
|
|
3
|
+
PARTIAL = "partial",// Show first/last chars: a***@b.com
|
|
4
|
+
HASH = "hash",// SHA256 hash (simulated for simplicity or real)
|
|
5
|
+
NONE = "none"
|
|
6
|
+
}
|
|
7
|
+
export interface MaskingRule {
|
|
8
|
+
columnPattern: RegExp;
|
|
9
|
+
strategy: MaskingStrategy;
|
|
10
|
+
}
|
|
11
|
+
export declare enum MaskingProfile {
|
|
12
|
+
NONE = "none",
|
|
13
|
+
SOFT = "soft",// Mask only credentials (passwords, secrets)
|
|
14
|
+
PARTIAL = "partial",// Mask credentials + partial mask PII (email, phone)
|
|
15
|
+
STRICT = "strict"
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Data Masking Layer
|
|
19
|
+
* Handles identifying and masking sensitive data in query results
|
|
20
|
+
*/
|
|
21
|
+
export declare class MaskingLayer {
|
|
22
|
+
private profile;
|
|
23
|
+
private rules;
|
|
24
|
+
constructor(profile?: string);
|
|
25
|
+
private parseProfile;
|
|
26
|
+
private getRulesForProfile;
|
|
27
|
+
/**
|
|
28
|
+
* Check if filtering is active
|
|
29
|
+
*/
|
|
30
|
+
isEnabled(): boolean;
|
|
31
|
+
getProfile(): MaskingProfile;
|
|
32
|
+
/**
|
|
33
|
+
* Apply masking to a dataset (array of objects)
|
|
34
|
+
*/
|
|
35
|
+
processResults(data: any[]): any[];
|
|
36
|
+
private applyStrategy;
|
|
37
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaskingLayer = exports.MaskingProfile = exports.MaskingStrategy = void 0;
|
|
4
|
+
var MaskingStrategy;
|
|
5
|
+
(function (MaskingStrategy) {
|
|
6
|
+
MaskingStrategy["REDACT"] = "redact";
|
|
7
|
+
MaskingStrategy["PARTIAL"] = "partial";
|
|
8
|
+
MaskingStrategy["HASH"] = "hash";
|
|
9
|
+
MaskingStrategy["NONE"] = "none"; // No masking
|
|
10
|
+
})(MaskingStrategy || (exports.MaskingStrategy = MaskingStrategy = {}));
|
|
11
|
+
var MaskingProfile;
|
|
12
|
+
(function (MaskingProfile) {
|
|
13
|
+
MaskingProfile["NONE"] = "none";
|
|
14
|
+
MaskingProfile["SOFT"] = "soft";
|
|
15
|
+
MaskingProfile["PARTIAL"] = "partial";
|
|
16
|
+
MaskingProfile["STRICT"] = "strict"; // Redact all PII and credentials
|
|
17
|
+
})(MaskingProfile || (exports.MaskingProfile = MaskingProfile = {}));
|
|
18
|
+
/**
|
|
19
|
+
* Data Masking Layer
|
|
20
|
+
* Handles identifying and masking sensitive data in query results
|
|
21
|
+
*/
|
|
22
|
+
class MaskingLayer {
|
|
23
|
+
constructor(profile = "none") {
|
|
24
|
+
this.profile = this.parseProfile(profile);
|
|
25
|
+
this.rules = this.getRulesForProfile(this.profile);
|
|
26
|
+
}
|
|
27
|
+
parseProfile(input) {
|
|
28
|
+
const normalized = input.toLowerCase().trim();
|
|
29
|
+
if (Object.values(MaskingProfile).includes(normalized)) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
return MaskingProfile.NONE;
|
|
33
|
+
}
|
|
34
|
+
getRulesForProfile(profile) {
|
|
35
|
+
const credentialsPattern = /^(password|passwd|pwd|secret|token|api_key|auth_key|access_token|refresh_token)$/i;
|
|
36
|
+
const piiPattern = /^(email|phone|mobile|cell|ssn|social_security|credit_card|cc_number|card_number|iban|dob|date_of_birth)$/i;
|
|
37
|
+
switch (profile) {
|
|
38
|
+
case MaskingProfile.NONE:
|
|
39
|
+
return [];
|
|
40
|
+
case MaskingProfile.SOFT:
|
|
41
|
+
return [
|
|
42
|
+
{ columnPattern: credentialsPattern, strategy: MaskingStrategy.REDACT }
|
|
43
|
+
];
|
|
44
|
+
case MaskingProfile.PARTIAL:
|
|
45
|
+
return [
|
|
46
|
+
{ columnPattern: credentialsPattern, strategy: MaskingStrategy.REDACT },
|
|
47
|
+
{ columnPattern: piiPattern, strategy: MaskingStrategy.PARTIAL }
|
|
48
|
+
];
|
|
49
|
+
case MaskingProfile.STRICT:
|
|
50
|
+
return [
|
|
51
|
+
{ columnPattern: credentialsPattern, strategy: MaskingStrategy.REDACT },
|
|
52
|
+
{ columnPattern: piiPattern, strategy: MaskingStrategy.REDACT }
|
|
53
|
+
];
|
|
54
|
+
default:
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if filtering is active
|
|
60
|
+
*/
|
|
61
|
+
isEnabled() {
|
|
62
|
+
return this.profile !== MaskingProfile.NONE;
|
|
63
|
+
}
|
|
64
|
+
getProfile() {
|
|
65
|
+
return this.profile;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Apply masking to a dataset (array of objects)
|
|
69
|
+
*/
|
|
70
|
+
processResults(data) {
|
|
71
|
+
if (!this.isEnabled() || !data || data.length === 0) {
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
// Identify columns to mask based on the first record (optimization)
|
|
75
|
+
const firstRecord = data[0];
|
|
76
|
+
const columns = Object.keys(firstRecord);
|
|
77
|
+
const columnsToMask = [];
|
|
78
|
+
for (const col of columns) {
|
|
79
|
+
for (const rule of this.rules) {
|
|
80
|
+
if (rule.columnPattern.test(col)) {
|
|
81
|
+
columnsToMask.push({ col, strategy: rule.strategy });
|
|
82
|
+
break; // Apply first matching rule
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (columnsToMask.length === 0) {
|
|
87
|
+
// No sensitive columns found
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
// Apply masking to all records
|
|
91
|
+
return data.map(record => {
|
|
92
|
+
const maskedRecord = { ...record };
|
|
93
|
+
for (const { col, strategy } of columnsToMask) {
|
|
94
|
+
if (maskedRecord[col] !== null && maskedRecord[col] !== undefined) {
|
|
95
|
+
maskedRecord[col] = this.applyStrategy(maskedRecord[col], strategy);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return maskedRecord;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
applyStrategy(value, strategy) {
|
|
102
|
+
const strVal = String(value);
|
|
103
|
+
switch (strategy) {
|
|
104
|
+
case MaskingStrategy.REDACT:
|
|
105
|
+
return "[REDACTED]";
|
|
106
|
+
case MaskingStrategy.PARTIAL:
|
|
107
|
+
if (strVal.includes('@')) {
|
|
108
|
+
// Email masking: j***@domain.com
|
|
109
|
+
const [local, domain] = strVal.split('@');
|
|
110
|
+
const maskedLocal = local.length > 2 ? local[0] + '***' + local[local.length - 1] : '***';
|
|
111
|
+
return `${maskedLocal}@${domain}`;
|
|
112
|
+
}
|
|
113
|
+
else if (strVal.length > 4) {
|
|
114
|
+
// Generic partial: show last 4 chars (e.g. phone/cc)
|
|
115
|
+
// or first 1 + last 4
|
|
116
|
+
return '***' + strVal.slice(-4);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
return "***";
|
|
120
|
+
}
|
|
121
|
+
case MaskingStrategy.HASH:
|
|
122
|
+
// Simple placeholder for hash to avoid crypto dependency if not needed,
|
|
123
|
+
// or use a simple consistent hash if strictly required.
|
|
124
|
+
// For now, let's use a redaction-like placeholder to indicate hashing intent.
|
|
125
|
+
return "[HASHED]";
|
|
126
|
+
default:
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.MaskingLayer = MaskingLayer;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { FeatureConfig } from "../config/featureConfig.js";
|
|
2
|
+
import { MaskingLayer } from "./maskingLayer.js";
|
|
2
3
|
export declare class SecurityLayer {
|
|
3
4
|
private ajv;
|
|
4
5
|
private readonly dangerousKeywords;
|
|
5
6
|
private readonly allowedOperations;
|
|
6
7
|
private readonly ddlOperations;
|
|
7
8
|
private featureConfig;
|
|
9
|
+
masking: MaskingLayer;
|
|
8
10
|
constructor(featureConfig?: FeatureConfig);
|
|
9
11
|
/**
|
|
10
12
|
* Check if a query is a read-only information query (SHOW, DESCRIBE, EXPLAIN, etc.)
|
|
@@ -6,10 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.SecurityLayer = void 0;
|
|
7
7
|
const ajv_1 = __importDefault(require("ajv"));
|
|
8
8
|
const featureConfig_js_1 = require("../config/featureConfig.js");
|
|
9
|
+
const maskingLayer_js_1 = require("./maskingLayer.js");
|
|
9
10
|
class SecurityLayer {
|
|
10
11
|
constructor(featureConfig) {
|
|
11
12
|
this.ajv = new ajv_1.default();
|
|
12
13
|
this.featureConfig = featureConfig || new featureConfig_js_1.FeatureConfig();
|
|
14
|
+
// Initialize masking layer from environment variable
|
|
15
|
+
const maskingProfile = process.env.MCP_MASKING_PROFILE || "none";
|
|
16
|
+
this.masking = new maskingLayer_js_1.MaskingLayer(maskingProfile);
|
|
13
17
|
// Define dangerous SQL keywords that should ALWAYS be blocked (critical security threats)
|
|
14
18
|
// These are blocked even with 'execute' permission
|
|
15
19
|
// Note: Avoid blocking common table/column names like "user" or "password"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SecurityLayer } from "../security/securityLayer";
|
|
2
|
+
export declare class AiTools {
|
|
3
|
+
private db;
|
|
4
|
+
private security;
|
|
5
|
+
private analyzer;
|
|
6
|
+
private optimizer;
|
|
7
|
+
constructor(security: SecurityLayer);
|
|
8
|
+
/**
|
|
9
|
+
* guided_query_fixer
|
|
10
|
+
* Analyzes a query (and optional error) to suggest repairs or optimizations using EXPLAIN.
|
|
11
|
+
*/
|
|
12
|
+
repairQuery(params: {
|
|
13
|
+
query: string;
|
|
14
|
+
error_message?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
status: string;
|
|
17
|
+
analysis?: any;
|
|
18
|
+
fixed_query?: string;
|
|
19
|
+
suggestions?: string[];
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AiTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
const explainAnalyzer_1 = require("../optimization/explainAnalyzer");
|
|
9
|
+
const queryOptimizer_1 = require("../optimization/queryOptimizer");
|
|
10
|
+
class AiTools {
|
|
11
|
+
constructor(security) {
|
|
12
|
+
this.db = connection_1.default.getInstance();
|
|
13
|
+
this.security = security;
|
|
14
|
+
this.analyzer = explainAnalyzer_1.ExplainAnalyzer.getInstance();
|
|
15
|
+
this.optimizer = queryOptimizer_1.QueryOptimizer.getInstance();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* guided_query_fixer
|
|
19
|
+
* Analyzes a query (and optional error) to suggest repairs or optimizations using EXPLAIN.
|
|
20
|
+
*/
|
|
21
|
+
async repairQuery(params) {
|
|
22
|
+
const { query, error_message } = params;
|
|
23
|
+
// 1. If there is a syntax error provided, we can't run EXPLAIN.
|
|
24
|
+
// We try to provided simple heuristics or just return the analysis of the error.
|
|
25
|
+
if (error_message) {
|
|
26
|
+
return {
|
|
27
|
+
status: "success",
|
|
28
|
+
suggestions: [
|
|
29
|
+
"Check SQL syntax matching your MySQL version.",
|
|
30
|
+
"Verify table and column names using 'list_tables' or 'read_table_schema'.",
|
|
31
|
+
"Ensure string literals are quoted correctly."
|
|
32
|
+
],
|
|
33
|
+
fixed_query: query, // We can't auto-fix syntax errors reliably without an LLM
|
|
34
|
+
analysis: {
|
|
35
|
+
issue: "Syntax/Execution Error",
|
|
36
|
+
details: error_message
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// 2. Validate query generally (security check)
|
|
41
|
+
// We assume this tool is used by an agent who might have 'read' permissions at least.
|
|
42
|
+
// If the query is unsafe (e.g. injection), we return that.
|
|
43
|
+
const validation = this.security.validateQuery(query, true); // validate with execute permission simulation to check structure
|
|
44
|
+
if (!validation.valid) {
|
|
45
|
+
return {
|
|
46
|
+
status: "error",
|
|
47
|
+
error: `Query rejected by security layer: ${validation.error}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// 3. Run EXPLAIN
|
|
51
|
+
try {
|
|
52
|
+
const explainQuery = `EXPLAIN FORMAT=JSON ${query}`;
|
|
53
|
+
// Note: We use the raw connection or executeSql equivalent.
|
|
54
|
+
// But EXPLAIN is safe-ish if the inner query is safe.
|
|
55
|
+
// validation passed, so we try EXPLAIN.
|
|
56
|
+
const explainResult = await this.db.query(explainQuery);
|
|
57
|
+
const analysis = this.analyzer.analyze(explainResult);
|
|
58
|
+
// 4. Try to apply simple fixes based on analysis (e.g. Missing Limit)
|
|
59
|
+
let fixedQuery = query;
|
|
60
|
+
if (analysis.complexity === "HIGH" && !query.toLowerCase().includes("limit")) {
|
|
61
|
+
// Suggest adding LIMIT if not present and complexity is high
|
|
62
|
+
analysis.suggestions.push("Consider adding 'LIMIT 100' to prevent massive data transfer.");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
status: "success",
|
|
66
|
+
analysis: analysis,
|
|
67
|
+
suggestions: analysis.suggestions,
|
|
68
|
+
fixed_query: fixedQuery
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return {
|
|
73
|
+
status: "error",
|
|
74
|
+
error: `Failed to analyze query: ${e.message}`,
|
|
75
|
+
suggestions: ["Verify the query is valid SQL before analyzing."]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.AiTools = AiTools;
|
package/dist/tools/crudTools.js
CHANGED
|
@@ -187,7 +187,7 @@ class CrudTools {
|
|
|
187
187
|
const results = await this.db.query(query, paramValidation.sanitizedParams);
|
|
188
188
|
return {
|
|
189
189
|
status: "success",
|
|
190
|
-
data: results,
|
|
190
|
+
data: this.security.masking.processResults(results),
|
|
191
191
|
total,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
@@ -197,7 +197,7 @@ class CrudTools {
|
|
|
197
197
|
const results = await this.db.query(query, paramValidation.sanitizedParams);
|
|
198
198
|
return {
|
|
199
199
|
status: "success",
|
|
200
|
-
data: results,
|
|
200
|
+
data: this.security.masking.processResults(results),
|
|
201
201
|
total: results.length,
|
|
202
202
|
};
|
|
203
203
|
}
|
package/dist/tools/queryTools.js
CHANGED
|
@@ -90,9 +90,10 @@ class QueryTools {
|
|
|
90
90
|
}
|
|
91
91
|
// Execute the query with sanitized parameters
|
|
92
92
|
const results = await this.db.query(finalQuery, paramValidation.sanitizedParams, useCache);
|
|
93
|
+
const maskedResults = this.security.masking.processResults(results);
|
|
93
94
|
return {
|
|
94
95
|
status: "success",
|
|
95
|
-
data:
|
|
96
|
+
data: maskedResults,
|
|
96
97
|
optimizedQuery,
|
|
97
98
|
};
|
|
98
99
|
}
|