@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.
@@ -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
+ }