@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/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 +17 -3
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
50
|
+
var OBJECT_CACHE_TTL_MS = 6e4;
|
|
51
|
+
var RestClientBinding = class _RestClientBinding {
|
|
162
52
|
baseUrl;
|
|
163
53
|
headers;
|
|
164
54
|
requestTimeoutMs;
|
|
165
|
-
|
|
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.
|
|
163
|
+
return this.get("/v1/info");
|
|
209
164
|
}
|
|
210
165
|
async getNamespaces() {
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
`/v1/objecttypes${query}`
|
|
218
|
-
);
|
|
219
|
-
return res.objectTypes || [];
|
|
170
|
+
return this.get(`/v1/objecttypes${query}`);
|
|
220
171
|
}
|
|
221
172
|
async getRelationshipTypes() {
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
"/v1/objects/
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
+
return this.postBulkPerElement(
|
|
279
224
|
"/v1/objects/history",
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
body: JSON.stringify({ elementIds, startTime, endTime, maxDepth })
|
|
283
|
-
}
|
|
225
|
+
elementIds,
|
|
226
|
+
{ startTime, endTime, maxDepth }
|
|
284
227
|
);
|
|
285
|
-
return res.history || [];
|
|
286
228
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
315
|
-
|
|
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
|
|
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:
|
|
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 ${
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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"
|
|
484
|
-
const targetType = schemaResult.value
|
|
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"
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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,
|
|
513
|
-
const
|
|
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: "
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
525
|
-
var queryTelemetrySchema =
|
|
526
|
-
elementIds:
|
|
527
|
-
|
|
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:
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
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((
|
|
597
|
-
|
|
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
|
-
|
|
601
|
-
|
|
742
|
+
dataPoints: result.values ?? [],
|
|
743
|
+
statistics: seriesStatistics(result.values ?? [])
|
|
602
744
|
};
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
953
|
+
description: "Array of asset elementIds to check; duplicates are ignored, at most 100"
|
|
789
954
|
},
|
|
790
|
-
|
|
791
|
-
type: "
|
|
792
|
-
description: "
|
|
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(
|
|
808
|
-
|
|
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:
|
|
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
|