@bytebase/dbhub 0.24.0 → 1.1.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 +8 -44
- package/dist/{chunk-JO7YQPBQ.js → chunk-EPOKWSFC.js} +24 -2
- package/dist/chunk-FU2ZJE4E.js +16 -0
- package/dist/{chunk-MQR6VX5F.js → chunk-GSSLHJB6.js} +13 -4
- package/dist/chunk-NIILHXTG.js +190 -0
- package/dist/{chunk-D2BIPC7V.js → chunk-S2V6RZ3C.js} +4 -2
- package/dist/index.js +377 -97
- package/dist/{mariadb-VZMU7LYA.js → mariadb-DUDFBY5W.js} +12 -13
- package/dist/{mysql-MHIZK2UY.js → mysql-265M3KBD.js} +15 -13
- package/dist/{postgres-ZQFXCBYA.js → postgres-5ZOFYV6X.js} +63 -14
- package/dist/{registry-ZU6ZFNQ2.js → registry-JYRRNC3E.js} +1 -1
- package/dist/{sqlite-JH5CNV5E.js → sqlite-5M7SDAQZ.js} +4 -18
- package/dist/{sqlserver-LHWZ7WOP.js → sqlserver-PSAZRH7G.js} +307 -65
- package/package.json +5 -3
- package/dist/chunk-DUCNPMNW.js +0 -113
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_EXPLAIN_SQL,
|
|
5
|
+
BUILTIN_TOOL_HEALTH_CHECK,
|
|
4
6
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
7
|
ConnectorManager,
|
|
6
8
|
classifyConnectionError,
|
|
@@ -15,11 +17,14 @@ import {
|
|
|
15
17
|
resolveSourceConfigs,
|
|
16
18
|
resolveTomlConfigPath,
|
|
17
19
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-GSSLHJB6.js";
|
|
19
21
|
import {
|
|
20
22
|
allowedKeywords,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
getFirstKeyword,
|
|
24
|
+
hasEscapeHatchFunction,
|
|
25
|
+
isReadOnlySQL,
|
|
26
|
+
sqlServerDynamicSqlPattern
|
|
27
|
+
} from "./chunk-EPOKWSFC.js";
|
|
23
28
|
import {
|
|
24
29
|
loadConnectors
|
|
25
30
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -31,13 +36,14 @@ import {
|
|
|
31
36
|
getDatabaseTypeFromDSN,
|
|
32
37
|
getDefaultPortForType,
|
|
33
38
|
parseConnectionInfoFromDSN,
|
|
34
|
-
splitSQLStatements
|
|
39
|
+
splitSQLStatements,
|
|
40
|
+
stripCommentsAndStrings
|
|
35
41
|
} from "./chunk-JEZZN2YZ.js";
|
|
36
42
|
|
|
37
43
|
// src/server.ts
|
|
38
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
44
|
+
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
45
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
46
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
41
47
|
import express from "express";
|
|
42
48
|
import http from "http";
|
|
43
49
|
import path from "path";
|
|
@@ -98,6 +104,53 @@ function createToolSuccessResponse(data, meta = {}) {
|
|
|
98
104
|
};
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
// src/utils/sql-access-policy.ts
|
|
108
|
+
var CLASS_SEVERITY = {
|
|
109
|
+
read: 0,
|
|
110
|
+
dml: 1,
|
|
111
|
+
ddl: 2,
|
|
112
|
+
admin: 3,
|
|
113
|
+
unknown: 4
|
|
114
|
+
};
|
|
115
|
+
var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
|
|
116
|
+
var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
|
|
117
|
+
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
|
|
118
|
+
function isAdminStatement(stripped, connectorType) {
|
|
119
|
+
if (grantRevokePattern.test(stripped)) return true;
|
|
120
|
+
if (hasEscapeHatchFunction(stripped, connectorType)) return true;
|
|
121
|
+
return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
|
|
122
|
+
}
|
|
123
|
+
function policyFromReadonly(readonly) {
|
|
124
|
+
const write = readonly === true ? "deny" : "allow";
|
|
125
|
+
return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
|
|
126
|
+
}
|
|
127
|
+
function isReadOnlyPolicy(policy) {
|
|
128
|
+
return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
|
|
129
|
+
}
|
|
130
|
+
function classifyStatement(sql, connectorType) {
|
|
131
|
+
if (isReadOnlySQL(sql, connectorType)) {
|
|
132
|
+
return "read";
|
|
133
|
+
}
|
|
134
|
+
const stripped = stripCommentsAndStrings(sql, connectorType);
|
|
135
|
+
if (isAdminStatement(stripped, connectorType)) return "admin";
|
|
136
|
+
if (ddlPattern.test(stripped)) return "ddl";
|
|
137
|
+
if (dmlPattern.test(stripped)) return "dml";
|
|
138
|
+
return "unknown";
|
|
139
|
+
}
|
|
140
|
+
function classifySQL(sql, connectorType) {
|
|
141
|
+
let strictest = "read";
|
|
142
|
+
for (const statement of splitSQLStatements(sql, connectorType)) {
|
|
143
|
+
const cls = classifyStatement(statement, connectorType);
|
|
144
|
+
if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
|
|
145
|
+
strictest = cls;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return strictest;
|
|
149
|
+
}
|
|
150
|
+
function sqlVerdict(policy, sql, connectorType) {
|
|
151
|
+
return policy[classifySQL(sql, connectorType)];
|
|
152
|
+
}
|
|
153
|
+
|
|
101
154
|
// src/requests/store.ts
|
|
102
155
|
var RequestStore = class {
|
|
103
156
|
constructor() {
|
|
@@ -149,18 +202,30 @@ var RequestStore = class {
|
|
|
149
202
|
var requestStore = new RequestStore();
|
|
150
203
|
|
|
151
204
|
// src/utils/client-identifier.ts
|
|
152
|
-
function getClientIdentifier(
|
|
153
|
-
const userAgent =
|
|
205
|
+
function getClientIdentifier(ctx) {
|
|
206
|
+
const userAgent = ctx?.http?.req?.headers.get("user-agent");
|
|
154
207
|
if (userAgent) {
|
|
155
208
|
return userAgent;
|
|
156
209
|
}
|
|
157
210
|
return "stdio";
|
|
158
211
|
}
|
|
159
212
|
|
|
213
|
+
// src/utils/normalize-id.ts
|
|
214
|
+
function normalizeSourceId(id) {
|
|
215
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
216
|
+
}
|
|
217
|
+
|
|
160
218
|
// src/utils/tool-handler-helpers.ts
|
|
161
219
|
function getEffectiveSourceId(sourceId) {
|
|
162
220
|
return sourceId || "default";
|
|
163
221
|
}
|
|
222
|
+
function resolveTrackedToolName(sourceId, baseName) {
|
|
223
|
+
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
224
|
+
if (isSingleSource) {
|
|
225
|
+
return baseName;
|
|
226
|
+
}
|
|
227
|
+
return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
|
|
228
|
+
}
|
|
164
229
|
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
165
230
|
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
166
231
|
}
|
|
@@ -196,10 +261,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
|
|
|
196
261
|
var executeSqlSchema = {
|
|
197
262
|
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
198
263
|
};
|
|
199
|
-
|
|
200
|
-
const statements = splitSQLStatements(sql, connectorType);
|
|
201
|
-
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
202
|
-
}
|
|
264
|
+
var executeSqlInputSchema = z.object(executeSqlSchema);
|
|
203
265
|
function createExecuteSqlToolHandler(sourceId) {
|
|
204
266
|
return async (args, extra) => {
|
|
205
267
|
const { sql } = args;
|
|
@@ -214,14 +276,14 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
214
276
|
const actualSourceId = connector.getId();
|
|
215
277
|
const registry = getToolRegistry();
|
|
216
278
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
279
|
+
const policy = policyFromReadonly(toolConfig?.readonly);
|
|
280
|
+
if (sqlVerdict(policy, sql, connector.id) === "deny") {
|
|
219
281
|
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
220
282
|
success = false;
|
|
221
283
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
222
284
|
}
|
|
223
285
|
const executeOptions = {
|
|
224
|
-
readonly:
|
|
286
|
+
readonly: isReadOnlyPolicy(policy),
|
|
225
287
|
maxRows: toolConfig?.max_rows
|
|
226
288
|
};
|
|
227
289
|
result = await connector.executeSQL(sql, executeOptions);
|
|
@@ -264,6 +326,7 @@ var searchDatabaseObjectsSchema = {
|
|
|
264
326
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
265
327
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
|
|
266
328
|
};
|
|
329
|
+
var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
|
|
267
330
|
function likePatternToRegex(pattern) {
|
|
268
331
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
269
332
|
return new RegExp(`^${escaped}$`, "i");
|
|
@@ -738,15 +801,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
738
801
|
};
|
|
739
802
|
}
|
|
740
803
|
|
|
741
|
-
// src/
|
|
804
|
+
// src/tools/explain-sql.ts
|
|
742
805
|
import { z as z3 } from "zod";
|
|
806
|
+
var explainSqlSchema = {
|
|
807
|
+
sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
|
|
808
|
+
};
|
|
809
|
+
var explainSqlInputSchema = z3.object(explainSqlSchema);
|
|
810
|
+
function buildExplainStatement(connectorType, sql) {
|
|
811
|
+
if (connectorType === "sqlite") {
|
|
812
|
+
return `EXPLAIN QUERY PLAN ${sql}`;
|
|
813
|
+
}
|
|
814
|
+
return `EXPLAIN ${sql}`;
|
|
815
|
+
}
|
|
816
|
+
function validateExplainInput(sql, connectorType) {
|
|
817
|
+
const statements = splitSQLStatements(sql, connectorType);
|
|
818
|
+
if (statements.length !== 1) {
|
|
819
|
+
return "explain_sql only supports a single SQL statement";
|
|
820
|
+
}
|
|
821
|
+
const firstWord = getFirstKeyword(statements[0], connectorType);
|
|
822
|
+
if (firstWord === "explain") {
|
|
823
|
+
return "explain_sql input must not itself start with EXPLAIN";
|
|
824
|
+
}
|
|
825
|
+
if (firstWord === "analyze") {
|
|
826
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
827
|
+
}
|
|
828
|
+
const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
|
|
829
|
+
const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
|
|
830
|
+
if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
|
|
831
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
function createExplainSqlToolHandler(sourceId) {
|
|
836
|
+
return async (args, extra) => {
|
|
837
|
+
const { sql } = args;
|
|
838
|
+
const startTime = Date.now();
|
|
839
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
840
|
+
let success = true;
|
|
841
|
+
let errorMessage;
|
|
842
|
+
let result;
|
|
843
|
+
try {
|
|
844
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
845
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
846
|
+
const validationError = validateExplainInput(sql, connector.id);
|
|
847
|
+
if (validationError) {
|
|
848
|
+
success = false;
|
|
849
|
+
errorMessage = validationError;
|
|
850
|
+
return createToolErrorResponse(errorMessage, "INVALID_INPUT");
|
|
851
|
+
}
|
|
852
|
+
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
853
|
+
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
854
|
+
const responseData = {
|
|
855
|
+
rows: result.rows,
|
|
856
|
+
count: result.rowCount,
|
|
857
|
+
source_id: effectiveSourceId,
|
|
858
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
859
|
+
};
|
|
860
|
+
return createToolSuccessResponse(responseData);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
success = false;
|
|
863
|
+
errorMessage = error.message;
|
|
864
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
865
|
+
if (classified) return classified;
|
|
866
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
867
|
+
} finally {
|
|
868
|
+
trackToolRequest(
|
|
869
|
+
{
|
|
870
|
+
sourceId: effectiveSourceId,
|
|
871
|
+
toolName: resolveTrackedToolName(sourceId, "explain_sql"),
|
|
872
|
+
sql
|
|
873
|
+
},
|
|
874
|
+
startTime,
|
|
875
|
+
extra,
|
|
876
|
+
success,
|
|
877
|
+
errorMessage
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
743
882
|
|
|
744
|
-
// src/
|
|
745
|
-
|
|
746
|
-
|
|
883
|
+
// src/tools/health-check.ts
|
|
884
|
+
import { z as z4 } from "zod";
|
|
885
|
+
var healthCheckSchema = {};
|
|
886
|
+
var healthCheckInputSchema = z4.object(healthCheckSchema);
|
|
887
|
+
function createHealthCheckToolHandler(sourceId) {
|
|
888
|
+
return async (_args, extra) => {
|
|
889
|
+
const startTime = Date.now();
|
|
890
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
891
|
+
let success = true;
|
|
892
|
+
let errorMessage;
|
|
893
|
+
try {
|
|
894
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
895
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
896
|
+
if (!connector.getHealthCheck) {
|
|
897
|
+
success = false;
|
|
898
|
+
errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
|
|
899
|
+
return createToolErrorResponse(errorMessage, "UNSUPPORTED");
|
|
900
|
+
}
|
|
901
|
+
const health = await connector.getHealthCheck();
|
|
902
|
+
return createToolSuccessResponse({
|
|
903
|
+
source_id: effectiveSourceId,
|
|
904
|
+
...health.connections ? {
|
|
905
|
+
connections: {
|
|
906
|
+
total: health.connections.total,
|
|
907
|
+
active: health.connections.active,
|
|
908
|
+
idle: health.connections.idle,
|
|
909
|
+
idle_in_transaction: health.connections.idleInTransaction,
|
|
910
|
+
...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
|
|
911
|
+
max_connections: health.connections.maxConnections,
|
|
912
|
+
longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
|
|
913
|
+
longest_active_query_seconds: health.connections.longestActiveQuerySeconds
|
|
914
|
+
}
|
|
915
|
+
} : {},
|
|
916
|
+
...health.bufferCache ? {
|
|
917
|
+
buffer_cache: {
|
|
918
|
+
hit_ratio_pct: health.bufferCache.hitRatioPct,
|
|
919
|
+
blocks_hit: health.bufferCache.blocksHit,
|
|
920
|
+
blocks_read: health.bufferCache.blocksRead
|
|
921
|
+
}
|
|
922
|
+
} : {},
|
|
923
|
+
...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
|
|
924
|
+
});
|
|
925
|
+
} catch (error) {
|
|
926
|
+
success = false;
|
|
927
|
+
errorMessage = error.message;
|
|
928
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
929
|
+
if (classified) return classified;
|
|
930
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
931
|
+
} finally {
|
|
932
|
+
trackToolRequest(
|
|
933
|
+
{
|
|
934
|
+
sourceId: effectiveSourceId,
|
|
935
|
+
toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
|
|
936
|
+
sql: ""
|
|
937
|
+
},
|
|
938
|
+
startTime,
|
|
939
|
+
extra,
|
|
940
|
+
success,
|
|
941
|
+
errorMessage
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
747
945
|
}
|
|
748
946
|
|
|
749
947
|
// src/utils/tool-metadata.ts
|
|
948
|
+
import { z as z5 } from "zod";
|
|
750
949
|
function buildSourceDescriptionPrefix(description) {
|
|
751
950
|
const trimmed = description?.trim() ?? "";
|
|
752
951
|
if (!trimmed) return "";
|
|
@@ -756,17 +955,17 @@ function zodToParameters(schema) {
|
|
|
756
955
|
const parameters = [];
|
|
757
956
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
758
957
|
const description = zodType.description || "";
|
|
759
|
-
const required = !(zodType instanceof
|
|
958
|
+
const required = !(zodType instanceof z5.ZodOptional);
|
|
760
959
|
let type = "string";
|
|
761
|
-
if (zodType instanceof
|
|
960
|
+
if (zodType instanceof z5.ZodString) {
|
|
762
961
|
type = "string";
|
|
763
|
-
} else if (zodType instanceof
|
|
962
|
+
} else if (zodType instanceof z5.ZodNumber) {
|
|
764
963
|
type = "number";
|
|
765
|
-
} else if (zodType instanceof
|
|
964
|
+
} else if (zodType instanceof z5.ZodBoolean) {
|
|
766
965
|
type = "boolean";
|
|
767
|
-
} else if (zodType instanceof
|
|
966
|
+
} else if (zodType instanceof z5.ZodArray) {
|
|
768
967
|
type = "array";
|
|
769
|
-
} else if (zodType instanceof
|
|
968
|
+
} else if (zodType instanceof z5.ZodObject) {
|
|
770
969
|
type = "object";
|
|
771
970
|
}
|
|
772
971
|
parameters.push({
|
|
@@ -778,24 +977,32 @@ function zodToParameters(schema) {
|
|
|
778
977
|
}
|
|
779
978
|
return parameters;
|
|
780
979
|
}
|
|
781
|
-
function
|
|
980
|
+
function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
|
|
782
981
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
783
982
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
784
983
|
const dbType = sourceConfig.type;
|
|
785
984
|
const isSingleSource = sourceIds.length === 1;
|
|
985
|
+
const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
|
|
986
|
+
const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
|
|
987
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
988
|
+
return { dbType, isSingleSource, name, title, userDescPrefix };
|
|
989
|
+
}
|
|
990
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
991
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
992
|
+
sourceId,
|
|
993
|
+
"execute_sql",
|
|
994
|
+
"Execute SQL"
|
|
995
|
+
);
|
|
786
996
|
const registry = getToolRegistry();
|
|
787
997
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
788
998
|
const executeOptions = {
|
|
789
999
|
readonly: toolConfig?.readonly,
|
|
790
1000
|
maxRows: toolConfig?.max_rows
|
|
791
1001
|
};
|
|
792
|
-
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
793
|
-
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
794
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
795
1002
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
796
1003
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
797
1004
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
798
|
-
const isReadonly = executeOptions.readonly
|
|
1005
|
+
const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
|
|
799
1006
|
const annotations = {
|
|
800
1007
|
title,
|
|
801
1008
|
readOnlyHint: isReadonly,
|
|
@@ -808,25 +1015,62 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
808
1015
|
openWorldHint: false
|
|
809
1016
|
};
|
|
810
1017
|
return {
|
|
811
|
-
name
|
|
1018
|
+
name,
|
|
812
1019
|
description,
|
|
813
1020
|
schema: executeSqlSchema,
|
|
814
1021
|
annotations
|
|
815
1022
|
};
|
|
816
1023
|
}
|
|
817
1024
|
function getSearchObjectsMetadata(sourceId) {
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
824
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
1025
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1026
|
+
sourceId,
|
|
1027
|
+
"search_objects",
|
|
1028
|
+
"Search Database Objects"
|
|
1029
|
+
);
|
|
825
1030
|
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
1031
|
+
return { name, description, title };
|
|
1032
|
+
}
|
|
1033
|
+
function getExplainSqlMetadata(sourceId) {
|
|
1034
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1035
|
+
sourceId,
|
|
1036
|
+
"explain_sql",
|
|
1037
|
+
"Explain Query Plan"
|
|
1038
|
+
);
|
|
1039
|
+
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.`;
|
|
1040
|
+
const annotations = {
|
|
1041
|
+
title,
|
|
1042
|
+
readOnlyHint: true,
|
|
1043
|
+
destructiveHint: false,
|
|
1044
|
+
idempotentHint: true,
|
|
1045
|
+
openWorldHint: false
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
1048
|
+
name,
|
|
1049
|
+
description,
|
|
1050
|
+
schema: explainSqlSchema,
|
|
1051
|
+
annotations
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function getHealthCheckMetadata(sourceId) {
|
|
1055
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1056
|
+
sourceId,
|
|
1057
|
+
"health_check",
|
|
1058
|
+
"Health Check"
|
|
1059
|
+
);
|
|
1060
|
+
const description = isSingleSource ? `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the ${dbType} database. Read-only, independent of the source's read/write mode.` : `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the '${sourceId}' ${dbType} database. Read-only, independent of the source's read/write mode.`;
|
|
1061
|
+
const annotations = {
|
|
1062
|
+
title,
|
|
1063
|
+
readOnlyHint: true,
|
|
1064
|
+
destructiveHint: false,
|
|
1065
|
+
idempotentHint: false,
|
|
1066
|
+
// Metrics change from call to call, unlike explain_sql's plan output
|
|
1067
|
+
openWorldHint: false
|
|
1068
|
+
};
|
|
826
1069
|
return {
|
|
827
|
-
name
|
|
1070
|
+
name,
|
|
828
1071
|
description,
|
|
829
|
-
|
|
1072
|
+
schema: healthCheckSchema,
|
|
1073
|
+
annotations
|
|
830
1074
|
};
|
|
831
1075
|
}
|
|
832
1076
|
function customParamsToToolParams(params) {
|
|
@@ -900,6 +1144,16 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
900
1144
|
// search_objects is always readonly
|
|
901
1145
|
};
|
|
902
1146
|
}
|
|
1147
|
+
function buildExplainSqlTool(sourceId) {
|
|
1148
|
+
const explainMetadata = getExplainSqlMetadata(sourceId);
|
|
1149
|
+
return {
|
|
1150
|
+
name: explainMetadata.name,
|
|
1151
|
+
description: explainMetadata.description,
|
|
1152
|
+
parameters: zodToParameters(explainMetadata.schema),
|
|
1153
|
+
readonly: true
|
|
1154
|
+
// explain_sql is always readonly
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
903
1157
|
function buildCustomTool(toolConfig) {
|
|
904
1158
|
return {
|
|
905
1159
|
name: toolConfig.name,
|
|
@@ -918,6 +1172,8 @@ function getToolsForSource(sourceId) {
|
|
|
918
1172
|
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
919
1173
|
} else if (toolConfig.name === "search_objects") {
|
|
920
1174
|
return buildSearchObjectsTool(sourceId);
|
|
1175
|
+
} else if (toolConfig.name === "explain_sql") {
|
|
1176
|
+
return buildExplainSqlTool(sourceId);
|
|
921
1177
|
} else {
|
|
922
1178
|
return buildCustomTool(toolConfig);
|
|
923
1179
|
}
|
|
@@ -925,7 +1181,7 @@ function getToolsForSource(sourceId) {
|
|
|
925
1181
|
}
|
|
926
1182
|
|
|
927
1183
|
// src/tools/custom-tool-handler.ts
|
|
928
|
-
import { z as
|
|
1184
|
+
import { z as z6 } from "zod";
|
|
929
1185
|
function buildZodSchemaFromParameters(parameters) {
|
|
930
1186
|
if (!parameters || parameters.length === 0) {
|
|
931
1187
|
return {};
|
|
@@ -935,26 +1191,26 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
935
1191
|
let fieldSchema;
|
|
936
1192
|
switch (param.type) {
|
|
937
1193
|
case "string":
|
|
938
|
-
fieldSchema =
|
|
1194
|
+
fieldSchema = z6.string().describe(param.description);
|
|
939
1195
|
break;
|
|
940
1196
|
case "integer":
|
|
941
|
-
fieldSchema =
|
|
1197
|
+
fieldSchema = z6.number().int().describe(param.description);
|
|
942
1198
|
break;
|
|
943
1199
|
case "float":
|
|
944
|
-
fieldSchema =
|
|
1200
|
+
fieldSchema = z6.number().describe(param.description);
|
|
945
1201
|
break;
|
|
946
1202
|
case "boolean":
|
|
947
|
-
fieldSchema =
|
|
1203
|
+
fieldSchema = z6.boolean().describe(param.description);
|
|
948
1204
|
break;
|
|
949
1205
|
case "array":
|
|
950
|
-
fieldSchema =
|
|
1206
|
+
fieldSchema = z6.array(z6.unknown()).describe(param.description);
|
|
951
1207
|
break;
|
|
952
1208
|
default:
|
|
953
1209
|
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
954
1210
|
}
|
|
955
1211
|
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
956
1212
|
if (param.type === "string") {
|
|
957
|
-
fieldSchema =
|
|
1213
|
+
fieldSchema = z6.enum(param.allowed_values).describe(param.description);
|
|
958
1214
|
} else {
|
|
959
1215
|
fieldSchema = fieldSchema.refine(
|
|
960
1216
|
(val) => param.allowed_values.includes(val),
|
|
@@ -971,9 +1227,17 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
971
1227
|
}
|
|
972
1228
|
return schemaShape;
|
|
973
1229
|
}
|
|
1230
|
+
var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
|
|
1231
|
+
function getCustomToolInputSchema(toolConfig) {
|
|
1232
|
+
let schema = customToolInputSchemas.get(toolConfig);
|
|
1233
|
+
if (!schema) {
|
|
1234
|
+
schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
|
|
1235
|
+
customToolInputSchemas.set(toolConfig, schema);
|
|
1236
|
+
}
|
|
1237
|
+
return schema;
|
|
1238
|
+
}
|
|
974
1239
|
function createCustomToolHandler(toolConfig) {
|
|
975
|
-
const
|
|
976
|
-
const zodSchema = z4.object(zodSchemaShape);
|
|
1240
|
+
const zodSchema = getCustomToolInputSchema(toolConfig);
|
|
977
1241
|
return async (args, extra) => {
|
|
978
1242
|
const startTime = Date.now();
|
|
979
1243
|
let success = true;
|
|
@@ -983,12 +1247,12 @@ function createCustomToolHandler(toolConfig) {
|
|
|
983
1247
|
const validatedArgs = zodSchema.parse(args);
|
|
984
1248
|
await ConnectorManager.ensureConnected(toolConfig.source);
|
|
985
1249
|
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
1250
|
+
const policy = policyFromReadonly(toolConfig.readonly);
|
|
986
1251
|
const executeOptions = {
|
|
987
|
-
readonly:
|
|
1252
|
+
readonly: isReadOnlyPolicy(policy),
|
|
988
1253
|
maxRows: toolConfig.max_rows
|
|
989
1254
|
};
|
|
990
|
-
|
|
991
|
-
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
1255
|
+
if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
|
|
992
1256
|
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
993
1257
|
success = false;
|
|
994
1258
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
@@ -1013,7 +1277,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1013
1277
|
errorMessage = error.message;
|
|
1014
1278
|
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1015
1279
|
if (classified) return classified;
|
|
1016
|
-
if (error instanceof
|
|
1280
|
+
if (error instanceof z6.ZodError) {
|
|
1017
1281
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1018
1282
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
1019
1283
|
} else {
|
|
@@ -1053,6 +1317,10 @@ function registerTools(server) {
|
|
|
1053
1317
|
registerExecuteSqlTool(server, sourceId);
|
|
1054
1318
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
1055
1319
|
registerSearchObjectsTool(server, sourceId);
|
|
1320
|
+
} else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
|
|
1321
|
+
registerExplainSqlTool(server, sourceId);
|
|
1322
|
+
} else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
|
|
1323
|
+
registerHealthCheckTool(server, sourceId);
|
|
1056
1324
|
} else {
|
|
1057
1325
|
registerCustomTool(server, sourceId, toolConfig);
|
|
1058
1326
|
}
|
|
@@ -1065,7 +1333,7 @@ function registerExecuteSqlTool(server, sourceId) {
|
|
|
1065
1333
|
metadata.name,
|
|
1066
1334
|
{
|
|
1067
1335
|
description: metadata.description,
|
|
1068
|
-
inputSchema:
|
|
1336
|
+
inputSchema: executeSqlInputSchema,
|
|
1069
1337
|
annotations: metadata.annotations
|
|
1070
1338
|
},
|
|
1071
1339
|
createExecuteSqlToolHandler(sourceId)
|
|
@@ -1077,7 +1345,7 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1077
1345
|
metadata.name,
|
|
1078
1346
|
{
|
|
1079
1347
|
description: metadata.description,
|
|
1080
|
-
inputSchema:
|
|
1348
|
+
inputSchema: searchDatabaseObjectsInputSchema,
|
|
1081
1349
|
annotations: {
|
|
1082
1350
|
title: metadata.title,
|
|
1083
1351
|
readOnlyHint: true,
|
|
@@ -1089,16 +1357,39 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1089
1357
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
1090
1358
|
);
|
|
1091
1359
|
}
|
|
1360
|
+
function registerExplainSqlTool(server, sourceId) {
|
|
1361
|
+
const metadata = getExplainSqlMetadata(sourceId);
|
|
1362
|
+
server.registerTool(
|
|
1363
|
+
metadata.name,
|
|
1364
|
+
{
|
|
1365
|
+
description: metadata.description,
|
|
1366
|
+
inputSchema: explainSqlInputSchema,
|
|
1367
|
+
annotations: metadata.annotations
|
|
1368
|
+
},
|
|
1369
|
+
createExplainSqlToolHandler(sourceId)
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
function registerHealthCheckTool(server, sourceId) {
|
|
1373
|
+
const metadata = getHealthCheckMetadata(sourceId);
|
|
1374
|
+
server.registerTool(
|
|
1375
|
+
metadata.name,
|
|
1376
|
+
{
|
|
1377
|
+
description: metadata.description,
|
|
1378
|
+
inputSchema: healthCheckInputSchema,
|
|
1379
|
+
annotations: metadata.annotations
|
|
1380
|
+
},
|
|
1381
|
+
createHealthCheckToolHandler(sourceId)
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1092
1384
|
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1093
1385
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1094
1386
|
const dbType = sourceConfig.type;
|
|
1095
|
-
const isReadOnly =
|
|
1096
|
-
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
1387
|
+
const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
|
|
1097
1388
|
server.registerTool(
|
|
1098
1389
|
toolConfig.name,
|
|
1099
1390
|
{
|
|
1100
1391
|
description: toolConfig.description,
|
|
1101
|
-
inputSchema:
|
|
1392
|
+
inputSchema: getCustomToolInputSchema(toolConfig),
|
|
1102
1393
|
annotations: {
|
|
1103
1394
|
title: `${toolConfig.name} (${dbType})`,
|
|
1104
1395
|
readOnlyHint: isReadOnly,
|
|
@@ -1538,6 +1829,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
|
1538
1829
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1539
1830
|
var SERVER_NAME = "DBHub MCP Server";
|
|
1540
1831
|
var SERVER_VERSION = packageJson.version;
|
|
1832
|
+
var TOOLS_LIST_CACHE_TTL_MS = 3e5;
|
|
1833
|
+
var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
|
|
1541
1834
|
function generateBanner(version, modes = []) {
|
|
1542
1835
|
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
1543
1836
|
return `
|
|
@@ -1583,7 +1876,7 @@ See documentation for more details on configuring database connections.
|
|
|
1583
1876
|
const sources = sourceConfigsData.sources;
|
|
1584
1877
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1585
1878
|
await connectorManager.connectWithSources(sources);
|
|
1586
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1879
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-JYRRNC3E.js");
|
|
1587
1880
|
initializeToolRegistry2({
|
|
1588
1881
|
sources: sourceConfigsData.sources,
|
|
1589
1882
|
tools: sourceConfigsData.tools
|
|
@@ -1594,10 +1887,18 @@ See documentation for more details on configuring database connections.
|
|
|
1594
1887
|
initialTools: sourceConfigsData.tools
|
|
1595
1888
|
});
|
|
1596
1889
|
const createServer = () => {
|
|
1597
|
-
const server = new McpServer(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1890
|
+
const server = new McpServer(
|
|
1891
|
+
{
|
|
1892
|
+
name: SERVER_NAME,
|
|
1893
|
+
version: SERVER_VERSION
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
// `private` keeps caching client-side (no shared/proxy caches).
|
|
1897
|
+
cacheHints: {
|
|
1898
|
+
"tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1601
1902
|
registerTools(server);
|
|
1602
1903
|
return server;
|
|
1603
1904
|
};
|
|
@@ -1639,7 +1940,7 @@ See documentation for more details on configuring database connections.
|
|
|
1639
1940
|
}
|
|
1640
1941
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1641
1942
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1642
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1943
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1643
1944
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1644
1945
|
if (req.method === "OPTIONS") {
|
|
1645
1946
|
return res.sendStatus(200);
|
|
@@ -1654,29 +1955,10 @@ See documentation for more details on configuring database connections.
|
|
|
1654
1955
|
app.get("/api/sources", listSources);
|
|
1655
1956
|
app.get("/api/sources/:sourceId", getSource);
|
|
1656
1957
|
app.get("/api/requests", listRequests);
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
});
|
|
1662
|
-
});
|
|
1663
|
-
app.post("/mcp", async (req, res) => {
|
|
1664
|
-
try {
|
|
1665
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1666
|
-
sessionIdGenerator: void 0,
|
|
1667
|
-
// Disable session management for stateless mode
|
|
1668
|
-
enableJsonResponse: true
|
|
1669
|
-
// Use JSON responses (SSE not supported in stateless mode)
|
|
1670
|
-
});
|
|
1671
|
-
const server = createServer();
|
|
1672
|
-
await server.connect(transport);
|
|
1673
|
-
await transport.handleRequest(req, res, req.body);
|
|
1674
|
-
} catch (error) {
|
|
1675
|
-
console.error("Error handling request:", error);
|
|
1676
|
-
if (!res.headersSent) {
|
|
1677
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1958
|
+
const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
|
|
1959
|
+
const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
|
|
1960
|
+
app.all("/mcp", (req, res) => {
|
|
1961
|
+
void mcpNodeHandler(req, res, req.body);
|
|
1680
1962
|
});
|
|
1681
1963
|
if (process.env.NODE_ENV !== "development") {
|
|
1682
1964
|
app.get("*", (req, res) => {
|
|
@@ -1712,16 +1994,14 @@ See documentation for more details on configuring database connections.
|
|
|
1712
1994
|
console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
|
|
1713
1995
|
});
|
|
1714
1996
|
} else {
|
|
1715
|
-
const
|
|
1716
|
-
const transport = new StdioServerTransport();
|
|
1717
|
-
await server.connect(transport);
|
|
1997
|
+
const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
|
|
1718
1998
|
console.error("MCP server running on stdio");
|
|
1719
1999
|
let isShuttingDown = false;
|
|
1720
2000
|
const shutdown = async () => {
|
|
1721
2001
|
if (isShuttingDown) return;
|
|
1722
2002
|
isShuttingDown = true;
|
|
1723
2003
|
console.error("Shutting down...");
|
|
1724
|
-
await
|
|
2004
|
+
await stdioHandle.close();
|
|
1725
2005
|
await connectorManager.disconnect();
|
|
1726
2006
|
process.exit(0);
|
|
1727
2007
|
};
|
|
@@ -1737,11 +2017,11 @@ See documentation for more details on configuring database connections.
|
|
|
1737
2017
|
|
|
1738
2018
|
// src/index.ts
|
|
1739
2019
|
var connectorModules = [
|
|
1740
|
-
{ load: () => import("./postgres-
|
|
1741
|
-
{ load: () => import("./sqlserver-
|
|
1742
|
-
{ load: () => import("./sqlite-
|
|
1743
|
-
{ load: () => import("./mysql-
|
|
1744
|
-
{ load: () => import("./mariadb-
|
|
2020
|
+
{ load: () => import("./postgres-5ZOFYV6X.js"), name: "PostgreSQL", driver: "pg" },
|
|
2021
|
+
{ load: () => import("./sqlserver-PSAZRH7G.js"), name: "SQL Server", driver: "mssql" },
|
|
2022
|
+
{ load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2023
|
+
{ load: () => import("./mysql-265M3KBD.js"), name: "MySQL", driver: "mysql2" },
|
|
2024
|
+
{ load: () => import("./mariadb-DUDFBY5W.js"), name: "MariaDB", driver: "mariadb" }
|
|
1745
2025
|
];
|
|
1746
2026
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1747
2027
|
console.error("Fatal error:", error);
|