@yanzhang123/readonly-db-mcp 0.7.2
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/ENTERPRISE_DEPLOYMENT.md +211 -0
- package/LICENSE +21 -0
- package/README.md +450 -0
- package/SECURITY.md +214 -0
- package/npm/bin/mysql-mcp-server-readonly.cjs +11 -0
- package/npm/lib/launcher.cjs +294 -0
- package/npm/requirements.lock +521 -0
- package/npm/vendor/mysql_mcp_server-0.7.2-py3-none-any.whl +0 -0
- package/package.json +53 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
## MySQL Security Configuration
|
|
2
|
+
|
|
3
|
+
### Creating a Restricted MySQL User
|
|
4
|
+
|
|
5
|
+
It's crucial to create a dedicated MySQL user with minimal permissions for the MCP server. Never use the root account or a user with full administrative privileges.
|
|
6
|
+
|
|
7
|
+
#### 1. Create a new MySQL user
|
|
8
|
+
|
|
9
|
+
```sql
|
|
10
|
+
-- Connect as root or administrator
|
|
11
|
+
CREATE USER 'mcp_user'@'localhost' IDENTIFIED BY 'your_secure_password';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
#### 2. Grant minimal required permissions
|
|
15
|
+
|
|
16
|
+
Basic read-only access (recommended for exploration and analysis):
|
|
17
|
+
```sql
|
|
18
|
+
-- Grant SELECT permission only
|
|
19
|
+
GRANT SELECT ON your_database.* TO 'mcp_user'@'localhost';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Do not grant `INSERT`, `UPDATE`, `DELETE`, DDL, routine execution, file, lock, or
|
|
23
|
+
temporary-table privileges to the MCP account. The server rejects those operations,
|
|
24
|
+
and MySQL permissions provide the final security boundary.
|
|
25
|
+
|
|
26
|
+
#### 3. Apply the permissions
|
|
27
|
+
```sql
|
|
28
|
+
FLUSH PRIVILEGES;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Additional Security Measures
|
|
32
|
+
|
|
33
|
+
1. **Network Access**
|
|
34
|
+
- Restrict the user to connecting only from localhost if the MCP server runs on the same machine
|
|
35
|
+
- If remote access is needed, specify exact IP addresses rather than using wildcards
|
|
36
|
+
|
|
37
|
+
2. **Query Restrictions**
|
|
38
|
+
- Consider using VIEWs to further restrict data access
|
|
39
|
+
- Set an appropriate `max_queries_per_hour` limit:
|
|
40
|
+
```sql
|
|
41
|
+
ALTER USER 'mcp_user'@'localhost'
|
|
42
|
+
WITH MAX_QUERIES_PER_HOUR 1000;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. **Data Access Control**
|
|
46
|
+
- Grant access only to specific tables when possible
|
|
47
|
+
- Use column-level permissions for sensitive data:
|
|
48
|
+
```sql
|
|
49
|
+
GRANT SELECT (public_column1, public_column2)
|
|
50
|
+
ON your_database.sensitive_table TO 'mcp_user'@'localhost';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
4. **Regular Auditing**
|
|
54
|
+
- Enable MySQL audit logging for the MCP user
|
|
55
|
+
- Regularly review logs for unusual patterns
|
|
56
|
+
- Periodically review and adjust permissions
|
|
57
|
+
|
|
58
|
+
### Environment Configuration
|
|
59
|
+
|
|
60
|
+
When setting up the MCP server, use these restricted credentials in your environment:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
MYSQL_USER=mcp_user
|
|
64
|
+
MYSQL_PASSWORD=your_secure_password
|
|
65
|
+
MYSQL_DATABASE=your_database
|
|
66
|
+
MYSQL_HOST=localhost
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Monitoring Usage
|
|
70
|
+
|
|
71
|
+
To monitor the MCP user's database usage:
|
|
72
|
+
|
|
73
|
+
```sql
|
|
74
|
+
-- Check current connections
|
|
75
|
+
SELECT * FROM information_schema.PROCESSLIST
|
|
76
|
+
WHERE user = 'mcp_user';
|
|
77
|
+
|
|
78
|
+
-- View user privileges
|
|
79
|
+
SHOW GRANTS FOR 'mcp_user'@'localhost';
|
|
80
|
+
|
|
81
|
+
-- Check resource limits
|
|
82
|
+
SELECT * FROM mysql.user
|
|
83
|
+
WHERE user = 'mcp_user' AND host = 'localhost';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Best Practices
|
|
87
|
+
|
|
88
|
+
1. **Regular Password Rotation**
|
|
89
|
+
- Change the MCP user's password periodically
|
|
90
|
+
- Use strong, randomly generated passwords
|
|
91
|
+
- Update application configurations after password changes
|
|
92
|
+
|
|
93
|
+
2. **Permission Review**
|
|
94
|
+
- Regularly audit granted permissions
|
|
95
|
+
- Remove unnecessary privileges
|
|
96
|
+
- Keep permissions as restrictive as possible
|
|
97
|
+
|
|
98
|
+
3. **Access Patterns**
|
|
99
|
+
- Monitor query patterns for potential issues
|
|
100
|
+
- Set up alerts for unusual activity
|
|
101
|
+
- Maintain detailed logs of database access
|
|
102
|
+
|
|
103
|
+
4. **Data Protection**
|
|
104
|
+
- Consider encrypting sensitive columns
|
|
105
|
+
- Use SSL/TLS for database connections (see **SSL/TLS Support** below)
|
|
106
|
+
- Implement data masking where appropriate
|
|
107
|
+
|
|
108
|
+
### Secure Remote Access (SSH Tunneling)
|
|
109
|
+
|
|
110
|
+
If your MySQL server is not on the same network as the MCP server, **do not expose MySQL directly to the internet.** Instead, use the built-in SSH tunneling support:
|
|
111
|
+
|
|
112
|
+
1. Enable SSH tunneling by setting `MYSQL_SSH_ENABLE=true`.
|
|
113
|
+
2. Configure your SSH jump host credentials and private key path.
|
|
114
|
+
3. The MCP server will establish a secure encrypted tunnel and connect to MySQL over `localhost`, keeping your database port closed to the outside world.
|
|
115
|
+
|
|
116
|
+
### SSL/TLS Support
|
|
117
|
+
|
|
118
|
+
For production environments, always encrypt the connection between the MCP server and MySQL.
|
|
119
|
+
|
|
120
|
+
Use the `MYSQL_SSL_MODE` environment variable to control encryption:
|
|
121
|
+
- `DISABLED`: Allows plaintext only when explicitly selected; do not use in production.
|
|
122
|
+
- `REQUIRED`: Ensures the connection is encrypted.
|
|
123
|
+
- `VERIFY_CA`: Encrypts and verifies the server's certificate against a CA.
|
|
124
|
+
- `VERIFY_IDENTITY`: Encrypts and verifies that the server's hostname matches the certificate.
|
|
125
|
+
|
|
126
|
+
`REQUIRED` is the default, and the MCP verifies after connection that the
|
|
127
|
+
connector reports an encrypted session. Specify the CA certificate path using
|
|
128
|
+
`MYSQL_SSL_CA` when using either verification mode.
|
|
129
|
+
|
|
130
|
+
### SQL Injection Protection
|
|
131
|
+
|
|
132
|
+
The MCP server includes built-in protection against SQL injection for administrative operations:
|
|
133
|
+
- All database and table identifiers provided via resources or tool arguments are strictly validated against a regex whitelist (`^[a-zA-Z0-9_$]+$`).
|
|
134
|
+
- Identifiers are automatically quoted with backticks in internal queries.
|
|
135
|
+
- `execute_sql` uses a lexer-style guard that ignores ordinary strings and comments but rejects MySQL `/*!...*/` and MariaDB `/*M!...*/` executable comments, multiple statements, DML, DDL, locks, transactions, `SELECT INTO`, and session assignment.
|
|
136
|
+
- Stored functions and UDFs are fail-closed unless sqlglot recognizes them as a
|
|
137
|
+
built-in or their reviewed name is explicitly listed in `allowed_functions`.
|
|
138
|
+
Sequence advancement, lock functions and other known side-effecting functions
|
|
139
|
+
remain blocked even inside `SELECT`.
|
|
140
|
+
- All exposed SQL then runs inside a MySQL read-only transaction. Normal completion rolls back; cancellation or result truncation closes the socket, forcing rollback and stopping unread-row production.
|
|
141
|
+
- Every MCP tool is advertised as read-only and non-destructive.
|
|
142
|
+
- Named profiles can define `allowed_databases`; SQL table/function qualifiers and resource URIs are checked against it with a MySQL AST parser.
|
|
143
|
+
- User-supplied `SHOW` statements must be safely scoped to an allowed database; server-wide account, process, engine, binary-log, status, and variable inspection is blocked.
|
|
144
|
+
- System schemas are blocked for user SQL unless `allow_system_databases=true` is explicitly configured.
|
|
145
|
+
- Full-call, connector socket, and MySQL/MariaDB statement timeouts limit resource exhaustion. MCP cancellation actively closes the connector socket.
|
|
146
|
+
- Audit logs never contain SQL text, result data, credentials, hosts, usernames,
|
|
147
|
+
or SSH key paths. They contain a literal-free fingerprint plus the policy and
|
|
148
|
+
execution metadata needed for investigation.
|
|
149
|
+
- Result cells whose output or source columns match `mask_columns` are replaced
|
|
150
|
+
with `[REDACTED]`. Source-column detection traverses aliases, expressions and
|
|
151
|
+
CTEs; review the patterns for your schema because name-based masking cannot
|
|
152
|
+
replace database views or column-level permissions.
|
|
153
|
+
|
|
154
|
+
These controls enforce the MCP read-only contract even when the supplied MySQL
|
|
155
|
+
account has write privileges. A `SELECT`-only account is still recommended as
|
|
156
|
+
an independent containment layer, and `check_connection` reports broader grants.
|
|
157
|
+
|
|
158
|
+
### Enterprise Audit Controls
|
|
159
|
+
|
|
160
|
+
For durable audit records, set an absolute `audit_log_file` on each named
|
|
161
|
+
profile. The server writes one canonical JSON object per line and rotates it by
|
|
162
|
+
size. Events include:
|
|
163
|
+
|
|
164
|
+
- schema version, UTC timestamp, unique event ID and MCP request ID
|
|
165
|
+
- MCP operation and client name/version when exposed by the SDK
|
|
166
|
+
- optional `actor`, `purpose`, and `ticket_id`
|
|
167
|
+
- connection profile, target database, query type and literal-free fingerprint
|
|
168
|
+
- allowed/denied policy decision, internal/user-query flag and read-only
|
|
169
|
+
enforcement marker
|
|
170
|
+
- success, denial, error, timeout or cancellation outcome, duration, row count,
|
|
171
|
+
truncation and error type
|
|
172
|
+
|
|
173
|
+
Set `audit_required_context = ["actor", "purpose", "ticket_id"]` to reject
|
|
174
|
+
queries missing attribution before connecting. Set `audit_hmac_key_env` to the
|
|
175
|
+
name of a secret-manager supplied environment variable to sign every event with
|
|
176
|
+
HMAC-SHA256. `audit_fail_closed=true` requires a durable audit file and makes
|
|
177
|
+
audit sink/signing failures fail the MCP call. `audit_fsync=true` trades
|
|
178
|
+
throughput for stronger local durability.
|
|
179
|
+
|
|
180
|
+
Fail-closed query execution uses two-phase audit recording: an fsynced
|
|
181
|
+
`status=started` event must be durable before a database connection is opened,
|
|
182
|
+
then a terminal success, denial, error, timeout, or cancellation event is
|
|
183
|
+
written. Monitoring should alert on started events without a matching terminal
|
|
184
|
+
event, which can indicate process termination or an unavailable audit sink.
|
|
185
|
+
|
|
186
|
+
The audit context and MCP client metadata are caller assertions. For
|
|
187
|
+
cryptographically strong human attribution, terminate the remote transport at
|
|
188
|
+
an authenticated gateway, map the authenticated principal into request
|
|
189
|
+
metadata, and forward the JSONL stream to an append-only SIEM or log service.
|
|
190
|
+
Restrict audit file and HMAC-key access at the operating-system level.
|
|
191
|
+
|
|
192
|
+
Rejected DML, DDL, multi-statement, locking and cross-database attempts are
|
|
193
|
+
audited without sending them to MySQL and without recording their SQL text.
|
|
194
|
+
|
|
195
|
+
Connection diagnostics do not return raw authenticated usernames, grant
|
|
196
|
+
statements, profile-file paths, or database-supplied error messages. They expose
|
|
197
|
+
an irreversible account fingerprint, grant count, privilege classification,
|
|
198
|
+
numeric MySQL error references and policy metadata instead.
|
|
199
|
+
|
|
200
|
+
### Remote Transport
|
|
201
|
+
|
|
202
|
+
SSE binds to `127.0.0.1` by default. A non-loopback bind is rejected unless
|
|
203
|
+
`MCP_SSE_BEARER_TOKEN` contains at least 32 characters or
|
|
204
|
+
`MCP_SSE_TRUST_PROXY_AUTH=true` explicitly declares that an authenticated
|
|
205
|
+
reverse proxy is the only entry point. DNS-rebinding Host validation remains
|
|
206
|
+
enabled. Never set the proxy-trust flag if clients can reach the MCP port
|
|
207
|
+
directly.
|
|
208
|
+
|
|
209
|
+
### SSH and Connection Reuse
|
|
210
|
+
|
|
211
|
+
Named SSH tunnels are started once per profile, use a dynamic local port by
|
|
212
|
+
default, are health-checked before reuse, restart after failure, and terminate at
|
|
213
|
+
server shutdown. Connection pools are isolated by profile, endpoint, database,
|
|
214
|
+
and credential fingerprint. Set `pool_size=0` to disable pooling.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const { main } = require("../lib/launcher.cjs");
|
|
6
|
+
|
|
7
|
+
main().catch((error) => {
|
|
8
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9
|
+
process.stderr.write(`mysql-mcp-server-readonly: ${message}\n`);
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const manifest = require("../../package.json");
|
|
10
|
+
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
11
|
+
|
|
12
|
+
function pythonCandidates(environment = process.env, platform = process.platform) {
|
|
13
|
+
if (environment.MYSQL_MCP_PYTHON) {
|
|
14
|
+
return [[environment.MYSQL_MCP_PYTHON, []]];
|
|
15
|
+
}
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
return [
|
|
18
|
+
["py", ["-3.11"]],
|
|
19
|
+
["py", ["-3"]],
|
|
20
|
+
["python", []],
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
["python3", []],
|
|
25
|
+
["python", []],
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectPython(options = {}) {
|
|
30
|
+
const candidates = options.candidates || pythonCandidates();
|
|
31
|
+
const run = options.spawnSync || spawnSync;
|
|
32
|
+
for (const [command, prefix] of candidates) {
|
|
33
|
+
const result = run(
|
|
34
|
+
command,
|
|
35
|
+
[
|
|
36
|
+
...prefix,
|
|
37
|
+
"-c",
|
|
38
|
+
"import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')",
|
|
39
|
+
],
|
|
40
|
+
{
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const match = String(result.stdout).trim().match(/^(\d+)\.(\d+)$/);
|
|
49
|
+
if (match && (Number(match[1]) > 3 || Number(match[2]) >= 11)) {
|
|
50
|
+
return { command, prefix };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Python 3.11 or newer is required. Install Python or set MYSQL_MCP_PYTHON.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cacheBase(environment = process.env, platform = process.platform) {
|
|
59
|
+
if (environment.MYSQL_MCP_NPM_CACHE_DIR) {
|
|
60
|
+
return path.resolve(environment.MYSQL_MCP_NPM_CACHE_DIR);
|
|
61
|
+
}
|
|
62
|
+
if (platform === "win32") {
|
|
63
|
+
return path.join(
|
|
64
|
+
environment.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
|
|
65
|
+
"mysql-mcp-server-readonly",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (platform === "darwin") {
|
|
69
|
+
return path.join(os.homedir(), "Library", "Caches", "mysql-mcp-server-readonly");
|
|
70
|
+
}
|
|
71
|
+
return path.join(
|
|
72
|
+
environment.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"),
|
|
73
|
+
"mysql-mcp-server-readonly",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bundledWheel(root = packageRoot) {
|
|
78
|
+
const vendor = path.join(root, "npm", "vendor");
|
|
79
|
+
if (!fs.existsSync(vendor)) {
|
|
80
|
+
throw new Error("The npm package is missing its bundled Python wheel.");
|
|
81
|
+
}
|
|
82
|
+
const wheels = fs
|
|
83
|
+
.readdirSync(vendor)
|
|
84
|
+
.filter((name) => name.endsWith(".whl"))
|
|
85
|
+
.sort();
|
|
86
|
+
if (wheels.length !== 1) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Expected exactly one bundled Python wheel, found ${wheels.length}.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return path.join(vendor, wheels[0]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dependencyLock(root = packageRoot) {
|
|
95
|
+
const lock = path.join(root, "npm", "requirements.lock");
|
|
96
|
+
if (!fs.existsSync(lock)) {
|
|
97
|
+
throw new Error("The npm package is missing its hashed Python dependency lock.");
|
|
98
|
+
}
|
|
99
|
+
return lock;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runtimeFingerprint(wheel, lock) {
|
|
103
|
+
const digest = crypto.createHash("sha256");
|
|
104
|
+
digest.update(fs.readFileSync(wheel));
|
|
105
|
+
digest.update(fs.readFileSync(lock));
|
|
106
|
+
return digest.digest("hex");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function runtimeIsReady(marker, runtimePython, fingerprint) {
|
|
110
|
+
if (!fs.existsSync(marker) || !fs.existsSync(runtimePython)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const state = JSON.parse(fs.readFileSync(marker, "utf8"));
|
|
115
|
+
return state.runtimeFingerprint === fingerprint;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function virtualenvPython(virtualenv, platform = process.platform) {
|
|
122
|
+
return platform === "win32"
|
|
123
|
+
? path.join(virtualenv, "Scripts", "python.exe")
|
|
124
|
+
: path.join(virtualenv, "bin", "python");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runChecked(command, args, label) {
|
|
128
|
+
const result = spawnSync(command, args, {
|
|
129
|
+
stdio: "inherit",
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
});
|
|
132
|
+
if (result.error) {
|
|
133
|
+
throw new Error(`${label} failed: ${result.error.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (result.status !== 0) {
|
|
136
|
+
throw new Error(`${label} failed with exit code ${result.status}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sleep(milliseconds) {
|
|
141
|
+
Atomics.wait(
|
|
142
|
+
new Int32Array(new SharedArrayBuffer(4)),
|
|
143
|
+
0,
|
|
144
|
+
0,
|
|
145
|
+
milliseconds,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function acquireInstallLock(lockPath, markerPath) {
|
|
150
|
+
const deadline = Date.now() + 180_000;
|
|
151
|
+
while (true) {
|
|
152
|
+
try {
|
|
153
|
+
return fs.openSync(lockPath, "wx");
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!error || error.code !== "EEXIST") {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (fs.existsSync(markerPath)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const age = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
162
|
+
if (age > 600_000) {
|
|
163
|
+
fs.rmSync(lockPath, { force: true });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (Date.now() >= deadline) {
|
|
167
|
+
throw new Error("Timed out waiting for another runtime installation.");
|
|
168
|
+
}
|
|
169
|
+
sleep(250);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ensureRuntime() {
|
|
175
|
+
const wheel = bundledWheel();
|
|
176
|
+
const lockfile = dependencyLock();
|
|
177
|
+
const fingerprint = runtimeFingerprint(wheel, lockfile);
|
|
178
|
+
const runtimeRoot = path.join(cacheBase(), manifest.version);
|
|
179
|
+
const virtualenv = path.join(runtimeRoot, "venv");
|
|
180
|
+
const marker = path.join(runtimeRoot, "ready.json");
|
|
181
|
+
const lock = path.join(runtimeRoot, "install.lock");
|
|
182
|
+
const runtimePython = virtualenvPython(virtualenv);
|
|
183
|
+
|
|
184
|
+
if (runtimeIsReady(marker, runtimePython, fingerprint)) {
|
|
185
|
+
return runtimePython;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(runtimeRoot, { recursive: true });
|
|
189
|
+
const lockHandle = acquireInstallLock(lock, marker);
|
|
190
|
+
if (lockHandle === null) {
|
|
191
|
+
return runtimePython;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
if (runtimeIsReady(marker, runtimePython, fingerprint)) {
|
|
195
|
+
return runtimePython;
|
|
196
|
+
}
|
|
197
|
+
const python = detectPython();
|
|
198
|
+
if (!fs.existsSync(runtimePython)) {
|
|
199
|
+
runChecked(
|
|
200
|
+
python.command,
|
|
201
|
+
[...python.prefix, "-m", "venv", virtualenv],
|
|
202
|
+
"Python virtual environment creation",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
runChecked(
|
|
206
|
+
runtimePython,
|
|
207
|
+
[
|
|
208
|
+
"-m",
|
|
209
|
+
"pip",
|
|
210
|
+
"install",
|
|
211
|
+
"--disable-pip-version-check",
|
|
212
|
+
"--no-input",
|
|
213
|
+
"--require-hashes",
|
|
214
|
+
"--only-binary=:all:",
|
|
215
|
+
"--upgrade",
|
|
216
|
+
"-r",
|
|
217
|
+
lockfile,
|
|
218
|
+
],
|
|
219
|
+
"Locked Python dependency installation",
|
|
220
|
+
);
|
|
221
|
+
runChecked(
|
|
222
|
+
runtimePython,
|
|
223
|
+
[
|
|
224
|
+
"-m",
|
|
225
|
+
"pip",
|
|
226
|
+
"install",
|
|
227
|
+
"--disable-pip-version-check",
|
|
228
|
+
"--no-input",
|
|
229
|
+
"--no-deps",
|
|
230
|
+
"--upgrade",
|
|
231
|
+
wheel,
|
|
232
|
+
],
|
|
233
|
+
"Bundled Python package installation",
|
|
234
|
+
);
|
|
235
|
+
fs.writeFileSync(
|
|
236
|
+
marker,
|
|
237
|
+
`${JSON.stringify({
|
|
238
|
+
packageVersion: manifest.version,
|
|
239
|
+
wheel: path.basename(wheel),
|
|
240
|
+
runtimeFingerprint: fingerprint,
|
|
241
|
+
})}\n`,
|
|
242
|
+
"utf8",
|
|
243
|
+
);
|
|
244
|
+
return runtimePython;
|
|
245
|
+
} finally {
|
|
246
|
+
fs.closeSync(lockHandle);
|
|
247
|
+
fs.rmSync(lock, { force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function main(argv = process.argv.slice(2)) {
|
|
252
|
+
if (argv.includes("--launcher-version")) {
|
|
253
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const python = ensureRuntime();
|
|
258
|
+
const child = spawn(python, ["-m", "mysql_mcp_server", ...argv], {
|
|
259
|
+
env: process.env,
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
windowsHide: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
265
|
+
process.on(signal, () => {
|
|
266
|
+
if (!child.killed) {
|
|
267
|
+
child.kill(signal);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await new Promise((resolve, reject) => {
|
|
273
|
+
child.once("error", reject);
|
|
274
|
+
child.once("exit", (code, signal) => {
|
|
275
|
+
if (signal) {
|
|
276
|
+
reject(new Error(`Python MCP exited after signal ${signal}`));
|
|
277
|
+
} else {
|
|
278
|
+
process.exitCode = code === null ? 1 : code;
|
|
279
|
+
resolve();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
bundledWheel,
|
|
287
|
+
cacheBase,
|
|
288
|
+
dependencyLock,
|
|
289
|
+
detectPython,
|
|
290
|
+
ensureRuntime,
|
|
291
|
+
main,
|
|
292
|
+
pythonCandidates,
|
|
293
|
+
virtualenvPython,
|
|
294
|
+
};
|