@neverinfamous/mysql-mcp 2.2.0 → 2.3.1
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/.github/workflows/codeql.yml +0 -8
- package/.github/workflows/docker-publish.yml +11 -10
- package/CHANGELOG.md +96 -0
- package/CODE_MODE.md +245 -0
- package/DOCKER_README.md +71 -254
- package/Dockerfile +5 -0
- package/README.md +102 -55
- package/VERSION +1 -1
- package/dist/adapters/mysql/MySQLAdapter.d.ts +4 -0
- package/dist/adapters/mysql/MySQLAdapter.d.ts.map +1 -1
- package/dist/adapters/mysql/MySQLAdapter.js +9 -0
- package/dist/adapters/mysql/MySQLAdapter.js.map +1 -1
- package/dist/adapters/mysql/prompts/index.d.ts +8 -1
- package/dist/adapters/mysql/prompts/index.d.ts.map +1 -1
- package/dist/adapters/mysql/prompts/index.js +8 -1
- package/dist/adapters/mysql/prompts/index.js.map +1 -1
- package/dist/adapters/mysql/prompts/routerSetup.d.ts.map +1 -1
- package/dist/adapters/mysql/prompts/routerSetup.js +5 -0
- package/dist/adapters/mysql/prompts/routerSetup.js.map +1 -1
- package/dist/adapters/mysql/resources/capabilities.d.ts.map +1 -1
- package/dist/adapters/mysql/resources/capabilities.js +6 -5
- package/dist/adapters/mysql/resources/capabilities.js.map +1 -1
- package/dist/adapters/mysql/resources/index.d.ts +9 -1
- package/dist/adapters/mysql/resources/index.d.ts.map +1 -1
- package/dist/adapters/mysql/resources/index.js +9 -1
- package/dist/adapters/mysql/resources/index.js.map +1 -1
- package/dist/adapters/mysql/tools/admin/backup.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/admin/backup.js +3 -3
- package/dist/adapters/mysql/tools/admin/backup.js.map +1 -1
- package/dist/adapters/mysql/tools/admin/maintenance.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/admin/maintenance.js +5 -5
- package/dist/adapters/mysql/tools/admin/maintenance.js.map +1 -1
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.js +26 -5
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.js.map +1 -1
- package/dist/adapters/mysql/tools/codemode/index.d.ts +38 -0
- package/dist/adapters/mysql/tools/codemode/index.d.ts.map +1 -0
- package/dist/adapters/mysql/tools/codemode/index.js +203 -0
- package/dist/adapters/mysql/tools/codemode/index.js.map +1 -0
- package/dist/adapters/mysql/tools/core.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/core.js +32 -20
- package/dist/adapters/mysql/tools/core.js.map +1 -1
- package/dist/adapters/mysql/tools/events.js +18 -6
- package/dist/adapters/mysql/tools/events.js.map +1 -1
- package/dist/adapters/mysql/tools/json/core.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/json/core.js +5 -5
- package/dist/adapters/mysql/tools/json/core.js.map +1 -1
- package/dist/adapters/mysql/tools/json/helpers.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/json/helpers.js +9 -3
- package/dist/adapters/mysql/tools/json/helpers.js.map +1 -1
- package/dist/adapters/mysql/tools/partitioning.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/partitioning.js +38 -6
- package/dist/adapters/mysql/tools/partitioning.js.map +1 -1
- package/dist/adapters/mysql/tools/performance/analysis.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/performance/analysis.js +67 -20
- package/dist/adapters/mysql/tools/performance/analysis.js.map +1 -1
- package/dist/adapters/mysql/tools/performance/optimization.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/performance/optimization.js +36 -6
- package/dist/adapters/mysql/tools/performance/optimization.js.map +1 -1
- package/dist/adapters/mysql/tools/security/data-protection.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/security/data-protection.js +9 -4
- package/dist/adapters/mysql/tools/security/data-protection.js.map +1 -1
- package/dist/adapters/mysql/tools/shell/common.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/shell/common.js +28 -2
- package/dist/adapters/mysql/tools/shell/common.js.map +1 -1
- package/dist/adapters/mysql/tools/shell/restore.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/shell/restore.js +54 -4
- package/dist/adapters/mysql/tools/shell/restore.js.map +1 -1
- package/dist/adapters/mysql/tools/spatial/operations.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/spatial/operations.js +10 -2
- package/dist/adapters/mysql/tools/spatial/operations.js.map +1 -1
- package/dist/adapters/mysql/tools/spatial/setup.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/spatial/setup.js +18 -0
- package/dist/adapters/mysql/tools/spatial/setup.js.map +1 -1
- package/dist/adapters/mysql/tools/sysschema/resources.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/sysschema/resources.js +5 -0
- package/dist/adapters/mysql/tools/sysschema/resources.js.map +1 -1
- package/dist/adapters/mysql/tools/text/fulltext.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/text/fulltext.js +6 -4
- package/dist/adapters/mysql/tools/text/fulltext.js.map +1 -1
- package/dist/adapters/mysql/tools/text/processing.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/text/processing.js +10 -45
- package/dist/adapters/mysql/tools/text/processing.js.map +1 -1
- package/dist/adapters/mysql/tools/transactions.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/transactions.js +8 -8
- package/dist/adapters/mysql/tools/transactions.js.map +1 -1
- package/dist/adapters/mysql/types.d.ts +968 -78
- package/dist/adapters/mysql/types.d.ts.map +1 -1
- package/dist/adapters/mysql/types.js +1084 -78
- package/dist/adapters/mysql/types.js.map +1 -1
- package/dist/auth/scopes.d.ts.map +1 -1
- package/dist/auth/scopes.js +1 -0
- package/dist/auth/scopes.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +12 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/codemode/api.d.ts +69 -0
- package/dist/codemode/api.d.ts.map +1 -0
- package/dist/codemode/api.js +1035 -0
- package/dist/codemode/api.js.map +1 -0
- package/dist/codemode/index.d.ts +13 -0
- package/dist/codemode/index.d.ts.map +1 -0
- package/dist/codemode/index.js +17 -0
- package/dist/codemode/index.js.map +1 -0
- package/dist/codemode/sandbox-factory.d.ts +72 -0
- package/dist/codemode/sandbox-factory.d.ts.map +1 -0
- package/dist/codemode/sandbox-factory.js +88 -0
- package/dist/codemode/sandbox-factory.js.map +1 -0
- package/dist/codemode/sandbox.d.ts +96 -0
- package/dist/codemode/sandbox.d.ts.map +1 -0
- package/dist/codemode/sandbox.js +345 -0
- package/dist/codemode/sandbox.js.map +1 -0
- package/dist/codemode/security.d.ts +44 -0
- package/dist/codemode/security.d.ts.map +1 -0
- package/dist/codemode/security.js +149 -0
- package/dist/codemode/security.js.map +1 -0
- package/dist/codemode/types.d.ts +137 -0
- package/dist/codemode/types.d.ts.map +1 -0
- package/dist/codemode/types.js +46 -0
- package/dist/codemode/types.js.map +1 -0
- package/dist/codemode/worker-sandbox.d.ts +82 -0
- package/dist/codemode/worker-sandbox.d.ts.map +1 -0
- package/dist/codemode/worker-sandbox.js +244 -0
- package/dist/codemode/worker-sandbox.js.map +1 -0
- package/dist/codemode/worker-script.d.ts +8 -0
- package/dist/codemode/worker-script.d.ts.map +1 -0
- package/dist/codemode/worker-script.js +113 -0
- package/dist/codemode/worker-script.js.map +1 -0
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +33 -9
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/filtering/ToolConstants.d.ts +11 -11
- package/dist/filtering/ToolConstants.d.ts.map +1 -1
- package/dist/filtering/ToolConstants.js +37 -19
- package/dist/filtering/ToolConstants.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +12 -0
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/server/McpServer.js +1 -1
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/modules/server.d.ts +2 -0
- package/dist/types/modules/server.d.ts.map +1 -1
- package/dist/types/modules/tools.d.ts +1 -1
- package/dist/types/modules/tools.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +12 -7
- package/releases/v2.2.0-release-notes.md +18 -18
- package/releases/v2.3.0-release-notes.md +191 -0
- package/releases/v2.3.1-release-notes.md +34 -0
- package/src/__tests__/perf.test.ts +12 -12
- package/src/adapters/mysql/MySQLAdapter.ts +10 -0
- package/src/adapters/mysql/__tests__/MySQLAdapter.test.ts +1 -1
- package/src/adapters/mysql/prompts/index.ts +8 -1
- package/src/adapters/mysql/prompts/routerSetup.ts +5 -0
- package/src/adapters/mysql/resources/__tests__/capabilities.test.ts +50 -1
- package/src/adapters/mysql/resources/capabilities.ts +6 -4
- package/src/adapters/mysql/resources/index.ts +9 -1
- package/src/adapters/mysql/tools/__tests__/core.test.ts +68 -0
- package/src/adapters/mysql/tools/__tests__/events.test.ts +56 -2
- package/src/adapters/mysql/tools/__tests__/json_core.test.ts +1 -1
- package/src/adapters/mysql/tools/__tests__/json_helpers.test.ts +46 -4
- package/src/adapters/mysql/tools/__tests__/replication.test.ts +144 -42
- package/src/adapters/mysql/tools/__tests__/security.test.ts +39 -0
- package/src/adapters/mysql/tools/__tests__/spatial.test.ts +39 -7
- package/src/adapters/mysql/tools/__tests__/spatial_handler.test.ts +35 -3
- package/src/adapters/mysql/tools/__tests__/transactions.test.ts +3 -5
- package/src/adapters/mysql/tools/admin/backup.ts +8 -3
- package/src/adapters/mysql/tools/admin/maintenance.ts +8 -4
- package/src/adapters/mysql/tools/cluster/__tests__/innodb-cluster.test.ts +35 -0
- package/src/adapters/mysql/tools/cluster/innodb-cluster.ts +26 -5
- package/src/adapters/mysql/tools/codemode/index.ts +249 -0
- package/src/adapters/mysql/tools/core.ts +44 -27
- package/src/adapters/mysql/tools/events.ts +23 -7
- package/src/adapters/mysql/tools/json/__tests__/helpers.test.ts +59 -14
- package/src/adapters/mysql/tools/json/core.ts +8 -4
- package/src/adapters/mysql/tools/json/helpers.ts +13 -3
- package/src/adapters/mysql/tools/partitioning.ts +53 -6
- package/src/adapters/mysql/tools/performance/__tests__/analysis.test.ts +227 -4
- package/src/adapters/mysql/tools/performance/__tests__/optimization.test.ts +35 -0
- package/src/adapters/mysql/tools/performance/analysis.ts +75 -21
- package/src/adapters/mysql/tools/performance/optimization.ts +44 -6
- package/src/adapters/mysql/tools/security/data-protection.ts +10 -4
- package/src/adapters/mysql/tools/shell/__tests__/common.test.ts +46 -0
- package/src/adapters/mysql/tools/shell/__tests__/restore.test.ts +28 -1
- package/src/adapters/mysql/tools/shell/common.ts +34 -2
- package/src/adapters/mysql/tools/shell/restore.ts +70 -7
- package/src/adapters/mysql/tools/spatial/__tests__/operations.test.ts +29 -0
- package/src/adapters/mysql/tools/spatial/operations.ts +13 -2
- package/src/adapters/mysql/tools/spatial/setup.ts +23 -0
- package/src/adapters/mysql/tools/sysschema/__tests__/resources.test.ts +21 -0
- package/src/adapters/mysql/tools/sysschema/resources.ts +5 -0
- package/src/adapters/mysql/tools/text/fulltext.ts +13 -5
- package/src/adapters/mysql/tools/text/processing.ts +20 -49
- package/src/adapters/mysql/tools/transactions.ts +11 -7
- package/src/adapters/mysql/types.ts +1241 -87
- package/src/auth/scopes.ts +1 -0
- package/src/cli/args.ts +14 -0
- package/src/codemode/api.ts +1224 -0
- package/src/codemode/index.ts +51 -0
- package/src/codemode/sandbox-factory.ts +146 -0
- package/src/codemode/sandbox.ts +450 -0
- package/src/codemode/security.ts +188 -0
- package/src/codemode/types.ts +194 -0
- package/src/codemode/worker-sandbox.ts +326 -0
- package/src/codemode/worker-script.ts +144 -0
- package/src/constants/ServerInstructions.ts +33 -9
- package/src/filtering/ToolConstants.ts +37 -19
- package/src/filtering/ToolFilter.ts +15 -0
- package/src/filtering/__tests__/ToolFilter.test.ts +65 -38
- package/src/server/McpServer.ts +1 -1
- package/src/types/modules/server.ts +3 -0
- package/src/types/modules/tools.ts +2 -1
- package/src/utils/logger.ts +2 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mysql-mcp - Code Mode Security
|
|
3
|
+
*
|
|
4
|
+
* Input validation, rate limiting, and audit logging for code execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_SECURITY_CONFIG,
|
|
10
|
+
type SecurityConfig,
|
|
11
|
+
type ValidationResult,
|
|
12
|
+
type ExecutionRecord,
|
|
13
|
+
type SandboxResult,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Security manager for Code Mode executions
|
|
18
|
+
*/
|
|
19
|
+
export class CodeModeSecurityManager {
|
|
20
|
+
private readonly config: SecurityConfig;
|
|
21
|
+
private readonly rateLimitMap = new Map<
|
|
22
|
+
string,
|
|
23
|
+
{ count: number; resetTime: number }
|
|
24
|
+
>();
|
|
25
|
+
|
|
26
|
+
constructor(config?: Partial<SecurityConfig>) {
|
|
27
|
+
this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate code before execution
|
|
32
|
+
*/
|
|
33
|
+
validateCode(code: string): ValidationResult {
|
|
34
|
+
const errors: string[] = [];
|
|
35
|
+
|
|
36
|
+
// Check code length
|
|
37
|
+
if (!code || typeof code !== "string") {
|
|
38
|
+
errors.push("Code must be a non-empty string");
|
|
39
|
+
return { valid: false, errors };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (code.length > this.config.maxCodeLength) {
|
|
43
|
+
errors.push(
|
|
44
|
+
`Code exceeds maximum length of ${String(this.config.maxCodeLength)} bytes`,
|
|
45
|
+
);
|
|
46
|
+
return { valid: false, errors };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for blocked patterns
|
|
50
|
+
for (const pattern of this.config.blockedPatterns) {
|
|
51
|
+
if (pattern.test(code)) {
|
|
52
|
+
errors.push(`Blocked pattern detected: ${pattern.source}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
valid: errors.length === 0,
|
|
58
|
+
errors,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check rate limit for a client
|
|
64
|
+
* @returns true if within limits, false if rate limited
|
|
65
|
+
*/
|
|
66
|
+
checkRateLimit(clientId: string): boolean {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const windowMs = 60000; // 1 minute window
|
|
69
|
+
|
|
70
|
+
const existing = this.rateLimitMap.get(clientId);
|
|
71
|
+
|
|
72
|
+
if (!existing || now >= existing.resetTime) {
|
|
73
|
+
// Start new window
|
|
74
|
+
this.rateLimitMap.set(clientId, {
|
|
75
|
+
count: 1,
|
|
76
|
+
resetTime: now + windowMs,
|
|
77
|
+
});
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (existing.count >= this.config.maxExecutionsPerMinute) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
existing.count++;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get remaining rate limit for a client
|
|
91
|
+
*/
|
|
92
|
+
getRateLimitRemaining(clientId: string): number {
|
|
93
|
+
const existing = this.rateLimitMap.get(clientId);
|
|
94
|
+
if (!existing || Date.now() >= existing.resetTime) {
|
|
95
|
+
return this.config.maxExecutionsPerMinute;
|
|
96
|
+
}
|
|
97
|
+
return Math.max(0, this.config.maxExecutionsPerMinute - existing.count);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sanitize and truncate result if too large
|
|
102
|
+
*/
|
|
103
|
+
sanitizeResult(result: unknown): unknown {
|
|
104
|
+
try {
|
|
105
|
+
const serialized = JSON.stringify(result);
|
|
106
|
+
if (serialized.length > this.config.maxResultSize) {
|
|
107
|
+
return {
|
|
108
|
+
_truncated: true,
|
|
109
|
+
_originalSize: serialized.length,
|
|
110
|
+
_maxSize: this.config.maxResultSize,
|
|
111
|
+
preview: serialized.substring(0, 1000) + "...",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
_error: "Result could not be serialized",
|
|
118
|
+
_type: typeof result,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log execution for audit purposes
|
|
125
|
+
*/
|
|
126
|
+
auditLog(execution: ExecutionRecord): void {
|
|
127
|
+
const { id, clientId, codePreview, result, readonly } = execution;
|
|
128
|
+
|
|
129
|
+
const logContext = {
|
|
130
|
+
module: "CODEMODE" as const,
|
|
131
|
+
operation: "execute",
|
|
132
|
+
entityId: id,
|
|
133
|
+
clientId: clientId ?? "anonymous",
|
|
134
|
+
readonly,
|
|
135
|
+
success: result.success,
|
|
136
|
+
wallTimeMs: result.metrics.wallTimeMs,
|
|
137
|
+
memoryUsedMb: result.metrics.memoryUsedMb,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (result.success) {
|
|
141
|
+
logger.info(
|
|
142
|
+
`Code execution completed: ${codePreview.substring(0, 50)}...`,
|
|
143
|
+
logContext,
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
const errorContext = {
|
|
147
|
+
...logContext,
|
|
148
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
149
|
+
...(result.stack !== undefined ? { stack: result.stack } : {}),
|
|
150
|
+
};
|
|
151
|
+
logger.warning(
|
|
152
|
+
`Code execution failed: ${result.error ?? "unknown error"}`,
|
|
153
|
+
errorContext,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create execution record for audit
|
|
160
|
+
*/
|
|
161
|
+
createExecutionRecord(
|
|
162
|
+
code: string,
|
|
163
|
+
result: SandboxResult,
|
|
164
|
+
readonly: boolean,
|
|
165
|
+
clientId?: string,
|
|
166
|
+
): ExecutionRecord {
|
|
167
|
+
return {
|
|
168
|
+
id: crypto.randomUUID(),
|
|
169
|
+
clientId,
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
codePreview: code.length > 200 ? code.substring(0, 200) + "..." : code,
|
|
172
|
+
result,
|
|
173
|
+
readonly,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clean up old rate limit entries
|
|
179
|
+
*/
|
|
180
|
+
cleanupRateLimits(): void {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
for (const [clientId, entry] of this.rateLimitMap) {
|
|
183
|
+
if (now >= entry.resetTime) {
|
|
184
|
+
this.rateLimitMap.delete(clientId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mysql-mcp - Code Mode Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the sandboxed code execution environment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolGroup } from "../types/index.js";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Sandbox Configuration
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for sandbox execution
|
|
15
|
+
*/
|
|
16
|
+
export interface SandboxOptions {
|
|
17
|
+
/** Memory limit in MB (default: 128) */
|
|
18
|
+
memoryLimitMb?: number;
|
|
19
|
+
/** Execution timeout in milliseconds (default: 30000) */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** CPU time limit in milliseconds (default: 10000) */
|
|
22
|
+
cpuLimitMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for the sandbox pool
|
|
27
|
+
*/
|
|
28
|
+
export interface PoolOptions {
|
|
29
|
+
/** Minimum instances to keep warm (default: 2) */
|
|
30
|
+
minInstances?: number;
|
|
31
|
+
/** Maximum instances in pool (default: 10) */
|
|
32
|
+
maxInstances?: number;
|
|
33
|
+
/** Idle timeout before disposing instance (default: 60000ms) */
|
|
34
|
+
idleTimeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default sandbox configuration
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_SANDBOX_OPTIONS: Required<SandboxOptions> = {
|
|
41
|
+
memoryLimitMb: 128,
|
|
42
|
+
timeoutMs: 30000,
|
|
43
|
+
cpuLimitMs: 10000,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default pool configuration
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_POOL_OPTIONS: Required<PoolOptions> = {
|
|
50
|
+
minInstances: 2,
|
|
51
|
+
maxInstances: 10,
|
|
52
|
+
idleTimeoutMs: 60000,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Execution Results
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Metrics collected during sandbox execution
|
|
61
|
+
*/
|
|
62
|
+
export interface ExecutionMetrics {
|
|
63
|
+
/** Wall clock time in milliseconds */
|
|
64
|
+
wallTimeMs: number;
|
|
65
|
+
/** CPU time consumed in milliseconds */
|
|
66
|
+
cpuTimeMs: number;
|
|
67
|
+
/** Peak memory usage in MB */
|
|
68
|
+
memoryUsedMb: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Result of sandbox code execution
|
|
73
|
+
*/
|
|
74
|
+
export interface SandboxResult {
|
|
75
|
+
/** Whether execution completed successfully */
|
|
76
|
+
success: boolean;
|
|
77
|
+
/** Return value from the code (if successful) */
|
|
78
|
+
result?: unknown;
|
|
79
|
+
/** Error message (if failed) */
|
|
80
|
+
error?: string | undefined;
|
|
81
|
+
/** Stack trace (if failed) */
|
|
82
|
+
stack?: string | undefined;
|
|
83
|
+
/** Execution metrics */
|
|
84
|
+
metrics: ExecutionMetrics;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// Security Configuration
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Security configuration for code validation
|
|
93
|
+
*/
|
|
94
|
+
export interface SecurityConfig {
|
|
95
|
+
/** Maximum code length in bytes (default: 50KB) */
|
|
96
|
+
maxCodeLength: number;
|
|
97
|
+
/** Maximum executions per minute per client (default: 60) */
|
|
98
|
+
maxExecutionsPerMinute: number;
|
|
99
|
+
/** Maximum result size in bytes (default: 10MB) */
|
|
100
|
+
maxResultSize: number;
|
|
101
|
+
/** Patterns to block in code */
|
|
102
|
+
blockedPatterns: RegExp[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Default security configuration
|
|
107
|
+
*/
|
|
108
|
+
export const DEFAULT_SECURITY_CONFIG: SecurityConfig = {
|
|
109
|
+
maxCodeLength: 50 * 1024, // 50KB
|
|
110
|
+
maxExecutionsPerMinute: 60,
|
|
111
|
+
maxResultSize: 10 * 1024 * 1024, // 10MB
|
|
112
|
+
blockedPatterns: [
|
|
113
|
+
/\brequire\s*\(/, // No require()
|
|
114
|
+
/\bimport\s*\(/, // No dynamic import()
|
|
115
|
+
/\bprocess\./, // No process access
|
|
116
|
+
/\bglobal\./, // No global access
|
|
117
|
+
/\bglobalThis\./, // No globalThis access
|
|
118
|
+
/\beval\s*\(/, // No eval()
|
|
119
|
+
/\bFunction\s*\(/, // No Function constructor
|
|
120
|
+
/\b__proto__\b/, // No prototype pollution
|
|
121
|
+
/\bconstructor\.constructor/, // No constructor chaining
|
|
122
|
+
/\bchild_process/, // No child processes
|
|
123
|
+
/\bfs\./, // No filesystem
|
|
124
|
+
/\bnet\./, // No networking
|
|
125
|
+
/\bhttp\./, // No HTTP
|
|
126
|
+
/\bhttps\./, // No HTTPS
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validation result from security checks
|
|
132
|
+
*/
|
|
133
|
+
export interface ValidationResult {
|
|
134
|
+
/** Whether the code passed validation */
|
|
135
|
+
valid: boolean;
|
|
136
|
+
/** Validation errors (if any) */
|
|
137
|
+
errors: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execution record for audit logging
|
|
142
|
+
*/
|
|
143
|
+
export interface ExecutionRecord {
|
|
144
|
+
/** Unique execution ID */
|
|
145
|
+
id: string;
|
|
146
|
+
/** Client identifier (for rate limiting) */
|
|
147
|
+
clientId?: string | undefined;
|
|
148
|
+
/** Timestamp of execution start */
|
|
149
|
+
timestamp: Date;
|
|
150
|
+
/** Code that was executed (truncated for logging) */
|
|
151
|
+
codePreview: string;
|
|
152
|
+
/** Execution result */
|
|
153
|
+
result: SandboxResult;
|
|
154
|
+
/** Whether code was in readonly mode */
|
|
155
|
+
readonly: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// =============================================================================
|
|
159
|
+
// API Types
|
|
160
|
+
// =============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Tool group API interface - each group exposes its tools as methods
|
|
164
|
+
*/
|
|
165
|
+
export interface GroupApi {
|
|
166
|
+
/** Tool group name */
|
|
167
|
+
readonly groupName: ToolGroup;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Options passed to mysql_execute_code tool
|
|
172
|
+
*/
|
|
173
|
+
export interface ExecuteCodeOptions {
|
|
174
|
+
/** TypeScript code to execute */
|
|
175
|
+
code: string;
|
|
176
|
+
/** Timeout in milliseconds (max 30000) */
|
|
177
|
+
timeout?: number;
|
|
178
|
+
/** Restrict to read-only operations */
|
|
179
|
+
readonly?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Result returned by mysql_execute_code tool
|
|
184
|
+
*/
|
|
185
|
+
export interface ExecuteCodeResult {
|
|
186
|
+
/** Whether execution succeeded */
|
|
187
|
+
success: boolean;
|
|
188
|
+
/** Return value from the code */
|
|
189
|
+
result?: unknown;
|
|
190
|
+
/** Error message (if failed) */
|
|
191
|
+
error?: string;
|
|
192
|
+
/** Execution metrics */
|
|
193
|
+
metrics: ExecutionMetrics;
|
|
194
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mysql-mcp - Code Mode Worker Sandbox
|
|
3
|
+
*
|
|
4
|
+
* Enhanced sandboxed execution using worker_threads for process-level isolation.
|
|
5
|
+
* Provides stronger isolation than vm module by running code in a separate thread
|
|
6
|
+
* with isolated memory space.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Separate V8 instance per worker thread
|
|
10
|
+
* - Hard timeout enforcement (worker termination)
|
|
11
|
+
* - Isolated memory space
|
|
12
|
+
* - Clean process state on each execution
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Worker } from "node:worker_threads";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { logger } from "../utils/logger.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_SANDBOX_OPTIONS,
|
|
21
|
+
DEFAULT_POOL_OPTIONS,
|
|
22
|
+
type SandboxOptions,
|
|
23
|
+
type PoolOptions,
|
|
24
|
+
type SandboxResult,
|
|
25
|
+
type ExecutionMetrics,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
|
|
28
|
+
// Get directory for worker script
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const WORKER_SCRIPT_PATH = join(__dirname, "worker-script.js");
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A sandboxed execution context using worker_threads
|
|
34
|
+
* Provides stronger isolation than vm module with separate V8 instance
|
|
35
|
+
*/
|
|
36
|
+
export class WorkerSandbox {
|
|
37
|
+
private readonly options: Required<SandboxOptions>;
|
|
38
|
+
private disposed = false;
|
|
39
|
+
|
|
40
|
+
private constructor(options: Required<SandboxOptions>) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a new worker sandbox instance
|
|
46
|
+
*/
|
|
47
|
+
static create(options?: SandboxOptions): WorkerSandbox {
|
|
48
|
+
const opts = { ...DEFAULT_SANDBOX_OPTIONS, ...options };
|
|
49
|
+
return new WorkerSandbox(opts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute code in a worker thread
|
|
54
|
+
* Each execution spawns a fresh worker for maximum isolation
|
|
55
|
+
*/
|
|
56
|
+
async execute(
|
|
57
|
+
code: string,
|
|
58
|
+
apiBindings: Record<string, unknown>,
|
|
59
|
+
): Promise<SandboxResult> {
|
|
60
|
+
if (this.disposed) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: "Sandbox has been disposed",
|
|
64
|
+
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startTime = performance.now();
|
|
69
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let worker: Worker | null = null;
|
|
73
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
74
|
+
let resolved = false;
|
|
75
|
+
|
|
76
|
+
const cleanup = (): void => {
|
|
77
|
+
if (timeoutId) {
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
timeoutId = null;
|
|
80
|
+
}
|
|
81
|
+
if (worker) {
|
|
82
|
+
worker.terminate().catch((): void => {
|
|
83
|
+
/* intentionally empty */
|
|
84
|
+
});
|
|
85
|
+
worker = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const respond = (result: SandboxResult): void => {
|
|
90
|
+
if (resolved) return;
|
|
91
|
+
resolved = true;
|
|
92
|
+
cleanup();
|
|
93
|
+
resolve(result);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
worker = new Worker(WORKER_SCRIPT_PATH, {
|
|
98
|
+
workerData: {
|
|
99
|
+
code,
|
|
100
|
+
apiBindings: this.serializeBindings(apiBindings),
|
|
101
|
+
timeout: this.options.timeoutMs,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Set hard timeout (will kill worker)
|
|
106
|
+
timeoutId = setTimeout(() => {
|
|
107
|
+
const endTime = performance.now();
|
|
108
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
109
|
+
respond({
|
|
110
|
+
success: false,
|
|
111
|
+
error: `Execution timeout: exceeded ${String(this.options.timeoutMs)}ms limit`,
|
|
112
|
+
metrics: this.calculateMetrics(
|
|
113
|
+
startTime,
|
|
114
|
+
endTime,
|
|
115
|
+
startMemory,
|
|
116
|
+
endMemory,
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
}, this.options.timeoutMs + 1000); // Extra buffer for cleanup
|
|
120
|
+
|
|
121
|
+
worker.on(
|
|
122
|
+
"message",
|
|
123
|
+
(result: {
|
|
124
|
+
success: boolean;
|
|
125
|
+
result?: unknown;
|
|
126
|
+
error?: string;
|
|
127
|
+
stack?: string;
|
|
128
|
+
}) => {
|
|
129
|
+
const endTime = performance.now();
|
|
130
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
131
|
+
respond({
|
|
132
|
+
success: result.success,
|
|
133
|
+
result: result.result,
|
|
134
|
+
error: result.error,
|
|
135
|
+
stack: result.stack,
|
|
136
|
+
metrics: this.calculateMetrics(
|
|
137
|
+
startTime,
|
|
138
|
+
endTime,
|
|
139
|
+
startMemory,
|
|
140
|
+
endMemory,
|
|
141
|
+
),
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
worker.on("error", (error: Error) => {
|
|
147
|
+
const endTime = performance.now();
|
|
148
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
149
|
+
respond({
|
|
150
|
+
success: false,
|
|
151
|
+
error: error.message,
|
|
152
|
+
stack: error.stack,
|
|
153
|
+
metrics: this.calculateMetrics(
|
|
154
|
+
startTime,
|
|
155
|
+
endTime,
|
|
156
|
+
startMemory,
|
|
157
|
+
endMemory,
|
|
158
|
+
),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
worker.on("exit", (exitCode: number) => {
|
|
163
|
+
if (!resolved && exitCode !== 0) {
|
|
164
|
+
const endTime = performance.now();
|
|
165
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
166
|
+
respond({
|
|
167
|
+
success: false,
|
|
168
|
+
error: `Worker exited with code ${String(exitCode)}`,
|
|
169
|
+
metrics: this.calculateMetrics(
|
|
170
|
+
startTime,
|
|
171
|
+
endTime,
|
|
172
|
+
startMemory,
|
|
173
|
+
endMemory,
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const endTime = performance.now();
|
|
180
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
181
|
+
respond({
|
|
182
|
+
success: false,
|
|
183
|
+
error: error instanceof Error ? error.message : String(error),
|
|
184
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
185
|
+
metrics: this.calculateMetrics(
|
|
186
|
+
startTime,
|
|
187
|
+
endTime,
|
|
188
|
+
startMemory,
|
|
189
|
+
endMemory,
|
|
190
|
+
),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Serialize API bindings for worker transfer
|
|
198
|
+
* We can't transfer functions directly, so we send method names
|
|
199
|
+
*/
|
|
200
|
+
private serializeBindings(
|
|
201
|
+
bindings: Record<string, unknown>,
|
|
202
|
+
): Record<string, string[]> {
|
|
203
|
+
const serialized: Record<string, string[]> = {};
|
|
204
|
+
for (const [group, methods] of Object.entries(bindings)) {
|
|
205
|
+
if (typeof methods === "object" && methods !== null) {
|
|
206
|
+
serialized[group] = Object.keys(methods);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return serialized;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Calculate execution metrics
|
|
214
|
+
*/
|
|
215
|
+
private calculateMetrics(
|
|
216
|
+
startTime: number,
|
|
217
|
+
endTime: number,
|
|
218
|
+
startMemory: number,
|
|
219
|
+
endMemory: number,
|
|
220
|
+
): ExecutionMetrics {
|
|
221
|
+
return {
|
|
222
|
+
wallTimeMs: Math.round(endTime - startTime),
|
|
223
|
+
cpuTimeMs: Math.round(endTime - startTime), // Approximation
|
|
224
|
+
memoryUsedMb: Math.max(
|
|
225
|
+
0,
|
|
226
|
+
Math.round(((endMemory - startMemory) / (1024 * 1024)) * 100) / 100,
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if sandbox is healthy
|
|
233
|
+
*/
|
|
234
|
+
isHealthy(): boolean {
|
|
235
|
+
return !this.disposed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Dispose of the sandbox
|
|
240
|
+
*/
|
|
241
|
+
dispose(): void {
|
|
242
|
+
this.disposed = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Pool of worker sandboxes
|
|
248
|
+
* Unlike VM pool, worker sandboxes are created fresh for each execution
|
|
249
|
+
* so this pool is simpler (mainly for statistics and control)
|
|
250
|
+
*/
|
|
251
|
+
export class WorkerSandboxPool {
|
|
252
|
+
private readonly options: Required<PoolOptions>;
|
|
253
|
+
private readonly sandboxOptions: Required<SandboxOptions>;
|
|
254
|
+
private activeCount = 0;
|
|
255
|
+
private disposed = false;
|
|
256
|
+
|
|
257
|
+
constructor(poolOptions?: PoolOptions, sandboxOptions?: SandboxOptions) {
|
|
258
|
+
this.options = { ...DEFAULT_POOL_OPTIONS, ...poolOptions };
|
|
259
|
+
this.sandboxOptions = { ...DEFAULT_SANDBOX_OPTIONS, ...sandboxOptions };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Initialize the pool
|
|
264
|
+
*/
|
|
265
|
+
initialize(): void {
|
|
266
|
+
logger.info(
|
|
267
|
+
`Worker sandbox pool initialized (max: ${String(this.options.maxInstances)} concurrent)`,
|
|
268
|
+
{
|
|
269
|
+
module: "CODEMODE" as const,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Execute code using a worker sandbox
|
|
276
|
+
*/
|
|
277
|
+
async execute(
|
|
278
|
+
code: string,
|
|
279
|
+
apiBindings: Record<string, unknown>,
|
|
280
|
+
): Promise<SandboxResult> {
|
|
281
|
+
if (this.disposed) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: "Pool has been disposed",
|
|
285
|
+
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.activeCount >= this.options.maxInstances) {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
error: `Worker pool exhausted (max: ${String(this.options.maxInstances)} concurrent)`,
|
|
293
|
+
metrics: { wallTimeMs: 0, cpuTimeMs: 0, memoryUsedMb: 0 },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.activeCount++;
|
|
298
|
+
try {
|
|
299
|
+
const sandbox = WorkerSandbox.create(this.sandboxOptions);
|
|
300
|
+
return await sandbox.execute(code, apiBindings);
|
|
301
|
+
} finally {
|
|
302
|
+
this.activeCount--;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get pool statistics
|
|
308
|
+
*/
|
|
309
|
+
getStats(): { available: number; inUse: number; max: number } {
|
|
310
|
+
return {
|
|
311
|
+
available: this.options.maxInstances - this.activeCount,
|
|
312
|
+
inUse: this.activeCount,
|
|
313
|
+
max: this.options.maxInstances,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Dispose of the pool
|
|
319
|
+
*/
|
|
320
|
+
dispose(): void {
|
|
321
|
+
this.disposed = true;
|
|
322
|
+
logger.info("Worker sandbox pool disposed", {
|
|
323
|
+
module: "CODEMODE" as const,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|