@bytebase/dbhub 0.23.0 → 1.0.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/README.md +6 -0
- package/dist/{chunk-RTB262PR.js → chunk-DUCNPMNW.js} +54 -1
- package/dist/chunk-JO7YQPBQ.js +130 -0
- package/dist/{chunk-COEDRFSK.js → chunk-MQR6VX5F.js} +54 -40
- package/dist/{chunk-BXXHWEQS.js → chunk-S2V6RZ3C.js} +14 -3
- package/dist/index.js +104 -154
- package/dist/{mariadb-SPFBL46R.js → mariadb-72C2BNCG.js} +51 -48
- package/dist/{mysql-MZWCPISB.js → mysql-CDUKJMNN.js} +52 -49
- package/dist/{postgres-BKBCGDZS.js → postgres-YAYHDXVD.js} +9 -15
- package/dist/{registry-ZME64VB2.js → registry-ZU6ZFNQ2.js} +1 -1
- package/dist/{sqlite-CQ4LCBB6.js → sqlite-5M7SDAQZ.js} +11 -19
- package/dist/{sqlserver-65AGCEJX.js → sqlserver-COKW7RIK.js} +285 -81
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
resolveSourceConfigs,
|
|
16
16
|
resolveTomlConfigPath,
|
|
17
17
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MQR6VX5F.js";
|
|
19
|
+
import {
|
|
20
|
+
allowedKeywords,
|
|
21
|
+
isReadOnlySQL,
|
|
22
|
+
sqlServerDynamicSqlPattern,
|
|
23
|
+
sqlServerPassThroughPattern
|
|
24
|
+
} from "./chunk-JO7YQPBQ.js";
|
|
19
25
|
import {
|
|
20
26
|
loadConnectors
|
|
21
27
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -32,9 +38,9 @@ import {
|
|
|
32
38
|
} from "./chunk-JEZZN2YZ.js";
|
|
33
39
|
|
|
34
40
|
// src/server.ts
|
|
35
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
41
|
+
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
42
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
43
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
38
44
|
import express from "express";
|
|
39
45
|
import http from "http";
|
|
40
46
|
import path from "path";
|
|
@@ -95,100 +101,50 @@ function createToolSuccessResponse(data, meta = {}) {
|
|
|
95
101
|
};
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
// src/utils/
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
|
|
106
|
-
sqlserver: ["select", "with", "explain"]
|
|
107
|
-
};
|
|
108
|
-
var mutatingKeywords = [
|
|
109
|
-
"insert",
|
|
110
|
-
"update",
|
|
111
|
-
"delete",
|
|
112
|
-
"drop",
|
|
113
|
-
"alter",
|
|
114
|
-
"create",
|
|
115
|
-
"truncate",
|
|
116
|
-
"merge",
|
|
117
|
-
"grant",
|
|
118
|
-
"revoke",
|
|
119
|
-
"rename"
|
|
120
|
-
];
|
|
121
|
-
var mutatingPattern = new RegExp(
|
|
122
|
-
`\\b(?:${mutatingKeywords.join("|")})\\b`,
|
|
123
|
-
"i"
|
|
124
|
-
);
|
|
125
|
-
var mutatingPatternWithReplace = new RegExp(
|
|
126
|
-
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
127
|
-
"i"
|
|
128
|
-
);
|
|
129
|
-
var mutatingPatternSqlServer = new RegExp(
|
|
130
|
-
`\\b(?:${[...mutatingKeywords, "execute", "exec", "sp_executesql", "xp_cmdshell"].join("|")})\\b`,
|
|
131
|
-
"i"
|
|
132
|
-
);
|
|
133
|
-
var mutatingPatterns = {
|
|
134
|
-
postgres: mutatingPattern,
|
|
135
|
-
mysql: mutatingPatternWithReplace,
|
|
136
|
-
mariadb: mutatingPatternWithReplace,
|
|
137
|
-
sqlite: mutatingPatternWithReplace,
|
|
138
|
-
sqlserver: mutatingPatternSqlServer
|
|
104
|
+
// src/utils/sql-access-policy.ts
|
|
105
|
+
var CLASS_SEVERITY = {
|
|
106
|
+
read: 0,
|
|
107
|
+
dml: 1,
|
|
108
|
+
ddl: 2,
|
|
109
|
+
admin: 3,
|
|
110
|
+
unknown: 4
|
|
139
111
|
};
|
|
140
|
-
var
|
|
141
|
-
var
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
]);
|
|
147
|
-
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
|
|
148
|
-
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
149
|
-
function isReadOnlySQL(sql, connectorType) {
|
|
150
|
-
return checkReadOnly(
|
|
151
|
-
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
|
|
152
|
-
connectorType
|
|
153
|
-
);
|
|
112
|
+
var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
|
|
113
|
+
var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
|
|
114
|
+
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
|
|
115
|
+
function isAdminStatement(stripped, connectorType) {
|
|
116
|
+
if (grantRevokePattern.test(stripped)) return true;
|
|
117
|
+
return connectorType === "sqlserver" && (sqlServerDynamicSqlPattern.test(stripped) || sqlServerPassThroughPattern.test(stripped));
|
|
154
118
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
if (firstWord === "explain") {
|
|
183
|
-
const m = explainAnalyzePattern.exec(cleanedSQL);
|
|
184
|
-
if (m) {
|
|
185
|
-
const afterExplain = cleanedSQL.slice(m[0].length).trim();
|
|
186
|
-
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
119
|
+
function policyFromReadonly(readonly) {
|
|
120
|
+
const write = readonly === true ? "deny" : "allow";
|
|
121
|
+
return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
|
|
122
|
+
}
|
|
123
|
+
function isReadOnlyPolicy(policy) {
|
|
124
|
+
return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
|
|
125
|
+
}
|
|
126
|
+
function classifyStatement(sql, connectorType) {
|
|
127
|
+
if (isReadOnlySQL(sql, connectorType)) {
|
|
128
|
+
return "read";
|
|
129
|
+
}
|
|
130
|
+
const stripped = stripCommentsAndStrings(sql, connectorType);
|
|
131
|
+
if (isAdminStatement(stripped, connectorType)) return "admin";
|
|
132
|
+
if (ddlPattern.test(stripped)) return "ddl";
|
|
133
|
+
if (dmlPattern.test(stripped)) return "dml";
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
|
136
|
+
function classifySQL(sql, connectorType) {
|
|
137
|
+
let strictest = "read";
|
|
138
|
+
for (const statement of splitSQLStatements(sql, connectorType)) {
|
|
139
|
+
const cls = classifyStatement(statement, connectorType);
|
|
140
|
+
if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
|
|
141
|
+
strictest = cls;
|
|
189
142
|
}
|
|
190
143
|
}
|
|
191
|
-
return
|
|
144
|
+
return strictest;
|
|
145
|
+
}
|
|
146
|
+
function sqlVerdict(policy, sql, connectorType) {
|
|
147
|
+
return policy[classifySQL(sql, connectorType)];
|
|
192
148
|
}
|
|
193
149
|
|
|
194
150
|
// src/requests/store.ts
|
|
@@ -242,8 +198,8 @@ var RequestStore = class {
|
|
|
242
198
|
var requestStore = new RequestStore();
|
|
243
199
|
|
|
244
200
|
// src/utils/client-identifier.ts
|
|
245
|
-
function getClientIdentifier(
|
|
246
|
-
const userAgent =
|
|
201
|
+
function getClientIdentifier(ctx) {
|
|
202
|
+
const userAgent = ctx?.http?.req?.headers.get("user-agent");
|
|
247
203
|
if (userAgent) {
|
|
248
204
|
return userAgent;
|
|
249
205
|
}
|
|
@@ -289,10 +245,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
|
|
|
289
245
|
var executeSqlSchema = {
|
|
290
246
|
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
291
247
|
};
|
|
292
|
-
|
|
293
|
-
const statements = splitSQLStatements(sql, connectorType);
|
|
294
|
-
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
295
|
-
}
|
|
248
|
+
var executeSqlInputSchema = z.object(executeSqlSchema);
|
|
296
249
|
function createExecuteSqlToolHandler(sourceId) {
|
|
297
250
|
return async (args, extra) => {
|
|
298
251
|
const { sql } = args;
|
|
@@ -307,14 +260,14 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
307
260
|
const actualSourceId = connector.getId();
|
|
308
261
|
const registry = getToolRegistry();
|
|
309
262
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
263
|
+
const policy = policyFromReadonly(toolConfig?.readonly);
|
|
264
|
+
if (sqlVerdict(policy, sql, connector.id) === "deny") {
|
|
312
265
|
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
313
266
|
success = false;
|
|
314
267
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
315
268
|
}
|
|
316
269
|
const executeOptions = {
|
|
317
|
-
readonly:
|
|
270
|
+
readonly: isReadOnlyPolicy(policy),
|
|
318
271
|
maxRows: toolConfig?.max_rows
|
|
319
272
|
};
|
|
320
273
|
result = await connector.executeSQL(sql, executeOptions);
|
|
@@ -357,6 +310,7 @@ var searchDatabaseObjectsSchema = {
|
|
|
357
310
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
358
311
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
|
|
359
312
|
};
|
|
313
|
+
var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
|
|
360
314
|
function likePatternToRegex(pattern) {
|
|
361
315
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
362
316
|
return new RegExp(`^${escaped}$`, "i");
|
|
@@ -377,7 +331,7 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
377
331
|
}
|
|
378
332
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
379
333
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
380
|
-
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
334
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
381
335
|
if (result.rows && result.rows.length > 0) {
|
|
382
336
|
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
383
337
|
}
|
|
@@ -510,7 +464,7 @@ async function searchViews(connector, pattern, schemaFilter, detailLevel, limit)
|
|
|
510
464
|
if (schemaFilter) {
|
|
511
465
|
schemasToSearch = [schemaFilter];
|
|
512
466
|
} else {
|
|
513
|
-
schemasToSearch = await connector
|
|
467
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
514
468
|
}
|
|
515
469
|
for (const schemaName of schemasToSearch) {
|
|
516
470
|
if (results.length >= limit) break;
|
|
@@ -888,7 +842,7 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
888
842
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
889
843
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
890
844
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
891
|
-
const isReadonly = executeOptions.readonly
|
|
845
|
+
const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
|
|
892
846
|
const annotations = {
|
|
893
847
|
title,
|
|
894
848
|
readOnlyHint: isReadonly,
|
|
@@ -1064,9 +1018,17 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
1064
1018
|
}
|
|
1065
1019
|
return schemaShape;
|
|
1066
1020
|
}
|
|
1021
|
+
var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
|
|
1022
|
+
function getCustomToolInputSchema(toolConfig) {
|
|
1023
|
+
let schema = customToolInputSchemas.get(toolConfig);
|
|
1024
|
+
if (!schema) {
|
|
1025
|
+
schema = z4.object(buildZodSchemaFromParameters(toolConfig.parameters));
|
|
1026
|
+
customToolInputSchemas.set(toolConfig, schema);
|
|
1027
|
+
}
|
|
1028
|
+
return schema;
|
|
1029
|
+
}
|
|
1067
1030
|
function createCustomToolHandler(toolConfig) {
|
|
1068
|
-
const
|
|
1069
|
-
const zodSchema = z4.object(zodSchemaShape);
|
|
1031
|
+
const zodSchema = getCustomToolInputSchema(toolConfig);
|
|
1070
1032
|
return async (args, extra) => {
|
|
1071
1033
|
const startTime = Date.now();
|
|
1072
1034
|
let success = true;
|
|
@@ -1076,12 +1038,12 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1076
1038
|
const validatedArgs = zodSchema.parse(args);
|
|
1077
1039
|
await ConnectorManager.ensureConnected(toolConfig.source);
|
|
1078
1040
|
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
1041
|
+
const policy = policyFromReadonly(toolConfig.readonly);
|
|
1079
1042
|
const executeOptions = {
|
|
1080
|
-
readonly:
|
|
1043
|
+
readonly: isReadOnlyPolicy(policy),
|
|
1081
1044
|
maxRows: toolConfig.max_rows
|
|
1082
1045
|
};
|
|
1083
|
-
|
|
1084
|
-
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
1046
|
+
if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
|
|
1085
1047
|
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
1086
1048
|
success = false;
|
|
1087
1049
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
@@ -1158,7 +1120,7 @@ function registerExecuteSqlTool(server, sourceId) {
|
|
|
1158
1120
|
metadata.name,
|
|
1159
1121
|
{
|
|
1160
1122
|
description: metadata.description,
|
|
1161
|
-
inputSchema:
|
|
1123
|
+
inputSchema: executeSqlInputSchema,
|
|
1162
1124
|
annotations: metadata.annotations
|
|
1163
1125
|
},
|
|
1164
1126
|
createExecuteSqlToolHandler(sourceId)
|
|
@@ -1170,7 +1132,7 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1170
1132
|
metadata.name,
|
|
1171
1133
|
{
|
|
1172
1134
|
description: metadata.description,
|
|
1173
|
-
inputSchema:
|
|
1135
|
+
inputSchema: searchDatabaseObjectsInputSchema,
|
|
1174
1136
|
annotations: {
|
|
1175
1137
|
title: metadata.title,
|
|
1176
1138
|
readOnlyHint: true,
|
|
@@ -1185,13 +1147,12 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1185
1147
|
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1186
1148
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1187
1149
|
const dbType = sourceConfig.type;
|
|
1188
|
-
const isReadOnly =
|
|
1189
|
-
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
1150
|
+
const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
|
|
1190
1151
|
server.registerTool(
|
|
1191
1152
|
toolConfig.name,
|
|
1192
1153
|
{
|
|
1193
1154
|
description: toolConfig.description,
|
|
1194
|
-
inputSchema:
|
|
1155
|
+
inputSchema: getCustomToolInputSchema(toolConfig),
|
|
1195
1156
|
annotations: {
|
|
1196
1157
|
title: `${toolConfig.name} (${dbType})`,
|
|
1197
1158
|
readOnlyHint: isReadOnly,
|
|
@@ -1631,6 +1592,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
|
1631
1592
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1632
1593
|
var SERVER_NAME = "DBHub MCP Server";
|
|
1633
1594
|
var SERVER_VERSION = packageJson.version;
|
|
1595
|
+
var TOOLS_LIST_CACHE_TTL_MS = 3e5;
|
|
1596
|
+
var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
|
|
1634
1597
|
function generateBanner(version, modes = []) {
|
|
1635
1598
|
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
1636
1599
|
return `
|
|
@@ -1676,7 +1639,7 @@ See documentation for more details on configuring database connections.
|
|
|
1676
1639
|
const sources = sourceConfigsData.sources;
|
|
1677
1640
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1678
1641
|
await connectorManager.connectWithSources(sources);
|
|
1679
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1642
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZU6ZFNQ2.js");
|
|
1680
1643
|
initializeToolRegistry2({
|
|
1681
1644
|
sources: sourceConfigsData.sources,
|
|
1682
1645
|
tools: sourceConfigsData.tools
|
|
@@ -1687,10 +1650,18 @@ See documentation for more details on configuring database connections.
|
|
|
1687
1650
|
initialTools: sourceConfigsData.tools
|
|
1688
1651
|
});
|
|
1689
1652
|
const createServer = () => {
|
|
1690
|
-
const server = new McpServer(
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1653
|
+
const server = new McpServer(
|
|
1654
|
+
{
|
|
1655
|
+
name: SERVER_NAME,
|
|
1656
|
+
version: SERVER_VERSION
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
// `private` keeps caching client-side (no shared/proxy caches).
|
|
1660
|
+
cacheHints: {
|
|
1661
|
+
"tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
1694
1665
|
registerTools(server);
|
|
1695
1666
|
return server;
|
|
1696
1667
|
};
|
|
@@ -1732,7 +1703,7 @@ See documentation for more details on configuring database connections.
|
|
|
1732
1703
|
}
|
|
1733
1704
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1734
1705
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1735
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1706
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1736
1707
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1737
1708
|
if (req.method === "OPTIONS") {
|
|
1738
1709
|
return res.sendStatus(200);
|
|
@@ -1747,29 +1718,10 @@ See documentation for more details on configuring database connections.
|
|
|
1747
1718
|
app.get("/api/sources", listSources);
|
|
1748
1719
|
app.get("/api/sources/:sourceId", getSource);
|
|
1749
1720
|
app.get("/api/requests", listRequests);
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
});
|
|
1755
|
-
});
|
|
1756
|
-
app.post("/mcp", async (req, res) => {
|
|
1757
|
-
try {
|
|
1758
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1759
|
-
sessionIdGenerator: void 0,
|
|
1760
|
-
// Disable session management for stateless mode
|
|
1761
|
-
enableJsonResponse: true
|
|
1762
|
-
// Use JSON responses (SSE not supported in stateless mode)
|
|
1763
|
-
});
|
|
1764
|
-
const server = createServer();
|
|
1765
|
-
await server.connect(transport);
|
|
1766
|
-
await transport.handleRequest(req, res, req.body);
|
|
1767
|
-
} catch (error) {
|
|
1768
|
-
console.error("Error handling request:", error);
|
|
1769
|
-
if (!res.headersSent) {
|
|
1770
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1721
|
+
const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
|
|
1722
|
+
const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
|
|
1723
|
+
app.all("/mcp", (req, res) => {
|
|
1724
|
+
void mcpNodeHandler(req, res, req.body);
|
|
1773
1725
|
});
|
|
1774
1726
|
if (process.env.NODE_ENV !== "development") {
|
|
1775
1727
|
app.get("*", (req, res) => {
|
|
@@ -1805,16 +1757,14 @@ See documentation for more details on configuring database connections.
|
|
|
1805
1757
|
console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
|
|
1806
1758
|
});
|
|
1807
1759
|
} else {
|
|
1808
|
-
const
|
|
1809
|
-
const transport = new StdioServerTransport();
|
|
1810
|
-
await server.connect(transport);
|
|
1760
|
+
const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
|
|
1811
1761
|
console.error("MCP server running on stdio");
|
|
1812
1762
|
let isShuttingDown = false;
|
|
1813
1763
|
const shutdown = async () => {
|
|
1814
1764
|
if (isShuttingDown) return;
|
|
1815
1765
|
isShuttingDown = true;
|
|
1816
1766
|
console.error("Shutting down...");
|
|
1817
|
-
await
|
|
1767
|
+
await stdioHandle.close();
|
|
1818
1768
|
await connectorManager.disconnect();
|
|
1819
1769
|
process.exit(0);
|
|
1820
1770
|
};
|
|
@@ -1830,11 +1780,11 @@ See documentation for more details on configuring database connections.
|
|
|
1830
1780
|
|
|
1831
1781
|
// src/index.ts
|
|
1832
1782
|
var connectorModules = [
|
|
1833
|
-
{ load: () => import("./postgres-
|
|
1834
|
-
{ load: () => import("./sqlserver-
|
|
1835
|
-
{ load: () => import("./sqlite-
|
|
1836
|
-
{ load: () => import("./mysql-
|
|
1837
|
-
{ load: () => import("./mariadb-
|
|
1783
|
+
{ load: () => import("./postgres-YAYHDXVD.js"), name: "PostgreSQL", driver: "pg" },
|
|
1784
|
+
{ load: () => import("./sqlserver-COKW7RIK.js"), name: "SQL Server", driver: "mssql" },
|
|
1785
|
+
{ load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1786
|
+
{ load: () => import("./mysql-CDUKJMNN.js"), name: "MySQL", driver: "mysql2" },
|
|
1787
|
+
{ load: () => import("./mariadb-72C2BNCG.js"), name: "MariaDB", driver: "mariadb" }
|
|
1838
1788
|
];
|
|
1839
1789
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1840
1790
|
console.error("Fatal error:", error);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MissingDatabaseError,
|
|
2
3
|
extractAffectedRows,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
isTiDBVersion,
|
|
5
|
+
parseQueryResults,
|
|
6
|
+
requireDatabaseInDSN,
|
|
7
|
+
withReadOnlyTransaction
|
|
8
|
+
} from "./chunk-DUCNPMNW.js";
|
|
5
9
|
import {
|
|
6
10
|
quoteIdentifier
|
|
7
11
|
} from "./chunk-JFWX35TB.js";
|
|
8
12
|
import {
|
|
9
|
-
SQLRowLimiter
|
|
10
|
-
|
|
13
|
+
SQLRowLimiter,
|
|
14
|
+
closeQuietly
|
|
15
|
+
} from "./chunk-S2V6RZ3C.js";
|
|
11
16
|
import {
|
|
12
17
|
ConnectorRegistry,
|
|
13
18
|
SafeURL,
|
|
@@ -32,11 +37,12 @@ Expected: ${expectedFormat}`
|
|
|
32
37
|
}
|
|
33
38
|
try {
|
|
34
39
|
const url = new SafeURL(dsn);
|
|
40
|
+
const database = url.pathname ? url.pathname.substring(1) : "";
|
|
41
|
+
requireDatabaseInDSN(database, dsn, "MariaDB");
|
|
35
42
|
const connectionConfig = {
|
|
36
43
|
host: url.hostname,
|
|
37
44
|
port: url.port ? parseInt(url.port) : 3306,
|
|
38
|
-
database
|
|
39
|
-
// Remove leading '/' if exists
|
|
45
|
+
database,
|
|
40
46
|
user: url.username,
|
|
41
47
|
password: url.password,
|
|
42
48
|
multipleStatements: true,
|
|
@@ -77,6 +83,9 @@ Expected: ${expectedFormat}`
|
|
|
77
83
|
}
|
|
78
84
|
return connectionConfig;
|
|
79
85
|
} catch (error) {
|
|
86
|
+
if (error instanceof MissingDatabaseError) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
80
89
|
throw new Error(
|
|
81
90
|
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
82
91
|
);
|
|
@@ -101,6 +110,9 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
101
110
|
this.pool = null;
|
|
102
111
|
// Source ID is set by ConnectorManager after cloning
|
|
103
112
|
this.sourceId = "default";
|
|
113
|
+
// TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
|
|
114
|
+
// unless tidb_enable_noop_functions is on. Detected once at connect time.
|
|
115
|
+
this.supportsReadOnlyTransaction = true;
|
|
104
116
|
}
|
|
105
117
|
getId() {
|
|
106
118
|
return this.sourceId;
|
|
@@ -112,8 +124,14 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
112
124
|
try {
|
|
113
125
|
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
114
126
|
this.pool = mariadb.createPool(connectionConfig);
|
|
115
|
-
await this.pool.query("SELECT
|
|
127
|
+
const rows = await this.pool.query("SELECT VERSION() AS version");
|
|
128
|
+
this.supportsReadOnlyTransaction = !isTiDBVersion(rows?.[0]?.version);
|
|
116
129
|
} catch (err) {
|
|
130
|
+
if (this.pool) {
|
|
131
|
+
const pool = this.pool;
|
|
132
|
+
this.pool = null;
|
|
133
|
+
await closeQuietly(() => pool.end());
|
|
134
|
+
}
|
|
117
135
|
console.error("Failed to connect to MariaDB database:", err);
|
|
118
136
|
throw err;
|
|
119
137
|
}
|
|
@@ -473,48 +491,33 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
473
491
|
}
|
|
474
492
|
const conn = await this.pool.getConnection();
|
|
475
493
|
try {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
results = await conn.query(processedSQL);
|
|
502
|
-
}
|
|
503
|
-
const rows = parseQueryResults(results);
|
|
504
|
-
const rowCount = extractAffectedRows(results);
|
|
505
|
-
if (options.readonly) {
|
|
506
|
-
await conn.query("COMMIT");
|
|
507
|
-
}
|
|
508
|
-
return { rows, rowCount };
|
|
509
|
-
} catch (error) {
|
|
510
|
-
if (options.readonly) {
|
|
511
|
-
try {
|
|
512
|
-
await conn.query("ROLLBACK");
|
|
513
|
-
} catch {
|
|
494
|
+
return await withReadOnlyTransaction(
|
|
495
|
+
conn,
|
|
496
|
+
options.readonly,
|
|
497
|
+
this.supportsReadOnlyTransaction,
|
|
498
|
+
async () => {
|
|
499
|
+
let processedSQL = sql;
|
|
500
|
+
if (options.maxRows) {
|
|
501
|
+
const statements = splitSQLStatements(sql, "mariadb");
|
|
502
|
+
const processedStatements = statements.map(
|
|
503
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
504
|
+
);
|
|
505
|
+
processedSQL = processedStatements.join("; ");
|
|
506
|
+
if (sql.trim().endsWith(";")) {
|
|
507
|
+
processedSQL += ";";
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
let results;
|
|
511
|
+
if (parameters && parameters.length > 0) {
|
|
512
|
+
results = await conn.query(processedSQL, parameters);
|
|
513
|
+
} else {
|
|
514
|
+
results = await conn.query(processedSQL);
|
|
515
|
+
}
|
|
516
|
+
const rows = parseQueryResults(results);
|
|
517
|
+
const rowCount = extractAffectedRows(results);
|
|
518
|
+
return { rows, rowCount };
|
|
514
519
|
}
|
|
515
|
-
|
|
516
|
-
console.error("Error executing query:", error);
|
|
517
|
-
throw error;
|
|
520
|
+
);
|
|
518
521
|
} finally {
|
|
519
522
|
conn.release();
|
|
520
523
|
}
|