@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,185 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * stdio-bridge.js
6
+ *
7
+ * A lightweight stdio ↔ Streamable HTTP bridge for MCP.
8
+ * Claude Desktop runs this as a stdio server, and it forwards
9
+ * all JSON-RPC messages to the remote HTTP MCP server.
10
+ *
11
+ * Usage in claude_desktop_config.json:
12
+ * {
13
+ * "dnio-remote": {
14
+ * "command": "node",
15
+ * "args": ["/path/to/stdio-bridge.js"],
16
+ * "env": {
17
+ * "MCP_REMOTE_URL": "http://localhost:3100/mcp",
18
+ * "MCP_AUTH_HEADER": "Basic YWRtaW46UWFhZG1pbkAxMjM="
19
+ * }
20
+ * }
21
+ * }
22
+ */
23
+
24
+ const http = require('http');
25
+ const https = require('https');
26
+
27
+ const REMOTE_URL = process.env.MCP_REMOTE_URL || 'http://localhost:3100/mcp';
28
+ const AUTH_HEADER = process.env.MCP_AUTH_HEADER || '';
29
+ const parsed = new URL(REMOTE_URL);
30
+ const transport = parsed.protocol === 'https:' ? https : http;
31
+
32
+ let sessionId = null;
33
+
34
+ // ─── Read stdin as newline-delimited JSON-RPC ─────────────────────────────────
35
+
36
+ let buffer = '';
37
+ process.stdin.setEncoding('utf8');
38
+
39
+ process.stdin.on('data', (chunk) => {
40
+ buffer += chunk;
41
+
42
+ // MCP stdio protocol: each message is a complete JSON object followed by newline
43
+ let newlineIndex;
44
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
45
+ const line = buffer.slice(0, newlineIndex).trim();
46
+ buffer = buffer.slice(newlineIndex + 1);
47
+
48
+ if (line.length === 0) continue;
49
+
50
+ try {
51
+ const message = JSON.parse(line);
52
+ forwardToRemote(message);
53
+ } catch (err) {
54
+ log(`Failed to parse stdin: ${err.message}`);
55
+ }
56
+ }
57
+ });
58
+
59
+ process.stdin.on('end', () => {
60
+ log('stdin closed, exiting');
61
+ process.exit(0);
62
+ });
63
+
64
+ // ─── Forward JSON-RPC message to HTTP server ──────────────────────────────────
65
+
66
+ function forwardToRemote(message) {
67
+ const body = JSON.stringify(message);
68
+
69
+ const headers = {
70
+ 'Content-Type': 'application/json',
71
+ 'Accept': 'application/json, text/event-stream',
72
+ 'Content-Length': Buffer.byteLength(body),
73
+ };
74
+
75
+ if (AUTH_HEADER) {
76
+ headers['Authorization'] = AUTH_HEADER;
77
+ }
78
+
79
+ if (sessionId) {
80
+ headers['Mcp-Session-Id'] = sessionId;
81
+ }
82
+
83
+ const options = {
84
+ hostname: parsed.hostname,
85
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
86
+ path: parsed.pathname + parsed.search,
87
+ method: 'POST',
88
+ headers,
89
+ };
90
+
91
+ const req = transport.request(options, (res) => {
92
+ // Capture session ID from first response
93
+ const newSessionId = res.headers['mcp-session-id'];
94
+ if (newSessionId) {
95
+ sessionId = newSessionId;
96
+ }
97
+
98
+ let responseBody = '';
99
+
100
+ res.on('data', (chunk) => {
101
+ responseBody += chunk;
102
+ });
103
+
104
+ res.on('end', () => {
105
+ if (res.statusCode >= 400) {
106
+ log(`HTTP ${res.statusCode}: ${responseBody}`);
107
+
108
+ // If this was an initialize request and it failed, we need to send an error back
109
+ if (message.id !== undefined) {
110
+ const errorResponse = {
111
+ jsonrpc: '2.0',
112
+ id: message.id,
113
+ error: {
114
+ code: -32603,
115
+ message: `Remote server error: HTTP ${res.statusCode}`
116
+ }
117
+ };
118
+ writeToStdout(errorResponse);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Handle JSON response (may contain multiple messages for batch)
124
+ if (responseBody.trim()) {
125
+ // Check if it's SSE format
126
+ if (res.headers['content-type']?.includes('text/event-stream')) {
127
+ // Parse SSE events
128
+ const events = responseBody.split('\n\n');
129
+ for (const event of events) {
130
+ const dataLine = event.split('\n').find(l => l.startsWith('data: '));
131
+ if (dataLine) {
132
+ try {
133
+ const data = JSON.parse(dataLine.slice(6));
134
+ writeToStdout(data);
135
+ } catch (_) {}
136
+ }
137
+ }
138
+ } else {
139
+ // JSON response
140
+ try {
141
+ const data = JSON.parse(responseBody);
142
+ if (Array.isArray(data)) {
143
+ // Batch response
144
+ data.forEach(msg => writeToStdout(msg));
145
+ } else {
146
+ writeToStdout(data);
147
+ }
148
+ } catch (err) {
149
+ log(`Failed to parse response: ${err.message}`);
150
+ }
151
+ }
152
+ }
153
+ });
154
+ });
155
+
156
+ req.on('error', (err) => {
157
+ log(`Request failed: ${err.message}`);
158
+ if (message.id !== undefined) {
159
+ writeToStdout({
160
+ jsonrpc: '2.0',
161
+ id: message.id,
162
+ error: { code: -32603, message: `Connection failed: ${err.message}` }
163
+ });
164
+ }
165
+ });
166
+
167
+ req.write(body);
168
+ req.end();
169
+ }
170
+
171
+ // ─── Write JSON-RPC message to stdout ─────────────────────────────────────────
172
+
173
+ function writeToStdout(message) {
174
+ const json = JSON.stringify(message);
175
+ process.stdout.write(json + '\n');
176
+ }
177
+
178
+ // ─── Logging (stderr only, never stdout) ──────────────────────────────────────
179
+
180
+ function log(msg) {
181
+ process.stderr.write(`[stdio-bridge] ${msg}\n`);
182
+ }
183
+
184
+ log(`Bridge started → ${REMOTE_URL}`);
185
+ log(`Auth: ${AUTH_HEADER ? 'configured' : 'none'}`);
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../utils/logger');
4
+
5
+ function toolError(message, error) {
6
+ const detail = error?.body?.message || error?.message || 'Unknown error';
7
+ logger.error(message, {error: detail, status: error?.status});
8
+ return {
9
+ content: [{type: 'text', text: `Error: ${message}. ${detail}`}],
10
+ isError: true
11
+ };
12
+ }
13
+
14
+ function resolveServiceOrError(registry, serviceName) {
15
+ if (!registry.selectedApp) {
16
+ return {
17
+ content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}],
18
+ isError: true
19
+ };
20
+ }
21
+ const svc = registry.resolveService(serviceName);
22
+ if (!svc) {
23
+ const available = registry.getServiceList().map(s => s.name).join(', ');
24
+ return {
25
+ content: [{type: 'text', text: `Service '${serviceName}' not found. Available: ${available || 'none'}`}],
26
+ isError: true
27
+ };
28
+ }
29
+ return svc;
30
+ }
31
+
32
+ module.exports = {toolError, resolveServiceOrError};
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerApiKeysTools(_ctx) {
4
+ // TODO: register DNIO API keys tools
5
+ };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const {z} = require('zod');
4
+ const {toolError} = require('./_helpers');
5
+
6
+ module.exports = function registerAppsTools({server, dnioClient, authManager, registry, userContext}) {
7
+ server.registerTool(
8
+ 'list_apps',
9
+ {
10
+ title: 'List Available Apps',
11
+ description: `List all apps on the DataNimbus BaaS platform.
12
+ Use this FIRST, then 'select_app' to choose one before using data tools.`,
13
+ inputSchema: {},
14
+ annotations: {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false}
15
+ },
16
+ async () => {
17
+ try {
18
+ const adminToken = await authManager.getAdminToken();
19
+ dnioClient.setToken(adminToken);
20
+ const apps = await dnioClient.apps.list();
21
+ const appList = (apps || []).map(a => ({appName: a._id, description: a.description || '—'}));
22
+ dnioClient.setToken(userContext.token);
23
+ return {
24
+ content: [{
25
+ type: 'text',
26
+ text: JSON.stringify({
27
+ platform: 'DataNimbus BaaS',
28
+ user: userContext.email,
29
+ selectedApp: registry.selectedApp || 'None (use select_app)',
30
+ apps: appList
31
+ }, null, 2)
32
+ }]
33
+ };
34
+ } catch (error) {
35
+ return toolError('Failed to list apps', error);
36
+ }
37
+ }
38
+ );
39
+
40
+ server.registerTool(
41
+ 'select_app',
42
+ {
43
+ title: 'Select App',
44
+ description: `Select a DataNimbus app to work with. Discovers services, verifies pods, loads schemas.
45
+ You MUST select an app before using data tools (list_records, create_record, etc.).
46
+
47
+ Args:
48
+ - appName (string, required): App name (e.g. 'DebuggerDNIO')`,
49
+ inputSchema: {
50
+ appName: z.string().min(1).describe('App name to select')
51
+ },
52
+ annotations: {readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false}
53
+ },
54
+ async ({appName}) => {
55
+ try {
56
+ await authManager.ensureUserAppAccess(appName);
57
+ const userToken = await authManager.getUserToken();
58
+ userContext.token = userToken;
59
+
60
+ const adminToken = await authManager.getAdminToken();
61
+ const {loaded, skipped} = await registry.loadApp(appName, adminToken, userContext.token);
62
+
63
+ return {
64
+ content: [{
65
+ type: 'text',
66
+ text: JSON.stringify({
67
+ action: 'app_selected',
68
+ appName,
69
+ user: userContext.email,
70
+ services: loaded.map(s => ({
71
+ name: s.name,
72
+ serviceId: s.serviceId,
73
+ apiPath: s.apiPath,
74
+ })),
75
+ skipped: skipped.length > 0 ? skipped : undefined,
76
+ totalLoaded: loaded.length,
77
+ usage: 'Use list_records, get_record, create_record, update_record, delete_record, count_records with serviceName parameter.'
78
+ }, null, 2)
79
+ }]
80
+ };
81
+ } catch (error) {
82
+ return toolError(`Failed to select app '${appName}'`, error);
83
+ }
84
+ }
85
+ );
86
+
87
+ server.registerTool(
88
+ 'list_services',
89
+ {
90
+ title: 'List Data Services',
91
+ description: 'List all loaded data services for the currently selected app.',
92
+ inputSchema: {},
93
+ annotations: {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false}
94
+ },
95
+ async () => {
96
+ if (!registry.selectedApp) {
97
+ return {
98
+ content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}],
99
+ isError: true
100
+ };
101
+ }
102
+ const services = registry.getServiceList();
103
+ return {
104
+ content: [{
105
+ type: 'text',
106
+ text: JSON.stringify({
107
+ appName: registry.selectedApp,
108
+ services: services.map(s => ({
109
+ name: s.name,
110
+ serviceId: s.serviceId,
111
+ apiPath: s.apiPath,
112
+ schema: s.schemaDesc
113
+ }))
114
+ }, null, 2)
115
+ }]
116
+ };
117
+ }
118
+ );
119
+
120
+ server.registerTool(
121
+ 'refresh_services',
122
+ {
123
+ title: 'Refresh Data Services',
124
+ description: 'Re-scan the selected app for new/changed services. Requires an app to be selected.',
125
+ inputSchema: {},
126
+ annotations: {readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false}
127
+ },
128
+ async () => {
129
+ if (!registry.selectedApp) {
130
+ return {content: [{type: 'text', text: 'No app selected. Use select_app first.'}], isError: true};
131
+ }
132
+ try {
133
+ const adminToken = await authManager.getAdminToken();
134
+ const {loaded, skipped} = await registry.loadApp(registry.selectedApp, adminToken, userContext.token);
135
+ return {
136
+ content: [{
137
+ type: 'text',
138
+ text: JSON.stringify({
139
+ action: 'refreshed',
140
+ appName: registry.selectedApp,
141
+ loaded: loaded.map(s => s.name),
142
+ skipped
143
+ })
144
+ }]
145
+ };
146
+ } catch (error) {
147
+ return toolError('Failed to refresh services', error);
148
+ }
149
+ }
150
+ );
151
+
152
+ server.registerTool(
153
+ 'get_service_schema',
154
+ {
155
+ title: 'Get Service Schema',
156
+ description: `Get the full schema of a data service. Use before creating or querying data.
157
+ Args: serviceName (string, required)`,
158
+ inputSchema: {serviceName: z.string().min(1).describe('Service name, prefix, or ID')},
159
+ annotations: {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false}
160
+ },
161
+ async ({serviceName}) => {
162
+ if (!registry.selectedApp) return {content: [{type: 'text', text: 'No app selected.'}], isError: true};
163
+ const svc = registry.resolveService(serviceName);
164
+ if (!svc) return {content: [{type: 'text', text: `Service '${serviceName}' not found.`}], isError: true};
165
+ try {
166
+ const adminToken = await authManager.getAdminToken();
167
+ dnioClient.setToken(adminToken);
168
+ const schema = await dnioClient.services.getSchema(registry.selectedApp, svc.serviceId);
169
+ dnioClient.setToken(userContext.token);
170
+ return {
171
+ content: [{
172
+ type: 'text',
173
+ text: JSON.stringify({
174
+ serviceId: schema._id, name: schema.name, api: schema.api,
175
+ attributeCount: schema.attributeCount, status: schema.status,
176
+ definition: schema.definition
177
+ }, null, 2)
178
+ }]
179
+ };
180
+ } catch (error) {
181
+ return toolError(`Failed to fetch schema for '${serviceName}'`, error);
182
+ }
183
+ }
184
+ );
185
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = function registerBotsTools(_ctx) {
4
+ // TODO: register DNIO bots tools
5
+ };
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const {z} = require('zod');
4
+ const {toolError} = require('./_helpers');
5
+
6
+ module.exports = function registerConnectorsTools({server, dnioClient, registry, userContext}) {
7
+ server.registerTool(
8
+ 'list_connectors',
9
+ {
10
+ title: 'List Connectors',
11
+ description: `List connectors available in the selected app. Connectors define WHERE a data service stores records (DB) and uploaded files (STORAGE). Call this BEFORE create_data_service so the user can confirm which connectors to use; if they don't specify, create_data_service will auto-attach the platform defaults.
12
+
13
+ Args:
14
+ - category (optional): Filter by 'DB' (record storage) or 'STORAGE' (file storage). Omit for all.`,
15
+ inputSchema: {
16
+ category: z.enum(['DB', 'STORAGE']).optional().describe("Filter: 'DB' or 'STORAGE'. Omit for all.")
17
+ },
18
+ annotations: {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false}
19
+ },
20
+ async (params) => {
21
+ if (!registry.selectedApp) {
22
+ return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
23
+ }
24
+ try {
25
+ dnioClient.setToken(userContext.token);
26
+ const result = await dnioClient.connectors.list(registry.selectedApp, {category: params.category});
27
+ const connectors = (Array.isArray(result) ? result : []).map(c => ({
28
+ _id: c._id,
29
+ name: c.name,
30
+ category: c.category,
31
+ subCategory: c.subCategory,
32
+ type: c.type,
33
+ isDefault: !!c.options?.default,
34
+ isValid: !!c.options?.isValid
35
+ }));
36
+ return {
37
+ content: [{
38
+ type: 'text',
39
+ text: JSON.stringify({
40
+ app: registry.selectedApp,
41
+ count: connectors.length,
42
+ connectors,
43
+ usage: "Pick a DB connector (category=DB) for 'connectors.data' and a STORAGE connector for 'connectors.file' when calling create_data_service."
44
+ }, null, 2)
45
+ }]
46
+ };
47
+ } catch (error) {
48
+ return toolError('Failed to list connectors', error);
49
+ }
50
+ }
51
+ );
52
+
53
+ server.registerTool(
54
+ 'list_connector_types',
55
+ {
56
+ title: 'List Available Connector Types',
57
+ description: `List all connector types available in the marketplace (e.g. MongoDB, S3, SFTP, ActiveMQ, SMTP). Each item shows the marketplace _id (use as 'marketItemId' for create_connector), label, type, category (DB / STORAGE / MESSAGING / etc.), tags, and the 'fields' schema describing what credentials each connector type requires.
58
+
59
+ Call this BEFORE create_connector. Workflow: list types → user picks one → ask user for values for each entry in that type's 'fields' (use field 'key' as the JSON key) → call create_connector.
60
+
61
+ Args:
62
+ - category (optional): Filter by category (e.g. 'DB', 'STORAGE', 'MESSAGING'). Omit for all.
63
+ - search (optional): Substring match against label, type, or tags. Omit for all.`,
64
+ inputSchema: {
65
+ category: z.string().optional().describe("Filter by category, e.g. 'DB' or 'STORAGE'."),
66
+ search: z.string().optional().describe('Substring match against label, type, or tags.')
67
+ },
68
+ annotations: {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false}
69
+ },
70
+ async (params) => {
71
+ if (!registry.selectedApp) {
72
+ return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
73
+ }
74
+ try {
75
+ dnioClient.setToken(userContext.token);
76
+ const result = await dnioClient.connectors.listMarketTypes(registry.selectedApp);
77
+ let items = Array.isArray(result) ? result : [];
78
+ if (params.category) {
79
+ const cat = params.category.toUpperCase();
80
+ items = items.filter(i => (i.category || '').toUpperCase() === cat);
81
+ }
82
+ if (params.search) {
83
+ const q = params.search.toLowerCase();
84
+ items = items.filter(i =>
85
+ (i.label || '').toLowerCase().includes(q) ||
86
+ (i.type || '').toLowerCase().includes(q) ||
87
+ (i.tags || '').toLowerCase().includes(q)
88
+ );
89
+ }
90
+ const summary = items.map(i => ({
91
+ marketItemId: i._id,
92
+ type: i.type,
93
+ label: i.label,
94
+ category: i.category,
95
+ tags: i.tags,
96
+ version: i.version,
97
+ fields: (i.fields || []).map(f => ({
98
+ key: f.key,
99
+ label: f.label,
100
+ type: f.type,
101
+ htmlInputType: f.htmlInputType,
102
+ required: !!f.required,
103
+ encrypted: !!f.encrypted
104
+ }))
105
+ }));
106
+ return {
107
+ content: [{
108
+ type: 'text',
109
+ text: JSON.stringify({
110
+ app: registry.selectedApp,
111
+ count: summary.length,
112
+ types: summary,
113
+ usage: "Pick a marketItemId and call create_connector with: name (display name) + values (object keyed by each field's 'key' from this type's fields config)."
114
+ }, null, 2)
115
+ }]
116
+ };
117
+ } catch (error) {
118
+ return toolError('Failed to list connector types', error);
119
+ }
120
+ }
121
+ );
122
+
123
+ server.registerTool(
124
+ 'create_connector',
125
+ {
126
+ title: 'Create Connector',
127
+ description: `Create a new connector instance. Call list_connector_types FIRST to discover available types and their required field schemas.
128
+
129
+ The platform's POST payload shape is:
130
+ { "name": "<display name>", "app": "<app>", "marketItemId": "<from list_connector_types>", "values": { <field key>: <value>, ... } }
131
+
132
+ The tool injects 'app' from the currently selected app — you don't need to set it. Required fields ('required: true' in the type's fields config) MUST be present in 'values'; optional fields can be omitted. Encrypted fields ('encrypted: true', e.g. passwords/secrets) are sent as-is — the platform encrypts at rest.
133
+
134
+ Args:
135
+ - name (required): Display name for the connector (e.g. 'My MongoDB Prod').
136
+ - marketItemId (required): _id of the connector type, from list_connector_types.
137
+ - values (required): JSON string keyed by each field's 'key' (e.g. '{"host":"...","port":1883,"username":"...","password":"..."}').`,
138
+ inputSchema: {
139
+ name: z.string().min(1).describe('Display name for the connector instance'),
140
+ marketItemId: z.string().min(1).describe("Marketplace item _id from list_connector_types (the connector type)"),
141
+ values: z.string().describe("JSON string of field values keyed by field 'key' from the type's fields config")
142
+ },
143
+ annotations: {destructiveHint: false, idempotentHint: false}
144
+ },
145
+ async (params) => {
146
+ if (!registry.selectedApp) {
147
+ return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
148
+ }
149
+ try {
150
+ const values = JSON.parse(params.values);
151
+ const payload = {
152
+ values,
153
+ name: params.name,
154
+ app: registry.selectedApp,
155
+ marketItemId: params.marketItemId
156
+ };
157
+ dnioClient.setToken(userContext.token);
158
+ const result = await dnioClient.connectors.create(registry.selectedApp, payload);
159
+ return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
160
+ } catch (error) {
161
+ return toolError('Failed to create connector', error);
162
+ }
163
+ }
164
+ );
165
+ };