@vscxml/mcp 0.1.3 → 0.1.5
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/bridges/editor-bridge.d.ts +33 -1
- package/dist/bridges/editor-bridge.js +38 -2
- package/dist/bridges/editor-bridge.js.map +1 -1
- package/dist/bridges/generator-bridge.d.ts +2 -0
- package/dist/bridges/generator-bridge.js +4 -0
- package/dist/bridges/generator-bridge.js.map +1 -1
- package/dist/bridges/simulator-bridge.d.ts +1 -0
- package/dist/bridges/simulator-bridge.js +3 -0
- package/dist/bridges/simulator-bridge.js.map +1 -1
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/process-manager.d.ts +21 -26
- package/dist/process-manager.js +61 -133
- package/dist/process-manager.js.map +1 -1
- package/dist/server.js +335 -183
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { GeneratorBridge } from './bridges/generator-bridge.js';
|
|
4
4
|
import { SimulatorBridge } from './bridges/simulator-bridge.js';
|
|
5
5
|
import { EditorBridge } from './bridges/editor-bridge.js';
|
|
6
|
-
import {
|
|
6
|
+
import { readDiscoveryFiles } from './process-manager.js';
|
|
7
7
|
// Semantic color presets for editor_highlight
|
|
8
8
|
const HIGHLIGHT_PRESETS = {
|
|
9
9
|
error: '#ef4444',
|
|
@@ -21,17 +21,96 @@ export function createServer(config = {}) {
|
|
|
21
21
|
const generator = new GeneratorBridge(config.generatorUrl);
|
|
22
22
|
const simulator = new SimulatorBridge(config.simulatorUrl);
|
|
23
23
|
const editor = new EditorBridge(config.editorUrl);
|
|
24
|
-
|
|
24
|
+
// ═══════════════════════════════════════════════
|
|
25
|
+
// Availability Guards
|
|
26
|
+
// ═══════════════════════════════════════════════
|
|
27
|
+
const GENERATOR_TOOLS = ['scxml_validate', 'scxml_inspect', 'scxml_create', 'scxml_generate', 'scxml_generate_project', 'scxml_list_targets'];
|
|
28
|
+
const SIMULATOR_TOOLS = ['scxml_sim_start', 'scxml_sim_send', 'scxml_sim_scenario', 'scxml_sim_get_state', 'scxml_sim_reset', 'scxml_sim_set_variable', 'scxml_trace_list', 'scxml_trace_embed', 'scxml_trace_get', 'scxml_trace_play', 'scxml_trace_step', 'scxml_trace_delete'];
|
|
29
|
+
const EDITOR_TOOLS = ['editor_push_scxml', 'editor_get_scxml', 'editor_highlight', 'editor_add_note', 'editor_remove_notes', 'editor_navigate', 'editor_show_notification', 'editor_save_file', 'editor_load_file', 'editor_screenshot', 'editor_export_svg', 'editor_export_png', 'editor_export_player_html', 'editor_get_selection', 'editor_get_viewport', 'editor_connect_simulator'];
|
|
30
|
+
function errorResult(obj) {
|
|
31
|
+
return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] };
|
|
32
|
+
}
|
|
33
|
+
async function checkGenerator() {
|
|
34
|
+
if (await generator.isAvailable())
|
|
35
|
+
return null;
|
|
36
|
+
return errorResult({
|
|
37
|
+
error: 'Generator is not running',
|
|
38
|
+
url: generator.getUrl(),
|
|
39
|
+
startHint: 'Start the generator server: vscxml-generator-cli serve --port 48620 --cors',
|
|
40
|
+
tools: GENERATOR_TOOLS,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function checkSimulator() {
|
|
44
|
+
if (await simulator.isAvailable())
|
|
45
|
+
return null;
|
|
46
|
+
return errorResult({
|
|
47
|
+
error: 'Simulator is not running',
|
|
48
|
+
url: simulator.getUrl(),
|
|
49
|
+
startHint: 'Start the simulator or open it from VSCXML-Editor',
|
|
50
|
+
tools: SIMULATOR_TOOLS,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function checkEditor() {
|
|
54
|
+
if (await editor.isAvailable())
|
|
55
|
+
return null;
|
|
56
|
+
return errorResult({
|
|
57
|
+
error: 'VSCXML-Editor is not running',
|
|
58
|
+
url: editor.getUrl(),
|
|
59
|
+
startHint: 'Start the VSCXML-Editor desktop application',
|
|
60
|
+
tools: EDITOR_TOOLS,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function requireSession() {
|
|
64
|
+
const unavailable = await checkSimulator();
|
|
65
|
+
if (unavailable)
|
|
66
|
+
return unavailable;
|
|
67
|
+
if (!simulator.getSessionId()) {
|
|
68
|
+
return errorResult({
|
|
69
|
+
error: 'No active simulation session',
|
|
70
|
+
action: 'Call scxml_sim_start with your SCXML content first.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function buildSummary(gen, sim, ed) {
|
|
76
|
+
const up = [gen && 'generator', sim && 'simulator', ed && 'editor'].filter(Boolean);
|
|
77
|
+
const down = [!gen && 'generator', !sim && 'simulator', !ed && 'editor'].filter(Boolean);
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (up.length === 3) {
|
|
80
|
+
parts.push('All backends connected.');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (up.length > 0)
|
|
84
|
+
parts.push(`Connected: ${up.join(', ')}.`);
|
|
85
|
+
if (down.length > 0)
|
|
86
|
+
parts.push(`Not connected: ${down.join(', ')}.`);
|
|
87
|
+
}
|
|
88
|
+
if (!gen)
|
|
89
|
+
parts.push('Start the generator to use design and code-generation tools.');
|
|
90
|
+
if (!sim)
|
|
91
|
+
parts.push('Start the simulator to use simulation and trace tools.');
|
|
92
|
+
if (!ed)
|
|
93
|
+
parts.push('Start VSCXML-Editor to use visual editing and export tools.');
|
|
94
|
+
if (ed && sim)
|
|
95
|
+
parts.push('Tip: Use editor_connect_simulator to link the editor to the simulator for live state highlights.');
|
|
96
|
+
return parts.join(' ');
|
|
97
|
+
}
|
|
25
98
|
// ═══════════════════════════════════════════════
|
|
26
99
|
// Design Tools
|
|
27
100
|
// ═══════════════════════════════════════════════
|
|
28
101
|
server.tool('scxml_validate', 'Validate SCXML against the W3C spec. Returns errors/warnings, state count, transition count, and datamodel type.', { scxml: z.string().describe('SCXML XML content to validate') }, async ({ scxml }) => {
|
|
102
|
+
const unavailable = await checkGenerator();
|
|
103
|
+
if (unavailable)
|
|
104
|
+
return unavailable;
|
|
29
105
|
const result = await generator.validate(scxml);
|
|
30
106
|
return {
|
|
31
107
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
32
108
|
};
|
|
33
109
|
});
|
|
34
110
|
server.tool('scxml_inspect', 'Inspect an SCXML file and return its full structured model: states, transitions, events, guards, actions, data variables, and hierarchy. Use this to reason about a state machine without seeing a diagram.', { scxml: z.string().describe('SCXML XML content to inspect') }, async ({ scxml }) => {
|
|
111
|
+
const unavailable = await checkGenerator();
|
|
112
|
+
if (unavailable)
|
|
113
|
+
return unavailable;
|
|
35
114
|
const result = await generator.inspect(scxml);
|
|
36
115
|
return {
|
|
37
116
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -68,6 +147,9 @@ export function createServer(config = {}) {
|
|
|
68
147
|
type: z.string().optional(),
|
|
69
148
|
})).optional().default([]).describe('Top-level data variables'),
|
|
70
149
|
}, async ({ name, datamodel, binding, states, transitions, data }) => {
|
|
150
|
+
const unavailable = await checkGenerator();
|
|
151
|
+
if (unavailable)
|
|
152
|
+
return unavailable;
|
|
71
153
|
const result = await generator.create({
|
|
72
154
|
name, datamodel, binding, states, transitions, data,
|
|
73
155
|
});
|
|
@@ -86,6 +168,9 @@ export function createServer(config = {}) {
|
|
|
86
168
|
plcPlatform: z.string().optional(),
|
|
87
169
|
}).optional().describe('Target-specific options'),
|
|
88
170
|
}, async ({ scxml, target, options }) => {
|
|
171
|
+
const unavailable = await checkGenerator();
|
|
172
|
+
if (unavailable)
|
|
173
|
+
return unavailable;
|
|
89
174
|
const result = await generator.generateProject(scxml, target, options);
|
|
90
175
|
// Build helpful metadata
|
|
91
176
|
const buildCommands = {
|
|
@@ -114,9 +199,9 @@ export function createServer(config = {}) {
|
|
|
114
199
|
// Simulation Tools
|
|
115
200
|
// ═══════════════════════════════════════════════
|
|
116
201
|
server.tool('scxml_sim_start', 'Load SCXML into the simulator and start a session. Returns the initial active states, variables, and enabled events.', { scxml: z.string().describe('SCXML XML content to simulate') }, async ({ scxml }) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
202
|
+
const unavailable = await checkSimulator();
|
|
203
|
+
if (unavailable)
|
|
204
|
+
return unavailable;
|
|
120
205
|
await simulator.loadScxml(scxml);
|
|
121
206
|
const state = await simulator.start();
|
|
122
207
|
return {
|
|
@@ -127,9 +212,9 @@ export function createServer(config = {}) {
|
|
|
127
212
|
event: z.string().describe('Event name to send'),
|
|
128
213
|
data: z.unknown().optional().describe('Optional event data'),
|
|
129
214
|
}, async ({ event, data }) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
215
|
+
const unavailable = await requireSession();
|
|
216
|
+
if (unavailable)
|
|
217
|
+
return unavailable;
|
|
133
218
|
const result = await simulator.sendEvent(event, data);
|
|
134
219
|
return {
|
|
135
220
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -146,9 +231,9 @@ export function createServer(config = {}) {
|
|
|
146
231
|
])).describe('Events to send in sequence'),
|
|
147
232
|
stopOnError: z.boolean().optional().default(true).describe('Stop on first error (default: true)'),
|
|
148
233
|
}, async ({ scxml, events, stopOnError }) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
234
|
+
const unavailable = await checkSimulator();
|
|
235
|
+
if (unavailable)
|
|
236
|
+
return unavailable;
|
|
152
237
|
// Start fresh session if SCXML provided
|
|
153
238
|
if (scxml) {
|
|
154
239
|
await simulator.loadScxml(scxml);
|
|
@@ -193,19 +278,81 @@ export function createServer(config = {}) {
|
|
|
193
278
|
}],
|
|
194
279
|
};
|
|
195
280
|
});
|
|
196
|
-
server.tool('
|
|
197
|
-
|
|
198
|
-
|
|
281
|
+
server.tool('scxml_sim_timed_scenario', 'Run events with real-time delays between them. Unlike scxml_sim_scenario (instant), this lets delayed sends (<send delay="500ms"/>) fire between events — perfect for demonstrating real-time behavior in the editor. Each event has a delay in ms before it is sent.', {
|
|
282
|
+
scxml: z.string().optional().describe('SCXML content (creates fresh session). Omit to use existing session.'),
|
|
283
|
+
events: z.array(z.object({
|
|
284
|
+
name: z.string().describe('Event name'),
|
|
285
|
+
delay: z.number().optional().default(0).describe('Delay in ms before sending this event (0 = immediate)'),
|
|
286
|
+
data: z.unknown().optional().describe('Optional event data'),
|
|
287
|
+
})).describe('Events with timing'),
|
|
288
|
+
}, async ({ scxml, events }) => {
|
|
289
|
+
const unavailable = await checkSimulator();
|
|
290
|
+
if (unavailable)
|
|
291
|
+
return unavailable;
|
|
292
|
+
// Start fresh session if SCXML provided
|
|
293
|
+
if (scxml) {
|
|
294
|
+
await simulator.loadScxml(scxml);
|
|
295
|
+
await simulator.start();
|
|
296
|
+
}
|
|
297
|
+
const perEvent = [];
|
|
298
|
+
for (const evt of events) {
|
|
299
|
+
// Wait the specified delay (real-time — delayed sends will fire)
|
|
300
|
+
if (evt.delay && evt.delay > 0) {
|
|
301
|
+
await new Promise(resolve => setTimeout(resolve, evt.delay));
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const result = await simulator.sendEvent(evt.name, evt.data);
|
|
305
|
+
perEvent.push({
|
|
306
|
+
event: evt.name,
|
|
307
|
+
delay: evt.delay || 0,
|
|
308
|
+
activeStates: result.state.activeStates,
|
|
309
|
+
variables: result.state.variables,
|
|
310
|
+
});
|
|
311
|
+
if (result.state.finished)
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
perEvent.push({
|
|
317
|
+
event: evt.name,
|
|
318
|
+
delay: evt.delay || 0,
|
|
319
|
+
activeStates: [],
|
|
320
|
+
variables: {},
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
content: [{
|
|
324
|
+
type: 'text',
|
|
325
|
+
text: JSON.stringify({ perEvent, error: msg }, null, 2),
|
|
326
|
+
}],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
199
329
|
}
|
|
330
|
+
const finalState = await simulator.getState();
|
|
331
|
+
return {
|
|
332
|
+
content: [{
|
|
333
|
+
type: 'text',
|
|
334
|
+
text: JSON.stringify({
|
|
335
|
+
perEvent,
|
|
336
|
+
finalActiveStates: finalState.activeStates,
|
|
337
|
+
finalVariables: finalState.variables,
|
|
338
|
+
finished: finalState.finished,
|
|
339
|
+
}, null, 2),
|
|
340
|
+
}],
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
server.tool('scxml_sim_get_state', 'Get the current simulation state without sending an event. Returns active states, variables, enabled events, execution mode, and full trace history.', {}, async () => {
|
|
344
|
+
const unavailable = await requireSession();
|
|
345
|
+
if (unavailable)
|
|
346
|
+
return unavailable;
|
|
200
347
|
const state = await simulator.getState();
|
|
201
348
|
return {
|
|
202
349
|
content: [{ type: 'text', text: JSON.stringify(state, null, 2) }],
|
|
203
350
|
};
|
|
204
351
|
});
|
|
205
352
|
server.tool('scxml_sim_reset', 'Reset the simulation session to its initial state.', {}, async () => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
353
|
+
const unavailable = await requireSession();
|
|
354
|
+
if (unavailable)
|
|
355
|
+
return unavailable;
|
|
209
356
|
const state = await simulator.reset();
|
|
210
357
|
return {
|
|
211
358
|
content: [{ type: 'text', text: JSON.stringify(state, null, 2) }],
|
|
@@ -228,12 +375,18 @@ export function createServer(config = {}) {
|
|
|
228
375
|
eventQueueDepth: z.number().optional(),
|
|
229
376
|
}).optional().describe('Target-specific generation options'),
|
|
230
377
|
}, async ({ scxml, target, options }) => {
|
|
378
|
+
const unavailable = await checkGenerator();
|
|
379
|
+
if (unavailable)
|
|
380
|
+
return unavailable;
|
|
231
381
|
const result = await generator.generate(scxml, target, options);
|
|
232
382
|
return {
|
|
233
383
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
234
384
|
};
|
|
235
385
|
});
|
|
236
386
|
server.tool('scxml_list_targets', 'List available code generation targets and their options.', {}, async () => {
|
|
387
|
+
const unavailable = await checkGenerator();
|
|
388
|
+
if (unavailable)
|
|
389
|
+
return unavailable;
|
|
237
390
|
const targets = await generator.listTargets();
|
|
238
391
|
return {
|
|
239
392
|
content: [{ type: 'text', text: JSON.stringify({ targets }, null, 2) }],
|
|
@@ -256,9 +409,9 @@ export function createServer(config = {}) {
|
|
|
256
409
|
// Trace Management Tools
|
|
257
410
|
// ═══════════════════════════════════════════════
|
|
258
411
|
server.tool('scxml_trace_list', 'List all embedded traces in the current SCXML document. Requires an active simulation session.', {}, async () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
412
|
+
const unavailable = await requireSession();
|
|
413
|
+
if (unavailable)
|
|
414
|
+
return unavailable;
|
|
262
415
|
const traces = await simulator.listTraces();
|
|
263
416
|
return {
|
|
264
417
|
content: [{ type: 'text', text: JSON.stringify({ traces }, null, 2) }],
|
|
@@ -269,9 +422,9 @@ export function createServer(config = {}) {
|
|
|
269
422
|
description: z.string().optional().describe('Human-readable description of this trace'),
|
|
270
423
|
content: z.string().optional().describe('Raw JSONL trace content to embed. If omitted, the current session execution history is used.'),
|
|
271
424
|
}, async ({ name, description, content }) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
425
|
+
const unavailable = await requireSession();
|
|
426
|
+
if (unavailable)
|
|
427
|
+
return unavailable;
|
|
275
428
|
const result = await simulator.saveTrace(name, { description, content });
|
|
276
429
|
return {
|
|
277
430
|
content: [{
|
|
@@ -286,9 +439,9 @@ export function createServer(config = {}) {
|
|
|
286
439
|
server.tool('scxml_trace_delete', 'Delete an embedded trace from the SCXML document by name.', {
|
|
287
440
|
name: z.string().describe('Trace name to delete'),
|
|
288
441
|
}, async ({ name }) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
442
|
+
const unavailable = await requireSession();
|
|
443
|
+
if (unavailable)
|
|
444
|
+
return unavailable;
|
|
292
445
|
await simulator.deleteTrace(name);
|
|
293
446
|
return {
|
|
294
447
|
content: [{ type: 'text', text: JSON.stringify({ deleted: name }, null, 2) }],
|
|
@@ -297,9 +450,9 @@ export function createServer(config = {}) {
|
|
|
297
450
|
server.tool('scxml_trace_get', 'Get an embedded trace by name. Returns the parsed trace entries for inspection, comparison, or export.', {
|
|
298
451
|
name: z.string().describe('Trace name to retrieve'),
|
|
299
452
|
}, async ({ name }) => {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
453
|
+
const unavailable = await requireSession();
|
|
454
|
+
if (unavailable)
|
|
455
|
+
return unavailable;
|
|
303
456
|
const result = await simulator.loadTrace(name);
|
|
304
457
|
return {
|
|
305
458
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -312,9 +465,9 @@ export function createServer(config = {}) {
|
|
|
312
465
|
name: z.string().describe('Embedded trace name to play'),
|
|
313
466
|
speed: z.number().optional().describe('Playback speed multiplier (1.0 = real-time, 2.0 = 2x speed). Default: 1.0'),
|
|
314
467
|
}, async ({ name, speed }) => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
468
|
+
const unavailable = await requireSession();
|
|
469
|
+
if (unavailable)
|
|
470
|
+
return unavailable;
|
|
318
471
|
const result = await simulator.playTrace(name, speed);
|
|
319
472
|
return {
|
|
320
473
|
content: [{
|
|
@@ -330,9 +483,9 @@ export function createServer(config = {}) {
|
|
|
330
483
|
server.tool('scxml_trace_step', 'Step forward or backward through a loaded trace. Returns the current position and trace entry.', {
|
|
331
484
|
delta: z.number().describe('Steps to move: positive = forward, negative = backward'),
|
|
332
485
|
}, async ({ delta }) => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
486
|
+
const unavailable = await requireSession();
|
|
487
|
+
if (unavailable)
|
|
488
|
+
return unavailable;
|
|
336
489
|
const result = await simulator.stepTrace(delta);
|
|
337
490
|
return {
|
|
338
491
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -342,9 +495,9 @@ export function createServer(config = {}) {
|
|
|
342
495
|
name: z.string().describe('Variable name'),
|
|
343
496
|
value: z.unknown().describe('New value for the variable'),
|
|
344
497
|
}, async ({ name, value }) => {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
498
|
+
const unavailable = await requireSession();
|
|
499
|
+
if (unavailable)
|
|
500
|
+
return unavailable;
|
|
348
501
|
await simulator.setVariable(name, value);
|
|
349
502
|
return {
|
|
350
503
|
content: [{ type: 'text', text: JSON.stringify({ set: name, value }, null, 2) }],
|
|
@@ -354,32 +507,18 @@ export function createServer(config = {}) {
|
|
|
354
507
|
// Editor Interaction Tools
|
|
355
508
|
// ═══════════════════════════════════════════════
|
|
356
509
|
server.tool('editor_push_scxml', 'Push SCXML content to the VSCXML-Generator editor for the user to see and edit. The editor will update its SCXML input pane.', { scxml: z.string().describe('SCXML XML content to push to the editor') }, async ({ scxml }) => {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
return {
|
|
363
|
-
content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected. Is VSCXML-Generator running?' }) }],
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
510
|
+
const unavailable = await checkEditor();
|
|
511
|
+
if (unavailable)
|
|
512
|
+
return unavailable;
|
|
367
513
|
const result = await editor.pushScxml(scxml);
|
|
368
514
|
return {
|
|
369
515
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
370
516
|
};
|
|
371
517
|
});
|
|
372
518
|
server.tool('editor_get_scxml', 'Get the current SCXML content from the VSCXML-Generator editor (what the user is currently editing).', {}, async () => {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
return {
|
|
379
|
-
content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected. Is VSCXML-Generator running?' }) }],
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
}
|
|
519
|
+
const unavailable = await checkEditor();
|
|
520
|
+
if (unavailable)
|
|
521
|
+
return unavailable;
|
|
383
522
|
const result = await editor.getScxml();
|
|
384
523
|
return {
|
|
385
524
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
@@ -390,16 +529,9 @@ export function createServer(config = {}) {
|
|
|
390
529
|
severity: z.enum(['info', 'warning', 'error', 'success']).optional().default('info'),
|
|
391
530
|
duration: z.number().optional().default(5000).describe('Duration in ms (0 = persistent)'),
|
|
392
531
|
}, async ({ message, severity, duration }) => {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return {
|
|
399
|
-
content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }],
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
532
|
+
const unavailable = await checkEditor();
|
|
533
|
+
if (unavailable)
|
|
534
|
+
return unavailable;
|
|
403
535
|
await editor.showNotification(message, severity, duration);
|
|
404
536
|
return {
|
|
405
537
|
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
@@ -411,14 +543,9 @@ export function createServer(config = {}) {
|
|
|
411
543
|
duration: z.number().optional().describe('Duration in ms (0 = persistent until cleared)'),
|
|
412
544
|
clear: z.boolean().optional().describe('Clear existing highlights first'),
|
|
413
545
|
}, async ({ states, color, duration, clear }) => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
420
|
-
}
|
|
421
|
-
}
|
|
546
|
+
const unavailable = await checkEditor();
|
|
547
|
+
if (unavailable)
|
|
548
|
+
return unavailable;
|
|
422
549
|
const resolvedColor = color ? (HIGHLIGHT_PRESETS[color.toLowerCase()] || color) : undefined;
|
|
423
550
|
await editor.highlight(states, { color: resolvedColor, duration, clear });
|
|
424
551
|
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
@@ -429,28 +556,18 @@ export function createServer(config = {}) {
|
|
|
429
556
|
color: z.string().optional().describe('Note background color (default: light blue)'),
|
|
430
557
|
visibleWhen: z.array(z.string()).optional().describe('Only show when these state IDs are active'),
|
|
431
558
|
}, async ({ content, attachedTo, color, visibleWhen }) => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
438
|
-
}
|
|
439
|
-
}
|
|
559
|
+
const unavailable = await checkEditor();
|
|
560
|
+
if (unavailable)
|
|
561
|
+
return unavailable;
|
|
440
562
|
const result = await editor.addNote(content, { attachedTo, color, visibleWhen });
|
|
441
563
|
return { content: [{ type: 'text', text: JSON.stringify({ success: true, noteId: result.noteId }) }] };
|
|
442
564
|
});
|
|
443
565
|
server.tool('editor_remove_notes', 'Remove notes previously added by MCP from the editor canvas. Call with no noteIds to remove all MCP-added notes.', {
|
|
444
566
|
noteIds: z.array(z.string()).optional().describe('Specific note IDs to remove (omit to remove all MCP notes)'),
|
|
445
567
|
}, async ({ noteIds }) => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
452
|
-
}
|
|
453
|
-
}
|
|
568
|
+
const unavailable = await checkEditor();
|
|
569
|
+
if (unavailable)
|
|
570
|
+
return unavailable;
|
|
454
571
|
await editor.removeNotes(noteIds);
|
|
455
572
|
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
456
573
|
});
|
|
@@ -460,14 +577,9 @@ export function createServer(config = {}) {
|
|
|
460
577
|
fitAll: z.boolean().optional().describe('Fit entire diagram in view'),
|
|
461
578
|
zoom: z.number().optional().describe('Zoom level (default: 1.0)'),
|
|
462
579
|
}, async ({ target, fitStates, fitAll, zoom }) => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
469
|
-
}
|
|
470
|
-
}
|
|
580
|
+
const unavailable = await checkEditor();
|
|
581
|
+
if (unavailable)
|
|
582
|
+
return unavailable;
|
|
471
583
|
await editor.navigate({ target, fitStates, fitAll, zoom });
|
|
472
584
|
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
473
585
|
});
|
|
@@ -475,14 +587,9 @@ export function createServer(config = {}) {
|
|
|
475
587
|
filePath: z.string().describe('Absolute file path to save to'),
|
|
476
588
|
content: z.string().optional().describe('SCXML content to save. If omitted, fetches current content from the editor.'),
|
|
477
589
|
}, async ({ filePath, content }) => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
484
|
-
}
|
|
485
|
-
}
|
|
590
|
+
const unavailable = await checkEditor();
|
|
591
|
+
if (unavailable)
|
|
592
|
+
return unavailable;
|
|
486
593
|
// If no content provided, get it from the editor
|
|
487
594
|
let scxmlContent = content;
|
|
488
595
|
if (!scxmlContent) {
|
|
@@ -495,14 +602,9 @@ export function createServer(config = {}) {
|
|
|
495
602
|
server.tool('editor_load_file', 'Load an SCXML file from disk into the editor. The editor will display the loaded content.', {
|
|
496
603
|
filePath: z.string().describe('Absolute file path to load'),
|
|
497
604
|
}, async ({ filePath }) => {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
504
|
-
}
|
|
505
|
-
}
|
|
605
|
+
const unavailable = await checkEditor();
|
|
606
|
+
if (unavailable)
|
|
607
|
+
return unavailable;
|
|
506
608
|
const result = await editor.loadFile(filePath);
|
|
507
609
|
return {
|
|
508
610
|
content: [{
|
|
@@ -516,14 +618,9 @@ export function createServer(config = {}) {
|
|
|
516
618
|
};
|
|
517
619
|
});
|
|
518
620
|
server.tool('editor_screenshot', 'Capture a screenshot of the VSCXML-Generator editor window. Returns a base64-encoded PNG image.', {}, async () => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
621
|
+
const unavailable = await checkEditor();
|
|
622
|
+
if (unavailable)
|
|
623
|
+
return unavailable;
|
|
527
624
|
const result = await editor.screenshot();
|
|
528
625
|
return {
|
|
529
626
|
content: [{
|
|
@@ -536,26 +633,16 @@ export function createServer(config = {}) {
|
|
|
536
633
|
server.tool('editor_connect_simulator', 'Connect the editor to the simulator backend so it shows live state highlights during MCP-driven simulation.', {
|
|
537
634
|
port: z.number().optional().describe('Simulator WebSocket port (default: 48621)'),
|
|
538
635
|
}, async ({ port }) => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
544
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
545
|
-
}
|
|
546
|
-
}
|
|
636
|
+
const unavailable = await checkEditor();
|
|
637
|
+
if (unavailable)
|
|
638
|
+
return unavailable;
|
|
547
639
|
const result = await editor.connectSimulator(port);
|
|
548
640
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
549
641
|
});
|
|
550
642
|
server.tool('editor_get_selection', 'Get the current selection state in the editor — selected states, transitions, and MCP highlights (with colors). Also includes the latest buffered selection event from user clicks.', {}, async () => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
557
|
-
}
|
|
558
|
-
}
|
|
643
|
+
const unavailable = await checkEditor();
|
|
644
|
+
if (unavailable)
|
|
645
|
+
return unavailable;
|
|
559
646
|
const result = await editor.getSelection();
|
|
560
647
|
const buffered = editor.getLastSelection();
|
|
561
648
|
return {
|
|
@@ -566,14 +653,9 @@ export function createServer(config = {}) {
|
|
|
566
653
|
};
|
|
567
654
|
});
|
|
568
655
|
server.tool('editor_get_viewport', 'Get the editor viewport: visible area, zoom level, pan offset. Also returns the latest buffered selection and document change events.', {}, async () => {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
656
|
+
const unavailable = await checkEditor();
|
|
657
|
+
if (unavailable)
|
|
658
|
+
return unavailable;
|
|
577
659
|
const viewport = await editor.getViewport();
|
|
578
660
|
return {
|
|
579
661
|
content: [{
|
|
@@ -587,29 +669,79 @@ export function createServer(config = {}) {
|
|
|
587
669
|
};
|
|
588
670
|
});
|
|
589
671
|
// ═══════════════════════════════════════════════
|
|
672
|
+
// Editor Layout & Editing Tools
|
|
673
|
+
// ═══════════════════════════════════════════════
|
|
674
|
+
server.tool('editor_auto_layout', 'Trigger automatic layout of the diagram in the editor. Rearranges states for clean visual presentation.', {
|
|
675
|
+
algorithm: z.enum(['hierarchical', 'grid']).optional().describe('Layout algorithm (default: hierarchical)'),
|
|
676
|
+
selectedOnly: z.boolean().optional().describe('Only layout selected states (default: false)'),
|
|
677
|
+
}, async ({ algorithm, selectedOnly }) => {
|
|
678
|
+
const unavailable = await checkEditor();
|
|
679
|
+
if (unavailable)
|
|
680
|
+
return unavailable;
|
|
681
|
+
const result = await editor.autoLayout({ algorithm, selectedOnly });
|
|
682
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
683
|
+
});
|
|
684
|
+
server.tool('editor_add_state', 'Add a new state to the diagram at the given canvas coordinates.', {
|
|
685
|
+
x: z.number().describe('X position on canvas'),
|
|
686
|
+
y: z.number().describe('Y position on canvas'),
|
|
687
|
+
stateType: z.enum(['simple', 'compound', 'parallel', 'initial', 'final', 'history', 'history-deep']).optional().default('simple').describe('State type'),
|
|
688
|
+
}, async ({ x, y, stateType }) => {
|
|
689
|
+
const unavailable = await checkEditor();
|
|
690
|
+
if (unavailable)
|
|
691
|
+
return unavailable;
|
|
692
|
+
const result = await editor.addState(x, y, stateType);
|
|
693
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
694
|
+
});
|
|
695
|
+
server.tool('editor_add_transition', 'Add a transition between two states in the editor.', {
|
|
696
|
+
source: z.string().describe('Source state ID'),
|
|
697
|
+
target: z.string().describe('Target state ID'),
|
|
698
|
+
}, async ({ source, target }) => {
|
|
699
|
+
const unavailable = await checkEditor();
|
|
700
|
+
if (unavailable)
|
|
701
|
+
return unavailable;
|
|
702
|
+
const result = await editor.addTransition(source, target);
|
|
703
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
704
|
+
});
|
|
705
|
+
server.tool('editor_set_property', 'Set properties on a state or transition. For states: label, color, x, y, width, height, onentry, onexit. For transitions: event, cond, actions, type.', {
|
|
706
|
+
elementId: z.string().describe('State or transition ID'),
|
|
707
|
+
elementType: z.enum(['state', 'transition']).describe('Element type'),
|
|
708
|
+
properties: z.record(z.unknown()).describe('Properties to set (e.g. { "label": "Idle", "color": "#3b82f6" })'),
|
|
709
|
+
}, async ({ elementId, elementType, properties }) => {
|
|
710
|
+
const unavailable = await checkEditor();
|
|
711
|
+
if (unavailable)
|
|
712
|
+
return unavailable;
|
|
713
|
+
const result = await editor.setProperty(elementId, elementType, properties);
|
|
714
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
715
|
+
});
|
|
716
|
+
server.tool('editor_add_image', 'Add an image to the editor canvas. Use for explanatory diagrams, icons, or annotations. The image data must be base64-encoded.', {
|
|
717
|
+
data: z.string().describe('Base64-encoded image data (no data: prefix)'),
|
|
718
|
+
mimeType: z.string().optional().default('image/png').describe('MIME type (image/png, image/jpeg, image/svg+xml)'),
|
|
719
|
+
x: z.number().optional().describe('X position on canvas'),
|
|
720
|
+
y: z.number().optional().describe('Y position on canvas'),
|
|
721
|
+
width: z.number().optional().describe('Display width (auto-scaled if omitted)'),
|
|
722
|
+
height: z.number().optional().describe('Display height (auto-scaled if omitted)'),
|
|
723
|
+
attachedTo: z.string().optional().describe('State ID to attach the image to'),
|
|
724
|
+
}, async ({ data, mimeType, x, y, width, height, attachedTo }) => {
|
|
725
|
+
const unavailable = await checkEditor();
|
|
726
|
+
if (unavailable)
|
|
727
|
+
return unavailable;
|
|
728
|
+
const result = await editor.addImage(data, { mimeType, x, y, width, height, attachedTo });
|
|
729
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
730
|
+
});
|
|
731
|
+
// ═══════════════════════════════════════════════
|
|
590
732
|
// Export Tools
|
|
591
733
|
// ═══════════════════════════════════════════════
|
|
592
734
|
server.tool('editor_export_svg', 'Export the current diagram from the editor as a complete SVG. Returns parseable text the agent can read directly — contains state IDs, labels, positions, transitions, colors.', {}, async () => {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
599
|
-
}
|
|
600
|
-
}
|
|
735
|
+
const unavailable = await checkEditor();
|
|
736
|
+
if (unavailable)
|
|
737
|
+
return unavailable;
|
|
601
738
|
const result = await editor.exportSvg();
|
|
602
739
|
return { content: [{ type: 'text', text: result.svg }] };
|
|
603
740
|
});
|
|
604
741
|
server.tool('editor_export_png', 'Export the current diagram from the editor as a full PNG image at 2x resolution. Unlike editor_screenshot (viewport only), this renders the entire diagram.', {}, async () => {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
catch {
|
|
610
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
611
|
-
}
|
|
612
|
-
}
|
|
742
|
+
const unavailable = await checkEditor();
|
|
743
|
+
if (unavailable)
|
|
744
|
+
return unavailable;
|
|
613
745
|
const result = await editor.exportPng();
|
|
614
746
|
return {
|
|
615
747
|
content: [{ type: 'image', data: result.data, mimeType: 'image/png' }],
|
|
@@ -621,14 +753,9 @@ export function createServer(config = {}) {
|
|
|
621
753
|
selectedTraceIds: z.array(z.string()).optional()
|
|
622
754
|
.describe('Trace IDs to include when exportMode is "traces" or "both"'),
|
|
623
755
|
}, async ({ exportMode, selectedTraceIds }) => {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
630
|
-
}
|
|
631
|
-
}
|
|
756
|
+
const unavailable = await checkEditor();
|
|
757
|
+
if (unavailable)
|
|
758
|
+
return unavailable;
|
|
632
759
|
const result = await editor.exportPlayerHtml({ exportMode, selectedTraceIds });
|
|
633
760
|
return { content: [{ type: 'text', text: result.html }] };
|
|
634
761
|
});
|
|
@@ -641,21 +768,46 @@ export function createServer(config = {}) {
|
|
|
641
768
|
// ═══════════════════════════════════════════════
|
|
642
769
|
// Connection Status Tool
|
|
643
770
|
// ═══════════════════════════════════════════════
|
|
644
|
-
server.tool('scxml_status', 'Check the connection status of VSCXML backends (generator, simulator, editor).', {}, async () => {
|
|
645
|
-
const [genAvail, simAvail,
|
|
771
|
+
server.tool('scxml_status', 'Check the connection status of VSCXML backends (generator, simulator, editor). Reports URLs, available tool groups, and how to start missing backends. Call this first to know which tools are available.', {}, async () => {
|
|
772
|
+
const [genAvail, simAvail, editorAvail] = await Promise.all([
|
|
646
773
|
generator.isAvailable(),
|
|
647
774
|
simulator.isAvailable(),
|
|
648
|
-
editor.
|
|
775
|
+
editor.isAvailable(),
|
|
649
776
|
]);
|
|
777
|
+
const editorStatus = editorAvail
|
|
778
|
+
? await editor.getStatus()
|
|
779
|
+
: { connected: false, hasDocument: false, documentName: null };
|
|
780
|
+
// Scan discovery files for all known instances
|
|
781
|
+
const allGenerators = readDiscoveryFiles('generator');
|
|
782
|
+
const allSimulators = readDiscoveryFiles('simulator');
|
|
783
|
+
const allEditors = readDiscoveryFiles('editor');
|
|
784
|
+
const result = {
|
|
785
|
+
generator: {
|
|
786
|
+
connected: genAvail,
|
|
787
|
+
url: generator.getUrl(),
|
|
788
|
+
tools: GENERATOR_TOOLS,
|
|
789
|
+
instances: allGenerators,
|
|
790
|
+
startHint: genAvail ? undefined : 'Run: vscxml-generator-cli serve --port 48620 --cors',
|
|
791
|
+
},
|
|
792
|
+
simulator: {
|
|
793
|
+
connected: simAvail,
|
|
794
|
+
url: simulator.getUrl(),
|
|
795
|
+
sessionId: simulator.getSessionId(),
|
|
796
|
+
tools: SIMULATOR_TOOLS,
|
|
797
|
+
instances: allSimulators,
|
|
798
|
+
startHint: simAvail ? undefined : 'Start the simulator or open it from VSCXML-Editor',
|
|
799
|
+
},
|
|
800
|
+
editor: {
|
|
801
|
+
...editorStatus,
|
|
802
|
+
url: editor.getUrl(),
|
|
803
|
+
tools: EDITOR_TOOLS,
|
|
804
|
+
instances: allEditors,
|
|
805
|
+
startHint: editorStatus.connected ? undefined : 'Start the VSCXML-Editor desktop application',
|
|
806
|
+
},
|
|
807
|
+
summary: buildSummary(genAvail, simAvail, editorStatus.connected),
|
|
808
|
+
};
|
|
650
809
|
return {
|
|
651
|
-
content: [{
|
|
652
|
-
type: 'text',
|
|
653
|
-
text: JSON.stringify({
|
|
654
|
-
generator: { connected: genAvail },
|
|
655
|
-
simulator: { connected: simAvail, sessionId: simulator.getSessionId() },
|
|
656
|
-
editor: editorStatus,
|
|
657
|
-
}, null, 2),
|
|
658
|
-
}],
|
|
810
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
659
811
|
};
|
|
660
812
|
});
|
|
661
813
|
return server;
|