@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.
@@ -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;
@@ -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
  }
@@ -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: results,
96
+ data: maskedResults,
96
97
  optimizedQuery,
97
98
  };
98
99
  }