@datanimbus/dnio-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +20 -0
- package/docs/README.md +35 -0
- package/docs/architecture.md +171 -0
- package/docs/authentication.md +74 -0
- package/docs/tools/apps.md +59 -0
- package/docs/tools/connectors.md +76 -0
- package/docs/tools/data-pipes.md +286 -0
- package/docs/tools/data-services.md +105 -0
- package/docs/tools/deployment-groups.md +152 -0
- package/docs/tools/plugins.md +94 -0
- package/docs/tools/records.md +97 -0
- package/docs/workflows.md +195 -0
- package/env.example +16 -0
- package/package.json +43 -0
- package/readme.md +144 -0
- package/src/clients/api-keys.js +10 -0
- package/src/clients/apps.js +13 -0
- package/src/clients/base-client.js +78 -0
- package/src/clients/bots.js +10 -0
- package/src/clients/connectors.js +30 -0
- package/src/clients/data-formats.js +40 -0
- package/src/clients/data-pipes.js +33 -0
- package/src/clients/deployment-groups.js +59 -0
- package/src/clients/formulas.js +10 -0
- package/src/clients/functions.js +10 -0
- package/src/clients/plugins.js +39 -0
- package/src/clients/records.js +51 -0
- package/src/clients/services.js +63 -0
- package/src/clients/user-groups.js +10 -0
- package/src/clients/users.js +10 -0
- package/src/examples/ai-sdk-client.js +165 -0
- package/src/examples/claude_desktop_config.json +34 -0
- package/src/examples/express-integration.js +181 -0
- package/src/index.js +283 -0
- package/src/schemas/schema-converter.js +179 -0
- package/src/services/auth-manager.js +277 -0
- package/src/services/dnio-client.js +40 -0
- package/src/services/service-registry.js +150 -0
- package/src/services/session-manager.js +161 -0
- package/src/stdio-bridge.js +185 -0
- package/src/tools/_helpers.js +32 -0
- package/src/tools/api-keys.js +5 -0
- package/src/tools/apps.js +185 -0
- package/src/tools/bots.js +5 -0
- package/src/tools/connectors.js +165 -0
- package/src/tools/data-formats.js +806 -0
- package/src/tools/data-pipes.js +1305 -0
- package/src/tools/data-service-registry.js +500 -0
- package/src/tools/deployment-groups.js +511 -0
- package/src/tools/formulas.js +5 -0
- package/src/tools/functions.js +5 -0
- package/src/tools/mcp-tools-registry.js +38 -0
- package/src/tools/plugins.js +250 -0
- package/src/tools/records.js +217 -0
- package/src/tools/services.js +476 -0
- package/src/tools/user-groups.js +5 -0
- package/src/tools/users.js +5 -0
- package/src/utils/constants.js +135 -0
- package/src/utils/logger.js +63 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {z} = require('zod');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
const {
|
|
6
|
+
definitionToCreateSchema,
|
|
7
|
+
definitionToUpdateSchema,
|
|
8
|
+
schemaToDescription
|
|
9
|
+
} = require('../schemas/schema-converter');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DataServiceToolRegistry
|
|
13
|
+
*
|
|
14
|
+
* Dynamically registers MCP tools for each DNIO Data Service.
|
|
15
|
+
* Each service gets 5 tools: list, get, create, update, delete.
|
|
16
|
+
* Tools are named: {serviceName}_list, {serviceName}_get, etc.
|
|
17
|
+
*
|
|
18
|
+
* Supports runtime registration/deregistration as services start/stop.
|
|
19
|
+
*/
|
|
20
|
+
class DataServiceToolRegistry {
|
|
21
|
+
/**
|
|
22
|
+
* @param {McpServer} mcpServer - MCP server instance
|
|
23
|
+
* @param {DNIOClient} dnioClient - authenticated DNIO API client
|
|
24
|
+
* @param {string} appName - DNIO app name (e.g. 'ravi')
|
|
25
|
+
*/
|
|
26
|
+
constructor(mcpServer, dnioClient, appName) {
|
|
27
|
+
this.server = mcpServer;
|
|
28
|
+
this.client = dnioClient;
|
|
29
|
+
this.appName = appName;
|
|
30
|
+
/** @type {Map<string, {serviceId: string, servicePath: string, toolHandles: any[]}>} */
|
|
31
|
+
this.registeredServices = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch all active services and register tools for each.
|
|
36
|
+
* Call this on server startup.
|
|
37
|
+
*/
|
|
38
|
+
async registerAllServices() {
|
|
39
|
+
try {
|
|
40
|
+
const services = await this.client.services.list(this.appName, {
|
|
41
|
+
filter: {status: 'Active'},
|
|
42
|
+
select: 'name,api,_id,attributeCount,status,definition,description'
|
|
43
|
+
});
|
|
44
|
+
const serviceList = Array.isArray(services) ? services : [];
|
|
45
|
+
logger.info(`Found ${serviceList.length} active data services for app '${this.appName}'`);
|
|
46
|
+
|
|
47
|
+
for (const svc of serviceList) {
|
|
48
|
+
await this.registerService(svc);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
logger.info(`Registered tools for ${this.registeredServices.size} data services`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error('Failed to register data services', {error: error.message});
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register MCP tools for a single data service.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} serviceConfig - Service object from DNIO API (needs _id, name, api, definition)
|
|
62
|
+
*/
|
|
63
|
+
async registerService(serviceConfig) {
|
|
64
|
+
const {_id: serviceId, name, api, definition, description: svcDescription} = serviceConfig;
|
|
65
|
+
const servicePath = (api || `/${name}`).replace(/^\//, '');
|
|
66
|
+
const toolPrefix = this._sanitizeToolName(name);
|
|
67
|
+
|
|
68
|
+
if (this.registeredServices.has(serviceId)) {
|
|
69
|
+
logger.warn(`Service '${name}' (${serviceId}) already registered, skipping`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If definition not included in list response, fetch full schema
|
|
74
|
+
let fullDefinition = definition;
|
|
75
|
+
if (!fullDefinition || fullDefinition.length === 0) {
|
|
76
|
+
try {
|
|
77
|
+
const fullSchema = await this.client.services.getSchema(this.appName, serviceId);
|
|
78
|
+
fullDefinition = fullSchema.definition || [];
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error(`Failed to fetch schema for ${name} (${serviceId})`, {error: err.message});
|
|
81
|
+
fullDefinition = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const schemaDesc = fullDefinition.length > 0
|
|
86
|
+
? schemaToDescription(fullDefinition)
|
|
87
|
+
: 'Schema not available';
|
|
88
|
+
|
|
89
|
+
const serviceDesc = svcDescription || `Data service '${name}'`;
|
|
90
|
+
|
|
91
|
+
logger.info(`Registering tools for service: ${name} (${serviceId}) -> ${toolPrefix}_*`);
|
|
92
|
+
|
|
93
|
+
const toolHandles = [];
|
|
94
|
+
const client = this.client;
|
|
95
|
+
const appName = this.appName;
|
|
96
|
+
|
|
97
|
+
// ─── 1. LIST records ─────────────────────────────────────────
|
|
98
|
+
toolHandles.push(
|
|
99
|
+
this.server.registerTool(
|
|
100
|
+
`${toolPrefix}_list`,
|
|
101
|
+
{
|
|
102
|
+
title: `List ${name} records`,
|
|
103
|
+
description: `List/search records from the '${name}' data service. ${serviceDesc}
|
|
104
|
+
|
|
105
|
+
Use this to browse, search, and paginate through ${name} records.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
- filter (string, optional): MongoDB-style JSON filter string (e.g. '{"active": true}')
|
|
109
|
+
- sort (string, optional): Sort field with direction (e.g. '-_metadata.createdAt' for newest first)
|
|
110
|
+
- select (string, optional): Comma-separated fields to include (e.g. 'name,_id,active')
|
|
111
|
+
- page (number, optional): Page number, starting at 1 (default: 1)
|
|
112
|
+
- count (number, optional): Records per page, 1-100 (default: 20)
|
|
113
|
+
|
|
114
|
+
Returns: Array of ${name} records.
|
|
115
|
+
|
|
116
|
+
Schema fields:
|
|
117
|
+
${schemaDesc}`,
|
|
118
|
+
inputSchema: {
|
|
119
|
+
filter: z.string().optional().describe('MongoDB-style JSON filter (e.g. \'{"active": true}\')'),
|
|
120
|
+
sort: z.string().optional().describe('Sort field, prefix with - for descending (e.g. \'-_metadata.createdAt\')'),
|
|
121
|
+
select: z.string().optional().describe('Comma-separated fields to return'),
|
|
122
|
+
page: z.number().int().min(1).default(1).optional().describe('Page number (default: 1)'),
|
|
123
|
+
count: z.number().int().min(1).max(100).default(20).optional().describe('Records per page (default: 20)')
|
|
124
|
+
},
|
|
125
|
+
annotations: {
|
|
126
|
+
readOnlyHint: true,
|
|
127
|
+
destructiveHint: false,
|
|
128
|
+
idempotentHint: true,
|
|
129
|
+
openWorldHint: false
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async (params) => {
|
|
133
|
+
try {
|
|
134
|
+
const filter = params.filter ? JSON.parse(params.filter) : undefined;
|
|
135
|
+
const result = await client.records.list(appName, servicePath, {
|
|
136
|
+
filter,
|
|
137
|
+
sort: params.sort,
|
|
138
|
+
select: params.select,
|
|
139
|
+
page: params.page || 1,
|
|
140
|
+
count: params.count || 20
|
|
141
|
+
});
|
|
142
|
+
const records = Array.isArray(result) ? result : (result.records || [result]);
|
|
143
|
+
return {
|
|
144
|
+
content: [{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
service: name,
|
|
148
|
+
count: records.length,
|
|
149
|
+
page: params.page || 1,
|
|
150
|
+
records
|
|
151
|
+
}, null, 2)
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return _toolError(`Failed to list ${name} records`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// ─── 2. GET single record ────────────────────────────────────
|
|
162
|
+
toolHandles.push(
|
|
163
|
+
this.server.registerTool(
|
|
164
|
+
`${toolPrefix}_get`,
|
|
165
|
+
{
|
|
166
|
+
title: `Get ${name} record`,
|
|
167
|
+
description: `Retrieve a single record from '${name}' by its ID.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
- recordId (string, required): The record ID (e.g. '${serviceConfig.definition?.[0]?.prefix || 'REC'}1001')
|
|
171
|
+
- expand (boolean, optional): Expand related/linked records (default: false)
|
|
172
|
+
|
|
173
|
+
Returns: Full ${name} record object.
|
|
174
|
+
|
|
175
|
+
Schema fields:
|
|
176
|
+
${schemaDesc}`,
|
|
177
|
+
inputSchema: {
|
|
178
|
+
recordId: z.string().min(1).describe(`Record ID of the ${name} record`),
|
|
179
|
+
expand: z.boolean().default(false).optional().describe('Expand related records')
|
|
180
|
+
},
|
|
181
|
+
annotations: {
|
|
182
|
+
readOnlyHint: true,
|
|
183
|
+
destructiveHint: false,
|
|
184
|
+
idempotentHint: true,
|
|
185
|
+
openWorldHint: false
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async (params) => {
|
|
189
|
+
try {
|
|
190
|
+
const result = await client.records.get(appName, servicePath, params.recordId, {
|
|
191
|
+
expand: params.expand
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
content: [{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: JSON.stringify({service: name, record: result}, null, 2)
|
|
197
|
+
}]
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.status === 404) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: `Record '${params.recordId}' not found in ${name}`
|
|
205
|
+
}]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return _toolError(`Failed to get ${name} record`, error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// ─── 3. CREATE record ────────────────────────────────────────
|
|
215
|
+
const createSchema = fullDefinition.length > 0
|
|
216
|
+
? definitionToCreateSchema(fullDefinition)
|
|
217
|
+
: z.record(z.any());
|
|
218
|
+
|
|
219
|
+
toolHandles.push(
|
|
220
|
+
this.server.registerTool(
|
|
221
|
+
`${toolPrefix}_create`,
|
|
222
|
+
{
|
|
223
|
+
title: `Create ${name} record`,
|
|
224
|
+
description: `Create a new record in the '${name}' data service.
|
|
225
|
+
|
|
226
|
+
Provide the record data as a JSON object. The _id is auto-generated.
|
|
227
|
+
|
|
228
|
+
Schema fields:
|
|
229
|
+
${schemaDesc}
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
- data (object, required): The record data matching the schema above
|
|
233
|
+
|
|
234
|
+
Returns: The created record with its generated _id.`,
|
|
235
|
+
inputSchema: {
|
|
236
|
+
data: z.record(z.any()).describe(`Record data for ${name}. Refer to schema fields in the description.`)
|
|
237
|
+
},
|
|
238
|
+
annotations: {
|
|
239
|
+
readOnlyHint: false,
|
|
240
|
+
destructiveHint: false,
|
|
241
|
+
idempotentHint: false,
|
|
242
|
+
openWorldHint: false
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async (params) => {
|
|
246
|
+
try {
|
|
247
|
+
const result = await client.records.create(appName, servicePath, params.data);
|
|
248
|
+
return {
|
|
249
|
+
content: [{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: JSON.stringify({
|
|
252
|
+
service: name,
|
|
253
|
+
action: 'created',
|
|
254
|
+
record: result
|
|
255
|
+
}, null, 2)
|
|
256
|
+
}]
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return _toolError(`Failed to create ${name} record`, error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// ─── 4. UPDATE record ────────────────────────────────────────
|
|
266
|
+
toolHandles.push(
|
|
267
|
+
this.server.registerTool(
|
|
268
|
+
`${toolPrefix}_update`,
|
|
269
|
+
{
|
|
270
|
+
title: `Update ${name} record`,
|
|
271
|
+
description: `Update an existing record in the '${name}' data service.
|
|
272
|
+
|
|
273
|
+
Provide the record ID and the fields to update.
|
|
274
|
+
|
|
275
|
+
Schema fields:
|
|
276
|
+
${schemaDesc}
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
- recordId (string, required): The record ID to update
|
|
280
|
+
- data (object, required): Fields to update (partial update supported)
|
|
281
|
+
|
|
282
|
+
Returns: The updated record.`,
|
|
283
|
+
inputSchema: {
|
|
284
|
+
recordId: z.string().min(1).describe(`Record ID of the ${name} record to update`),
|
|
285
|
+
data: z.record(z.any()).describe(`Fields to update for ${name}. Refer to schema fields in the description.`)
|
|
286
|
+
},
|
|
287
|
+
annotations: {
|
|
288
|
+
readOnlyHint: false,
|
|
289
|
+
destructiveHint: false,
|
|
290
|
+
idempotentHint: true,
|
|
291
|
+
openWorldHint: false
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async (params) => {
|
|
295
|
+
try {
|
|
296
|
+
const result = await client.records.update(appName, servicePath, params.recordId, params.data);
|
|
297
|
+
return {
|
|
298
|
+
content: [{
|
|
299
|
+
type: 'text',
|
|
300
|
+
text: JSON.stringify({
|
|
301
|
+
service: name,
|
|
302
|
+
action: 'updated',
|
|
303
|
+
recordId: params.recordId,
|
|
304
|
+
record: result
|
|
305
|
+
}, null, 2)
|
|
306
|
+
}]
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error.status === 404) {
|
|
310
|
+
return {
|
|
311
|
+
content: [{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: `Record '${params.recordId}' not found in ${name}`
|
|
314
|
+
}]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return _toolError(`Failed to update ${name} record`, error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// ─── 5. DELETE record ────────────────────────────────────────
|
|
324
|
+
toolHandles.push(
|
|
325
|
+
this.server.registerTool(
|
|
326
|
+
`${toolPrefix}_delete`,
|
|
327
|
+
{
|
|
328
|
+
title: `Delete ${name} record`,
|
|
329
|
+
description: `Delete
|
|
330
|
+
a record from the '${name}' data service.
|
|
331
|
+
|
|
332
|
+
⚠️ This is a destructive operation and cannot be undone.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
- recordId (string, required): The record ID to delete
|
|
336
|
+
|
|
337
|
+
Returns: Confirmation of deletion.`,
|
|
338
|
+
inputSchema: {
|
|
339
|
+
recordId: z.string().min(1).describe(`Record ID of the ${name} record to delete`)
|
|
340
|
+
},
|
|
341
|
+
annotations: {
|
|
342
|
+
readOnlyHint: false,
|
|
343
|
+
destructiveHint: true,
|
|
344
|
+
idempotentHint: false,
|
|
345
|
+
openWorldHint: false
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
async (params) => {
|
|
349
|
+
try {
|
|
350
|
+
const result = await client.records.delete(appName, servicePath, params.recordId);
|
|
351
|
+
return {
|
|
352
|
+
content: [{
|
|
353
|
+
type: 'text',
|
|
354
|
+
text: JSON.stringify({
|
|
355
|
+
service: name,
|
|
356
|
+
action: 'deleted',
|
|
357
|
+
recordId: params.recordId,
|
|
358
|
+
result
|
|
359
|
+
}, null, 2)
|
|
360
|
+
}]
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error.status === 404) {
|
|
364
|
+
return {
|
|
365
|
+
content: [{
|
|
366
|
+
type: 'text',
|
|
367
|
+
text: `Record '${params.recordId}' not found in ${name}`
|
|
368
|
+
}]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return _toolError(`Failed to delete ${name} record`, error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// ─── 6. COUNT records ────────────────────────────────────────
|
|
378
|
+
toolHandles.push(
|
|
379
|
+
this.server.registerTool(
|
|
380
|
+
`${toolPrefix}_count`,
|
|
381
|
+
{
|
|
382
|
+
title: `Count ${name} records`,
|
|
383
|
+
description: `Count the total number of records in the '${name}' data service, with optional filtering.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
- filter (string, optional): MongoDB-style JSON filter string (e.g. '{"active": true}')
|
|
387
|
+
|
|
388
|
+
Returns: The total count of matching records.`,
|
|
389
|
+
inputSchema: {
|
|
390
|
+
filter: z.string().optional().describe('MongoDB-style JSON filter (e.g. \'{"active": true}\')')
|
|
391
|
+
},
|
|
392
|
+
annotations: {
|
|
393
|
+
readOnlyHint: true,
|
|
394
|
+
destructiveHint: false,
|
|
395
|
+
idempotentHint: true,
|
|
396
|
+
openWorldHint: false
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
async (params) => {
|
|
400
|
+
try {
|
|
401
|
+
const filter = params.filter ? JSON.parse(params.filter) : undefined;
|
|
402
|
+
const result = await client.records.count(appName, servicePath, filter);
|
|
403
|
+
return {
|
|
404
|
+
content: [{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: JSON.stringify({service: name, count: result}, null, 2)
|
|
407
|
+
}]
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return _toolError(`Failed to count ${name} records`, error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
this.registeredServices.set(serviceId, {
|
|
417
|
+
serviceId,
|
|
418
|
+
name,
|
|
419
|
+
servicePath,
|
|
420
|
+
toolPrefix,
|
|
421
|
+
toolHandles,
|
|
422
|
+
toolCount: toolHandles.length
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
logger.info(`Registered ${toolHandles.length} tools for '${name}': ${toolPrefix}_[list|get|create|update|delete|count]`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Deregister tools for a service (e.g. when service is stopped/deleted).
|
|
430
|
+
* Uses the MCP tool handle's .remove() method to cleanly deregister.
|
|
431
|
+
*
|
|
432
|
+
* @param {string} serviceId - DNIO service ID (e.g. 'SRVC3683')
|
|
433
|
+
*/
|
|
434
|
+
async deregisterService(serviceId) {
|
|
435
|
+
const entry = this.registeredServices.get(serviceId);
|
|
436
|
+
if (!entry) return;
|
|
437
|
+
|
|
438
|
+
for (const handle of entry.toolHandles) {
|
|
439
|
+
if (handle && typeof handle.remove === 'function') {
|
|
440
|
+
handle.remove();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
logger.info(`Deregistered tools for service '${entry.name}' (${serviceId})`);
|
|
445
|
+
this.registeredServices.delete(serviceId);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Re-register a service (e.g. after schema change).
|
|
450
|
+
*/
|
|
451
|
+
async refreshService(serviceId) {
|
|
452
|
+
await this.deregisterService(serviceId);
|
|
453
|
+
const schema = await this.client.services.getSchema(this.appName, serviceId);
|
|
454
|
+
await this.registerService(schema);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get summary of all registered services and their tools.
|
|
459
|
+
*/
|
|
460
|
+
getSummary() {
|
|
461
|
+
const services = [];
|
|
462
|
+
for (const [id, entry] of this.registeredServices) {
|
|
463
|
+
services.push({
|
|
464
|
+
serviceId: id,
|
|
465
|
+
name: entry.name,
|
|
466
|
+
path: entry.servicePath,
|
|
467
|
+
toolPrefix: entry.toolPrefix,
|
|
468
|
+
toolCount: entry.toolCount
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return services;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Sanitize a service name into a valid MCP tool name prefix.
|
|
476
|
+
* MCP tool names must be snake_case, lowercase.
|
|
477
|
+
*/
|
|
478
|
+
_sanitizeToolName(name) {
|
|
479
|
+
return name
|
|
480
|
+
.toLowerCase()
|
|
481
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
482
|
+
.replace(/^_|_$/g, '');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
function _toolError(message, error) {
|
|
489
|
+
const detail = error.body?.message || error.message || 'Unknown error';
|
|
490
|
+
logger.error(message, {error: detail, status: error.status});
|
|
491
|
+
return {
|
|
492
|
+
content: [{
|
|
493
|
+
type: 'text',
|
|
494
|
+
text: `Error: ${message}. ${detail}`
|
|
495
|
+
}],
|
|
496
|
+
isError: true
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
module.exports = DataServiceToolRegistry;
|