dbgate-api-premium 7.2.1 → 7.2.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/package.json +6 -6
- package/src/auth/authProvider.js +3 -0
- package/src/auth/storageAuthProvider.js +37 -0
- package/src/controllers/auth.js +80 -2
- package/src/controllers/config.js +12 -3
- package/src/controllers/connections.js +10 -3
- package/src/controllers/databaseConnections.js +9 -0
- package/src/controllers/mcpAdmin.js +156 -0
- package/src/controllers/storage.js +26 -17
- package/src/controllers/storageDb.js +6 -0
- package/src/currentVersion.js +2 -2
- package/src/main.js +15 -0
- package/src/mcp.js +1719 -0
- package/src/proc/databaseConnectionProcess.js +10 -0
- package/src/storageModel.js +4 -0
- package/src/utility/envtools.js +5 -0
- package/src/utility/mcpAuth.js +137 -0
package/src/mcp.js
ADDED
|
@@ -0,0 +1,1719 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
const connections = require('./controllers/connections');
|
|
4
|
+
const serverConnections = require('./controllers/serverConnections');
|
|
5
|
+
const databaseConnections = require('./controllers/databaseConnections');
|
|
6
|
+
const { maskConnection } = require('./utility/crypting');
|
|
7
|
+
const { isProApp } = require('./utility/checkLicense');
|
|
8
|
+
const requireEngineDriver = require('./utility/requireEngineDriver');
|
|
9
|
+
const {
|
|
10
|
+
getDatabasePermissionRole,
|
|
11
|
+
getTablePermissionRole,
|
|
12
|
+
getTablePermissionRoleLevelIndex,
|
|
13
|
+
} = require('./utility/hasPermission');
|
|
14
|
+
const { getTokenSecret, getTokenLifetime } = require('./auth/authCommon');
|
|
15
|
+
const { markTokenAsLoggedIn } = require('./utility/loginchecker');
|
|
16
|
+
const getExpressPath = require('./utility/getExpressPath');
|
|
17
|
+
const {
|
|
18
|
+
readMcpConfig,
|
|
19
|
+
getPublicBaseUrl,
|
|
20
|
+
getMcpResourceUrl,
|
|
21
|
+
safeTokenHashEquals,
|
|
22
|
+
} = require('./utility/mcpAuth');
|
|
23
|
+
|
|
24
|
+
const protocolVersion = '2025-06-18';
|
|
25
|
+
const mcpRoleId = -4;
|
|
26
|
+
const objectTypes = ['tables', 'collections', 'views', 'procedures', 'functions', 'triggers'];
|
|
27
|
+
const defaultDataLimit = 100;
|
|
28
|
+
const maxDataLimit = 1000;
|
|
29
|
+
const oauthAuthorizationCodes = new Map();
|
|
30
|
+
const oauthCodeLifetimeMs = 5 * 60 * 1000;
|
|
31
|
+
const filterOperators = ['eq', 'ne', '<', '<=', '>', '>=', 'contains', 'startsWith', 'endsWith', 'in', 'isNull', 'isNotNull'];
|
|
32
|
+
const filterSyntaxDescription = [
|
|
33
|
+
'Optional JSON filter. Valid shapes:',
|
|
34
|
+
`Comparison: {"column":"<columnName>","op":"${filterOperators
|
|
35
|
+
.filter(op => !['in', 'isNull', 'isNotNull'].includes(op))
|
|
36
|
+
.join('|')}","value":<value>}.`,
|
|
37
|
+
'IN: {"column":"<columnName>","op":"in","values":[<value1>,<value2>]}.',
|
|
38
|
+
'Null check: {"column":"<columnName>","op":"isNull"} or {"column":"<columnName>","op":"isNotNull"}.',
|
|
39
|
+
'Compound: {"and":[<filter>,<filter>]} or {"or":[<filter>,<filter>]}. Compound filters may be nested.',
|
|
40
|
+
'Examples: {"column":"status","op":"eq","value":"open"}; {"and":[{"column":"status","op":"eq","value":"open"},{"column":"created_at","op":">=","value":"2026-01-01"}]}.',
|
|
41
|
+
].join(' ');
|
|
42
|
+
|
|
43
|
+
function base64Url(buffer) {
|
|
44
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sha256Base64Url(value) {
|
|
48
|
+
return base64Url(crypto.createHash('sha256').update(value).digest());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createOAuthId() {
|
|
52
|
+
return base64Url(crypto.randomBytes(32));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getOAuthAuthorizationServerUrl(req) {
|
|
56
|
+
return getPublicBaseUrl(req);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getOAuthEndpoint(req, path) {
|
|
60
|
+
return `${getPublicBaseUrl(req)}${getExpressPath(path)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getOAuthProtectedResourceMetadataUrl(req) {
|
|
64
|
+
return getOAuthEndpoint(req, '/.well-known/oauth-protected-resource');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function requireOAuthMode(res) {
|
|
68
|
+
let config;
|
|
69
|
+
try {
|
|
70
|
+
config = await readMcpConfig();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
res.status(500).json({ apiErrorMessage: 'DBGM-00000 Could not load MCP authentication configuration' });
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (!config.enabled) {
|
|
76
|
+
res.status(403).json({ apiErrorMessage: 'DBGM-00000 MCP server is disabled' });
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (config.authMode != 'oauth') {
|
|
80
|
+
res.status(404).json({ apiErrorMessage: 'DBGM-00000 MCP OAuth is not enabled' });
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isSafeRedirectUri(redirectUri) {
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(redirectUri);
|
|
89
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertValidOAuthRedirect(clientId, redirectUri, config) {
|
|
96
|
+
if (
|
|
97
|
+
!config.oauthClientId ||
|
|
98
|
+
!config.oauthClientSecretHash ||
|
|
99
|
+
clientId !== config.oauthClientId ||
|
|
100
|
+
!isSafeRedirectUri(redirectUri)
|
|
101
|
+
) {
|
|
102
|
+
throw new Error('DBGM-00000 Invalid OAuth client');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getOAuthClientCredentials(req, params) {
|
|
107
|
+
const authorization = req.headers?.authorization;
|
|
108
|
+
if (authorization?.startsWith('Basic ')) {
|
|
109
|
+
try {
|
|
110
|
+
const decoded = Buffer.from(authorization.slice(6), 'base64').toString('utf8');
|
|
111
|
+
const separator = decoded.indexOf(':');
|
|
112
|
+
if (separator >= 0) {
|
|
113
|
+
return {
|
|
114
|
+
clientId: decodeURIComponent(decoded.slice(0, separator)),
|
|
115
|
+
clientSecret: decodeURIComponent(decoded.slice(separator + 1)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
clientId: params.client_id,
|
|
124
|
+
clientSecret: params.client_secret,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function pruneOAuthAuthorizationCodes() {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [code, data] of oauthAuthorizationCodes.entries()) {
|
|
131
|
+
if (data.expiresAt <= now) {
|
|
132
|
+
oauthAuthorizationCodes.delete(code);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createConnectionDatabaseInputSchema() {
|
|
138
|
+
return {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
conid: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'DbGate connection ID.',
|
|
144
|
+
},
|
|
145
|
+
database: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Database name.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
required: ['conid', 'database'],
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const listConnectionsTool = {
|
|
156
|
+
name: 'list_connections',
|
|
157
|
+
title: 'List DbGate Connections',
|
|
158
|
+
description: 'List DbGate connections available to the current user.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {},
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
},
|
|
164
|
+
outputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
connections: {
|
|
168
|
+
type: 'array',
|
|
169
|
+
items: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ['connections'],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const listDatabasesTool = {
|
|
179
|
+
name: 'list_databases',
|
|
180
|
+
title: 'List DbGate Databases',
|
|
181
|
+
description: 'List databases for a DbGate connection.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
conid: {
|
|
186
|
+
type: 'string',
|
|
187
|
+
description: 'DbGate connection ID.',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ['conid'],
|
|
191
|
+
additionalProperties: false,
|
|
192
|
+
},
|
|
193
|
+
outputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
databases: {
|
|
197
|
+
type: 'array',
|
|
198
|
+
items: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required: ['databases'],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const listObjectsTool = {
|
|
208
|
+
name: 'list_objects',
|
|
209
|
+
title: 'List DbGate Database Objects',
|
|
210
|
+
description: 'List all tables, collections, views, procedures, functions, and triggers in a DbGate database.',
|
|
211
|
+
inputSchema: createConnectionDatabaseInputSchema(),
|
|
212
|
+
outputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: objectTypes.reduce(
|
|
215
|
+
(res, objectType) => ({
|
|
216
|
+
...res,
|
|
217
|
+
[objectType]: {
|
|
218
|
+
type: 'array',
|
|
219
|
+
items: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
{}
|
|
225
|
+
),
|
|
226
|
+
required: objectTypes,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getTableInfoTool = {
|
|
231
|
+
name: 'get_table_info',
|
|
232
|
+
title: 'Get DbGate Table Info',
|
|
233
|
+
description: 'Get details and columns for one DbGate table.',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
conid: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'DbGate connection ID.',
|
|
240
|
+
},
|
|
241
|
+
database: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
description: 'Database name.',
|
|
244
|
+
},
|
|
245
|
+
schemaName: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: 'Table schema name. Optional when the table name is unique.',
|
|
248
|
+
},
|
|
249
|
+
pureName: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'Table name without schema.',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['conid', 'database', 'pureName'],
|
|
255
|
+
additionalProperties: false,
|
|
256
|
+
},
|
|
257
|
+
outputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
table: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required: ['table'],
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getTableDataTool = {
|
|
269
|
+
name: 'get_table_data',
|
|
270
|
+
title: 'Get DbGate Table Or Collection Data',
|
|
271
|
+
description:
|
|
272
|
+
'Get rows/documents from one DbGate table or NoSQL collection with optional JSON filter, selected fields, ordering, and pagination.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
conid: {
|
|
277
|
+
type: 'string',
|
|
278
|
+
description: 'DbGate connection ID.',
|
|
279
|
+
},
|
|
280
|
+
database: {
|
|
281
|
+
type: 'string',
|
|
282
|
+
description: 'Database name.',
|
|
283
|
+
},
|
|
284
|
+
schemaName: {
|
|
285
|
+
type: 'string',
|
|
286
|
+
description: 'Table schema name. Optional when the table/collection name is unique.',
|
|
287
|
+
},
|
|
288
|
+
pureName: {
|
|
289
|
+
type: 'string',
|
|
290
|
+
description: 'Table or collection name without schema.',
|
|
291
|
+
},
|
|
292
|
+
columns: {
|
|
293
|
+
type: 'array',
|
|
294
|
+
items: {
|
|
295
|
+
type: 'string',
|
|
296
|
+
},
|
|
297
|
+
description:
|
|
298
|
+
'Optional list of column/field names to return. Defaults to all table columns for SQL tables and all returned document fields for collections.',
|
|
299
|
+
},
|
|
300
|
+
filter: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
description: filterSyntaxDescription,
|
|
303
|
+
properties: {
|
|
304
|
+
column: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'Column name for comparison, in, and null-check filters.',
|
|
307
|
+
},
|
|
308
|
+
op: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
enum: filterOperators,
|
|
311
|
+
description: 'Filter operator. Defaults to eq when omitted for a single-column filter.',
|
|
312
|
+
},
|
|
313
|
+
value: {
|
|
314
|
+
description: 'Value for comparison operators except in, isNull, and isNotNull.',
|
|
315
|
+
},
|
|
316
|
+
values: {
|
|
317
|
+
type: 'array',
|
|
318
|
+
description: 'Values array required when op is in.',
|
|
319
|
+
},
|
|
320
|
+
and: {
|
|
321
|
+
type: 'array',
|
|
322
|
+
items: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
},
|
|
325
|
+
description: 'Array of nested filters combined with AND.',
|
|
326
|
+
},
|
|
327
|
+
or: {
|
|
328
|
+
type: 'array',
|
|
329
|
+
items: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
},
|
|
332
|
+
description: 'Array of nested filters combined with OR.',
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
orderBy: {
|
|
337
|
+
type: 'array',
|
|
338
|
+
items: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
column: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
},
|
|
344
|
+
direction: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
enum: ['asc', 'desc', 'ASC', 'DESC'],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
required: ['column'],
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
limit: {
|
|
354
|
+
type: 'number',
|
|
355
|
+
description: `Maximum rows to return. Defaults to ${defaultDataLimit}, capped at ${maxDataLimit}.`,
|
|
356
|
+
},
|
|
357
|
+
offset: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Rows to skip. Defaults to 0.',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
required: ['conid', 'database', 'pureName'],
|
|
363
|
+
additionalProperties: false,
|
|
364
|
+
},
|
|
365
|
+
outputSchema: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
rows: {
|
|
369
|
+
type: 'array',
|
|
370
|
+
items: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
columns: {
|
|
375
|
+
type: 'array',
|
|
376
|
+
items: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
limit: {
|
|
381
|
+
type: 'number',
|
|
382
|
+
},
|
|
383
|
+
offset: {
|
|
384
|
+
type: 'number',
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
required: ['rows', 'columns', 'limit', 'offset'],
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const executeQueryTool = {
|
|
392
|
+
name: 'execute_query',
|
|
393
|
+
title: 'Execute DbGate Query',
|
|
394
|
+
description:
|
|
395
|
+
'Execute a SQL query against a DbGate database. This tool is available only with a Team Premium license.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
conid: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
description: 'DbGate connection ID.',
|
|
402
|
+
},
|
|
403
|
+
database: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'Database name.',
|
|
406
|
+
},
|
|
407
|
+
sql: {
|
|
408
|
+
type: 'string',
|
|
409
|
+
description: 'SQL query to execute.',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ['conid', 'database', 'sql'],
|
|
413
|
+
additionalProperties: false,
|
|
414
|
+
},
|
|
415
|
+
outputSchema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
response: {
|
|
419
|
+
description: 'Raw query response returned by DbGate.',
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
required: ['response'],
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
function createTableMutationInputProperties(pureNameDescription = 'Table name.') {
|
|
427
|
+
return {
|
|
428
|
+
conid: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
description: 'DbGate connection ID.',
|
|
431
|
+
},
|
|
432
|
+
database: {
|
|
433
|
+
type: 'string',
|
|
434
|
+
description: 'Database name.',
|
|
435
|
+
},
|
|
436
|
+
schemaName: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'Optional table schema name.',
|
|
439
|
+
},
|
|
440
|
+
pureName: {
|
|
441
|
+
type: 'string',
|
|
442
|
+
description: pureNameDescription,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const insertRowsTool = {
|
|
448
|
+
name: 'insert_rows',
|
|
449
|
+
title: 'Insert DbGate Rows',
|
|
450
|
+
description:
|
|
451
|
+
'Insert rows into a DbGate SQL table or documents into a MongoDB collection. Always pass rows as an array; for one row/document, pass an array with one object. SQL table rows must use the same columns; MongoDB collection documents may use different fields. This tool is available only with a Team Premium license.',
|
|
452
|
+
inputSchema: {
|
|
453
|
+
type: 'object',
|
|
454
|
+
properties: {
|
|
455
|
+
...createTableMutationInputProperties('Table or collection name.'),
|
|
456
|
+
rows: {
|
|
457
|
+
type: 'array',
|
|
458
|
+
description:
|
|
459
|
+
'Rows or MongoDB documents to insert, as [{"columnName": value}] or [{"fieldName": value}]. Must be a non-empty array. SQL table rows must use the same columns; MongoDB collection documents may use different fields.',
|
|
460
|
+
items: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
additionalProperties: true,
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
required: ['conid', 'database', 'pureName', 'rows'],
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
},
|
|
469
|
+
outputSchema: {
|
|
470
|
+
type: 'object',
|
|
471
|
+
properties: {
|
|
472
|
+
response: {
|
|
473
|
+
description: 'Raw save response returned by DbGate.',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
required: ['response'],
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const updateRowsTool = {
|
|
481
|
+
name: 'update_rows',
|
|
482
|
+
title: 'Update DbGate Rows',
|
|
483
|
+
description:
|
|
484
|
+
'Update rows in a DbGate SQL table or documents in a MongoDB collection. SQL tables use an equality WHERE condition object; MongoDB collections use the condition object as the MongoDB update filter. This tool is available only with a Team Premium license.',
|
|
485
|
+
inputSchema: {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
...createTableMutationInputProperties('Table or collection name.'),
|
|
489
|
+
fields: {
|
|
490
|
+
type: 'object',
|
|
491
|
+
description: 'Column values or MongoDB document fields to update, as {"columnName": value} or {"fieldName": value}.',
|
|
492
|
+
additionalProperties: true,
|
|
493
|
+
},
|
|
494
|
+
condition: {
|
|
495
|
+
type: 'object',
|
|
496
|
+
description:
|
|
497
|
+
'Required condition. For SQL tables, pass an equality WHERE condition as {"columnName": value}; all provided columns are combined with AND. For MongoDB collections, pass the MongoDB update filter object, for example {"_id": {"$oid": "..."}} or {"email": "a@example.com"}.',
|
|
498
|
+
additionalProperties: true,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
required: ['conid', 'database', 'pureName', 'fields', 'condition'],
|
|
502
|
+
additionalProperties: false,
|
|
503
|
+
},
|
|
504
|
+
outputSchema: {
|
|
505
|
+
type: 'object',
|
|
506
|
+
properties: {
|
|
507
|
+
response: {
|
|
508
|
+
description: 'Raw save response returned by DbGate.',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
required: ['response'],
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function createObjectListTool(objectType) {
|
|
516
|
+
return {
|
|
517
|
+
name: `list_${objectType}`,
|
|
518
|
+
title: `List DbGate ${objectType}`,
|
|
519
|
+
description: `List ${objectType} in a DbGate database.`,
|
|
520
|
+
inputSchema: createConnectionDatabaseInputSchema(),
|
|
521
|
+
outputSchema: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
properties: {
|
|
524
|
+
[objectType]: {
|
|
525
|
+
type: 'array',
|
|
526
|
+
items: {
|
|
527
|
+
type: 'object',
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
required: [objectType],
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const objectListTools = objectTypes.map(createObjectListTool);
|
|
537
|
+
const tools = [
|
|
538
|
+
listConnectionsTool,
|
|
539
|
+
listDatabasesTool,
|
|
540
|
+
listObjectsTool,
|
|
541
|
+
getTableInfoTool,
|
|
542
|
+
getTableDataTool,
|
|
543
|
+
...objectListTools,
|
|
544
|
+
];
|
|
545
|
+
const teamPremiumTools = [executeQueryTool, insertRowsTool, updateRowsTool];
|
|
546
|
+
|
|
547
|
+
function hasTeamPremiumLicense() {
|
|
548
|
+
return isProApp();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function requireTeamPremiumLicense() {
|
|
552
|
+
if (!hasTeamPremiumLicense()) {
|
|
553
|
+
throw new Error('DBGM-00000 Tool requires Team Premium license');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getAvailableTools() {
|
|
558
|
+
return [...tools, ...(hasTeamPremiumLicense() ? teamPremiumTools : [])];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function jsonRpcResult(id, result) {
|
|
562
|
+
return {
|
|
563
|
+
jsonrpc: '2.0',
|
|
564
|
+
id,
|
|
565
|
+
result,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function jsonRpcError(id, code, message) {
|
|
570
|
+
return {
|
|
571
|
+
jsonrpc: '2.0',
|
|
572
|
+
id,
|
|
573
|
+
error: {
|
|
574
|
+
code,
|
|
575
|
+
message: withDbgmCode(message),
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function withDbgmCode(message) {
|
|
581
|
+
if (typeof message !== 'string' || message.startsWith('DBGM-')) {
|
|
582
|
+
return message;
|
|
583
|
+
}
|
|
584
|
+
return `DBGM-00000 ${message}`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isMcpEnabledValue(value) {
|
|
588
|
+
return [true, 1, '1', 'true'].includes(value);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function getMcpConnectionId(connection) {
|
|
592
|
+
return connection?._id ?? connection?.conid;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function loadMcpRolePermissions(req) {
|
|
596
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (!req.__mcpRolePermissions) {
|
|
600
|
+
const { storageReadRolePermissions } = require('./controllers/storageDb');
|
|
601
|
+
req.__mcpRolePermissions = (await storageReadRolePermissions(mcpRoleId)) ?? [];
|
|
602
|
+
}
|
|
603
|
+
return req.__mcpRolePermissions;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function loadMcpDatabasePermissions(req) {
|
|
607
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
if (!req.__mcpDatabasePermissions) {
|
|
611
|
+
const { readComplexRolePermissions, resolvePermissionConnectionIds } = require('./controllers/storageDb');
|
|
612
|
+
req.__mcpDatabasePermissions = await resolvePermissionConnectionIds(
|
|
613
|
+
(await readComplexRolePermissions(mcpRoleId, 'role_databases')) ?? []
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return req.__mcpDatabasePermissions;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function loadMcpTablePermissions(req) {
|
|
620
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
if (!req.__mcpTablePermissions) {
|
|
624
|
+
const { readComplexRolePermissions, resolvePermissionConnectionIds } = require('./controllers/storageDb');
|
|
625
|
+
req.__mcpTablePermissions = await resolvePermissionConnectionIds(
|
|
626
|
+
(await readComplexRolePermissions(mcpRoleId, 'role_tables')) ?? []
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return req.__mcpTablePermissions;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function mcpHasPermission(permission, req) {
|
|
633
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
const permissions = (await loadMcpRolePermissions(req)) ?? [];
|
|
637
|
+
if (permissions.includes(`~${permission}`) || permissions.includes('~*')) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
return permissions.includes(permission) || permissions.includes('*');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function isMcpConnectionEnabled(connection, req) {
|
|
644
|
+
if (!connection) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
if (process.env.STORAGE_DATABASE) {
|
|
648
|
+
if (await mcpHasPermission('all-connections', req)) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
const { storageCheckMcpConnectionAccess } = require('./controllers/storageDb');
|
|
652
|
+
return storageCheckMcpConnectionAccess(req, getMcpConnectionId(connection));
|
|
653
|
+
}
|
|
654
|
+
return isMcpEnabledValue(connection.mcpEnabled);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function filterMcpConnections(list, req) {
|
|
658
|
+
const enabled = await Promise.all(list.map(connection => isMcpConnectionEnabled(connection, req)));
|
|
659
|
+
return list.filter((connection, index) => enabled[index]);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function requireMcpConnection(conid, req) {
|
|
663
|
+
const list = (await connections.list({}, req)) ?? [];
|
|
664
|
+
const connection = list.find(item => getMcpConnectionId(item) === conid);
|
|
665
|
+
if (!(await isMcpConnectionEnabled(connection, req))) {
|
|
666
|
+
throw new Error(`DBGM-00000 Connection is not enabled for MCP: ${conid}`);
|
|
667
|
+
}
|
|
668
|
+
return connection;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function getMcpDatabaseRole(conid, database, req) {
|
|
672
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
673
|
+
return 'run_script';
|
|
674
|
+
}
|
|
675
|
+
return getDatabasePermissionRole(conid, database, await loadMcpDatabasePermissions(req));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function getDatabaseRoleLevelIndex(roleName) {
|
|
679
|
+
if (!roleName) return 6;
|
|
680
|
+
if (roleName == 'run_script') return 5;
|
|
681
|
+
if (roleName == 'write_data') return 4;
|
|
682
|
+
if (roleName == 'read_content') return 3;
|
|
683
|
+
if (roleName == 'view') return 2;
|
|
684
|
+
if (roleName == 'deny') return 1;
|
|
685
|
+
return 6;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function requireMcpDatabase(conid, database, req, requiredRole = 'view') {
|
|
689
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (await mcpHasPermission('all-databases', req)) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const role = await getMcpDatabaseRole(conid, database, req);
|
|
696
|
+
if (getDatabaseRoleLevelIndex(role) < getDatabaseRoleLevelIndex(requiredRole)) {
|
|
697
|
+
throw new Error(`DBGM-00000 Database is not enabled for MCP: ${database}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function filterMcpDatabases(conid, databases, req) {
|
|
702
|
+
if (!process.env.STORAGE_DATABASE || (await mcpHasPermission('all-databases', req))) {
|
|
703
|
+
return databases;
|
|
704
|
+
}
|
|
705
|
+
const databasePermissions = await loadMcpDatabasePermissions(req);
|
|
706
|
+
return databases.filter(database => getDatabasePermissionRole(conid, database.name, databasePermissions) != 'deny');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function getMcpTableRole(conid, database, objectTypeField, schemaName, pureName, req) {
|
|
710
|
+
const databasePermissionRole = (await mcpHasPermission('all-databases', req))
|
|
711
|
+
? 'deny'
|
|
712
|
+
: await getMcpDatabaseRole(conid, database, req);
|
|
713
|
+
return getTablePermissionRole(
|
|
714
|
+
conid,
|
|
715
|
+
database,
|
|
716
|
+
objectTypeField,
|
|
717
|
+
schemaName,
|
|
718
|
+
pureName,
|
|
719
|
+
await loadMcpTablePermissions(req),
|
|
720
|
+
databasePermissionRole
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function requireMcpObject(conid, database, objectTypeField, schemaName, pureName, req, requiredRole = 'read') {
|
|
725
|
+
if (!process.env.STORAGE_DATABASE) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (await mcpHasPermission('all-tables', req)) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const role = await getMcpTableRole(conid, database, objectTypeField, schemaName, pureName, req);
|
|
732
|
+
if (getTablePermissionRoleLevelIndex(role) < getTablePermissionRoleLevelIndex(requiredRole)) {
|
|
733
|
+
throw new Error(`DBGM-00000 Object is not enabled for MCP: ${schemaName ? `${schemaName}.` : ''}${pureName}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function filterMcpStructure(conid, database, structure, req) {
|
|
738
|
+
if (!process.env.STORAGE_DATABASE || (await mcpHasPermission('all-tables', req))) {
|
|
739
|
+
return structure;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function filterObjects(list = [], objectTypeField) {
|
|
743
|
+
const result = [];
|
|
744
|
+
for (const item of list) {
|
|
745
|
+
const role = await getMcpTableRole(conid, database, objectTypeField, item.schemaName, item.pureName, req);
|
|
746
|
+
if (role != 'deny') {
|
|
747
|
+
result.push({
|
|
748
|
+
...item,
|
|
749
|
+
tablePermissionRole: role,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
...structure,
|
|
758
|
+
tables: await filterObjects(structure.tables, 'tables'),
|
|
759
|
+
collections: await filterObjects(structure.collections, 'collections'),
|
|
760
|
+
views: await filterObjects(structure.views, 'views'),
|
|
761
|
+
procedures: await filterObjects(structure.procedures, 'procedures'),
|
|
762
|
+
functions: await filterObjects(structure.functions, 'functions'),
|
|
763
|
+
triggers: await filterObjects(structure.triggers, 'triggers'),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function callListConnectionsTool(req) {
|
|
768
|
+
const list = (await connections.list({}, req)) ?? [];
|
|
769
|
+
const maskedConnections = (await filterMcpConnections(list, req)).map(connection => maskConnection(connection));
|
|
770
|
+
|
|
771
|
+
return createToolResult({
|
|
772
|
+
connections: maskedConnections,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function callListDatabasesTool(args, req) {
|
|
777
|
+
const conid = args?.conid;
|
|
778
|
+
if (!conid) {
|
|
779
|
+
throw new Error('DBGM-00000 Missing required argument: conid');
|
|
780
|
+
}
|
|
781
|
+
await requireMcpConnection(conid, req);
|
|
782
|
+
|
|
783
|
+
const databases = (await serverConnections.listDatabases({ conid }, req)) ?? [];
|
|
784
|
+
|
|
785
|
+
return createToolResult({
|
|
786
|
+
databases: await filterMcpDatabases(conid, databases, req),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function requireConnectionDatabaseArgs(args) {
|
|
791
|
+
if (!args?.conid) {
|
|
792
|
+
throw new Error('DBGM-00000 Missing required argument: conid');
|
|
793
|
+
}
|
|
794
|
+
if (!args?.database) {
|
|
795
|
+
throw new Error('DBGM-00000 Missing required argument: database');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function requireTableInfoArgs(args) {
|
|
800
|
+
requireConnectionDatabaseArgs(args);
|
|
801
|
+
if (!args?.pureName) {
|
|
802
|
+
throw new Error('DBGM-00000 Missing required argument: pureName');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function requireExecuteQueryArgs(args) {
|
|
807
|
+
requireConnectionDatabaseArgs(args);
|
|
808
|
+
if (!args?.sql || typeof args.sql !== 'string') {
|
|
809
|
+
throw new Error('DBGM-00000 Missing required argument: sql');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function isPlainObject(value) {
|
|
814
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function requireNonEmptyObject(value, argName) {
|
|
818
|
+
if (!isPlainObject(value) || Object.keys(value).length === 0) {
|
|
819
|
+
throw new Error(`DBGM-00000 ${argName} must be a non-empty object`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function createMutationDumper(conid) {
|
|
824
|
+
const connection = await connections.getCore({ conid });
|
|
825
|
+
const driver = requireEngineDriver(connection);
|
|
826
|
+
if (!driver?.createDumper) {
|
|
827
|
+
throw new Error('DBGM-00000 SQL mutation tools are not supported by this connection driver');
|
|
828
|
+
}
|
|
829
|
+
return driver.createDumper();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function getTableName(args) {
|
|
833
|
+
return {
|
|
834
|
+
pureName: args.pureName,
|
|
835
|
+
...(args.schemaName == null ? {} : { schemaName: args.schemaName }),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function getInsertRowsColumns(rows) {
|
|
840
|
+
const columns = Object.keys(rows[0]);
|
|
841
|
+
for (const row of rows) {
|
|
842
|
+
const rowColumns = Object.keys(row);
|
|
843
|
+
if (
|
|
844
|
+
rowColumns.length !== columns.length ||
|
|
845
|
+
rowColumns.some(column => !columns.includes(column)) ||
|
|
846
|
+
columns.some(column => !rowColumns.includes(column))
|
|
847
|
+
) {
|
|
848
|
+
throw new Error('DBGM-00000 All rows must use the same columns');
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return columns;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function buildInsertRowsSql(dmp, args) {
|
|
855
|
+
const columns = getInsertRowsColumns(args.rows);
|
|
856
|
+
dmp.put('^insert ^into %f (%,i) ^values ', getTableName(args), columns);
|
|
857
|
+
dmp.putCollection(', ', args.rows, row => dmp.put('(%,v)', columns.map(column => row[column])));
|
|
858
|
+
return dmp.s;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildCollectionInsertChangeSet(collection, rows) {
|
|
862
|
+
const collectionName = getCollectionName(collection);
|
|
863
|
+
return {
|
|
864
|
+
inserts: rows.map((fields, index) => ({
|
|
865
|
+
...collectionName,
|
|
866
|
+
insertId: index + 1,
|
|
867
|
+
fields,
|
|
868
|
+
})),
|
|
869
|
+
updates: [],
|
|
870
|
+
deletes: [],
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function getCollectionName(collection) {
|
|
875
|
+
const collectionName = {
|
|
876
|
+
pureName: collection.pureName,
|
|
877
|
+
};
|
|
878
|
+
if (collection.schemaName != null) {
|
|
879
|
+
collectionName.schemaName = collection.schemaName;
|
|
880
|
+
}
|
|
881
|
+
return collectionName;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function buildCollectionUpdateChangeSet(collection, args) {
|
|
885
|
+
return {
|
|
886
|
+
inserts: [],
|
|
887
|
+
updates: [
|
|
888
|
+
{
|
|
889
|
+
...getCollectionName(collection),
|
|
890
|
+
fields: args.fields,
|
|
891
|
+
condition: args.condition,
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
deletes: [],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function buildUpdateSql(dmp, args) {
|
|
899
|
+
const fields = Object.keys(args.fields);
|
|
900
|
+
const conditionColumns = Object.keys(args.condition);
|
|
901
|
+
dmp.put('^update %f ^set ', getTableName(args));
|
|
902
|
+
dmp.putCollection(', ', fields, column => dmp.put('%i = %v', column, args.fields[column]));
|
|
903
|
+
dmp.put(' ^where ');
|
|
904
|
+
dmp.putCollection(' ^and ', conditionColumns, column => {
|
|
905
|
+
if (args.condition[column] === null) {
|
|
906
|
+
dmp.put('%i ^is ^null', column);
|
|
907
|
+
} else {
|
|
908
|
+
dmp.put('%i = %v', column, args.condition[column]);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
return dmp.s;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function requireInsertRowsArgs(args) {
|
|
915
|
+
requireTableInfoArgs(args);
|
|
916
|
+
if (!Array.isArray(args?.rows) || args.rows.length === 0) {
|
|
917
|
+
throw new Error('DBGM-00000 rows must be a non-empty array');
|
|
918
|
+
}
|
|
919
|
+
args.rows.forEach((row, index) => requireNonEmptyObject(row, `rows[${index}]`));
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function requireUpdateRowsArgs(args) {
|
|
923
|
+
requireTableInfoArgs(args);
|
|
924
|
+
requireNonEmptyObject(args?.fields, 'fields');
|
|
925
|
+
requireNonEmptyObject(args?.condition, 'condition');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function normalizeLimit(limit) {
|
|
929
|
+
if (limit == null) {
|
|
930
|
+
return defaultDataLimit;
|
|
931
|
+
}
|
|
932
|
+
const parsed = Number(limit);
|
|
933
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
934
|
+
throw new Error('DBGM-00000 limit must be a positive integer');
|
|
935
|
+
}
|
|
936
|
+
return Math.min(parsed, maxDataLimit);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function normalizeOffset(offset) {
|
|
940
|
+
if (offset == null) {
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
const parsed = Number(offset);
|
|
944
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
945
|
+
throw new Error('DBGM-00000 offset must be a non-negative integer');
|
|
946
|
+
}
|
|
947
|
+
return parsed;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function createToolResult(data) {
|
|
951
|
+
return {
|
|
952
|
+
content: [
|
|
953
|
+
{
|
|
954
|
+
type: 'text',
|
|
955
|
+
text: JSON.stringify(data, null, 2),
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
structuredContent: data,
|
|
959
|
+
isError: false,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function pickObjectNames(array = []) {
|
|
964
|
+
return array
|
|
965
|
+
.map(({ pureName, schemaName }) => ({ pureName, schemaName }))
|
|
966
|
+
.sort((a, b) => `${a.schemaName}.${a.pureName}`.localeCompare(`${b.schemaName}.${b.pureName}`));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function loadDatabaseObjects(args, req) {
|
|
970
|
+
requireConnectionDatabaseArgs(args);
|
|
971
|
+
await requireMcpConnection(args.conid, req);
|
|
972
|
+
await requireMcpDatabase(args.conid, args.database, req);
|
|
973
|
+
const structure = await loadMcpStructure(args, req);
|
|
974
|
+
|
|
975
|
+
return objectTypes.reduce(
|
|
976
|
+
(res, objectType) => ({
|
|
977
|
+
...res,
|
|
978
|
+
[objectType]: pickObjectNames(structure[objectType]),
|
|
979
|
+
}),
|
|
980
|
+
{}
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function callListObjectsTool(args, req) {
|
|
985
|
+
return createToolResult(await loadDatabaseObjects(args, req));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function callObjectListTool(objectType, args, req) {
|
|
989
|
+
const objects = await loadDatabaseObjects(args, req);
|
|
990
|
+
return createToolResult({
|
|
991
|
+
[objectType]: objects[objectType],
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function callGetTableInfoTool(args, req) {
|
|
996
|
+
const { table, tables } = await loadTableInfo(args, req);
|
|
997
|
+
const dependencies = getTableDependencies(table, tables);
|
|
998
|
+
|
|
999
|
+
return createToolResult({
|
|
1000
|
+
table: {
|
|
1001
|
+
...table,
|
|
1002
|
+
columns: table.columns ?? [],
|
|
1003
|
+
foreignKeys: table.foreignKeys ?? [],
|
|
1004
|
+
dependencies,
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function loadTableInfo(args, req) {
|
|
1010
|
+
requireTableInfoArgs(args);
|
|
1011
|
+
await requireMcpConnection(args.conid, req);
|
|
1012
|
+
await requireMcpDatabase(args.conid, args.database, req);
|
|
1013
|
+
const structure = await loadMcpStructure(args, req);
|
|
1014
|
+
const tables = structure.tables ?? [];
|
|
1015
|
+
const matchingTables = findMatchingNamedObject(tables, args);
|
|
1016
|
+
|
|
1017
|
+
if (matchingTables.length === 0) {
|
|
1018
|
+
throw new Error(`DBGM-00000 Table not found: ${args.schemaName ? `${args.schemaName}.` : ''}${args.pureName}`);
|
|
1019
|
+
}
|
|
1020
|
+
if (matchingTables.length > 1) {
|
|
1021
|
+
throw new Error(`DBGM-00000 Ambiguous table name, provide schemaName: ${args.pureName}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
table: matchingTables[0],
|
|
1026
|
+
tables,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async function loadMcpStructure(args, req) {
|
|
1031
|
+
let structure = process.env.STORAGE_DATABASE
|
|
1032
|
+
? await databaseConnections.structure({ conid: args.conid, database: args.database }, req)
|
|
1033
|
+
: (await databaseConnections.ensureOpened(args.conid, args.database))?.structure;
|
|
1034
|
+
const hasLoadedObjects = objectTypes.some(objectType => (structure?.[objectType]?.length ?? 0) > 0);
|
|
1035
|
+
if (!hasLoadedObjects) {
|
|
1036
|
+
structure = await databaseConnections.ensureStructureLoaded(args.conid, args.database);
|
|
1037
|
+
}
|
|
1038
|
+
return filterMcpStructure(args.conid, args.database, structure ?? {}, req);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function getTableDependencies(table, tables) {
|
|
1042
|
+
return tables
|
|
1043
|
+
.flatMap(item => item.foreignKeys ?? [])
|
|
1044
|
+
.filter(item => item.refSchemaName === table.schemaName && item.refTableName === table.pureName);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function getColumnInfoByName(table) {
|
|
1048
|
+
return new Map((table.columns ?? []).map(column => [column.columnName, column]));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function requireColumn(columnInfoByName, columnName) {
|
|
1052
|
+
const column = columnInfoByName.get(columnName);
|
|
1053
|
+
if (!column) {
|
|
1054
|
+
throw new Error(`DBGM-00000 Unknown column: ${columnName}`);
|
|
1055
|
+
}
|
|
1056
|
+
return column;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function createDynamicColumn(columnName) {
|
|
1060
|
+
if (!columnName || typeof columnName !== 'string') {
|
|
1061
|
+
throw new Error('DBGM-00000 Filter column is required');
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
columnName,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function createColumnExpression(column) {
|
|
1069
|
+
return {
|
|
1070
|
+
exprType: 'column',
|
|
1071
|
+
columnName: column.columnName,
|
|
1072
|
+
source: {
|
|
1073
|
+
alias: 'basetbl',
|
|
1074
|
+
},
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function createValueExpression(value, column) {
|
|
1079
|
+
return {
|
|
1080
|
+
exprType: 'value',
|
|
1081
|
+
value,
|
|
1082
|
+
dataType: column?.dataType,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function buildFilterCondition(filter, columnInfoByName) {
|
|
1087
|
+
if (!filter) {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
if (Array.isArray(filter)) {
|
|
1091
|
+
throw new Error('DBGM-00000 filter must be an object');
|
|
1092
|
+
}
|
|
1093
|
+
if (filter.and != null || filter.or != null) {
|
|
1094
|
+
const key = filter.and != null ? 'and' : 'or';
|
|
1095
|
+
if (!Array.isArray(filter[key])) {
|
|
1096
|
+
throw new Error(`DBGM-00000 ${key} filter must be an array`);
|
|
1097
|
+
}
|
|
1098
|
+
const conditions = filter[key].map(item => buildFilterCondition(item, columnInfoByName)).filter(Boolean);
|
|
1099
|
+
if (conditions.length === 0) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
conditionType: key,
|
|
1104
|
+
conditions,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const column = requireColumn(columnInfoByName, filter.column);
|
|
1109
|
+
const expr = createColumnExpression(column);
|
|
1110
|
+
const op = filter.op ?? 'eq';
|
|
1111
|
+
|
|
1112
|
+
switch (op) {
|
|
1113
|
+
case 'eq':
|
|
1114
|
+
return {
|
|
1115
|
+
conditionType: 'binary',
|
|
1116
|
+
operator: '=',
|
|
1117
|
+
left: expr,
|
|
1118
|
+
right: createValueExpression(filter.value, column),
|
|
1119
|
+
};
|
|
1120
|
+
case 'ne':
|
|
1121
|
+
return {
|
|
1122
|
+
conditionType: 'binary',
|
|
1123
|
+
operator: '<>',
|
|
1124
|
+
left: expr,
|
|
1125
|
+
right: createValueExpression(filter.value, column),
|
|
1126
|
+
};
|
|
1127
|
+
case '<':
|
|
1128
|
+
case '<=':
|
|
1129
|
+
case '>':
|
|
1130
|
+
case '>=':
|
|
1131
|
+
return {
|
|
1132
|
+
conditionType: 'binary',
|
|
1133
|
+
operator: op,
|
|
1134
|
+
left: expr,
|
|
1135
|
+
right: createValueExpression(filter.value, column),
|
|
1136
|
+
};
|
|
1137
|
+
case 'contains':
|
|
1138
|
+
return {
|
|
1139
|
+
conditionType: 'like',
|
|
1140
|
+
left: expr,
|
|
1141
|
+
right: createValueExpression(`%${filter.value ?? ''}%`, column),
|
|
1142
|
+
};
|
|
1143
|
+
case 'startsWith':
|
|
1144
|
+
return {
|
|
1145
|
+
conditionType: 'like',
|
|
1146
|
+
left: expr,
|
|
1147
|
+
right: createValueExpression(`${filter.value ?? ''}%`, column),
|
|
1148
|
+
};
|
|
1149
|
+
case 'endsWith':
|
|
1150
|
+
return {
|
|
1151
|
+
conditionType: 'like',
|
|
1152
|
+
left: expr,
|
|
1153
|
+
right: createValueExpression(`%${filter.value ?? ''}`, column),
|
|
1154
|
+
};
|
|
1155
|
+
case 'in':
|
|
1156
|
+
if (!Array.isArray(filter.values)) {
|
|
1157
|
+
throw new Error('DBGM-00000 in filter requires values array');
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
conditionType: 'in',
|
|
1161
|
+
expr,
|
|
1162
|
+
values: filter.values,
|
|
1163
|
+
};
|
|
1164
|
+
case 'isNull':
|
|
1165
|
+
return {
|
|
1166
|
+
conditionType: 'isNull',
|
|
1167
|
+
expr,
|
|
1168
|
+
};
|
|
1169
|
+
case 'isNotNull':
|
|
1170
|
+
return {
|
|
1171
|
+
conditionType: 'isNotNull',
|
|
1172
|
+
expr,
|
|
1173
|
+
};
|
|
1174
|
+
default:
|
|
1175
|
+
throw new Error(`DBGM-00000 Unsupported filter operator: ${op}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function buildCollectionFilterCondition(filter) {
|
|
1180
|
+
const dynamicColumns = {
|
|
1181
|
+
get(columnName) {
|
|
1182
|
+
return createDynamicColumn(columnName);
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
return buildFilterCondition(filter, dynamicColumns);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function buildTableDataSelect(args, table) {
|
|
1189
|
+
const columnInfoByName = getColumnInfoByName(table);
|
|
1190
|
+
const selectedColumnNames = args.columns?.length ? args.columns : (table.columns ?? []).map(column => column.columnName);
|
|
1191
|
+
if (!Array.isArray(selectedColumnNames) || selectedColumnNames.length === 0) {
|
|
1192
|
+
throw new Error('DBGM-00000 No columns selected');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const selectedColumns = selectedColumnNames.map(columnName => requireColumn(columnInfoByName, columnName));
|
|
1196
|
+
const limit = normalizeLimit(args.limit);
|
|
1197
|
+
const offset = normalizeOffset(args.offset);
|
|
1198
|
+
const select = {
|
|
1199
|
+
commandType: 'select',
|
|
1200
|
+
from: {
|
|
1201
|
+
name: {
|
|
1202
|
+
schemaName: table.schemaName,
|
|
1203
|
+
pureName: table.pureName,
|
|
1204
|
+
},
|
|
1205
|
+
alias: 'basetbl',
|
|
1206
|
+
},
|
|
1207
|
+
columns: selectedColumns.map(column => ({
|
|
1208
|
+
...createColumnExpression(column),
|
|
1209
|
+
alias: column.columnName,
|
|
1210
|
+
})),
|
|
1211
|
+
range: {
|
|
1212
|
+
limit,
|
|
1213
|
+
offset,
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const where = buildFilterCondition(args.filter, columnInfoByName);
|
|
1218
|
+
if (where) {
|
|
1219
|
+
select.where = where;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (args.orderBy != null) {
|
|
1223
|
+
if (!Array.isArray(args.orderBy)) {
|
|
1224
|
+
throw new Error('DBGM-00000 orderBy must be an array');
|
|
1225
|
+
}
|
|
1226
|
+
select.orderBy = args.orderBy.map(item => {
|
|
1227
|
+
const column = requireColumn(columnInfoByName, item.column);
|
|
1228
|
+
return {
|
|
1229
|
+
...createColumnExpression(column),
|
|
1230
|
+
direction: String(item.direction ?? 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC',
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return {
|
|
1236
|
+
select,
|
|
1237
|
+
columns: selectedColumns.map(column => column.columnName),
|
|
1238
|
+
limit,
|
|
1239
|
+
offset,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function findMatchingNamedObject(list = [], args) {
|
|
1244
|
+
return list.filter(item => {
|
|
1245
|
+
if (item.pureName !== args.pureName) {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
return args.schemaName == null || item.schemaName === args.schemaName;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async function loadDataTarget(args, req, options = {}) {
|
|
1253
|
+
requireTableInfoArgs(args);
|
|
1254
|
+
await requireMcpConnection(args.conid, req);
|
|
1255
|
+
await requireMcpDatabase(args.conid, args.database, req);
|
|
1256
|
+
const structure = await loadMcpStructure(args, req);
|
|
1257
|
+
const matchingTables = findMatchingNamedObject(structure.tables, args);
|
|
1258
|
+
const matchingCollections = findMatchingNamedObject(structure.collections, args);
|
|
1259
|
+
|
|
1260
|
+
if (options.preferCollections) {
|
|
1261
|
+
if (matchingCollections.length > 1) {
|
|
1262
|
+
throw new Error(`DBGM-00000 Ambiguous collection name, provide schemaName: ${args.pureName}`);
|
|
1263
|
+
}
|
|
1264
|
+
if (matchingCollections.length === 1) {
|
|
1265
|
+
return {
|
|
1266
|
+
objectTypeField: 'collections',
|
|
1267
|
+
collection: matchingCollections[0],
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (matchingTables.length > 1) {
|
|
1273
|
+
throw new Error(`DBGM-00000 Ambiguous table name, provide schemaName: ${args.pureName}`);
|
|
1274
|
+
}
|
|
1275
|
+
if (matchingTables.length === 1) {
|
|
1276
|
+
return {
|
|
1277
|
+
objectTypeField: 'tables',
|
|
1278
|
+
table: matchingTables[0],
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (matchingCollections.length > 1) {
|
|
1283
|
+
throw new Error(`DBGM-00000 Ambiguous collection name, provide schemaName: ${args.pureName}`);
|
|
1284
|
+
}
|
|
1285
|
+
if (matchingCollections.length === 1) {
|
|
1286
|
+
return {
|
|
1287
|
+
objectTypeField: 'collections',
|
|
1288
|
+
collection: matchingCollections[0],
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
throw new Error(`DBGM-00000 Table or collection not found: ${args.schemaName ? `${args.schemaName}.` : ''}${args.pureName}`);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function inferRowColumns(rows) {
|
|
1296
|
+
const columns = [];
|
|
1297
|
+
const used = new Set();
|
|
1298
|
+
for (const row of rows) {
|
|
1299
|
+
for (const column of Object.keys(row ?? {})) {
|
|
1300
|
+
if (!used.has(column)) {
|
|
1301
|
+
columns.push(column);
|
|
1302
|
+
used.add(column);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return columns;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function projectRows(rows, columns) {
|
|
1310
|
+
if (!columns?.length) {
|
|
1311
|
+
return rows;
|
|
1312
|
+
}
|
|
1313
|
+
return rows.map(row =>
|
|
1314
|
+
columns.reduce(
|
|
1315
|
+
(res, column) => ({
|
|
1316
|
+
...res,
|
|
1317
|
+
[column]: row?.[column],
|
|
1318
|
+
}),
|
|
1319
|
+
{}
|
|
1320
|
+
)
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function buildCollectionDataOptions(args) {
|
|
1325
|
+
const limit = normalizeLimit(args.limit);
|
|
1326
|
+
const offset = normalizeOffset(args.offset);
|
|
1327
|
+
const options = {
|
|
1328
|
+
pureName: args.pureName,
|
|
1329
|
+
condition: buildCollectionFilterCondition(args.filter),
|
|
1330
|
+
skip: offset,
|
|
1331
|
+
limit,
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
if (args.orderBy != null) {
|
|
1335
|
+
if (!Array.isArray(args.orderBy)) {
|
|
1336
|
+
throw new Error('DBGM-00000 orderBy must be an array');
|
|
1337
|
+
}
|
|
1338
|
+
options.sort = args.orderBy.map(item => ({
|
|
1339
|
+
columnName: item.column,
|
|
1340
|
+
direction: String(item.direction ?? 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC',
|
|
1341
|
+
}));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return {
|
|
1345
|
+
options,
|
|
1346
|
+
limit,
|
|
1347
|
+
offset,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
async function callGetTableDataTool(args, req) {
|
|
1352
|
+
const target = await loadDataTarget(args, req);
|
|
1353
|
+
if (target.objectTypeField === 'collections') {
|
|
1354
|
+
const { options, limit, offset } = buildCollectionDataOptions(args);
|
|
1355
|
+
const response = await databaseConnections.collectionData(
|
|
1356
|
+
{
|
|
1357
|
+
conid: args.conid,
|
|
1358
|
+
database: args.database,
|
|
1359
|
+
options,
|
|
1360
|
+
auditLogSessionGroup: 'mcp.getTableData',
|
|
1361
|
+
},
|
|
1362
|
+
req
|
|
1363
|
+
);
|
|
1364
|
+
const rows = projectRows(response?.rows ?? [], args.columns);
|
|
1365
|
+
return createToolResult({
|
|
1366
|
+
rows,
|
|
1367
|
+
columns: args.columns?.length ? args.columns : inferRowColumns(rows),
|
|
1368
|
+
limit,
|
|
1369
|
+
offset,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const { table } = target;
|
|
1374
|
+
const { select, columns, limit, offset } = buildTableDataSelect(args, table);
|
|
1375
|
+
const response = await databaseConnections.sqlSelect(
|
|
1376
|
+
{
|
|
1377
|
+
conid: args.conid,
|
|
1378
|
+
database: args.database,
|
|
1379
|
+
select,
|
|
1380
|
+
auditLogSessionGroup: 'mcp.getTableData',
|
|
1381
|
+
},
|
|
1382
|
+
req
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
return createToolResult({
|
|
1386
|
+
rows: response?.rows ?? [],
|
|
1387
|
+
columns,
|
|
1388
|
+
limit,
|
|
1389
|
+
offset,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
async function callExecuteQueryTool(args, req) {
|
|
1394
|
+
requireTeamPremiumLicense();
|
|
1395
|
+
requireExecuteQueryArgs(args);
|
|
1396
|
+
await requireMcpConnection(args.conid, req);
|
|
1397
|
+
await requireMcpDatabase(args.conid, args.database, req, 'run_script');
|
|
1398
|
+
const response = await databaseConnections.queryData(
|
|
1399
|
+
{
|
|
1400
|
+
conid: args.conid,
|
|
1401
|
+
database: args.database,
|
|
1402
|
+
sql: args.sql,
|
|
1403
|
+
},
|
|
1404
|
+
req
|
|
1405
|
+
);
|
|
1406
|
+
return createToolResult({
|
|
1407
|
+
response: response ?? null,
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function callInsertRowsTool(args, req) {
|
|
1412
|
+
requireTeamPremiumLicense();
|
|
1413
|
+
requireInsertRowsArgs(args);
|
|
1414
|
+
const target = await loadDataTarget(args, req, { preferCollections: true });
|
|
1415
|
+
if (target.objectTypeField === 'collections') {
|
|
1416
|
+
await requireMcpObject(args.conid, args.database, 'collections', target.collection.schemaName, target.collection.pureName, req, 'create_update_delete');
|
|
1417
|
+
const response = await databaseConnections.updateCollection(
|
|
1418
|
+
{
|
|
1419
|
+
conid: args.conid,
|
|
1420
|
+
database: args.database,
|
|
1421
|
+
changeSet: buildCollectionInsertChangeSet(target.collection, args.rows),
|
|
1422
|
+
},
|
|
1423
|
+
req
|
|
1424
|
+
);
|
|
1425
|
+
return createToolResult({
|
|
1426
|
+
response: response ?? null,
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
await requireMcpObject(args.conid, args.database, 'tables', target.table.schemaName, target.table.pureName, req, 'create_update_delete');
|
|
1431
|
+
const sql = buildInsertRowsSql(await createMutationDumper(args.conid), {
|
|
1432
|
+
...args,
|
|
1433
|
+
schemaName: target.table.schemaName,
|
|
1434
|
+
pureName: target.table.pureName,
|
|
1435
|
+
});
|
|
1436
|
+
const response = await databaseConnections.queryData(
|
|
1437
|
+
{
|
|
1438
|
+
conid: args.conid,
|
|
1439
|
+
database: args.database,
|
|
1440
|
+
sql,
|
|
1441
|
+
},
|
|
1442
|
+
req
|
|
1443
|
+
);
|
|
1444
|
+
return createToolResult({
|
|
1445
|
+
response: response ?? null,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
async function callUpdateRowsTool(args, req) {
|
|
1450
|
+
requireTeamPremiumLicense();
|
|
1451
|
+
requireUpdateRowsArgs(args);
|
|
1452
|
+
const target = await loadDataTarget(args, req, { preferCollections: true });
|
|
1453
|
+
if (target.objectTypeField === 'collections') {
|
|
1454
|
+
await requireMcpObject(args.conid, args.database, 'collections', target.collection.schemaName, target.collection.pureName, req, 'update_only');
|
|
1455
|
+
const response = await databaseConnections.updateCollection(
|
|
1456
|
+
{
|
|
1457
|
+
conid: args.conid,
|
|
1458
|
+
database: args.database,
|
|
1459
|
+
changeSet: buildCollectionUpdateChangeSet(target.collection, args),
|
|
1460
|
+
},
|
|
1461
|
+
req
|
|
1462
|
+
);
|
|
1463
|
+
return createToolResult({
|
|
1464
|
+
response: response ?? null,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
await requireMcpObject(args.conid, args.database, 'tables', target.table.schemaName, target.table.pureName, req, 'update_only');
|
|
1469
|
+
const sql = buildUpdateSql(await createMutationDumper(args.conid), {
|
|
1470
|
+
...args,
|
|
1471
|
+
schemaName: target.table.schemaName,
|
|
1472
|
+
pureName: target.table.pureName,
|
|
1473
|
+
});
|
|
1474
|
+
const response = await databaseConnections.queryData(
|
|
1475
|
+
{
|
|
1476
|
+
conid: args.conid,
|
|
1477
|
+
database: args.database,
|
|
1478
|
+
sql,
|
|
1479
|
+
},
|
|
1480
|
+
req
|
|
1481
|
+
);
|
|
1482
|
+
return createToolResult({
|
|
1483
|
+
response: response ?? null,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function getToolHandler(name) {
|
|
1488
|
+
const handlers = {
|
|
1489
|
+
[listConnectionsTool.name]: (args, req) => callListConnectionsTool(req),
|
|
1490
|
+
[listDatabasesTool.name]: callListDatabasesTool,
|
|
1491
|
+
[listObjectsTool.name]: callListObjectsTool,
|
|
1492
|
+
[getTableInfoTool.name]: callGetTableInfoTool,
|
|
1493
|
+
[getTableDataTool.name]: callGetTableDataTool,
|
|
1494
|
+
[executeQueryTool.name]: callExecuteQueryTool,
|
|
1495
|
+
[insertRowsTool.name]: callInsertRowsTool,
|
|
1496
|
+
[updateRowsTool.name]: callUpdateRowsTool,
|
|
1497
|
+
...Object.fromEntries(objectTypes.map(objectType => [`list_${objectType}`, (args, req) => callObjectListTool(objectType, args, req)])),
|
|
1498
|
+
};
|
|
1499
|
+
return handlers[name];
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async function handleJsonRpcRequest(body, req, res) {
|
|
1503
|
+
const id = body?.id ?? null;
|
|
1504
|
+
|
|
1505
|
+
if (body?.jsonrpc !== '2.0' || typeof body?.method !== 'string') {
|
|
1506
|
+
return res.status(400).json(jsonRpcError(id, -32600, 'Invalid Request'));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (body.id == null) {
|
|
1510
|
+
if (body.method === 'notifications/initialized') {
|
|
1511
|
+
return res.status(204).end();
|
|
1512
|
+
}
|
|
1513
|
+
return res.status(400).json(jsonRpcError(null, -32600, 'Invalid Request'));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
switch (body.method) {
|
|
1517
|
+
case 'initialize':
|
|
1518
|
+
return res.json(
|
|
1519
|
+
jsonRpcResult(id, {
|
|
1520
|
+
protocolVersion,
|
|
1521
|
+
capabilities: {
|
|
1522
|
+
tools: {
|
|
1523
|
+
listChanged: false,
|
|
1524
|
+
},
|
|
1525
|
+
},
|
|
1526
|
+
serverInfo: {
|
|
1527
|
+
name: 'dbgate',
|
|
1528
|
+
title: 'DbGate',
|
|
1529
|
+
version: '1.0.0',
|
|
1530
|
+
},
|
|
1531
|
+
})
|
|
1532
|
+
);
|
|
1533
|
+
case 'tools/list':
|
|
1534
|
+
return res.json(
|
|
1535
|
+
jsonRpcResult(id, {
|
|
1536
|
+
tools: getAvailableTools(),
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
case 'tools/call':
|
|
1540
|
+
const toolHandler = getToolHandler(body.params?.name);
|
|
1541
|
+
if (!toolHandler) {
|
|
1542
|
+
return res.json(jsonRpcError(id, -32602, `Unknown tool: ${body.params?.name ?? ''}`));
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
return res.json(jsonRpcResult(id, await toolHandler(body.params?.arguments, req)));
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
return res.json(jsonRpcError(id, -32602, err.message));
|
|
1548
|
+
}
|
|
1549
|
+
default:
|
|
1550
|
+
return res.json(jsonRpcError(id, -32601, `Method not found: ${body.method}`));
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async function handleMcpRequest(req, res) {
|
|
1555
|
+
if (req.body?.jsonrpc === '2.0') {
|
|
1556
|
+
return handleJsonRpcRequest(req.body, req, res);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return res.status(400).json({ apiErrorMessage: 'DBGM-00000 Unsupported MCP message' });
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function handleOAuthProtectedResourceMetadata(req, res) {
|
|
1563
|
+
if (!(await requireOAuthMode(res))) return;
|
|
1564
|
+
try {
|
|
1565
|
+
return res.json({
|
|
1566
|
+
resource: getMcpResourceUrl(req),
|
|
1567
|
+
authorization_servers: [getOAuthAuthorizationServerUrl(req)],
|
|
1568
|
+
bearer_methods_supported: ['header'],
|
|
1569
|
+
});
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
return res.status(500).json({ apiErrorMessage: withDbgmCode(error.message) });
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async function handleOAuthAuthorizationServerMetadata(req, res) {
|
|
1576
|
+
if (!(await requireOAuthMode(res))) return;
|
|
1577
|
+
try {
|
|
1578
|
+
return res.json({
|
|
1579
|
+
issuer: getOAuthAuthorizationServerUrl(req),
|
|
1580
|
+
authorization_endpoint: getOAuthEndpoint(req, '/authorize'),
|
|
1581
|
+
token_endpoint: getOAuthEndpoint(req, '/token'),
|
|
1582
|
+
response_types_supported: ['code'],
|
|
1583
|
+
grant_types_supported: ['authorization_code'],
|
|
1584
|
+
code_challenge_methods_supported: ['S256'],
|
|
1585
|
+
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
|
|
1586
|
+
scopes_supported: ['mcp'],
|
|
1587
|
+
});
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
return res.status(500).json({ apiErrorMessage: withDbgmCode(error.message) });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
async function handleOAuthAuthorize(req, res) {
|
|
1594
|
+
if (!(await requireOAuthMode(res))) return;
|
|
1595
|
+
const config = await readMcpConfig();
|
|
1596
|
+
const params = {
|
|
1597
|
+
...req.query,
|
|
1598
|
+
...req.body,
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
try {
|
|
1602
|
+
if (params.response_type !== 'code') {
|
|
1603
|
+
throw new Error('DBGM-00000 Unsupported response_type');
|
|
1604
|
+
}
|
|
1605
|
+
if (!params.redirect_uri || !params.client_id) {
|
|
1606
|
+
throw new Error('DBGM-00000 Missing OAuth client parameters');
|
|
1607
|
+
}
|
|
1608
|
+
assertValidOAuthRedirect(params.client_id, params.redirect_uri, config);
|
|
1609
|
+
if (!params.code_challenge || params.code_challenge_method !== 'S256') {
|
|
1610
|
+
throw new Error('DBGM-00000 PKCE S256 code challenge is required');
|
|
1611
|
+
}
|
|
1612
|
+
const resource = getMcpResourceUrl(req);
|
|
1613
|
+
if (params.resource && params.resource !== resource) {
|
|
1614
|
+
throw new Error('DBGM-00000 Invalid OAuth resource');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
pruneOAuthAuthorizationCodes();
|
|
1618
|
+
const code = createOAuthId();
|
|
1619
|
+
oauthAuthorizationCodes.set(code, {
|
|
1620
|
+
clientId: params.client_id,
|
|
1621
|
+
redirectUri: params.redirect_uri,
|
|
1622
|
+
codeChallenge: params.code_challenge,
|
|
1623
|
+
resource,
|
|
1624
|
+
expiresAt: Date.now() + oauthCodeLifetimeMs,
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
const redirectUrl = new URL(params.redirect_uri);
|
|
1628
|
+
redirectUrl.searchParams.set('code', code);
|
|
1629
|
+
if (params.state) {
|
|
1630
|
+
redirectUrl.searchParams.set('state', params.state);
|
|
1631
|
+
}
|
|
1632
|
+
return res.redirect(redirectUrl.toString());
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
return res.status(400).json({
|
|
1635
|
+
error: 'invalid_request',
|
|
1636
|
+
error_description: withDbgmCode(err.message),
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function handleOAuthToken(req, res) {
|
|
1642
|
+
if (!(await requireOAuthMode(res))) return;
|
|
1643
|
+
const config = await readMcpConfig();
|
|
1644
|
+
const params = {
|
|
1645
|
+
...req.body,
|
|
1646
|
+
...req.query,
|
|
1647
|
+
};
|
|
1648
|
+
if (params.grant_type !== 'authorization_code') {
|
|
1649
|
+
return res.status(400).json({
|
|
1650
|
+
error: 'unsupported_grant_type',
|
|
1651
|
+
error_description: 'DBGM-00000 Unsupported grant_type',
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const credentials = getOAuthClientCredentials(req, params);
|
|
1656
|
+
if (
|
|
1657
|
+
credentials.clientId !== config.oauthClientId ||
|
|
1658
|
+
!safeTokenHashEquals(credentials.clientSecret, config.oauthClientSecretHash)
|
|
1659
|
+
) {
|
|
1660
|
+
return res.status(401).json({
|
|
1661
|
+
error: 'invalid_client',
|
|
1662
|
+
error_description: 'DBGM-00000 Invalid OAuth client credentials',
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
pruneOAuthAuthorizationCodes();
|
|
1667
|
+
const codeData = oauthAuthorizationCodes.get(params.code);
|
|
1668
|
+
oauthAuthorizationCodes.delete(params.code);
|
|
1669
|
+
if (!codeData) {
|
|
1670
|
+
return res.status(400).json({
|
|
1671
|
+
error: 'invalid_grant',
|
|
1672
|
+
error_description: 'DBGM-00000 Invalid or expired authorization code',
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (codeData.clientId !== credentials.clientId || codeData.redirectUri !== params.redirect_uri) {
|
|
1676
|
+
return res.status(400).json({
|
|
1677
|
+
error: 'invalid_grant',
|
|
1678
|
+
error_description: 'DBGM-00000 Authorization code was issued for a different client',
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
if (sha256Base64Url(params.code_verifier || '') !== codeData.codeChallenge) {
|
|
1682
|
+
return res.status(400).json({
|
|
1683
|
+
error: 'invalid_grant',
|
|
1684
|
+
error_description: 'DBGM-00000 Invalid PKCE verifier',
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
const licenseUid = `mcp-oauth:${credentials.clientId}`;
|
|
1689
|
+
const accessToken = jwt.sign(
|
|
1690
|
+
{
|
|
1691
|
+
amoid: 'mcp',
|
|
1692
|
+
roleId: mcpRoleId,
|
|
1693
|
+
login: 'mcp-oauth',
|
|
1694
|
+
licenseUid,
|
|
1695
|
+
tokenUse: 'mcp',
|
|
1696
|
+
oauthClientId: credentials.clientId,
|
|
1697
|
+
aud: codeData.resource,
|
|
1698
|
+
},
|
|
1699
|
+
getTokenSecret(),
|
|
1700
|
+
{ expiresIn: getTokenLifetime() }
|
|
1701
|
+
);
|
|
1702
|
+
markTokenAsLoggedIn(licenseUid, accessToken);
|
|
1703
|
+
|
|
1704
|
+
return res.json({
|
|
1705
|
+
access_token: accessToken,
|
|
1706
|
+
token_type: 'Bearer',
|
|
1707
|
+
expires_in: 24 * 60 * 60,
|
|
1708
|
+
scope: 'mcp',
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
module.exports = {
|
|
1713
|
+
handleMcpRequest,
|
|
1714
|
+
handleOAuthProtectedResourceMetadata,
|
|
1715
|
+
handleOAuthAuthorizationServerMetadata,
|
|
1716
|
+
handleOAuthAuthorize,
|
|
1717
|
+
handleOAuthToken,
|
|
1718
|
+
getOAuthProtectedResourceMetadataUrl,
|
|
1719
|
+
};
|