@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,806 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const {z} = require('zod');
|
|
5
|
+
const {toolError} = require('./_helpers');
|
|
6
|
+
|
|
7
|
+
// ─── Mapping tables (per spec §4, §5, §6) ───────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const FORMAT_TYPE_MAP = {
|
|
10
|
+
JSON: 'JSON',
|
|
11
|
+
XML: 'XML',
|
|
12
|
+
XLS: 'XLS',
|
|
13
|
+
XLSX: 'XLSX',
|
|
14
|
+
FIXED: 'FLATFILE',
|
|
15
|
+
FLATFILE: 'FLATFILE',
|
|
16
|
+
TXT: 'TXT',
|
|
17
|
+
CSV: 'CSV',
|
|
18
|
+
DELIMITER: 'DELIMITER'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const FORMATS_WITH_SUBTYPE = new Set(['FLATFILE', 'TXT', 'CSV', 'DELIMITER']);
|
|
22
|
+
const FORMATS_NESTING = new Set(['JSON', 'XML']);
|
|
23
|
+
|
|
24
|
+
const TYPE_MAP = {
|
|
25
|
+
Text: 'String', text: 'String', String: 'String', string: 'String',
|
|
26
|
+
Number: 'Number', number: 'Number',
|
|
27
|
+
Boolean: 'Boolean', boolean: 'Boolean', 'True/False': 'Boolean',
|
|
28
|
+
Date: 'Date', date: 'Date',
|
|
29
|
+
Group: 'Object', group: 'Object', Object: 'Object', object: 'Object',
|
|
30
|
+
Collection: 'Array', collection: 'Array', Array: 'Array', array: 'Array'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PRIMITIVE_TYPES = new Set(['String', 'Number', 'Boolean', 'Date']);
|
|
34
|
+
|
|
35
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function _uuid() {
|
|
38
|
+
return crypto.randomUUID();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _resolveFormatType(input) {
|
|
42
|
+
if (!input) throw new Error('formatType required');
|
|
43
|
+
const mapped = FORMAT_TYPE_MAP[String(input).toUpperCase()] || FORMAT_TYPE_MAP[input];
|
|
44
|
+
if (!mapped) throw new Error(`Unknown formatType '${input}'. Allowed: ${Object.keys(FORMAT_TYPE_MAP).join(', ')}`);
|
|
45
|
+
return mapped;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _resolveSubType(formatType, requested) {
|
|
49
|
+
// JSON/XML/XLS/XLSX → always 'flat'.
|
|
50
|
+
if (!FORMATS_WITH_SUBTYPE.has(formatType)) return 'flat';
|
|
51
|
+
// FLATFILE/TXT/CSV/DELIMITER → user picks.
|
|
52
|
+
const sub = requested || 'flat';
|
|
53
|
+
if (sub !== 'flat' && sub !== 'HRSF') {
|
|
54
|
+
throw new Error(`subType for ${formatType} must be 'flat' or 'HRSF', got '${sub}'`);
|
|
55
|
+
}
|
|
56
|
+
return sub;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _resolveType(input) {
|
|
60
|
+
const mapped = TYPE_MAP[input];
|
|
61
|
+
if (!mapped) throw new Error(`Unknown type '${input}'. Allowed: ${Object.keys(TYPE_MAP).join(', ')}`);
|
|
62
|
+
return mapped;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _allowedForFormat(formatType) {
|
|
66
|
+
if (FORMATS_NESTING.has(formatType)) return new Set(['String', 'Number', 'Boolean', 'Date', 'Object', 'Array']);
|
|
67
|
+
return PRIMITIVE_TYPES;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _splitDataPath(dataPath) {
|
|
71
|
+
if (!dataPath) return [];
|
|
72
|
+
return String(dataPath).replace(/\[#\]/g, '.[#]').split('.').filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Walk path like 'cars[#].model.year' → [cars, _self, model, year] node.
|
|
76
|
+
// '[#]' segment maps to the '_self' child of the preceding Array.
|
|
77
|
+
function _findNodeByPath(definition, dataPath) {
|
|
78
|
+
if (!dataPath) return {definition, node: null}; // root
|
|
79
|
+
const segs = _splitDataPath(dataPath);
|
|
80
|
+
let parentDef = definition;
|
|
81
|
+
let current = null;
|
|
82
|
+
for (const seg of segs) {
|
|
83
|
+
const matchKey = seg === '[#]' ? '_self' : seg;
|
|
84
|
+
const child = (parentDef || []).find(n => n.key === matchKey);
|
|
85
|
+
if (!child) return null;
|
|
86
|
+
current = child;
|
|
87
|
+
parentDef = child.definition || [];
|
|
88
|
+
}
|
|
89
|
+
return {node: current, parentDefinition: parentDef};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Locate the parent's definition[] for inserting/removing a node at the given path.
|
|
93
|
+
function _findParentDefinition(definition, dataPath) {
|
|
94
|
+
if (!dataPath) return definition; // insert at root
|
|
95
|
+
const segs = _splitDataPath(dataPath);
|
|
96
|
+
let parentDef = definition;
|
|
97
|
+
let node = null;
|
|
98
|
+
for (const seg of segs) {
|
|
99
|
+
const matchKey = seg === '[#]' ? '_self' : seg;
|
|
100
|
+
node = (parentDef || []).find(n => n.key === matchKey);
|
|
101
|
+
if (!node) return null;
|
|
102
|
+
parentDef = node.definition || (node.definition = []);
|
|
103
|
+
}
|
|
104
|
+
return parentDef;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Recompute every node's properties.dataPath / dataPathSegs + root attributeCount.
|
|
108
|
+
// Spec §9: append `key` for normal step; append '[#]' when entering Array's _self.
|
|
109
|
+
function _annotatePaths(definition, parentSegs) {
|
|
110
|
+
const segs = parentSegs || [];
|
|
111
|
+
for (const node of (definition || [])) {
|
|
112
|
+
const isSelf = node.key === '_self';
|
|
113
|
+
const myPathSegs = isSelf ? [...segs, '[#]'] : [...segs, node.key];
|
|
114
|
+
const dataPath = myPathSegs.join('.').replace(/\.\[#\]/g, '[#]');
|
|
115
|
+
node.properties = node.properties || {};
|
|
116
|
+
node.properties.dataPath = dataPath;
|
|
117
|
+
node.properties.dataPathSegs = myPathSegs;
|
|
118
|
+
if (Array.isArray(node.definition)) {
|
|
119
|
+
_annotatePaths(node.definition, myPathSegs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Attribute builder (simple LLM-friendly input → DNIO attribute node) ────
|
|
125
|
+
|
|
126
|
+
const VARIANT_FLAGS = {
|
|
127
|
+
longText: {richText: true},
|
|
128
|
+
richText: {richText: true},
|
|
129
|
+
email: {email: true},
|
|
130
|
+
currency: {currency: true}, // §13 verify
|
|
131
|
+
date: {dateType: 'date'},
|
|
132
|
+
datetime: {dateType: 'datetime-local'}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Per-format property defaults (spec §8). Always include fieldLength/paddingChar/paddingPosition.
|
|
136
|
+
function _defaultProperties(name, storedType) {
|
|
137
|
+
return {
|
|
138
|
+
name: name || '',
|
|
139
|
+
_typeChanged: storedType,
|
|
140
|
+
default: null,
|
|
141
|
+
fieldLength: 10,
|
|
142
|
+
paddingChar: ' ',
|
|
143
|
+
paddingPosition: 'suffix'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build a DNIO attribute node from a simple LLM-friendly spec.
|
|
149
|
+
*
|
|
150
|
+
* Spec shape:
|
|
151
|
+
* {
|
|
152
|
+
* key: 'firstName', // required
|
|
153
|
+
* type: 'Text|Number|Boolean|Date|Group|Collection' (or stored equivalents),
|
|
154
|
+
* name?: 'FIRST_NAME', // display name; defaults to key.toUpperCase()
|
|
155
|
+
* variant?: 'longText'|'richText'|'email'|'currency'|'date'|'datetime',
|
|
156
|
+
* enum?: [...], // for listOfValues/listOfNumber
|
|
157
|
+
* precision?: number, // Number
|
|
158
|
+
* required?: boolean, // CSV/DELIMITER
|
|
159
|
+
* fieldNumber?: number, // CSV/DELIMITER
|
|
160
|
+
* mask?: 'all'|'data'|'disable'|boolean,
|
|
161
|
+
* pattern?, minLength?, maxLength?,
|
|
162
|
+
* fieldLength?, paddingChar?, paddingPosition?,
|
|
163
|
+
* _description?,
|
|
164
|
+
* children?: [<spec>...], // Group → nested defs
|
|
165
|
+
* itemType?: 'Text'|...|'Group', // Collection child type (defaults to 'Group')
|
|
166
|
+
* }
|
|
167
|
+
*/
|
|
168
|
+
function _buildAttribute(spec, formatType) {
|
|
169
|
+
if (!spec || !spec.key) throw new Error('attribute spec needs `key`');
|
|
170
|
+
const allowed = _allowedForFormat(formatType);
|
|
171
|
+
|
|
172
|
+
let storedType;
|
|
173
|
+
let isCollection = false;
|
|
174
|
+
let isGroup = false;
|
|
175
|
+
if (spec.type === 'Collection' || spec.type === 'Array' || spec.type === 'array' || spec.type === 'collection') {
|
|
176
|
+
storedType = 'Array';
|
|
177
|
+
isCollection = true;
|
|
178
|
+
} else if (spec.type === 'Group' || spec.type === 'Object' || spec.type === 'group' || spec.type === 'object') {
|
|
179
|
+
storedType = 'Object';
|
|
180
|
+
isGroup = true;
|
|
181
|
+
} else {
|
|
182
|
+
storedType = _resolveType(spec.type);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!allowed.has(storedType)) {
|
|
186
|
+
throw new Error(`Type '${storedType}' not allowed for formatType '${formatType}' (allowed: ${[...allowed].join(', ')})`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const props = _defaultProperties(spec.name || spec.key.toUpperCase(), storedType);
|
|
190
|
+
|
|
191
|
+
// Variant flags (primitives only).
|
|
192
|
+
if (spec.variant) {
|
|
193
|
+
const flag = VARIANT_FLAGS[spec.variant];
|
|
194
|
+
if (!flag) throw new Error(`Unknown variant '${spec.variant}'`);
|
|
195
|
+
Object.assign(props, flag);
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(spec.enum)) props.enum = spec.enum;
|
|
198
|
+
if (typeof spec.precision === 'number') props.precision = spec.precision;
|
|
199
|
+
if (typeof spec.required === 'boolean') props.required = spec.required;
|
|
200
|
+
if (typeof spec.fieldNumber === 'number') props.fieldNumber = spec.fieldNumber;
|
|
201
|
+
if (spec.mask !== undefined) props.mask = spec.mask;
|
|
202
|
+
if (spec.pattern) props.pattern = spec.pattern;
|
|
203
|
+
if (typeof spec.minLength === 'number') props.minLength = spec.minLength;
|
|
204
|
+
if (typeof spec.maxLength === 'number') props.maxLength = spec.maxLength;
|
|
205
|
+
if (typeof spec.fieldLength === 'number') props.fieldLength = spec.fieldLength;
|
|
206
|
+
if (spec.paddingChar !== undefined) props.paddingChar = spec.paddingChar;
|
|
207
|
+
if (spec.paddingPosition) props.paddingPosition = spec.paddingPosition;
|
|
208
|
+
if (spec.description) props._description = spec.description;
|
|
209
|
+
|
|
210
|
+
const node = {
|
|
211
|
+
_fieldId: _uuid(),
|
|
212
|
+
key: spec.key,
|
|
213
|
+
type: storedType,
|
|
214
|
+
properties: props
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (isGroup) {
|
|
218
|
+
node.definition = (spec.children || []).map(c => _buildAttribute(c, formatType));
|
|
219
|
+
} else if (isCollection) {
|
|
220
|
+
// Always wrap children in a `_self` entry per §6.
|
|
221
|
+
const itemType = spec.itemType || (Array.isArray(spec.children) && spec.children.length > 0 ? 'Group' : 'String');
|
|
222
|
+
const isSelfPrimitive = !(itemType === 'Group' || itemType === 'Object');
|
|
223
|
+
let selfStored;
|
|
224
|
+
try {
|
|
225
|
+
selfStored = isSelfPrimitive ? _resolveType(itemType) : 'Object';
|
|
226
|
+
} catch (e) {
|
|
227
|
+
selfStored = 'Object';
|
|
228
|
+
}
|
|
229
|
+
const selfProps = _defaultProperties('', selfStored);
|
|
230
|
+
const selfNode = {
|
|
231
|
+
_fieldId: _uuid(),
|
|
232
|
+
key: '_self',
|
|
233
|
+
type: selfStored,
|
|
234
|
+
properties: selfProps
|
|
235
|
+
};
|
|
236
|
+
if (selfStored === 'Object') {
|
|
237
|
+
selfNode.definition = (spec.children || []).map(c => _buildAttribute(c, formatType));
|
|
238
|
+
}
|
|
239
|
+
node.definition = [selfNode];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return node;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// HRSF rigid skeleton per §4. Optional `sections` populates each section with primitives.
|
|
246
|
+
//
|
|
247
|
+
// sections shape:
|
|
248
|
+
// { header?: [<spec>, ...], records?: [<spec>, ...], footer?: [<spec>, ...] }
|
|
249
|
+
// Each spec uses the same shape as _buildAttribute. HRSF only allows primitives inside
|
|
250
|
+
// sections — Group/Collection are rejected at build time.
|
|
251
|
+
function _buildHrsfSkeleton(formatType, sections) {
|
|
252
|
+
const s = sections || {};
|
|
253
|
+
|
|
254
|
+
const buildPrimitives = (specs, sectionLabel) => {
|
|
255
|
+
if (!Array.isArray(specs)) return [];
|
|
256
|
+
return specs.map((spec) => {
|
|
257
|
+
const node = _buildAttribute(spec, formatType);
|
|
258
|
+
if (!PRIMITIVE_TYPES.has(node.type)) {
|
|
259
|
+
throw new Error(`HRSF ${sectionLabel} only allows primitives, got '${node.type}' on '${node.key}'`);
|
|
260
|
+
}
|
|
261
|
+
return node;
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const header = {
|
|
266
|
+
_fieldId: _uuid(),
|
|
267
|
+
key: 'Header',
|
|
268
|
+
type: 'Object',
|
|
269
|
+
definition: buildPrimitives(s.header, 'Header'),
|
|
270
|
+
properties: _defaultProperties('HEADER', 'Object')
|
|
271
|
+
};
|
|
272
|
+
const recordsSelf = {
|
|
273
|
+
_fieldId: _uuid(),
|
|
274
|
+
key: '_self',
|
|
275
|
+
type: 'Object',
|
|
276
|
+
definition: buildPrimitives(s.records, 'Records._self'),
|
|
277
|
+
properties: _defaultProperties('', 'Object')
|
|
278
|
+
};
|
|
279
|
+
const records = {
|
|
280
|
+
_fieldId: _uuid(),
|
|
281
|
+
key: 'Records',
|
|
282
|
+
type: 'Array',
|
|
283
|
+
definition: [recordsSelf],
|
|
284
|
+
properties: _defaultProperties('RECORDS', 'Array')
|
|
285
|
+
};
|
|
286
|
+
const footer = {
|
|
287
|
+
_fieldId: _uuid(),
|
|
288
|
+
key: 'Footer',
|
|
289
|
+
type: 'Object',
|
|
290
|
+
definition: buildPrimitives(s.footer, 'Footer'),
|
|
291
|
+
properties: _defaultProperties('FOOTER', 'Object')
|
|
292
|
+
};
|
|
293
|
+
return [header, records, footer];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Pre-PUT validation (spec §11.9) ────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
function _validateRoot(doc) {
|
|
299
|
+
if (doc.type !== 'Object') throw new Error('root type must be "Object"');
|
|
300
|
+
|
|
301
|
+
// HRSF root has its own contract: exactly [Header(Object), Records(Array), Footer(Object)].
|
|
302
|
+
// Sections themselves use Object/Array even though FLATFILE/CSV/etc. would otherwise
|
|
303
|
+
// restrict root types to primitives — so handle HRSF before the format-allowed check.
|
|
304
|
+
if (doc.subType === 'HRSF') {
|
|
305
|
+
const keys = (doc.definition || []).map(n => n.key);
|
|
306
|
+
if (keys.length !== 3 || keys[0] !== 'Header' || keys[1] !== 'Records' || keys[2] !== 'Footer') {
|
|
307
|
+
throw new Error('HRSF skeleton must be exactly [Header, Records, Footer] in that order');
|
|
308
|
+
}
|
|
309
|
+
const [header, records, footer] = doc.definition;
|
|
310
|
+
if (header.type !== 'Object') throw new Error('HRSF Header must be Object');
|
|
311
|
+
if (footer.type !== 'Object') throw new Error('HRSF Footer must be Object');
|
|
312
|
+
if (records.type !== 'Array' || !Array.isArray(records.definition) || records.definition[0]?.key !== '_self' || records.definition[0].type !== 'Object') {
|
|
313
|
+
throw new Error('HRSF Records must be Array with _self Object wrapper');
|
|
314
|
+
}
|
|
315
|
+
for (const section of doc.definition) {
|
|
316
|
+
_walkValidate(section, doc.formatType, doc.subType);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const allowed = _allowedForFormat(doc.formatType);
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
for (const node of (doc.definition || [])) {
|
|
324
|
+
if (!allowed.has(node.type)) {
|
|
325
|
+
throw new Error(`Type '${node.type}' on '${node.key}' not allowed for formatType '${doc.formatType}'`);
|
|
326
|
+
}
|
|
327
|
+
if (seen.has(node.key)) throw new Error(`duplicate key '${node.key}' at root`);
|
|
328
|
+
seen.add(node.key);
|
|
329
|
+
_walkValidate(node, doc.formatType, doc.subType);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function _walkValidate(node, formatType, subType) {
|
|
334
|
+
if (node.type === 'Array') {
|
|
335
|
+
if (!Array.isArray(node.definition) || node.definition.length === 0 || node.definition[0].key !== '_self') {
|
|
336
|
+
throw new Error(`Array '${node.key}' must have a _self wrapper child`);
|
|
337
|
+
}
|
|
338
|
+
const self = node.definition[0];
|
|
339
|
+
// HRSF: only primitives inside sections.
|
|
340
|
+
if (subType === 'HRSF' && self.type === 'Object') {
|
|
341
|
+
for (const child of (self.definition || [])) {
|
|
342
|
+
if (!PRIMITIVE_TYPES.has(child.type)) {
|
|
343
|
+
throw new Error(`HRSF Records._self only allows primitives, got '${child.type}' on '${child.key}'`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(self.definition)) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
for (const child of self.definition) {
|
|
350
|
+
if (seen.has(child.key)) throw new Error(`duplicate key '${child.key}' inside ${node.key}._self`);
|
|
351
|
+
seen.add(child.key);
|
|
352
|
+
_walkValidate(child, formatType, subType);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (node.type === 'Object' && Array.isArray(node.definition)) {
|
|
356
|
+
const seen = new Set();
|
|
357
|
+
for (const child of node.definition) {
|
|
358
|
+
if (subType === 'HRSF' && (node.key === 'Header' || node.key === 'Footer') && !PRIMITIVE_TYPES.has(child.type)) {
|
|
359
|
+
throw new Error(`HRSF ${node.key} only allows primitives, got '${child.type}' on '${child.key}'`);
|
|
360
|
+
}
|
|
361
|
+
if (seen.has(child.key)) throw new Error(`duplicate key '${child.key}' inside ${node.key}`);
|
|
362
|
+
seen.add(child.key);
|
|
363
|
+
_walkValidate(child, formatType, subType);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─── Tool registrations ─────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
module.exports = function registerDataFormatsTools({server, dnioClient, registry, userContext}) {
|
|
371
|
+
const requireApp = () => {
|
|
372
|
+
if (!registry.selectedApp) {
|
|
373
|
+
return {content: [{type: 'text', text: 'No app selected. Use list_apps → select_app first.'}], isError: true};
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
server.registerTool(
|
|
379
|
+
'list_data_formats',
|
|
380
|
+
{
|
|
381
|
+
title: 'List Data Formats',
|
|
382
|
+
description: `List Data Formats in the selected app.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
- filter (optional): JSON string, extra filter merged onto {app}.
|
|
386
|
+
- select (optional): comma-separated fields.
|
|
387
|
+
- count (optional, default -1): max items.
|
|
388
|
+
- page (optional, default 1).`,
|
|
389
|
+
inputSchema: {
|
|
390
|
+
filter: z.string().optional(),
|
|
391
|
+
select: z.string().optional(),
|
|
392
|
+
count: z.number().int().optional(),
|
|
393
|
+
page: z.number().int().optional()
|
|
394
|
+
},
|
|
395
|
+
annotations: {readOnlyHint: true, idempotentHint: true}
|
|
396
|
+
},
|
|
397
|
+
async (params) => {
|
|
398
|
+
const guard = requireApp();
|
|
399
|
+
if (guard) return guard;
|
|
400
|
+
try {
|
|
401
|
+
dnioClient.setToken(userContext.token);
|
|
402
|
+
const filter = params.filter ? JSON.parse(params.filter) : undefined;
|
|
403
|
+
const result = await dnioClient.dataFormats.list(registry.selectedApp, {
|
|
404
|
+
filter,
|
|
405
|
+
select: params.select,
|
|
406
|
+
count: params.count,
|
|
407
|
+
page: params.page
|
|
408
|
+
});
|
|
409
|
+
return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return toolError('Failed to list data formats', error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
server.registerTool(
|
|
417
|
+
'get_data_format',
|
|
418
|
+
{
|
|
419
|
+
title: 'Get Data Format',
|
|
420
|
+
description: 'Fetch a Data Format by id (e.g. "DF6482").',
|
|
421
|
+
inputSchema: {id: z.string().min(1)},
|
|
422
|
+
annotations: {readOnlyHint: true, idempotentHint: true}
|
|
423
|
+
},
|
|
424
|
+
async (params) => {
|
|
425
|
+
const guard = requireApp();
|
|
426
|
+
if (guard) return guard;
|
|
427
|
+
try {
|
|
428
|
+
dnioClient.setToken(userContext.token);
|
|
429
|
+
const result = await dnioClient.dataFormats.get(registry.selectedApp, params.id);
|
|
430
|
+
return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return toolError(`Failed to get data format ${params.id}`, error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
server.registerTool(
|
|
438
|
+
'create_data_format',
|
|
439
|
+
{
|
|
440
|
+
title: 'Create Data Format',
|
|
441
|
+
description: `Create a Data Format in two steps automatically: POST shell → PUT populated definition.
|
|
442
|
+
|
|
443
|
+
UI label → stored formatType: JSON→JSON, XML→XML, XLS→XLS, XLSX→XLSX, FIXED→FLATFILE, TXT→TXT, CSV→CSV, DELIMITER→DELIMITER.
|
|
444
|
+
subType: JSON/XML/XLS/XLSX always 'flat'. FLATFILE/TXT/CSV/DELIMITER accept 'flat' or 'HRSF'.
|
|
445
|
+
HRSF: rigid 3-section skeleton (Header/Records/Footer) — auto-generated when subType='HRSF'. The 'definition' arg is ignored for HRSF; pass section fields via 'hrsfSections' instead. Header/Records/Footer accept primitives only (Text/Number/Boolean/Date).
|
|
446
|
+
|
|
447
|
+
Attribute spec (per node, recursive — pass via 'definition'):
|
|
448
|
+
{ key, type: 'Text|Number|Boolean|Date|Group|Collection',
|
|
449
|
+
name?, variant?: 'longText|richText|email|currency|date|datetime',
|
|
450
|
+
enum?, precision?, required?, fieldNumber?, mask?,
|
|
451
|
+
pattern?, minLength?, maxLength?,
|
|
452
|
+
fieldLength?, paddingChar?, paddingPosition?, description?,
|
|
453
|
+
children?: [<spec>...], // for Group
|
|
454
|
+
itemType?: 'Text|...|Group', children?: [<spec>...] // for Collection
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
- name (required)
|
|
459
|
+
- formatType (required): JSON|XML|XLS|XLSX|FIXED|TXT|CSV|DELIMITER
|
|
460
|
+
- subType (optional): flat|HRSF (only for FIXED/TXT/CSV/DELIMITER)
|
|
461
|
+
- description, character, lineSeparator, excelType, strictValidation (all optional)
|
|
462
|
+
- definition (optional JSON string): array of attribute specs (flat formats only).
|
|
463
|
+
- hrsfSections (optional JSON string): for subType=HRSF — populates the rigid skeleton's sections.
|
|
464
|
+
Shape: { "header": [<spec>,...], "records": [<spec>,...], "footer": [<spec>,...] }
|
|
465
|
+
Each spec is the same shape as 'definition' entries, BUT must be primitives only
|
|
466
|
+
(Text/Number/Boolean/Date — no Group/Collection inside HRSF sections).
|
|
467
|
+
Omit to create an empty skeleton; you can later use add_attribute with parentPath
|
|
468
|
+
'Header', 'Records[#]', or 'Footer' to populate it.`,
|
|
469
|
+
inputSchema: {
|
|
470
|
+
name: z.string().min(1),
|
|
471
|
+
formatType: z.string().min(1),
|
|
472
|
+
subType: z.enum(['flat', 'HRSF']).optional(),
|
|
473
|
+
description: z.string().optional(),
|
|
474
|
+
character: z.string().optional(),
|
|
475
|
+
lineSeparator: z.string().optional(),
|
|
476
|
+
excelType: z.enum(['xls', 'xlsx']).optional(),
|
|
477
|
+
strictValidation: z.boolean().optional(),
|
|
478
|
+
definition: z.string().optional(),
|
|
479
|
+
hrsfSections: z.string().optional()
|
|
480
|
+
},
|
|
481
|
+
annotations: {destructiveHint: false, idempotentHint: false}
|
|
482
|
+
},
|
|
483
|
+
async (params) => {
|
|
484
|
+
const guard = requireApp();
|
|
485
|
+
if (guard) return guard;
|
|
486
|
+
try {
|
|
487
|
+
const app = registry.selectedApp;
|
|
488
|
+
const formatType = _resolveFormatType(params.formatType);
|
|
489
|
+
const subType = _resolveSubType(formatType, params.subType);
|
|
490
|
+
|
|
491
|
+
dnioClient.setToken(userContext.token);
|
|
492
|
+
|
|
493
|
+
// Step 1: POST shell with empty definition.
|
|
494
|
+
const shell = {
|
|
495
|
+
name: params.name,
|
|
496
|
+
description: params.description ?? null,
|
|
497
|
+
strictValidation: params.strictValidation ?? null,
|
|
498
|
+
type: 'Object',
|
|
499
|
+
formatType,
|
|
500
|
+
subType,
|
|
501
|
+
character: params.character ?? ',',
|
|
502
|
+
excelType: params.excelType ?? 'xls',
|
|
503
|
+
lineSeparator: params.lineSeparator ?? '\\n',
|
|
504
|
+
app,
|
|
505
|
+
definition: []
|
|
506
|
+
};
|
|
507
|
+
const created = await dnioClient.dataFormats.create(app, shell);
|
|
508
|
+
if (!created || !created._id) {
|
|
509
|
+
return toolError('Create returned no _id', new Error(JSON.stringify(created)));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Step 2: build definition, validate, PUT.
|
|
513
|
+
let definition;
|
|
514
|
+
if (subType === 'HRSF') {
|
|
515
|
+
let sections;
|
|
516
|
+
if (params.hrsfSections) {
|
|
517
|
+
try {
|
|
518
|
+
sections = JSON.parse(params.hrsfSections);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return toolError('hrsfSections is not valid JSON', e);
|
|
521
|
+
}
|
|
522
|
+
if (!sections || typeof sections !== 'object' || Array.isArray(sections)) {
|
|
523
|
+
return toolError('hrsfSections must be an object with header/records/footer arrays', new Error(''));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
definition = _buildHrsfSkeleton(formatType, sections);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
return toolError('Failed to build HRSF skeleton', e);
|
|
530
|
+
}
|
|
531
|
+
} else if (params.definition) {
|
|
532
|
+
let specs;
|
|
533
|
+
try {
|
|
534
|
+
specs = JSON.parse(params.definition);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return toolError('definition is not valid JSON', e);
|
|
537
|
+
}
|
|
538
|
+
if (!Array.isArray(specs)) {
|
|
539
|
+
return toolError('definition must be a JSON array of attribute specs', new Error(''));
|
|
540
|
+
}
|
|
541
|
+
definition = specs.map(s => _buildAttribute(s, formatType));
|
|
542
|
+
} else {
|
|
543
|
+
definition = [];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const populated = {...created, definition};
|
|
547
|
+
_annotatePaths(populated.definition, []);
|
|
548
|
+
populated.attributeCount = populated.definition.length;
|
|
549
|
+
_validateRoot(populated);
|
|
550
|
+
|
|
551
|
+
const final = await dnioClient.dataFormats.update(app, created._id, populated);
|
|
552
|
+
return {content: [{type: 'text', text: JSON.stringify({
|
|
553
|
+
dataFormatId: final._id,
|
|
554
|
+
formatType: final.formatType,
|
|
555
|
+
subType: final.subType,
|
|
556
|
+
attributeCount: final.attributeCount,
|
|
557
|
+
dataFormat: final
|
|
558
|
+
}, null, 2)}]};
|
|
559
|
+
} catch (error) {
|
|
560
|
+
return toolError('Failed to create data format', error);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
server.registerTool(
|
|
566
|
+
'update_data_format',
|
|
567
|
+
{
|
|
568
|
+
title: 'Update Data Format',
|
|
569
|
+
description: `Replace a Data Format document. Send the FULL doc — partial PUT not supported.
|
|
570
|
+
|
|
571
|
+
Use this when you have the entire mutated doc ready (e.g. from get_data_format). For attribute-only changes, prefer add_attribute / update_attribute / remove_attribute.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
- id (required)
|
|
575
|
+
- data (required, JSON string): full document.`,
|
|
576
|
+
inputSchema: {
|
|
577
|
+
id: z.string().min(1),
|
|
578
|
+
data: z.string().min(2)
|
|
579
|
+
},
|
|
580
|
+
annotations: {destructiveHint: true, idempotentHint: false}
|
|
581
|
+
},
|
|
582
|
+
async (params) => {
|
|
583
|
+
const guard = requireApp();
|
|
584
|
+
if (guard) return guard;
|
|
585
|
+
try {
|
|
586
|
+
const payload = JSON.parse(params.data);
|
|
587
|
+
payload.app = registry.selectedApp;
|
|
588
|
+
_annotatePaths(payload.definition || [], []);
|
|
589
|
+
payload.attributeCount = (payload.definition || []).length;
|
|
590
|
+
_validateRoot(payload);
|
|
591
|
+
dnioClient.setToken(userContext.token);
|
|
592
|
+
const result = await dnioClient.dataFormats.update(registry.selectedApp, params.id, payload);
|
|
593
|
+
return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]};
|
|
594
|
+
} catch (error) {
|
|
595
|
+
return toolError(`Failed to update data format ${params.id}`, error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
server.registerTool(
|
|
601
|
+
'delete_data_format',
|
|
602
|
+
{
|
|
603
|
+
title: 'Delete Data Format',
|
|
604
|
+
description: 'Soft-delete a Data Format by id.',
|
|
605
|
+
inputSchema: {id: z.string().min(1)},
|
|
606
|
+
annotations: {destructiveHint: true, idempotentHint: false}
|
|
607
|
+
},
|
|
608
|
+
async (params) => {
|
|
609
|
+
const guard = requireApp();
|
|
610
|
+
if (guard) return guard;
|
|
611
|
+
try {
|
|
612
|
+
dnioClient.setToken(userContext.token);
|
|
613
|
+
const result = await dnioClient.dataFormats.delete(registry.selectedApp, params.id);
|
|
614
|
+
return {content: [{type: 'text', text: JSON.stringify({id: params.id, action: 'deleted', result}, null, 2)}]};
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return toolError(`Failed to delete data format ${params.id}`, error);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
server.registerTool(
|
|
622
|
+
'add_attribute',
|
|
623
|
+
{
|
|
624
|
+
title: 'Add Attribute to Data Format',
|
|
625
|
+
description: `Insert an attribute at a parent dataPath. Fetches the current doc, mutates definition[], PUTs the full doc back.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
- id (required): Data Format id.
|
|
629
|
+
- parentPath (optional): dataPath of the parent Group/Array (e.g. 'address' or 'cars[#]'). Omit/empty to insert at root.
|
|
630
|
+
- attribute (required, JSON string): attribute spec — same shape as create_data_format.definition entries.`,
|
|
631
|
+
inputSchema: {
|
|
632
|
+
id: z.string().min(1),
|
|
633
|
+
parentPath: z.string().optional(),
|
|
634
|
+
attribute: z.string().min(2)
|
|
635
|
+
},
|
|
636
|
+
annotations: {destructiveHint: false, idempotentHint: false}
|
|
637
|
+
},
|
|
638
|
+
async (params) => {
|
|
639
|
+
const guard = requireApp();
|
|
640
|
+
if (guard) return guard;
|
|
641
|
+
try {
|
|
642
|
+
const app = registry.selectedApp;
|
|
643
|
+
dnioClient.setToken(userContext.token);
|
|
644
|
+
const doc = await dnioClient.dataFormats.get(app, params.id);
|
|
645
|
+
if (!doc || !doc.formatType) return toolError('Failed to fetch data format for edit', new Error('missing formatType'));
|
|
646
|
+
|
|
647
|
+
const spec = JSON.parse(params.attribute);
|
|
648
|
+
const newNode = _buildAttribute(spec, doc.formatType);
|
|
649
|
+
|
|
650
|
+
const targetDef = _findParentDefinition(doc.definition || (doc.definition = []), params.parentPath || '');
|
|
651
|
+
if (!targetDef) {
|
|
652
|
+
return toolError(`parentPath '${params.parentPath}' not found in data format`, new Error(''));
|
|
653
|
+
}
|
|
654
|
+
if (targetDef.some(n => n.key === newNode.key)) {
|
|
655
|
+
return toolError(`duplicate key '${newNode.key}' at parent '${params.parentPath || '(root)'}'`, new Error(''));
|
|
656
|
+
}
|
|
657
|
+
targetDef.push(newNode);
|
|
658
|
+
|
|
659
|
+
_annotatePaths(doc.definition, []);
|
|
660
|
+
doc.attributeCount = doc.definition.length;
|
|
661
|
+
doc.app = app;
|
|
662
|
+
_validateRoot(doc);
|
|
663
|
+
|
|
664
|
+
const updated = await dnioClient.dataFormats.update(app, params.id, doc);
|
|
665
|
+
return {content: [{type: 'text', text: JSON.stringify({
|
|
666
|
+
addedKey: newNode.key,
|
|
667
|
+
addedDataPath: newNode.properties.dataPath,
|
|
668
|
+
dataFormat: updated
|
|
669
|
+
}, null, 2)}]};
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return toolError(`Failed to add attribute to ${params.id}`, error);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
server.registerTool(
|
|
677
|
+
'update_attribute',
|
|
678
|
+
{
|
|
679
|
+
title: 'Update Attribute in Data Format',
|
|
680
|
+
description: `Edit an existing attribute identified by dataPath. Fetches → mutates → PUTs.
|
|
681
|
+
|
|
682
|
+
Pass only the fields to change in 'patch'. Properties merge onto existing properties; nested 'children' (for Group/Collection) replace the existing definition entirely.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
- id (required)
|
|
686
|
+
- dataPath (required): e.g. 'firstName' or 'address.pin' or 'cars[#].model'.
|
|
687
|
+
- patch (required, JSON string): partial spec (any of name, variant, enum, precision, required, fieldNumber, mask, pattern, minLength, maxLength, fieldLength, paddingChar, paddingPosition, description, children, itemType).`,
|
|
688
|
+
inputSchema: {
|
|
689
|
+
id: z.string().min(1),
|
|
690
|
+
dataPath: z.string().min(1),
|
|
691
|
+
patch: z.string().min(2)
|
|
692
|
+
},
|
|
693
|
+
annotations: {destructiveHint: false, idempotentHint: true}
|
|
694
|
+
},
|
|
695
|
+
async (params) => {
|
|
696
|
+
const guard = requireApp();
|
|
697
|
+
if (guard) return guard;
|
|
698
|
+
try {
|
|
699
|
+
const app = registry.selectedApp;
|
|
700
|
+
dnioClient.setToken(userContext.token);
|
|
701
|
+
const doc = await dnioClient.dataFormats.get(app, params.id);
|
|
702
|
+
if (!doc) return toolError('Failed to fetch data format for edit', new Error('not found'));
|
|
703
|
+
|
|
704
|
+
const found = _findNodeByPath(doc.definition || [], params.dataPath);
|
|
705
|
+
if (!found || !found.node) return toolError(`dataPath '${params.dataPath}' not found`, new Error(''));
|
|
706
|
+
const node = found.node;
|
|
707
|
+
|
|
708
|
+
const patch = JSON.parse(params.patch);
|
|
709
|
+
node.properties = node.properties || {};
|
|
710
|
+
|
|
711
|
+
if (patch.name) node.properties.name = patch.name;
|
|
712
|
+
if (patch.variant) {
|
|
713
|
+
const flag = VARIANT_FLAGS[patch.variant];
|
|
714
|
+
if (!flag) return toolError(`Unknown variant '${patch.variant}'`, new Error(''));
|
|
715
|
+
Object.assign(node.properties, flag);
|
|
716
|
+
}
|
|
717
|
+
for (const k of ['enum', 'precision', 'required', 'fieldNumber', 'mask', 'pattern', 'minLength', 'maxLength', 'fieldLength', 'paddingChar', 'paddingPosition', 'default']) {
|
|
718
|
+
if (k in patch) node.properties[k] = patch[k];
|
|
719
|
+
}
|
|
720
|
+
// Description stored under `_description` per platform schema (not `description`).
|
|
721
|
+
if ('description' in patch) node.properties._description = patch.description;
|
|
722
|
+
if (patch.children && (node.type === 'Object' || (node.type === 'Array' && node.definition?.[0]?.type === 'Object'))) {
|
|
723
|
+
const built = patch.children.map(c => _buildAttribute(c, doc.formatType));
|
|
724
|
+
if (node.type === 'Object') node.definition = built;
|
|
725
|
+
else node.definition[0].definition = built;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
_annotatePaths(doc.definition, []);
|
|
729
|
+
doc.attributeCount = doc.definition.length;
|
|
730
|
+
doc.app = app;
|
|
731
|
+
_validateRoot(doc);
|
|
732
|
+
|
|
733
|
+
const updated = await dnioClient.dataFormats.update(app, params.id, doc);
|
|
734
|
+
return {content: [{type: 'text', text: JSON.stringify({
|
|
735
|
+
updatedDataPath: params.dataPath,
|
|
736
|
+
dataFormat: updated
|
|
737
|
+
}, null, 2)}]};
|
|
738
|
+
} catch (error) {
|
|
739
|
+
return toolError(`Failed to update attribute ${params.dataPath} in ${params.id}`, error);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
server.registerTool(
|
|
745
|
+
'remove_attribute',
|
|
746
|
+
{
|
|
747
|
+
title: 'Remove Attribute from Data Format',
|
|
748
|
+
description: `⚠️ Destructive. Delete an attribute identified by dataPath. Fetches → mutates → PUTs.
|
|
749
|
+
|
|
750
|
+
Refuses to remove HRSF skeleton sections (Header/Records/Footer at root).
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
- id (required)
|
|
754
|
+
- dataPath (required)`,
|
|
755
|
+
inputSchema: {
|
|
756
|
+
id: z.string().min(1),
|
|
757
|
+
dataPath: z.string().min(1)
|
|
758
|
+
},
|
|
759
|
+
annotations: {destructiveHint: true, idempotentHint: false}
|
|
760
|
+
},
|
|
761
|
+
async (params) => {
|
|
762
|
+
const guard = requireApp();
|
|
763
|
+
if (guard) return guard;
|
|
764
|
+
try {
|
|
765
|
+
const app = registry.selectedApp;
|
|
766
|
+
dnioClient.setToken(userContext.token);
|
|
767
|
+
const doc = await dnioClient.dataFormats.get(app, params.id);
|
|
768
|
+
if (!doc) return toolError('Failed to fetch data format for edit', new Error('not found'));
|
|
769
|
+
|
|
770
|
+
if (doc.subType === 'HRSF') {
|
|
771
|
+
const top = _splitDataPath(params.dataPath)[0];
|
|
772
|
+
if (top === 'Header' || top === 'Records' || top === 'Footer') {
|
|
773
|
+
if (_splitDataPath(params.dataPath).length === 1) {
|
|
774
|
+
return toolError('Cannot remove HRSF skeleton section. Edit its children instead.', new Error(''));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const segs = _splitDataPath(params.dataPath);
|
|
780
|
+
if (segs.length === 0) return toolError('dataPath required', new Error(''));
|
|
781
|
+
const lastSeg = segs[segs.length - 1];
|
|
782
|
+
const parentPath = segs.slice(0, -1).join('.').replace(/\.\[#\]/g, '[#]');
|
|
783
|
+
const parentDef = _findParentDefinition(doc.definition || [], parentPath);
|
|
784
|
+
if (!parentDef) return toolError(`parent of '${params.dataPath}' not found`, new Error(''));
|
|
785
|
+
|
|
786
|
+
const matchKey = lastSeg === '[#]' ? '_self' : lastSeg;
|
|
787
|
+
const idx = parentDef.findIndex(n => n.key === matchKey);
|
|
788
|
+
if (idx < 0) return toolError(`'${params.dataPath}' not found`, new Error(''));
|
|
789
|
+
parentDef.splice(idx, 1);
|
|
790
|
+
|
|
791
|
+
_annotatePaths(doc.definition, []);
|
|
792
|
+
doc.attributeCount = doc.definition.length;
|
|
793
|
+
doc.app = app;
|
|
794
|
+
_validateRoot(doc);
|
|
795
|
+
|
|
796
|
+
const updated = await dnioClient.dataFormats.update(app, params.id, doc);
|
|
797
|
+
return {content: [{type: 'text', text: JSON.stringify({
|
|
798
|
+
removedDataPath: params.dataPath,
|
|
799
|
+
dataFormat: updated
|
|
800
|
+
}, null, 2)}]};
|
|
801
|
+
} catch (error) {
|
|
802
|
+
return toolError(`Failed to remove attribute ${params.dataPath} from ${params.id}`, error);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
};
|