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