@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.
Files changed (59) hide show
  1. package/Dockerfile +20 -0
  2. package/docs/README.md +35 -0
  3. package/docs/architecture.md +171 -0
  4. package/docs/authentication.md +74 -0
  5. package/docs/tools/apps.md +59 -0
  6. package/docs/tools/connectors.md +76 -0
  7. package/docs/tools/data-pipes.md +286 -0
  8. package/docs/tools/data-services.md +105 -0
  9. package/docs/tools/deployment-groups.md +152 -0
  10. package/docs/tools/plugins.md +94 -0
  11. package/docs/tools/records.md +97 -0
  12. package/docs/workflows.md +195 -0
  13. package/env.example +16 -0
  14. package/package.json +43 -0
  15. package/readme.md +144 -0
  16. package/src/clients/api-keys.js +10 -0
  17. package/src/clients/apps.js +13 -0
  18. package/src/clients/base-client.js +78 -0
  19. package/src/clients/bots.js +10 -0
  20. package/src/clients/connectors.js +30 -0
  21. package/src/clients/data-formats.js +40 -0
  22. package/src/clients/data-pipes.js +33 -0
  23. package/src/clients/deployment-groups.js +59 -0
  24. package/src/clients/formulas.js +10 -0
  25. package/src/clients/functions.js +10 -0
  26. package/src/clients/plugins.js +39 -0
  27. package/src/clients/records.js +51 -0
  28. package/src/clients/services.js +63 -0
  29. package/src/clients/user-groups.js +10 -0
  30. package/src/clients/users.js +10 -0
  31. package/src/examples/ai-sdk-client.js +165 -0
  32. package/src/examples/claude_desktop_config.json +34 -0
  33. package/src/examples/express-integration.js +181 -0
  34. package/src/index.js +283 -0
  35. package/src/schemas/schema-converter.js +179 -0
  36. package/src/services/auth-manager.js +277 -0
  37. package/src/services/dnio-client.js +40 -0
  38. package/src/services/service-registry.js +150 -0
  39. package/src/services/session-manager.js +161 -0
  40. package/src/stdio-bridge.js +185 -0
  41. package/src/tools/_helpers.js +32 -0
  42. package/src/tools/api-keys.js +5 -0
  43. package/src/tools/apps.js +185 -0
  44. package/src/tools/bots.js +5 -0
  45. package/src/tools/connectors.js +165 -0
  46. package/src/tools/data-formats.js +806 -0
  47. package/src/tools/data-pipes.js +1305 -0
  48. package/src/tools/data-service-registry.js +500 -0
  49. package/src/tools/deployment-groups.js +511 -0
  50. package/src/tools/formulas.js +5 -0
  51. package/src/tools/functions.js +5 -0
  52. package/src/tools/mcp-tools-registry.js +38 -0
  53. package/src/tools/plugins.js +250 -0
  54. package/src/tools/records.js +217 -0
  55. package/src/tools/services.js +476 -0
  56. package/src/tools/user-groups.js +5 -0
  57. package/src/tools/users.js +5 -0
  58. package/src/utils/constants.js +135 -0
  59. 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;