@node-i3x/mcp 0.11.0 → 0.12.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/dist/index.js CHANGED
@@ -25,150 +25,50 @@ function computeSimilarity(a, b) {
25
25
  const maxLen = Math.max(m, n);
26
26
  return Math.max(0, 1 - distance / maxLen);
27
27
  }
28
- function rankSuggestions(query, candidates, topN = 5, minScore = 0.4) {
29
- return candidates.map((candidate) => ({
30
- candidate,
31
- score: computeSimilarity(query, candidate)
32
- })).filter((item) => item.score >= minScore).sort((a, b) => b.score - a.score).slice(0, topN).map((item) => item.candidate);
28
+ var MAX_FUZZY_QUERY_LENGTH = 128;
29
+ function boundedSimilarity(query, candidate, minScore) {
30
+ const q = query.toLowerCase().trim();
31
+ const c = candidate.toLowerCase().trim();
32
+ if (!q || !c) return 0;
33
+ if (q.includes(c) || c.includes(q)) return computeSimilarity(q, c);
34
+ const lengthBound = 1 - Math.abs(q.length - c.length) / Math.max(q.length, c.length);
35
+ return lengthBound < minScore ? 0 : computeSimilarity(q, c);
36
+ }
37
+ function rankObjectSuggestions(query, objects, topN = 5, minScore = 0.4) {
38
+ if (query.trim().length > MAX_FUZZY_QUERY_LENGTH) return [];
39
+ return objects.map(({ elementId, displayName }) => ({
40
+ elementId,
41
+ displayName,
42
+ score: Math.max(
43
+ boundedSimilarity(query, elementId, minScore),
44
+ displayName ? boundedSimilarity(query, displayName, minScore) : 0
45
+ )
46
+ })).filter((item) => item.score >= minScore).sort((a, b) => b.score - a.score).slice(0, topN).map(({ elementId, displayName }) => ({ elementId, displayName }));
33
47
  }
34
-
35
- // src/bindings/connector-binding.ts
36
- var InProcessConnectorBinding = class {
37
- constructor(stack) {
38
- this.stack = stack;
39
- }
40
- stack;
41
- async getInfo() {
42
- return {
43
- specVersion: "1.0.0-beta",
44
- serverVersion: "0.10.0",
45
- serverName: "node-i3x (In-Process Direct)",
46
- capabilities: {
47
- query: { history: true },
48
- update: { current: true, history: true },
49
- subscribe: { stream: true }
50
- }
51
- };
52
- }
53
- async getNamespaces() {
54
- const model = await this.stack.modelService.getOrBuildModel();
55
- const uris = /* @__PURE__ */ new Set();
56
- for (const node of model.nodesById.values()) {
57
- if (node.namespaceUri) uris.add(node.namespaceUri);
58
- }
59
- return Array.from(uris).map((uri) => ({
60
- uri,
61
- displayName: uri
62
- }));
63
- }
64
- async getObjectTypes(namespaceUri) {
65
- return this.stack.typeService.getObjectTypes(namespaceUri);
66
- }
67
- async getRelationshipTypes() {
68
- return [
69
- {
70
- elementId: "HasComponent",
71
- displayName: "Has Component",
72
- namespaceUri: "http://opcfoundation.org/UA/",
73
- relationshipId: "i=47",
74
- reverseOf: "ComponentOf"
75
- },
76
- {
77
- elementId: "HasProperty",
78
- displayName: "Has Property",
79
- namespaceUri: "http://opcfoundation.org/UA/",
80
- relationshipId: "i=46",
81
- reverseOf: "PropertyOf"
82
- }
83
- ];
84
- }
85
- async getObjects(options) {
86
- const model = await this.stack.modelService.getOrBuildModel();
87
- let nodes;
88
- if (options?.rootOnly) {
89
- nodes = model.rootIds.map((id) => model.nodesById.get(id)).filter((n) => n !== void 0 && n.kind === "instance");
90
- } else {
91
- nodes = Array.from(model.nodesById.values()).filter(
92
- (n) => n.kind === "instance" || n.kind === "property"
93
- );
94
- }
95
- if (options?.typeElementId) {
96
- nodes = nodes.filter((n) => n.type === options.typeElementId);
97
- }
98
- const rootSet = new Set(model.rootIds);
99
- return nodes.map((n) => ({
100
- elementId: n.id,
101
- displayName: n.name,
102
- typeElementId: n.type || "BaseObjectType",
103
- parentId: rootSet.has(n.id) ? null : void 0,
104
- isComposition: n.kind === "instance",
105
- metadata: {
106
- typeNamespaceUri: n.namespaceUri,
107
- engUnit: n.engUnit || null,
108
- sourceTypeId: n.sourceTypeId || null
109
- }
110
- }));
111
- }
112
- async getObjectById(elementId) {
113
- const objects = await this.getObjects();
114
- return objects.find((o) => o.elementId === elementId) || null;
115
- }
116
- async getRelatedObjects(elementId) {
117
- const model = await this.stack.modelService.getOrBuildModel();
118
- const node = model.nodesById.get(elementId);
119
- if (!node?.children) return [];
120
- const children = node.children.map((childId) => model.nodesById.get(childId)).filter((n) => n !== void 0);
121
- return children.map((n) => ({
122
- elementId: n.id,
123
- displayName: n.name,
124
- typeElementId: n.type || "BaseObjectType",
125
- parentId: elementId,
126
- isComposition: n.kind === "instance",
127
- metadata: {
128
- typeNamespaceUri: n.namespaceUri,
129
- engUnit: n.engUnit || null,
130
- sourceTypeId: n.sourceTypeId || null
131
- }
132
- }));
133
- }
134
- async getObjectValues(elementIds, maxDepth = 1) {
135
- const bulkResults = await this.stack.valueService.readValues(elementIds, maxDepth);
136
- return bulkResults.map((r) => r.result).filter((v) => v !== null && v !== void 0);
137
- }
138
- async getHistoricalValues(elementIds, startTime, endTime, maxDepth = 1) {
139
- const start = startTime ? new Date(startTime) : null;
140
- const end = endTime ? new Date(endTime) : null;
141
- const bulkResults = await this.stack.historyService.readHistory(
142
- elementIds,
143
- start,
144
- end,
145
- maxDepth
146
- );
147
- return bulkResults.map((r) => r.result).filter((v) => v !== null && v !== void 0);
148
- }
149
- async findSuggestions(unknownId) {
150
- try {
151
- const allObjects = await this.getObjects();
152
- const candidates = allObjects.map((o) => o.elementId);
153
- return rankSuggestions(unknownId, candidates, 5, 0.4);
154
- } catch {
155
- return [];
156
- }
157
- }
158
- };
159
48
 
160
49
  // src/bindings/rest-binding.ts
161
- var RestClientBinding = class {
50
+ var OBJECT_CACHE_TTL_MS = 6e4;
51
+ var RestClientBinding = class _RestClientBinding {
162
52
  baseUrl;
163
53
  headers;
164
54
  requestTimeoutMs;
165
- constructor(baseUrl = "http://localhost:3000", token, timeoutMs = 15e3) {
55
+ fetchImpl;
56
+ token;
57
+ objectCache;
58
+ /**
59
+ * @param fetchImpl - replaces the global fetch, e.g. to route requests into an
60
+ * in-process server (see InProcessConnectorBinding).
61
+ */
62
+ constructor(baseUrl = "http://localhost:3000", token, timeoutMs = 15e3, fetchImpl) {
166
63
  let cleanUrl = baseUrl.trim().replace(/\/+$/, "");
167
64
  if (!cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://")) {
168
65
  cleanUrl = `http://${cleanUrl}`;
169
66
  }
67
+ cleanUrl = cleanUrl.replace(/\/v1$/i, "");
170
68
  this.baseUrl = cleanUrl;
171
69
  this.requestTimeoutMs = timeoutMs;
70
+ this.fetchImpl = fetchImpl;
71
+ this.token = token;
172
72
  this.headers = {
173
73
  "Content-Type": "application/json",
174
74
  Accept: "application/json"
@@ -177,11 +77,27 @@ var RestClientBinding = class {
177
77
  this.headers.Authorization = `Bearer ${token}`;
178
78
  }
179
79
  }
80
+ /** Normalized base URL, without a trailing /v1. */
81
+ getBaseUrl() {
82
+ return this.baseUrl;
83
+ }
84
+ get hasToken() {
85
+ return Boolean(this.token);
86
+ }
87
+ /**
88
+ * A binding to `baseUrl`. Without `token`, the current token is kept when the URL
89
+ * is this binding's own (re-connecting must not silently drop the configured token).
90
+ */
91
+ connectTo(baseUrl, token) {
92
+ const next = new _RestClientBinding(baseUrl, token, this.requestTimeoutMs);
93
+ if (token !== void 0 || next.baseUrl !== this.baseUrl || !this.token) return next;
94
+ return new _RestClientBinding(baseUrl, this.token, this.requestTimeoutMs);
95
+ }
180
96
  async request(path, options = {}) {
181
97
  const url = `${this.baseUrl}${path}`;
182
98
  const signal = options.signal || AbortSignal.timeout(this.requestTimeoutMs);
183
99
  try {
184
- const response = await fetch(url, {
100
+ const response = await (this.fetchImpl ?? fetch)(url, {
185
101
  ...options,
186
102
  signal,
187
103
  headers: {
@@ -204,98 +120,134 @@ var RestClientBinding = class {
204
120
  throw err;
205
121
  }
206
122
  }
123
+ /** Unwrap a `{ success, result }` envelope; a `success: false` envelope becomes an error. */
124
+ async get(path) {
125
+ const res = await this.request(path);
126
+ if (res?.success !== true || !("result" in res)) {
127
+ throw new Error(`i3X request ${path} failed: ${describeFailure(res)}`);
128
+ }
129
+ return res.result;
130
+ }
131
+ /**
132
+ * POST a bulk request and return its `results` items, one per requested elementId.
133
+ * `success` is false on the envelope as soon as one item failed, so items are
134
+ * returned as they are; only a body without `results` is an error here.
135
+ */
136
+ async postBulk(path, body) {
137
+ const res = await this.request(path, {
138
+ method: "POST",
139
+ body: JSON.stringify(body)
140
+ });
141
+ if (!Array.isArray(res?.results)) {
142
+ throw new Error(`i3X request ${path} failed: ${describeFailure(res)}`);
143
+ }
144
+ return res.results;
145
+ }
146
+ /** Bulk items as per-element results: a failed item keeps its elementId and error. */
147
+ async postBulkPerElement(path, elementIds, body) {
148
+ const items = await this.postBulk(path, { elementIds, ...body });
149
+ return items.map((item, idx) => {
150
+ const elementId = item.elementId ?? elementIds[idx] ?? `element_${idx}`;
151
+ if (!item.success) return { elementId, error: describeFailure(item) };
152
+ if (item.result == null) return { elementId, error: "Server returned no result" };
153
+ return { elementId, result: item.result };
154
+ });
155
+ }
156
+ /** The single item of a one-element bulk request. */
157
+ async postBulkOne(path, body) {
158
+ const [item] = await this.postBulk(path, body);
159
+ if (!item) throw new Error(`i3X request ${path} returned no result`);
160
+ return item;
161
+ }
207
162
  async getInfo() {
208
- return this.request("/v1/info");
163
+ return this.get("/v1/info");
209
164
  }
210
165
  async getNamespaces() {
211
- const res = await this.request("/v1/namespaces");
212
- return res.namespaces || [];
166
+ return this.get("/v1/namespaces");
213
167
  }
214
168
  async getObjectTypes(namespaceUri) {
215
169
  const query = namespaceUri ? `?namespaceUri=${encodeURIComponent(namespaceUri)}` : "";
216
- const res = await this.request(
217
- `/v1/objecttypes${query}`
218
- );
219
- return res.objectTypes || [];
170
+ return this.get(`/v1/objecttypes${query}`);
220
171
  }
221
172
  async getRelationshipTypes() {
222
- const res = await this.request(
223
- "/v1/relationshiptypes"
224
- );
225
- return res.relationshipTypes || [];
173
+ return this.get("/v1/relationshiptypes");
226
174
  }
227
175
  async getObjects(options) {
228
176
  const params = new URLSearchParams();
229
177
  if (options?.rootOnly) params.set("root", "true");
230
178
  if (options?.typeElementId) params.set("typeElementId", options.typeElementId);
231
179
  const query = params.toString() ? `?${params.toString()}` : "";
232
- const res = await this.request(
233
- `/v1/objects${query}`
234
- );
235
- return res.objects || [];
180
+ return this.get(`/v1/objects${query}`);
236
181
  }
182
+ /** `null` only when the server reports the element as not found (404). */
237
183
  async getObjectById(elementId) {
238
- try {
239
- const res = await this.request(
240
- "/v1/objects/list",
241
- {
242
- method: "POST",
243
- body: JSON.stringify({ elementIds: [elementId] })
244
- }
245
- );
246
- if (res.objects && res.objects.length > 0) {
247
- return res.objects[0] ?? null;
248
- }
249
- return null;
250
- } catch {
251
- return null;
252
- }
184
+ const path = "/v1/objects/list";
185
+ const item = await this.postBulkOne(path, {
186
+ elementIds: [elementId]
187
+ });
188
+ if (item.success) return item.result ?? null;
189
+ if (item.responseDetail?.status === 404) return null;
190
+ throw new Error(
191
+ `i3X request ${path} failed for '${elementId}': ${describeFailure(item)}`
192
+ );
253
193
  }
254
194
  async getRelatedObjects(elementId, relationshipType) {
255
- try {
256
- const body = { elementId };
257
- if (relationshipType) body.relationshipType = relationshipType;
258
- const res = await this.request("/v1/objects/related", {
259
- method: "POST",
260
- body: JSON.stringify(body)
261
- });
262
- return (res.relatedObjects || []).map((r) => r.targetObject);
263
- } catch {
264
- return [];
195
+ const path = "/v1/objects/related";
196
+ const body = {
197
+ elementIds: [elementId]
198
+ };
199
+ if (relationshipType) body.relationshipType = relationshipType;
200
+ const item = await this.postBulkOne(path, body);
201
+ if (!item.success) {
202
+ throw new Error(
203
+ `i3X request ${path} failed for '${elementId}': ${describeFailure(item)}`
204
+ );
265
205
  }
206
+ return (item.result ?? []).filter(
207
+ (r) => !relationshipType || r.sourceRelationship === relationshipType
208
+ );
266
209
  }
267
- async getObjectValues(elementIds, maxDepth = 1) {
268
- const res = await this.request(
269
- "/v1/objects/value",
270
- {
271
- method: "POST",
272
- body: JSON.stringify({ elementIds, maxDepth })
273
- }
210
+ async getRelatedObjectsBulk(elementIds) {
211
+ return this.postBulkPerElement(
212
+ "/v1/objects/related",
213
+ elementIds,
214
+ {}
274
215
  );
275
- return res.values || [];
216
+ }
217
+ async getObjectValues(elementIds, maxDepth = 1) {
218
+ return this.postBulkPerElement("/v1/objects/value", elementIds, {
219
+ maxDepth
220
+ });
276
221
  }
277
222
  async getHistoricalValues(elementIds, startTime, endTime, maxDepth = 1) {
278
- const res = await this.request(
223
+ return this.postBulkPerElement(
279
224
  "/v1/objects/history",
280
- {
281
- method: "POST",
282
- body: JSON.stringify({ elementIds, startTime, endTime, maxDepth })
283
- }
225
+ elementIds,
226
+ { startTime, endTime, maxDepth }
284
227
  );
285
- return res.history || [];
286
228
  }
287
- async findSuggestions(unknownId) {
288
- try {
289
- const allObjects = await this.getObjects();
290
- const candidates = allObjects.map((o) => o.elementId);
291
- return rankSuggestions(unknownId, candidates, 5, 0.4);
292
- } catch {
293
- return [];
229
+ /** Closest objects by elementId or displayName; a failure to list objects propagates. */
230
+ async findSuggestions(query) {
231
+ if (query.trim().length > MAX_FUZZY_QUERY_LENGTH) return [];
232
+ const now = Date.now();
233
+ if (!this.objectCache || now - this.objectCache.at > OBJECT_CACHE_TTL_MS) {
234
+ const objects = this.getObjects();
235
+ this.objectCache = { at: now, objects };
236
+ objects.catch(() => {
237
+ if (this.objectCache?.objects === objects) this.objectCache = void 0;
238
+ });
294
239
  }
240
+ return rankObjectSuggestions(query, await this.objectCache.objects);
295
241
  }
296
242
  };
243
+ function describeFailure(res) {
244
+ const d = res?.responseDetail;
245
+ if (d) return [d.status, d.title, d.detail].filter(Boolean).join(" ");
246
+ return `unexpected response ${JSON.stringify(res)?.slice(0, 200)}`;
247
+ }
297
248
 
298
249
  // src/server.ts
250
+ import { createRequire } from "module";
299
251
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
300
252
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
301
253
  import {
@@ -311,12 +263,18 @@ var connectSchema = z.object({
311
263
  serverUrl: z.string().describe(
312
264
  "The base URL of the i3X Gateway (e.g. 'http://192.168.1.50:8000' or 'https://i3x-portal.sterfive.io/instance/SESSION_ID')"
313
265
  ),
314
- authToken: z.string().optional().describe("Optional Bearer authentication token for secured endpoints"),
315
- verifyConnection: z.boolean().optional().default(true).describe("Whether to immediately verify connectivity by querying server /v1/info")
266
+ authToken: z.string().optional().describe(
267
+ "Optional Bearer authentication token. Omitted when re-connecting to the current URL: the current token is kept."
268
+ ),
269
+ verifyConnection: z.boolean().optional().default(true).describe(
270
+ "Whether to verify connectivity and credentials before switching (reads /v1/info and /v1/namespaces)"
271
+ )
316
272
  });
317
- async function handleConnect(args, onBindingSwitch) {
273
+ async function handleConnect(args, onBindingSwitch, current) {
318
274
  const { serverUrl, authToken, verifyConnection } = args;
319
- const newBinding = new RestClientBinding(serverUrl, authToken);
275
+ const newBinding = current instanceof RestClientBinding ? current.connectTo(serverUrl, authToken) : new RestClientBinding(serverUrl, authToken);
276
+ const tokenKept = authToken === void 0 && newBinding.hasToken;
277
+ const url = newBinding.getBaseUrl();
320
278
  let serverInfo;
321
279
  if (verifyConnection) {
322
280
  try {
@@ -324,19 +282,39 @@ async function handleConnect(args, onBindingSwitch) {
324
282
  } catch (err) {
325
283
  return {
326
284
  status: "connection_failed",
327
- serverUrl,
285
+ serverUrl: url,
328
286
  error: err.message,
329
- tip: "Please check that the server URL is reachable and the authentication token (if required) is valid."
287
+ tip: "Please check that the server URL is reachable."
288
+ };
289
+ }
290
+ try {
291
+ await newBinding.getNamespaces();
292
+ } catch (err) {
293
+ const message = err.message;
294
+ if (/^HTTP 40[13]\b/.test(message)) {
295
+ return {
296
+ status: "auth_failed",
297
+ serverUrl: url,
298
+ error: message,
299
+ tip: "The server rejected the credentials. Pass a valid authToken; the previous connection is still active."
300
+ };
301
+ }
302
+ return {
303
+ status: "connection_failed",
304
+ serverUrl: url,
305
+ error: message,
306
+ tip: "The server answered /v1/info but not /v1/namespaces; the previous connection is still active."
330
307
  };
331
308
  }
332
309
  }
333
310
  onBindingSwitch(newBinding);
334
311
  return {
335
312
  status: "connected",
336
- serverUrl,
337
- authenticated: Boolean(authToken),
313
+ serverUrl: url,
314
+ authenticated: newBinding.hasToken,
315
+ ...tokenKept ? { note: "No authToken given: kept the token already in use for this URL." } : {},
338
316
  serverInfo: serverInfo || "Connection established without verification.",
339
- message: `Successfully connected to ${serverUrl}. All subsequent i3X tool calls will query this endpoint.`
317
+ message: `Successfully connected to ${url}. All subsequent i3X tool calls will query this endpoint.`
340
318
  };
341
319
  }
342
320
 
@@ -388,14 +366,30 @@ async function handleDiscoverTypes(binding, args) {
388
366
 
389
367
  // src/tools/explore-hierarchy.ts
390
368
  import { z as z3 } from "zod";
369
+
370
+ // src/utils/relationships.ts
371
+ var PARENT_RELATIONSHIPS = /* @__PURE__ */ new Set(["IsComponentOf", "ComponentOf", "HasParent"]);
372
+ function isParentRelationship(sourceRelationship) {
373
+ return PARENT_RELATIONSHIPS.has(sourceRelationship);
374
+ }
375
+
376
+ // src/tools/explore-hierarchy.ts
377
+ var RELATED_BATCH_SIZE = 100;
378
+ var RELATED_CONCURRENCY = 4;
391
379
  var exploreHierarchySchema = z3.object({
392
380
  rootElementId: z3.string().optional().describe(
393
381
  "Optional specific root asset elementId. If omitted, explores all root assets."
394
382
  ),
395
- maxDepth: z3.number().optional().default(2).describe("Maximum depth to traverse child components (1 to 5, default: 2)")
383
+ maxDepth: z3.number().int().min(0).optional().default(2).describe(
384
+ "Levels to show: 1 = roots only, 2 = roots and their children (default), 0 = unlimited (bounded by maxNodes)"
385
+ ),
386
+ maxNodes: z3.number().int().min(1).max(5e3).optional().default(500).describe(
387
+ "Node budget for the whole tree (default 500, max 5000); the tree is marked truncated when reached"
388
+ )
396
389
  });
397
390
  async function handleExploreHierarchy(binding, args) {
398
- const maxDepth = Math.min(Math.max(args.maxDepth || 2, 1), 5);
391
+ const maxDepth = args.maxDepth ?? 2;
392
+ const maxNodes = args.maxNodes ?? 500;
399
393
  let roots;
400
394
  if (args.rootElementId) {
401
395
  const root = await binding.getObjectById(args.rootElementId);
@@ -410,45 +404,107 @@ async function handleExploreHierarchy(binding, args) {
410
404
  } else {
411
405
  roots = await binding.getObjects({ rootOnly: true });
412
406
  }
413
- async function buildNode(asset, currentDepth, visited) {
414
- const node = {
415
- elementId: asset.elementId,
416
- displayName: asset.displayName || asset.elementId,
417
- typeElementId: asset.typeElementId
418
- };
419
- visited.add(asset.elementId);
420
- if (currentDepth < maxDepth) {
421
- const children = await binding.getRelatedObjects(asset.elementId);
422
- const unvisitedChildren = children.filter((c) => !visited.has(c.elementId));
423
- if (unvisitedChildren.length > 0) {
424
- node.children = await Promise.all(
425
- unvisitedChildren.map(
426
- (child) => buildNode(child, currentDepth + 1, new Set(visited))
427
- )
428
- );
407
+ const toNode = (asset) => ({
408
+ elementId: asset.elementId,
409
+ displayName: asset.displayName || asset.elementId,
410
+ typeElementId: asset.typeElementId
411
+ });
412
+ let nodeCount = 0;
413
+ let truncated = false;
414
+ const expanded = /* @__PURE__ */ new Set();
415
+ const tree = [];
416
+ for (const root of roots) {
417
+ if (nodeCount >= maxNodes) {
418
+ truncated = true;
419
+ break;
420
+ }
421
+ tree.push(toNode(root));
422
+ nodeCount++;
423
+ }
424
+ let level = tree;
425
+ for (let depth = 1; level.length > 0 && (maxDepth === 0 || depth < maxDepth); depth++) {
426
+ const toExpand = [];
427
+ for (const node of level) {
428
+ if (expanded.has(node.elementId)) node.repeated = true;
429
+ else {
430
+ expanded.add(node.elementId);
431
+ toExpand.push(node);
429
432
  }
430
433
  }
431
- return node;
434
+ const related = await readRelatedInBatches(
435
+ binding,
436
+ toExpand.map((n) => n.elementId)
437
+ );
438
+ const next = [];
439
+ toExpand.forEach((node, idx) => {
440
+ const entry = related[idx];
441
+ if (entry?.error) {
442
+ node.error = entry.error;
443
+ return;
444
+ }
445
+ const children = (entry?.result ?? []).filter((r) => !isParentRelationship(r.sourceRelationship)).map((r) => r.object);
446
+ if (children.length === 0) return;
447
+ if (nodeCount + children.length > maxNodes) {
448
+ truncated = true;
449
+ node.truncated = true;
450
+ return;
451
+ }
452
+ node.children = children.map(toNode);
453
+ nodeCount += children.length;
454
+ next.push(...node.children);
455
+ });
456
+ level = next;
432
457
  }
433
- const hierarchyTree = await Promise.all(
434
- roots.map((root) => buildNode(root, 1, /* @__PURE__ */ new Set()))
435
- );
436
458
  return {
437
459
  totalRoots: roots.length,
438
- traversalDepth: maxDepth,
439
- plantTopology: hierarchyTree
460
+ traversalDepth: maxDepth === 0 ? "unlimited" : maxDepth,
461
+ nodeCount,
462
+ ...truncated ? {
463
+ truncated: {
464
+ maxNodes,
465
+ hint: "Node budget reached; nodes marked truncated were not expanded. Use rootElementId on one of them, a smaller maxDepth, or a larger maxNodes."
466
+ }
467
+ } : {},
468
+ plantTopology: tree
440
469
  };
441
470
  }
471
+ async function readRelatedInBatches(binding, elementIds) {
472
+ const batches = [];
473
+ for (let i = 0; i < elementIds.length; i += RELATED_BATCH_SIZE) {
474
+ batches.push(elementIds.slice(i, i + RELATED_BATCH_SIZE));
475
+ }
476
+ const results = new Array(batches.length);
477
+ let nextBatch = 0;
478
+ const worker = async () => {
479
+ while (nextBatch < batches.length) {
480
+ const idx = nextBatch++;
481
+ const batch = batches[idx];
482
+ try {
483
+ results[idx] = await binding.getRelatedObjectsBulk(batch);
484
+ } catch (err) {
485
+ const error = err instanceof Error ? err.message : String(err);
486
+ results[idx] = batch.map((elementId) => ({ elementId, error }));
487
+ }
488
+ }
489
+ };
490
+ await Promise.all(
491
+ Array.from({ length: Math.min(RELATED_CONCURRENCY, batches.length) }, worker)
492
+ );
493
+ return results.flat();
494
+ }
442
495
 
443
496
  // src/tools/inspect-asset.ts
444
497
  import { z as z4 } from "zod";
445
498
  var inspectAssetSchema = z4.object({
446
499
  elementId: z4.string().describe("The elementId of the asset to inspect (e.g. 'Pump_101', 'Boiler_101')"),
447
500
  includeValues: z4.boolean().optional().default(true).describe("Whether to include live VQT (Value, Quality, Timestamp) telemetry values"),
448
- includeSchema: z4.boolean().optional().default(true).describe("Whether to include the ObjectType JSON Schema definition")
501
+ includeSchema: z4.boolean().optional().default(true).describe("Whether to include the ObjectType JSON Schema definition"),
502
+ maxDepth: z4.number().int().min(0).optional().default(2).describe(
503
+ "Value read depth through components: 1 = the asset only (an Object usually has no value of its own), 2 = with its components (default), 0 = unlimited"
504
+ )
449
505
  });
450
506
  async function handleInspectAsset(binding, args) {
451
- const { elementId, includeValues, includeSchema } = args;
507
+ const { elementId, includeValues, includeSchema, maxDepth } = args;
452
508
  const asset = await binding.getObjectById(elementId);
453
509
  if (!asset) {
454
510
  const suggestions = await binding.findSuggestions(elementId);
@@ -463,25 +519,35 @@ async function handleInspectAsset(binding, args) {
463
519
  displayName: asset.displayName || asset.elementId,
464
520
  description: asset.metadata?.description || void 0,
465
521
  typeElementId: asset.typeElementId,
522
+ parentId: asset.parentId ?? null,
466
523
  isRoot: !asset.parentId
467
524
  };
525
+ const warnings = [];
468
526
  const relatedPromise = binding.getRelatedObjects(elementId);
469
527
  const schemaPromise = includeSchema && asset.typeElementId ? binding.getObjectTypes() : Promise.resolve(null);
470
- const valuesPromise = includeValues ? binding.getObjectValues([elementId]) : Promise.resolve(null);
528
+ const valuesPromise = includeValues ? binding.getObjectValues([elementId], maxDepth) : Promise.resolve(null);
471
529
  const [relatedResult, schemaResult, valuesResult] = await Promise.allSettled([
472
530
  relatedPromise,
473
531
  schemaPromise,
474
532
  valuesPromise
475
533
  ]);
476
- if (relatedResult.status === "fulfilled" && Array.isArray(relatedResult.value) && relatedResult.value.length > 0) {
477
- result.subComponents = relatedResult.value.map((r) => ({
478
- elementId: r.elementId,
479
- displayName: r.displayName || r.elementId,
480
- typeElementId: r.typeElementId
481
- }));
534
+ if (relatedResult.status === "fulfilled") {
535
+ const subComponents = relatedResult.value.filter(
536
+ (r) => !isParentRelationship(r.sourceRelationship)
537
+ );
538
+ if (subComponents.length > 0) {
539
+ result.subComponents = subComponents.map((r) => ({
540
+ elementId: r.object.elementId,
541
+ displayName: r.object.displayName || r.object.elementId,
542
+ typeElementId: r.object.typeElementId,
543
+ relationship: r.sourceRelationship
544
+ }));
545
+ }
546
+ } else {
547
+ warnings.push(`Sub-components unavailable: ${errorMessage(relatedResult.reason)}`);
482
548
  }
483
- if (schemaResult.status === "fulfilled" && Array.isArray(schemaResult.value)) {
484
- const targetType = schemaResult.value.find(
549
+ if (schemaResult.status === "fulfilled") {
550
+ const targetType = schemaResult.value?.find(
485
551
  (t) => t.elementId === asset.typeElementId
486
552
  );
487
553
  if (targetType) {
@@ -491,101 +557,177 @@ async function handleInspectAsset(binding, args) {
491
557
  schema: targetType.schema
492
558
  };
493
559
  }
560
+ } else {
561
+ warnings.push(`Type schema unavailable: ${errorMessage(schemaResult.reason)}`);
494
562
  }
495
- if (valuesResult.status === "fulfilled" && Array.isArray(valuesResult.value) && valuesResult.value.length > 0) {
496
- result.currentValues = valuesResult.value[0];
497
- } else if (valuesResult.status === "rejected") {
498
- result.currentValuesWarning = "Telemetry data temporarily unavailable";
563
+ if (valuesResult.status === "fulfilled") {
564
+ const entry = valuesResult.value?.[0];
565
+ if (entry?.result) result.currentValues = entry.result;
566
+ else if (entry?.error) warnings.push(`Telemetry unavailable: ${entry.error}`);
567
+ } else {
568
+ warnings.push(`Telemetry unavailable: ${errorMessage(valuesResult.reason)}`);
499
569
  }
570
+ if (warnings.length > 0) result.warnings = warnings;
500
571
  return result;
501
572
  }
573
+ function errorMessage(reason) {
574
+ return reason instanceof Error ? reason.message : String(reason);
575
+ }
502
576
 
503
577
  // src/tools/monitor-events.ts
578
+ import { z as z6 } from "zod";
579
+
580
+ // src/utils/element-ids.ts
504
581
  import { z as z5 } from "zod";
505
- var monitorEventsSchema = z5.object({
506
- elementIds: z5.array(z5.string()).min(1).describe("Array of asset elementIds to monitor (e.g. ['Boiler_101', 'Pump_101'])"),
507
- condition: z5.string().optional().describe(
508
- "Optional monitoring alert condition filter (e.g. 'temperature > 85 || quality != Good')"
582
+ var MAX_ELEMENT_IDS = 100;
583
+ function elementIdsSchema(description) {
584
+ return z5.array(z5.string()).min(1).transform((ids) => [...new Set(ids)]).refine((ids) => ids.length <= MAX_ELEMENT_IDS, {
585
+ message: `At most ${MAX_ELEMENT_IDS} distinct elementIds per call.`
586
+ }).describe(`${description}; duplicates are ignored, at most ${MAX_ELEMENT_IDS}`);
587
+ }
588
+
589
+ // src/tools/monitor-events.ts
590
+ var MAX_LISTED_ALERTS = 50;
591
+ var monitorEventsSchema = z6.object({
592
+ elementIds: elementIdsSchema(
593
+ "Array of asset elementIds to check (e.g. ['Boiler_101', 'Pump_101'])"
594
+ ),
595
+ maxDepth: z6.number().int().min(0).optional().default(2).describe(
596
+ "Read depth through components: 1 = the element only, 2 = with its components (default), 0 = unlimited"
509
597
  )
510
598
  });
511
599
  async function handleMonitorEvents(binding, args) {
512
- const { elementIds, condition } = args;
513
- const currentValues = await binding.getObjectValues(elementIds);
600
+ const { elementIds, maxDepth } = args;
601
+ const snapshot = await binding.getObjectValues(elementIds, maxDepth);
602
+ const alerts = [];
603
+ for (const { elementId, result, error } of snapshot) {
604
+ if (error) {
605
+ alerts.push({ elementId, error });
606
+ continue;
607
+ }
608
+ if (result && !isGood(result.quality)) {
609
+ alerts.push({ elementId, quality: result.quality });
610
+ }
611
+ for (const [componentId, vqt] of Object.entries(result?.components ?? {})) {
612
+ if (!isGood(vqt.quality))
613
+ alerts.push({ elementId: componentId, quality: vqt.quality });
614
+ }
615
+ }
616
+ const summary = {
617
+ bad: alerts.filter((a) => a.quality && /^bad/i.test(a.quality)).length,
618
+ uncertain: alerts.filter((a) => a.quality && /^uncertain/i.test(a.quality)).length,
619
+ readErrors: alerts.filter((a) => a.error).length
620
+ };
514
621
  return {
515
- status: "monitoring_active",
516
- monitoredAssets: elementIds,
517
- alertCondition: condition || "All value or quality changes",
518
- initialSnapshot: currentValues,
519
- instructions: "Subscription stream registered. The MCP server will emit notification events on matching VQT telemetry updates or threshold breaches."
622
+ status: "snapshot",
623
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
624
+ summary,
625
+ alerts: alerts.slice(0, MAX_LISTED_ALERTS),
626
+ ...alerts.length > MAX_LISTED_ALERTS ? { alertsOmitted: alerts.length - MAX_LISTED_ALERTS } : {},
627
+ snapshot,
628
+ note: "Point-in-time read; no subscription is created. Call again to re-check."
520
629
  };
521
630
  }
631
+ function isGood(quality) {
632
+ return String(quality).toLowerCase().startsWith("good");
633
+ }
522
634
 
523
635
  // src/tools/query-telemetry.ts
524
- import { z as z6 } from "zod";
525
- var queryTelemetrySchema = z6.object({
526
- elementIds: z6.array(z6.string()).min(1).describe("Array of asset elementIds to query (e.g. ['Pump_101', 'Boiler_101'])"),
527
- mode: z6.enum(["current", "history"]).optional().default("current").describe(
636
+ import { z as z7 } from "zod";
637
+ var queryTelemetrySchema = z7.object({
638
+ elementIds: elementIdsSchema(
639
+ "Array of asset elementIds to query (e.g. ['Pump_101', 'Boiler_101'])"
640
+ ),
641
+ mode: z7.enum(["current", "history"]).optional().default("current").describe(
528
642
  "Query mode: 'current' for live VQT telemetry, 'history' for time-series trend analysis"
529
643
  ),
530
- startTime: z6.string().optional().describe(
531
- "ISO 8601 start time or relative time string (e.g. '-2h', '-30m', '-1d') for history mode"
644
+ startTime: z7.string().optional().describe(
645
+ "ISO 8601 start time or relative time string (e.g. '-2h', '-30m', '-1d') for history mode (default: -1h)"
646
+ ),
647
+ endTime: z7.string().optional().describe(
648
+ "ISO 8601 end time or relative time string (e.g. '-10m') for history mode (default: now)"
532
649
  ),
533
- endTime: z6.string().optional().describe("ISO 8601 end time (defaults to now) for history mode"),
534
- aggregateStats: z6.boolean().optional().default(true).describe(
650
+ maxDepth: z7.number().int().min(0).optional().default(2).describe(
651
+ "Read depth through components: 1 = the element only, 2 = with its components (default), 0 = unlimited"
652
+ ),
653
+ aggregateStats: z7.boolean().optional().default(true).describe(
535
654
  "Whether to pre-compute min/max/mean statistical summaries for numeric sensor streams"
536
655
  )
537
656
  });
538
- function parseStartTime(input) {
539
- if (!input) {
540
- const d = /* @__PURE__ */ new Date();
541
- d.setHours(d.getHours() - 1);
542
- return d.toISOString();
543
- }
544
- const match = input.match(/^-(\d+)([smhdw])$/i);
657
+ var RELATIVE_TIME = /^-(\d+)([smhdw])$/i;
658
+ var UNIT_MS = {
659
+ s: 1e3,
660
+ m: 6e4,
661
+ h: 36e5,
662
+ d: 864e5,
663
+ w: 6048e5
664
+ };
665
+ function parseTime(input, fallback, name) {
666
+ if (!input) return fallback.toISOString();
667
+ const match = input.match(RELATIVE_TIME);
545
668
  if (match) {
546
- const amount = parseInt(match[1], 10);
547
- const unit = match[2].toLowerCase();
548
- const d = /* @__PURE__ */ new Date();
549
- switch (unit) {
550
- case "s":
551
- d.setSeconds(d.getSeconds() - amount);
552
- break;
553
- case "m":
554
- d.setMinutes(d.getMinutes() - amount);
555
- break;
556
- case "h":
557
- d.setHours(d.getHours() - amount);
558
- break;
559
- case "d":
560
- d.setDate(d.getDate() - amount);
561
- break;
562
- case "w":
563
- d.setDate(d.getDate() - amount * 7);
564
- break;
565
- }
566
- return d.toISOString();
669
+ const amount = Number.parseInt(match[1], 10);
670
+ return new Date(
671
+ Date.now() - amount * UNIT_MS[match[2].toLowerCase()]
672
+ ).toISOString();
567
673
  }
568
674
  const parsed = new Date(input);
569
675
  if (Number.isNaN(parsed.getTime())) {
570
- const defaultDate = /* @__PURE__ */ new Date();
571
- defaultDate.setHours(defaultDate.getHours() - 1);
572
- return defaultDate.toISOString();
676
+ throw new Error(
677
+ `Invalid ${name} '${input}': expected ISO 8601 (e.g. '2026-10-25T10:00:00Z') or a relative time (e.g. '-2h').`
678
+ );
573
679
  }
574
680
  return parsed.toISOString();
575
681
  }
682
+ function describeDetail(d) {
683
+ return [d.status, d.title, d.detail].filter(Boolean).join(" ");
684
+ }
685
+ function isBadQuality(quality) {
686
+ return quality != null && !String(quality).toLowerCase().startsWith("good");
687
+ }
688
+ function seriesStatistics(values) {
689
+ const numericValues = [];
690
+ let badQualityCount = 0;
691
+ for (const v of values) {
692
+ if (isBadQuality(v.quality)) badQualityCount++;
693
+ if (typeof v.value === "number" && !Number.isNaN(v.value))
694
+ numericValues.push(v.value);
695
+ }
696
+ if (numericValues.length === 0) return { count: values.length, badQualityCount };
697
+ const sum = numericValues.reduce((a, b) => a + b, 0);
698
+ return {
699
+ min: Math.min(...numericValues),
700
+ max: Math.max(...numericValues),
701
+ mean: Math.round(sum / numericValues.length * 100) / 100,
702
+ count: numericValues.length,
703
+ badQualityCount
704
+ };
705
+ }
576
706
  async function handleQueryTelemetry(binding, args) {
577
- const { elementIds, mode, aggregateStats } = args;
707
+ const { elementIds, mode, aggregateStats, maxDepth } = args;
578
708
  if (mode === "current") {
579
- const values = await binding.getObjectValues(elementIds);
709
+ const values = await binding.getObjectValues(elementIds, maxDepth);
580
710
  return {
581
711
  mode: "current",
582
712
  telemetry: values
583
713
  };
584
714
  }
585
- const startTime = parseStartTime(args.startTime);
586
- const endTimeDate = args.endTime ? new Date(args.endTime) : /* @__PURE__ */ new Date();
587
- const endTime = Number.isNaN(endTimeDate.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : endTimeDate.toISOString();
588
- const history = await binding.getHistoricalValues(elementIds, startTime, endTime);
715
+ const now = /* @__PURE__ */ new Date();
716
+ const startTime = parseTime(
717
+ args.startTime,
718
+ new Date(now.getTime() - UNIT_MS.h),
719
+ "startTime"
720
+ );
721
+ const endTime = parseTime(args.endTime, now, "endTime");
722
+ if (startTime > endTime) {
723
+ throw new Error(`startTime ${startTime} is after endTime ${endTime}.`);
724
+ }
725
+ const history = await binding.getHistoricalValues(
726
+ elementIds,
727
+ startTime,
728
+ endTime,
729
+ maxDepth
730
+ );
589
731
  if (!aggregateStats) {
590
732
  return {
591
733
  mode: "history",
@@ -593,41 +735,27 @@ async function handleQueryTelemetry(binding, args) {
593
735
  history
594
736
  };
595
737
  }
596
- const reports = history.map((record, idx) => {
597
- const elementId = elementIds[idx] || `element_${idx}`;
738
+ const reports = history.map(({ elementId, result, error }) => {
739
+ if (error || !result) return { elementId, error: error ?? "No history returned" };
598
740
  const report = {
599
741
  elementId,
600
- timeRange: { startTime, endTime },
601
- dataPoints: record.values || []
742
+ dataPoints: result.values ?? [],
743
+ statistics: seriesStatistics(result.values ?? [])
602
744
  };
603
- const numericValues = [];
604
- let badQualityCount = 0;
605
- for (const v of record.values || []) {
606
- if (v.quality && String(v.quality).toUpperCase() !== "GOOD") {
607
- badQualityCount++;
608
- }
609
- if (typeof v.value === "number" && !Number.isNaN(v.value)) {
610
- numericValues.push(v.value);
745
+ if (result.components) {
746
+ const entries = Object.entries(result.components);
747
+ report.components = Object.fromEntries(
748
+ entries.map(([componentId, { values, responseDetail }]) => [
749
+ componentId,
750
+ // A failed component read is not an empty time range: report why.
751
+ responseDetail ? { error: describeDetail(responseDetail) } : { dataPoints: values ?? [], statistics: seriesStatistics(values ?? []) }
752
+ ])
753
+ );
754
+ const failed = entries.filter(([, c]) => c.responseDetail);
755
+ if (failed.length > 0 && failed.length === entries.length && !result.values?.length) {
756
+ report.note = `No component history could be read (${describeDetail(failed[0][1].responseDetail)}): the server keeps no history for this asset's variables.`;
611
757
  }
612
758
  }
613
- if (numericValues.length > 0) {
614
- const min = Math.min(...numericValues);
615
- const max = Math.max(...numericValues);
616
- const sum = numericValues.reduce((a, b) => a + b, 0);
617
- const mean = Math.round(sum / numericValues.length * 100) / 100;
618
- report.statistics = {
619
- min,
620
- max,
621
- mean,
622
- count: numericValues.length,
623
- badQualityCount
624
- };
625
- } else {
626
- report.statistics = {
627
- count: (record.values || []).length,
628
- badQualityCount
629
- };
630
- }
631
759
  return report;
632
760
  });
633
761
  return {
@@ -637,16 +765,41 @@ async function handleQueryTelemetry(binding, args) {
637
765
  };
638
766
  }
639
767
 
768
+ // src/utils/output-limit.ts
769
+ var DEFAULT_MAX_RESULT_CHARS = 1e5;
770
+ var NARROWING_HINTS = {
771
+ i3x_explore_plant_hierarchy: "pass rootElementId, or lower maxDepth or maxNodes",
772
+ i3x_inspect_asset: "lower maxDepth, or set includeSchema to false",
773
+ i3x_query_telemetry: "pass fewer elementIds, lower maxDepth, or shorten the time range",
774
+ i3x_discover_types_and_schemas: "pass typeElementId or namespaceUri",
775
+ i3x_monitor_asset_events: "pass fewer elementIds or lower maxDepth"
776
+ };
777
+ function serializeToolResult(tool, result, maxChars) {
778
+ const text = JSON.stringify(result, null, 2);
779
+ if (text.length <= maxChars) return text;
780
+ const hint = NARROWING_HINTS[tool] ?? "narrow the query";
781
+ return `${text.slice(0, maxChars)}
782
+
783
+ [Result truncated: showing the first ${maxChars} of ${text.length} characters, so the JSON above is incomplete. To see everything, ${hint}.]`;
784
+ }
785
+
640
786
  // src/server.ts
787
+ var { version } = createRequire(import.meta.url)("../package.json");
641
788
  var NodeI3xMcpServer = class {
642
789
  server;
643
790
  binding;
644
- constructor(binding) {
791
+ maxResultChars;
792
+ /**
793
+ * @param options.maxResultChars - cap on a serialized tool result (default
794
+ * I3X_MCP_MAX_RESULT_CHARS, else 100 000 characters).
795
+ */
796
+ constructor(binding, options = {}) {
645
797
  this.binding = binding || new RestClientBinding();
798
+ this.maxResultChars = options.maxResultChars ?? (Number(process.env.I3X_MCP_MAX_RESULT_CHARS) || DEFAULT_MAX_RESULT_CHARS);
646
799
  this.server = new Server(
647
800
  {
648
801
  name: "@node-i3x/mcp",
649
- version: "0.10.0"
802
+ version
650
803
  },
651
804
  {
652
805
  capabilities: {
@@ -678,11 +831,11 @@ var NodeI3xMcpServer = class {
678
831
  },
679
832
  authToken: {
680
833
  type: "string",
681
- description: "Optional Bearer authentication token for secured endpoints"
834
+ description: "Optional Bearer authentication token. Omit it to keep the current token when re-connecting to the same URL."
682
835
  },
683
836
  verifyConnection: {
684
837
  type: "boolean",
685
- description: "Whether to verify connectivity by querying server /v1/info (default: true)"
838
+ description: "Whether to verify connectivity and credentials before switching (default: true); a rejected token returns status auth_failed"
686
839
  }
687
840
  },
688
841
  required: ["serverUrl"]
@@ -705,6 +858,10 @@ var NodeI3xMcpServer = class {
705
858
  includeSchema: {
706
859
  type: "boolean",
707
860
  description: "Whether to include the ObjectType JSON Schema definition (default: true)"
861
+ },
862
+ maxDepth: {
863
+ type: "number",
864
+ description: "Value read depth through components: 1 = the element only, 2 = with its components (default), 0 = unlimited"
708
865
  }
709
866
  },
710
867
  required: ["elementId"]
@@ -722,7 +879,11 @@ var NodeI3xMcpServer = class {
722
879
  },
723
880
  maxDepth: {
724
881
  type: "number",
725
- description: "Maximum depth to traverse child components (1 to 5, default: 2)"
882
+ description: "Levels to show: 1 = roots only, 2 = roots and their children (default), 0 = unlimited (bounded by maxNodes)"
883
+ },
884
+ maxNodes: {
885
+ type: "number",
886
+ description: "Node budget for the whole tree (default 500, max 5000); the result is marked truncated when reached"
726
887
  }
727
888
  }
728
889
  }
@@ -736,7 +897,7 @@ var NodeI3xMcpServer = class {
736
897
  elementIds: {
737
898
  type: "array",
738
899
  items: { type: "string" },
739
- description: "Array of asset elementIds to query (e.g. ['Pump_101', 'Boiler_101'])"
900
+ description: "Array of asset elementIds to query (e.g. ['Pump_101', 'Boiler_101']); duplicates are ignored, at most 100"
740
901
  },
741
902
  mode: {
742
903
  type: "string",
@@ -749,7 +910,11 @@ var NodeI3xMcpServer = class {
749
910
  },
750
911
  endTime: {
751
912
  type: "string",
752
- description: "ISO 8601 end time (defaults to now) for history mode"
913
+ description: "ISO 8601 or relative duration (e.g. '-10m') end time (defaults to now) for history mode"
914
+ },
915
+ maxDepth: {
916
+ type: "number",
917
+ description: "Value read depth through components: 1 = the element only, 2 = with its components (default), 0 = unlimited"
753
918
  },
754
919
  aggregateStats: {
755
920
  type: "boolean",
@@ -778,18 +943,18 @@ var NodeI3xMcpServer = class {
778
943
  },
779
944
  {
780
945
  name: "i3x_monitor_asset_events",
781
- description: "Create an i3X subscription stream to monitor real-time VQT value changes or threshold alert conditions.",
946
+ description: "One-shot health check of assets: current VQT values plus an alert list of every series whose quality is not Good. Does not subscribe; call again to re-check.",
782
947
  inputSchema: {
783
948
  type: "object",
784
949
  properties: {
785
950
  elementIds: {
786
951
  type: "array",
787
952
  items: { type: "string" },
788
- description: "Array of asset elementIds to monitor"
953
+ description: "Array of asset elementIds to check; duplicates are ignored, at most 100"
789
954
  },
790
- condition: {
791
- type: "string",
792
- description: "Optional alert condition filter (e.g. 'temperature > 85 || quality != Good')"
955
+ maxDepth: {
956
+ type: "number",
957
+ description: "Value read depth through components: 1 = the element only, 2 = with its components (default), 0 = unlimited"
793
958
  }
794
959
  },
795
960
  required: ["elementIds"]
@@ -804,9 +969,13 @@ var NodeI3xMcpServer = class {
804
969
  switch (name) {
805
970
  case "i3x_connect": {
806
971
  const parsed = connectSchema.parse(rawArgs);
807
- result = await handleConnect(parsed, (newBinding) => {
808
- this.binding = newBinding;
809
- });
972
+ result = await handleConnect(
973
+ parsed,
974
+ (newBinding) => {
975
+ this.binding = newBinding;
976
+ },
977
+ this.binding
978
+ );
810
979
  break;
811
980
  }
812
981
  case "i3x_inspect_asset": {
@@ -841,7 +1010,7 @@ var NodeI3xMcpServer = class {
841
1010
  content: [
842
1011
  {
843
1012
  type: "text",
844
- text: JSON.stringify(result, null, 2)
1013
+ text: serializeToolResult(name, result, this.maxResultChars)
845
1014
  }
846
1015
  ]
847
1016
  };
@@ -915,7 +1084,6 @@ var NodeI3xMcpServer = class {
915
1084
  }
916
1085
  };
917
1086
  export {
918
- InProcessConnectorBinding,
919
1087
  NodeI3xMcpServer,
920
1088
  RestClientBinding,
921
1089
  discoverTypesSchema,
@@ -927,6 +1095,7 @@ export {
927
1095
  handleQueryTelemetry,
928
1096
  inspectAssetSchema,
929
1097
  monitorEventsSchema,
1098
+ parseTime,
930
1099
  queryTelemetrySchema
931
1100
  };
932
1101
  //# sourceMappingURL=index.js.map