@yarkivaev/scada 2.3.84 → 2.3.86
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/README.md +20 -0
- package/index.js +4 -0
- package/package.json +4 -2
- package/src/application/mcp/analyticsMcp.js +376 -0
- package/src/application/mcp/mcpCoverage.js +95 -0
- package/src/application/mcp/mcpJournal.js +102 -0
- package/src/application/mcp/mcpMetrics.js +69 -0
- package/src/application/mcp/mcpObserve.js +168 -0
- package/src/application/mcp/mcpQuery.js +80 -0
- package/src/application/mcp/mcpReads.js +221 -0
- package/src/application/mcp/mcpResources.js +106 -0
- package/src/application/mcp/mcpText.js +106 -0
- package/src/domain/namespace/namespaceTree.js +182 -0
- package/src/domain/span/coverageSpans.js +229 -0
- package/src/domain/sql/sqlGuard.js +175 -0
- package/src/domain/sql/sqlShape.js +16 -0
- package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +14 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import note from './mcpJournal.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps a tool name to the engine gauge label.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} name - tool name
|
|
7
|
+
* @returns {string} clickhouse, postgres, or plant
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* engineOf('query_clickhouse');
|
|
11
|
+
*/
|
|
12
|
+
function engineOf(name) {
|
|
13
|
+
if (name === 'query_clickhouse') {
|
|
14
|
+
return 'clickhouse';
|
|
15
|
+
}
|
|
16
|
+
if (name === 'query_postgres') {
|
|
17
|
+
return 'postgres';
|
|
18
|
+
}
|
|
19
|
+
return 'plant';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reads ok or error from a tool result.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} result - tool result
|
|
26
|
+
* @returns {string} status
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* statusOf({ isError: true });
|
|
30
|
+
*/
|
|
31
|
+
function statusOf(result) {
|
|
32
|
+
if (result && result.isError === true) {
|
|
33
|
+
return 'error';
|
|
34
|
+
}
|
|
35
|
+
return 'ok';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runs a callback and keeps the status and any thrown error.
|
|
40
|
+
*
|
|
41
|
+
* @param {Function} cb - tool or resource callback
|
|
42
|
+
* @param {object} args - first callback argument
|
|
43
|
+
* @param {object} extra - SDK extra
|
|
44
|
+
* @returns {Promise<object>} outcome
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* invoke(() => ({ content: [] }), {}, {});
|
|
48
|
+
*/
|
|
49
|
+
async function invoke(cb, args, extra) {
|
|
50
|
+
try {
|
|
51
|
+
const result = await cb(args, extra);
|
|
52
|
+
return { result, status: statusOf(result) };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return { err, status: 'error' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the callback result or rethrows its error.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} outcome - invoke outcome
|
|
62
|
+
* @returns {object} result
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* settle({ result: { content: [] }, status: 'ok' });
|
|
66
|
+
*/
|
|
67
|
+
function settle(outcome) {
|
|
68
|
+
if (outcome.err) {
|
|
69
|
+
throw outcome.err;
|
|
70
|
+
}
|
|
71
|
+
return outcome.result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Times one tool call, counts it, and tracks the engine gauge.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} name - tool name
|
|
78
|
+
* @param {object} args - tool arguments
|
|
79
|
+
* @param {object} extra - SDK extra
|
|
80
|
+
* @param {Function} cb - tool callback
|
|
81
|
+
* @param {object} ctx - journal context
|
|
82
|
+
* @returns {Promise<object>} tool result
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* trackedTool('state', {}, {}, () => ({ content: [] }), ctx);
|
|
86
|
+
*/
|
|
87
|
+
async function trackedTool(name, args, extra, cb, ctx) {
|
|
88
|
+
const engine = engineOf(name);
|
|
89
|
+
const started = Date.now();
|
|
90
|
+
ctx.metrics.begin(engine);
|
|
91
|
+
const outcome = await invoke(cb, args, extra);
|
|
92
|
+
ctx.metrics.end(engine);
|
|
93
|
+
note(ctx, name, outcome.status, started, args);
|
|
94
|
+
return settle(outcome);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Times one resource read.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} uri - resource URI
|
|
101
|
+
* @param {object} target - SDK uri argument
|
|
102
|
+
* @param {object} extra - SDK extra
|
|
103
|
+
* @param {Function} cb - read callback
|
|
104
|
+
* @param {object} ctx - journal context
|
|
105
|
+
* @returns {Promise<object>} resource result
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* trackedRead('plant://topology', new URL('plant://topology'), {}, () => ({}), ctx);
|
|
109
|
+
*/
|
|
110
|
+
async function trackedRead(uri, target, extra, cb, ctx) {
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
const outcome = await invoke(cb, target, extra);
|
|
113
|
+
note(ctx, String(uri), outcome.status, started);
|
|
114
|
+
return settle(outcome);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wraps registerTool so every tools/call is journaled.
|
|
119
|
+
*
|
|
120
|
+
* @param {Function} registerTool - McpServer.registerTool
|
|
121
|
+
* @param {object} ctx - journal context
|
|
122
|
+
* @returns {Function} wrapped register
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* wrapTool(server.registerTool, ctx);
|
|
126
|
+
*/
|
|
127
|
+
function wrapTool(registerTool, ctx) {
|
|
128
|
+
return function wrap(name, config, cb) {
|
|
129
|
+
return registerTool(name, config, (args, extra) => {
|
|
130
|
+
return trackedTool(name, args, extra, cb, ctx);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wraps registerResource so every resources/read is journaled.
|
|
137
|
+
*
|
|
138
|
+
* @param {Function} registerResource - McpServer.registerResource
|
|
139
|
+
* @param {object} ctx - journal context
|
|
140
|
+
* @returns {Function} wrapped register
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* wrapResource(server.registerResource, ctx);
|
|
144
|
+
*/
|
|
145
|
+
function wrapResource(registerResource, ctx) {
|
|
146
|
+
return function wrap(name, uri, config, cb) {
|
|
147
|
+
return registerResource(name, uri, config, (target, extra) => {
|
|
148
|
+
return trackedRead(uri, target, extra, cb, ctx);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Journals later tool and resource registrations on this server.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} server - McpServer
|
|
157
|
+
* @param {object} ctx - caller, journal, metrics
|
|
158
|
+
* @returns {void}
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* observe(server, { caller: 'analyst', journal() {}, metrics });
|
|
162
|
+
*/
|
|
163
|
+
export default function observe(server, ctx) {
|
|
164
|
+
const registerResource = server.registerResource.bind(server);
|
|
165
|
+
const registerTool = server.registerTool.bind(server);
|
|
166
|
+
server.registerResource = wrapResource(registerResource, ctx);
|
|
167
|
+
server.registerTool = wrapTool(registerTool, ctx);
|
|
168
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import sqlGuard from '../../domain/sql/sqlGuard.js';
|
|
3
|
+
import { capRows, toolText } from './mcpText.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guards SQL, runs the port, and caps rows.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} sql - statement
|
|
9
|
+
* @param {string} engine - clickhouse or postgres
|
|
10
|
+
* @param {Function} query - port
|
|
11
|
+
* @returns {Promise<object>} tool result
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* runQuery('SELECT 1', 'postgres', () => []);
|
|
15
|
+
*/
|
|
16
|
+
async function runQuery(sql, engine, query) {
|
|
17
|
+
const guarded = sqlGuard(sql, engine);
|
|
18
|
+
const rows = await query(guarded);
|
|
19
|
+
return toolText(capRows(rows));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registers read-only ClickHouse SQL.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} server - McpServer
|
|
26
|
+
* @param {object} ports - read ports
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* registerClickhouse(server, ports);
|
|
31
|
+
*/
|
|
32
|
+
function registerClickhouse(server, ports) {
|
|
33
|
+
server.registerTool('query_clickhouse', {
|
|
34
|
+
description: 'Read-only ClickHouse SQL with a ts predicate',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
sql: z.string()
|
|
37
|
+
}
|
|
38
|
+
}, (args) => {
|
|
39
|
+
return runQuery(args.sql, 'clickhouse', ports.queryClickhouse);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Registers read-only Postgres SQL when a standby port exists.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} server - McpServer
|
|
47
|
+
* @param {object} ports - read ports
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* registerPostgres(server, ports);
|
|
52
|
+
*/
|
|
53
|
+
function registerPostgres(server, ports) {
|
|
54
|
+
if (typeof ports.queryPostgres !== 'function') {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
server.registerTool('query_postgres', {
|
|
58
|
+
description: 'Read-only Postgres SQL',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
sql: z.string()
|
|
61
|
+
}
|
|
62
|
+
}, (args) => {
|
|
63
|
+
return runQuery(args.sql, 'postgres', ports.queryPostgres);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Registers SQL tools. Postgres is omitted without a standby port.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} server - McpServer
|
|
71
|
+
* @param {object} ports - read ports
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* registerQueries(server, ports);
|
|
76
|
+
*/
|
|
77
|
+
export default function registerQueries(server, ports) {
|
|
78
|
+
registerClickhouse(server, ports);
|
|
79
|
+
registerPostgres(server, ports);
|
|
80
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { capMeasurements, capRows, toolText } from './mcpText.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads rows for one namespace path and caps them.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} tree - namespaceTree
|
|
8
|
+
* @param {string} path - namespace path
|
|
9
|
+
* @param {Function} load - port call
|
|
10
|
+
* @returns {Promise<object>} tool result
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* loadRows(tree, 'site', () => []);
|
|
14
|
+
*/
|
|
15
|
+
async function loadRows(tree, path, load) {
|
|
16
|
+
tree.resolve(path);
|
|
17
|
+
const rows = await load();
|
|
18
|
+
return toolText(capRows(rows));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds age and live or stale from the node silence threshold.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} raw - state port payload
|
|
25
|
+
* @param {number} silenceSeconds - liveness seconds
|
|
26
|
+
* @returns {object} state payload
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* fresh({ found: false }, 60);
|
|
30
|
+
*/
|
|
31
|
+
function fresh(raw, silenceSeconds) {
|
|
32
|
+
if (raw && raw.found === true && typeof raw.time === 'string') {
|
|
33
|
+
const ageMs = Date.now() - Date.parse(raw.time);
|
|
34
|
+
const freshness = ageMs <= silenceSeconds * 1000 ? 'live' : 'stale';
|
|
35
|
+
return { ...raw, ageMs, freshness };
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reads metric points for namespace paths.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} args - paths, from, to, step, mode
|
|
44
|
+
* @param {object} ports - read ports
|
|
45
|
+
* @param {object} tree - namespaceTree
|
|
46
|
+
* @returns {Promise<object>} tool result
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* runMeasurements({ paths: ['site'], from: 'a', to: 'b', step: 60 }, ports, tree);
|
|
50
|
+
*/
|
|
51
|
+
async function runMeasurements(args, ports, tree) {
|
|
52
|
+
for (const path of args.paths) {
|
|
53
|
+
tree.resolve(path);
|
|
54
|
+
}
|
|
55
|
+
const raw = await ports.measurements(args);
|
|
56
|
+
return toolText(capMeasurements(raw, args.step));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reads the latest values under a path.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} args - path
|
|
63
|
+
* @param {object} ports - read ports
|
|
64
|
+
* @param {object} tree - namespaceTree
|
|
65
|
+
* @returns {Promise<object>} tool result
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* runState({ path: 'site' }, ports, tree);
|
|
69
|
+
*/
|
|
70
|
+
async function runState(args, ports, tree) {
|
|
71
|
+
const node = tree.resolve(args.path);
|
|
72
|
+
const raw = await ports.state(args);
|
|
73
|
+
return toolText(fresh(raw, node.silenceSeconds));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Registers the measurements tool.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} server - McpServer
|
|
80
|
+
* @param {object} ports - read ports
|
|
81
|
+
* @param {object} tree - namespaceTree
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* registerMeasurements(server, ports, tree);
|
|
86
|
+
*/
|
|
87
|
+
function registerMeasurements(server, ports, tree) {
|
|
88
|
+
server.registerTool('measurements', {
|
|
89
|
+
description: 'Metric points for namespace paths over an interval',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
from: z.string(),
|
|
92
|
+
mode: z.string().optional(),
|
|
93
|
+
paths: z.array(z.string()),
|
|
94
|
+
step: z.number(),
|
|
95
|
+
to: z.string()
|
|
96
|
+
}
|
|
97
|
+
}, (args) => {
|
|
98
|
+
return runMeasurements(args, ports, tree);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Registers the segments tool.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} server - McpServer
|
|
106
|
+
* @param {object} ports - read ports
|
|
107
|
+
* @param {object} tree - namespaceTree
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* registerSegments(server, ports, tree);
|
|
112
|
+
*/
|
|
113
|
+
function registerSegments(server, ports, tree) {
|
|
114
|
+
server.registerTool('segments', {
|
|
115
|
+
description: 'Timeline segments for a namespace path',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
from: z.string(),
|
|
118
|
+
kinds: z.array(z.string()).optional(),
|
|
119
|
+
path: z.string(),
|
|
120
|
+
to: z.string()
|
|
121
|
+
}
|
|
122
|
+
}, (args) => {
|
|
123
|
+
return loadRows(tree, args.path, () => {
|
|
124
|
+
return ports.segments(args);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Registers the operations tool.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} server - McpServer
|
|
133
|
+
* @param {object} ports - read ports
|
|
134
|
+
* @param {object} tree - namespaceTree
|
|
135
|
+
* @returns {void}
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* registerOperations(server, ports, tree);
|
|
139
|
+
*/
|
|
140
|
+
function registerOperations(server, ports, tree) {
|
|
141
|
+
server.registerTool('operations', {
|
|
142
|
+
description: 'Operation rows for a namespace path and an opaque kind',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
from: z.string(),
|
|
145
|
+
kind: z.string(),
|
|
146
|
+
path: z.string(),
|
|
147
|
+
to: z.string()
|
|
148
|
+
}
|
|
149
|
+
}, (args) => {
|
|
150
|
+
return loadRows(tree, args.path, () => {
|
|
151
|
+
return ports.operations(args);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Registers the alerts tool.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} server - McpServer
|
|
160
|
+
* @param {object} ports - read ports
|
|
161
|
+
* @param {object} tree - namespaceTree
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* registerAlerts(server, ports, tree);
|
|
166
|
+
*/
|
|
167
|
+
function registerAlerts(server, ports, tree) {
|
|
168
|
+
server.registerTool('alerts', {
|
|
169
|
+
description: 'Alert rows for a namespace path',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
from: z.string(),
|
|
172
|
+
path: z.string(),
|
|
173
|
+
to: z.string()
|
|
174
|
+
}
|
|
175
|
+
}, (args) => {
|
|
176
|
+
return loadRows(tree, args.path, () => {
|
|
177
|
+
return ports.alerts(args);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Registers the state tool.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} server - McpServer
|
|
186
|
+
* @param {object} ports - read ports
|
|
187
|
+
* @param {object} tree - namespaceTree
|
|
188
|
+
* @returns {void}
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* registerState(server, ports, tree);
|
|
192
|
+
*/
|
|
193
|
+
function registerState(server, ports, tree) {
|
|
194
|
+
server.registerTool('state', {
|
|
195
|
+
description: 'Latest readings under a namespace path',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
path: z.string()
|
|
198
|
+
}
|
|
199
|
+
}, (args) => {
|
|
200
|
+
return runState(args, ports, tree);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Registers measurement, segment, operation, alert, and state tools.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} server - McpServer
|
|
208
|
+
* @param {object} ports - read ports
|
|
209
|
+
* @param {object} tree - namespaceTree
|
|
210
|
+
* @returns {void}
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* registerReads(server, ports, tree);
|
|
214
|
+
*/
|
|
215
|
+
export default function registerReads(server, ports, tree) {
|
|
216
|
+
registerMeasurements(server, ports, tree);
|
|
217
|
+
registerSegments(server, ports, tree);
|
|
218
|
+
registerOperations(server, ports, tree);
|
|
219
|
+
registerAlerts(server, ports, tree);
|
|
220
|
+
registerState(server, ports, tree);
|
|
221
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent path of a node, or an empty string at the root.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} path - namespace path
|
|
5
|
+
* @returns {string} parent path
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* parentOf('site/shop');
|
|
9
|
+
*/
|
|
10
|
+
function parentOf(path) {
|
|
11
|
+
const cut = path.lastIndexOf('/');
|
|
12
|
+
if (cut < 0) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return path.slice(0, cut);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Topology resource body without source topics.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} tree - namespaceTree
|
|
22
|
+
* @returns {Array<object>} nodes
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* topology(tree);
|
|
26
|
+
*/
|
|
27
|
+
function topology(tree) {
|
|
28
|
+
return tree.list().map((node) => {
|
|
29
|
+
return {
|
|
30
|
+
kind: node.kind,
|
|
31
|
+
liveness: node.silenceSeconds,
|
|
32
|
+
parent: parentOf(node.path),
|
|
33
|
+
path: node.path,
|
|
34
|
+
title: node.title
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Binding resource body: path, source topic, and unit.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} tree - namespaceTree
|
|
43
|
+
* @returns {Array<object>} bindings
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* bindings(tree);
|
|
47
|
+
*/
|
|
48
|
+
function bindings(tree) {
|
|
49
|
+
return tree.list().filter((node) => {
|
|
50
|
+
return typeof node.source === 'string';
|
|
51
|
+
}).map((node) => {
|
|
52
|
+
return {
|
|
53
|
+
path: node.path,
|
|
54
|
+
topic: node.source,
|
|
55
|
+
unit: typeof node.unit === 'string' ? node.unit : ''
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JSON resource contents for one URI.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} uri - resource URI
|
|
64
|
+
* @param {object} payload - JSON body
|
|
65
|
+
* @returns {object} read result
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* body('plant://topology', []);
|
|
69
|
+
*/
|
|
70
|
+
function body(uri, payload) {
|
|
71
|
+
return {
|
|
72
|
+
contents: [{
|
|
73
|
+
mimeType: 'application/json',
|
|
74
|
+
text: JSON.stringify(payload),
|
|
75
|
+
uri
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registers plant topology and source bindings.
|
|
82
|
+
*
|
|
83
|
+
* Topology has paths, kinds, titles, and liveness. Bindings map a path to a
|
|
84
|
+
* topic and a unit. Gateway topics are not part of topology.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} server - McpServer
|
|
87
|
+
* @param {object} tree - namespaceTree
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* registerResources(server, tree);
|
|
92
|
+
*/
|
|
93
|
+
export default function registerResources(server, tree) {
|
|
94
|
+
server.registerResource('topology', 'plant://topology', {
|
|
95
|
+
description: 'Paths, kinds, titles, and liveness',
|
|
96
|
+
mimeType: 'application/json'
|
|
97
|
+
}, () => {
|
|
98
|
+
return body('plant://topology', topology(tree));
|
|
99
|
+
});
|
|
100
|
+
server.registerResource('bindings', 'plant://bindings', {
|
|
101
|
+
description: 'Source topic and unit for each bound path',
|
|
102
|
+
mimeType: 'application/json'
|
|
103
|
+
}, () => {
|
|
104
|
+
return body('plant://bindings', bindings(tree));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caps a row list at the analytics response limit.
|
|
3
|
+
*
|
|
4
|
+
* @param {Array} rows - port rows
|
|
5
|
+
* @returns {{rows: Array, truncated: boolean}} capped rows
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* capRows([{ id: 1 }]);
|
|
9
|
+
*/
|
|
10
|
+
export function capRows(rows) {
|
|
11
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
12
|
+
return {
|
|
13
|
+
rows: list.slice(0, 200),
|
|
14
|
+
truncated: list.length > 200
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a payload as MCP text content.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} payload - JSON payload
|
|
22
|
+
* @returns {object} tool result
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* toolText({ rows: [] });
|
|
26
|
+
*/
|
|
27
|
+
export function toolText(payload) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
text: JSON.stringify(payload),
|
|
31
|
+
type: 'text'
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads a point list from an array or a points field.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array|object} raw - port payload
|
|
40
|
+
* @returns {Array} points
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* readPoints({ points: [1] });
|
|
44
|
+
*/
|
|
45
|
+
function readPoints(raw) {
|
|
46
|
+
if (Array.isArray(raw)) {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
if (raw && Array.isArray(raw.points)) {
|
|
50
|
+
return raw.points;
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reads the step the port actually used.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array|object} raw - port payload
|
|
59
|
+
* @param {number} step - requested step seconds
|
|
60
|
+
* @returns {number} step seconds
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* readStep({ points: [], step: 30 }, 15);
|
|
64
|
+
*/
|
|
65
|
+
function readStep(raw, step) {
|
|
66
|
+
if (raw && typeof raw.step === 'number') {
|
|
67
|
+
return raw.step;
|
|
68
|
+
}
|
|
69
|
+
return step;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keeps a truncated flag supplied by the port.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array|object} raw - port payload
|
|
76
|
+
* @returns {boolean} whether the port already truncated
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* flagged({ truncated: true });
|
|
80
|
+
*/
|
|
81
|
+
function flagged(raw) {
|
|
82
|
+
if (raw && raw.truncated === true) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Caps measurement points at 200 and reports the step.
|
|
90
|
+
*
|
|
91
|
+
* @param {Array|object} raw - port payload
|
|
92
|
+
* @param {number} step - requested step seconds
|
|
93
|
+
* @returns {object} points, step, truncated
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* capMeasurements([{ v: 1 }], 15);
|
|
97
|
+
*/
|
|
98
|
+
export function capMeasurements(raw, step) {
|
|
99
|
+
const points = readPoints(raw);
|
|
100
|
+
const truncated = points.length > 200 || flagged(raw);
|
|
101
|
+
return {
|
|
102
|
+
points: points.slice(0, 200),
|
|
103
|
+
step: readStep(raw, step),
|
|
104
|
+
truncated
|
|
105
|
+
};
|
|
106
|
+
}
|