@cainli/mcp-server-mysql 2.0.14 → 2.0.17
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 +1 -0
- package/dist/index.js +8 -1
- package/dist/src/config/index.js +1 -0
- package/dist/src/db/index.js +19 -2
- package/dist/src/db/utils.js +28 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -617,6 +617,7 @@ When `MYSQL_CONNECTION_STRING` is provided, it takes precedence over individual
|
|
|
617
617
|
- `ALLOW_UPDATE_OPERATION`: Enable UPDATE operations (default: "false")
|
|
618
618
|
- `ALLOW_DELETE_OPERATION`: Enable DELETE operations (default: "false")
|
|
619
619
|
- `ALLOW_DDL_OPERATION`: Enable DDL operations (default: "false")
|
|
620
|
+
- `REQUIRE_DB_FILTER_FOR_INFO_SCHEMA`: **[NEW]** Require database filter for `information_schema.tables` queries (default: "false"). When enabled in single-DB mode, queries against `information_schema.tables` must include a `WHERE table_schema = '<database_name>'` clause. This prevents full table scans on databases with many tables (4500+) which can cause high CPU usage.
|
|
620
621
|
- `MYSQL_DISABLE_READ_ONLY_TRANSACTIONS`: **[NEW]** Disable read-only transaction enforcement (default: "false") ⚠️ **Security Warning:** Only enable this if you need full write capabilities and trust the LLM with your database
|
|
621
622
|
- `SCHEMA_INSERT_PERMISSIONS`: Schema-specific INSERT permissions
|
|
622
623
|
- `SCHEMA_UPDATE_PERMISSIONS`: Schema-specific UPDATE permissions
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,7 @@ export default function createMcpServer({ sessionId, config, }) {
|
|
|
113
113
|
? `socket: ${process.env.MYSQL_SOCKET_PATH}`
|
|
114
114
|
: `host: ${process.env.MYSQL_HOST || "localhost"}, port: ${process.env.MYSQL_PORT || 3306}`;
|
|
115
115
|
log("info", `Connection info: ${connectionInfo}`);
|
|
116
|
+
log("info", `isMultiDbMode: ${isMultiDbMode}, configured database: ${mcpConfig.mysql.database || 'none'}`);
|
|
116
117
|
const tablesQuery = `
|
|
117
118
|
SELECT
|
|
118
119
|
table_name as name,
|
|
@@ -172,13 +173,19 @@ export default function createMcpServer({ sessionId, config, }) {
|
|
|
172
173
|
}
|
|
173
174
|
let columnsQuery = "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?";
|
|
174
175
|
let queryParams = [tableName];
|
|
175
|
-
|
|
176
|
+
let schemaName = dbName;
|
|
177
|
+
if (!schemaName && !isMultiDbMode && mcpConfig.mysql.database) {
|
|
178
|
+
schemaName = mcpConfig.mysql.database;
|
|
179
|
+
}
|
|
176
180
|
if (!/^[a-zA-Z0-9_]+$/.test(tableName)) {
|
|
177
181
|
throw new Error(`Invalid table name: ${tableName}`);
|
|
178
182
|
}
|
|
179
183
|
if (schemaName && !/^[a-zA-Z0-9_]+$/.test(schemaName)) {
|
|
180
184
|
throw new Error(`Invalid schema name: ${schemaName}`);
|
|
181
185
|
}
|
|
186
|
+
if (!isMultiDbMode && !schemaName) {
|
|
187
|
+
throw new Error(`In single-DB mode, a schema/database must be specified. Configured database: ${mcpConfig.mysql.database || 'none'}`);
|
|
188
|
+
}
|
|
182
189
|
if (schemaName) {
|
|
183
190
|
columnsQuery += " AND table_schema = ?";
|
|
184
191
|
queryParams.push(schemaName);
|
package/dist/src/config/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export const REMOTE_SECRET_KEY = process.env.REMOTE_SECRET_KEY || "";
|
|
|
47
47
|
export const PORT = process.env.PORT || 3000;
|
|
48
48
|
const dbFromEnvOrConnString = connectionStringConfig.database || process.env.MYSQL_DB;
|
|
49
49
|
export const isMultiDbMode = !dbFromEnvOrConnString || dbFromEnvOrConnString.trim() === "";
|
|
50
|
+
export const REQUIRE_DB_FILTER_FOR_INFO_SCHEMA = process.env.REQUIRE_DB_FILTER_FOR_INFO_SCHEMA === "true";
|
|
50
51
|
export const mcpConfig = {
|
|
51
52
|
server: {
|
|
52
53
|
name: "@benborla29/mcp-server-mysql",
|
package/dist/src/db/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { performance } from "perf_hooks";
|
|
2
|
-
import { isMultiDbMode } from "./../config/index.js";
|
|
2
|
+
import { isMultiDbMode, REQUIRE_DB_FILTER_FOR_INFO_SCHEMA, } from "./../config/index.js";
|
|
3
3
|
import { isDDLAllowedForSchema, isInsertAllowedForSchema, isUpdateAllowedForSchema, isDeleteAllowedForSchema, } from "./permissions.js";
|
|
4
|
-
import { extractSchemaFromQuery, getQueryTypes } from "./utils.js";
|
|
4
|
+
import { extractSchemaFromQuery, getQueryTypes, validateInfoSchemaQuery, } from "./utils.js";
|
|
5
5
|
import * as mysql2 from "mysql2/promise";
|
|
6
6
|
import { log } from "./../utils/index.js";
|
|
7
7
|
import { mcpConfig as config, MYSQL_DISABLE_READ_ONLY_TRANSACTIONS } from "./../config/index.js";
|
|
@@ -142,6 +142,23 @@ async function executeWriteQuery(sql) {
|
|
|
142
142
|
async function executeReadOnlyQuery(sql) {
|
|
143
143
|
let connection;
|
|
144
144
|
try {
|
|
145
|
+
if (REQUIRE_DB_FILTER_FOR_INFO_SCHEMA &&
|
|
146
|
+
!isMultiDbMode &&
|
|
147
|
+
config.mysql.database) {
|
|
148
|
+
const validation = validateInfoSchemaQuery(sql, config.mysql.database);
|
|
149
|
+
if (!validation.valid) {
|
|
150
|
+
log("error", `information_schema.tables query validation failed: ${validation.reason}`);
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: `Error: ${validation.reason}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
145
162
|
const queryTypes = await getQueryTypes(sql);
|
|
146
163
|
const schema = extractSchemaFromQuery(sql);
|
|
147
164
|
const isUpdateOperation = queryTypes.some((type) => ["update"].includes(type));
|
package/dist/src/db/utils.js
CHANGED
|
@@ -31,4 +31,31 @@ async function getQueryTypes(query) {
|
|
|
31
31
|
throw new Error(`Parsing failed: ${err.message}`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
function validateInfoSchemaQuery(sql, requiredDb) {
|
|
35
|
+
if (!requiredDb) {
|
|
36
|
+
return { valid: true };
|
|
37
|
+
}
|
|
38
|
+
const normalizedSql = sql.replace(/\s+/g, " ").trim().toLowerCase();
|
|
39
|
+
const infoSchemaPatterns = [
|
|
40
|
+
/from\s+information_schema\.tables\b/,
|
|
41
|
+
/from\s+information_schema\.`tables`\b/,
|
|
42
|
+
/from\s+`information_schema`\.tables\b/,
|
|
43
|
+
/from\s+`information_schema`\.`tables`\b/,
|
|
44
|
+
];
|
|
45
|
+
const targetsInfoSchemaTables = infoSchemaPatterns.some((pattern) => pattern.test(normalizedSql));
|
|
46
|
+
if (!targetsInfoSchemaTables) {
|
|
47
|
+
return { valid: true };
|
|
48
|
+
}
|
|
49
|
+
const hasTableSchemaFilter = normalizedSql.includes("table_schema") &&
|
|
50
|
+
(normalizedSql.includes("=") ||
|
|
51
|
+
normalizedSql.includes("in") ||
|
|
52
|
+
normalizedSql.includes("?"));
|
|
53
|
+
if (hasTableSchemaFilter) {
|
|
54
|
+
return { valid: true };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
reason: `Querying information_schema.tables requires a database filter. Please add 'WHERE table_schema = '${requiredDb}'' to your query. Required database: ${requiredDb}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export { extractSchemaFromQuery, getQueryTypes, validateInfoSchemaQuery };
|
package/package.json
CHANGED