@vscxml/mcp 0.1.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/README.md +252 -0
- package/dist/bin/scxml-mcp.d.ts +2 -0
- package/dist/bin/scxml-mcp.js +51 -0
- package/dist/bin/scxml-mcp.js.map +1 -0
- package/dist/bridges/editor-bridge.d.ts +135 -0
- package/dist/bridges/editor-bridge.js +285 -0
- package/dist/bridges/editor-bridge.js.map +1 -0
- package/dist/bridges/generator-bridge.d.ts +72 -0
- package/dist/bridges/generator-bridge.js +190 -0
- package/dist/bridges/generator-bridge.js.map +1 -0
- package/dist/bridges/simulator-bridge.d.ts +89 -0
- package/dist/bridges/simulator-bridge.js +451 -0
- package/dist/bridges/simulator-bridge.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/process-manager.d.ts +42 -0
- package/dist/process-manager.js +211 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +721 -0
- package/dist/server.js.map +1 -0
- package/dist/trace-buffer.d.ts +22 -0
- package/dist/trace-buffer.js +36 -0
- package/dist/trace-buffer.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
- package/server.json +78 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { GeneratorBridge } from './bridges/generator-bridge.js';
|
|
4
|
+
import { SimulatorBridge } from './bridges/simulator-bridge.js';
|
|
5
|
+
import { EditorBridge } from './bridges/editor-bridge.js';
|
|
6
|
+
import { ProcessManager } from './process-manager.js';
|
|
7
|
+
// Semantic color presets for editor_highlight
|
|
8
|
+
const HIGHLIGHT_PRESETS = {
|
|
9
|
+
error: '#ef4444',
|
|
10
|
+
danger: '#dc2626',
|
|
11
|
+
warning: '#f59e0b',
|
|
12
|
+
success: '#16a34a',
|
|
13
|
+
info: '#2563eb',
|
|
14
|
+
active: '#8b5cf6',
|
|
15
|
+
};
|
|
16
|
+
export function createServer(config = {}) {
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: 'scxml-gen',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
});
|
|
21
|
+
const generator = new GeneratorBridge(config.generatorUrl);
|
|
22
|
+
const simulator = new SimulatorBridge(config.simulatorUrl);
|
|
23
|
+
const editor = new EditorBridge(config.editorUrl);
|
|
24
|
+
const processManager = new ProcessManager();
|
|
25
|
+
// ═══════════════════════════════════════════════
|
|
26
|
+
// Design Tools
|
|
27
|
+
// ═══════════════════════════════════════════════
|
|
28
|
+
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 }) => {
|
|
29
|
+
const result = await generator.validate(scxml);
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
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 }) => {
|
|
35
|
+
const result = await generator.inspect(scxml);
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
server.tool('scxml_create', 'Create a new SCXML state machine from a structured specification. Returns valid SCXML XML. This is easier than writing raw XML — specify states, transitions, and data as structured objects.', {
|
|
41
|
+
name: z.string().optional().default('StateMachine').describe('Machine name'),
|
|
42
|
+
datamodel: z.enum(['ecmascript', 'null', 'native-java', 'native-js', 'native-python', 'native-go', 'native-st']).optional().default('ecmascript'),
|
|
43
|
+
binding: z.enum(['early', 'late']).optional().default('early'),
|
|
44
|
+
states: z.array(z.object({
|
|
45
|
+
id: z.string().describe('State ID'),
|
|
46
|
+
type: z.enum(['atomic', 'compound', 'parallel', 'final']).optional().default('atomic'),
|
|
47
|
+
parent: z.string().nullable().optional().describe('Parent state ID (null = top-level)'),
|
|
48
|
+
initial: z.string().nullable().optional().describe('Initial child state (for compound)'),
|
|
49
|
+
onEntry: z.string().nullable().optional().describe('Script to execute on entry'),
|
|
50
|
+
onExit: z.string().nullable().optional().describe('Script to execute on exit'),
|
|
51
|
+
data: z.array(z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
expr: z.string().optional(),
|
|
54
|
+
type: z.string().optional(),
|
|
55
|
+
})).optional().describe('State-local data variables'),
|
|
56
|
+
})).describe('States'),
|
|
57
|
+
transitions: z.array(z.object({
|
|
58
|
+
source: z.string().describe('Source state ID'),
|
|
59
|
+
target: z.string().nullable().optional().describe('Target state ID'),
|
|
60
|
+
event: z.string().nullable().optional().describe('Trigger event'),
|
|
61
|
+
cond: z.string().nullable().optional().describe('Guard condition'),
|
|
62
|
+
type: z.enum(['external', 'internal']).optional(),
|
|
63
|
+
actions: z.string().nullable().optional().describe('Script actions'),
|
|
64
|
+
})).optional().default([]).describe('Transitions'),
|
|
65
|
+
data: z.array(z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
expr: z.string().optional(),
|
|
68
|
+
type: z.string().optional(),
|
|
69
|
+
})).optional().default([]).describe('Top-level data variables'),
|
|
70
|
+
}, async ({ name, datamodel, binding, states, transitions, data }) => {
|
|
71
|
+
const result = await generator.create({
|
|
72
|
+
name, datamodel, binding, states, transitions, data,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
server.tool('scxml_generate_project', 'Generate a complete runnable project for a target language. Unlike scxml_generate with codeOnly, this includes build files, runtime dependencies, and project scaffolding.', {
|
|
79
|
+
scxml: z.string().describe('SCXML XML content'),
|
|
80
|
+
target: z.enum(['java', 'js', 'javascript', 'csharp', 'c', 'python', 'go', 'st']).describe('Target language'),
|
|
81
|
+
options: z.object({
|
|
82
|
+
className: z.string().optional(),
|
|
83
|
+
packageName: z.string().optional(),
|
|
84
|
+
namespace: z.string().optional(),
|
|
85
|
+
bundleAuto: z.boolean().optional(),
|
|
86
|
+
plcPlatform: z.string().optional(),
|
|
87
|
+
}).optional().describe('Target-specific options'),
|
|
88
|
+
}, async ({ scxml, target, options }) => {
|
|
89
|
+
const result = await generator.generateProject(scxml, target, options);
|
|
90
|
+
// Build helpful metadata
|
|
91
|
+
const buildCommands = {
|
|
92
|
+
java: 'gradle build',
|
|
93
|
+
javascript: 'npm install && npm test',
|
|
94
|
+
js: 'npm install && npm test',
|
|
95
|
+
csharp: 'dotnet build',
|
|
96
|
+
c: 'mkdir build && cd build && cmake .. && make',
|
|
97
|
+
python: 'pip install -e . && python -m pytest',
|
|
98
|
+
go: 'go build ./...',
|
|
99
|
+
st: '(Import .st file into CODESYS/TwinCAT IDE)',
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: JSON.stringify({
|
|
105
|
+
files: result.files,
|
|
106
|
+
target: result.target,
|
|
107
|
+
buildCommand: buildCommands[target] || '',
|
|
108
|
+
fileCount: result.files.length,
|
|
109
|
+
}, null, 2),
|
|
110
|
+
}],
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
// ═══════════════════════════════════════════════
|
|
114
|
+
// Simulation Tools
|
|
115
|
+
// ═══════════════════════════════════════════════
|
|
116
|
+
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
|
+
if (!simulator.isConnected()) {
|
|
118
|
+
await simulator.connect();
|
|
119
|
+
}
|
|
120
|
+
await simulator.loadScxml(scxml);
|
|
121
|
+
const state = await simulator.start();
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: JSON.stringify(state, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
server.tool('scxml_sim_send', 'Send a single event to the running simulation and return the resulting trace (state exits, entries, transitions, variable changes) and new state.', {
|
|
127
|
+
event: z.string().describe('Event name to send'),
|
|
128
|
+
data: z.unknown().optional().describe('Optional event data'),
|
|
129
|
+
}, async ({ event, data }) => {
|
|
130
|
+
if (!simulator.isConnected()) {
|
|
131
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
132
|
+
}
|
|
133
|
+
const result = await simulator.sendEvent(event, data);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
server.tool('scxml_sim_scenario', 'Run a batch of events against a simulation and return the complete trace. This is the most useful tool for testing behavior — "send these events and show me what happens."', {
|
|
139
|
+
scxml: z.string().optional().describe('SCXML content (creates fresh session). Omit to use existing session.'),
|
|
140
|
+
events: z.array(z.union([
|
|
141
|
+
z.string(),
|
|
142
|
+
z.object({
|
|
143
|
+
name: z.string(),
|
|
144
|
+
data: z.unknown().optional(),
|
|
145
|
+
}),
|
|
146
|
+
])).describe('Events to send in sequence'),
|
|
147
|
+
stopOnError: z.boolean().optional().default(true).describe('Stop on first error (default: true)'),
|
|
148
|
+
}, async ({ scxml, events, stopOnError }) => {
|
|
149
|
+
if (!simulator.isConnected()) {
|
|
150
|
+
await simulator.connect();
|
|
151
|
+
}
|
|
152
|
+
// Start fresh session if SCXML provided
|
|
153
|
+
if (scxml) {
|
|
154
|
+
await simulator.loadScxml(scxml);
|
|
155
|
+
await simulator.start();
|
|
156
|
+
}
|
|
157
|
+
const allTrace = [];
|
|
158
|
+
const perEvent = [];
|
|
159
|
+
let error;
|
|
160
|
+
for (const evt of events) {
|
|
161
|
+
const eventName = typeof evt === 'string' ? evt : evt.name;
|
|
162
|
+
const eventData = typeof evt === 'string' ? undefined : evt.data;
|
|
163
|
+
try {
|
|
164
|
+
const result = await simulator.sendEvent(eventName, eventData);
|
|
165
|
+
allTrace.push(...result.trace);
|
|
166
|
+
perEvent.push({
|
|
167
|
+
event: eventName,
|
|
168
|
+
trace: result.trace,
|
|
169
|
+
activeStates: result.state.activeStates,
|
|
170
|
+
variables: result.state.variables,
|
|
171
|
+
});
|
|
172
|
+
if (result.state.finished)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
error = err instanceof Error ? err.message : String(err);
|
|
177
|
+
if (stopOnError)
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const finalState = await simulator.getState();
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: JSON.stringify({
|
|
186
|
+
trace: allTrace,
|
|
187
|
+
perEvent,
|
|
188
|
+
finalActiveStates: finalState.activeStates,
|
|
189
|
+
finalVariables: finalState.variables,
|
|
190
|
+
finished: finalState.finished,
|
|
191
|
+
error,
|
|
192
|
+
}, null, 2),
|
|
193
|
+
}],
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
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 () => {
|
|
197
|
+
if (!simulator.isConnected()) {
|
|
198
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
199
|
+
}
|
|
200
|
+
const state = await simulator.getState();
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: 'text', text: JSON.stringify(state, null, 2) }],
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
server.tool('scxml_sim_reset', 'Reset the simulation session to its initial state.', {}, async () => {
|
|
206
|
+
if (!simulator.isConnected()) {
|
|
207
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
208
|
+
}
|
|
209
|
+
const state = await simulator.reset();
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: 'text', text: JSON.stringify(state, null, 2) }],
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
// ═══════════════════════════════════════════════
|
|
215
|
+
// Code Generation Tools
|
|
216
|
+
// ═══════════════════════════════════════════════
|
|
217
|
+
server.tool('scxml_generate', 'Generate code for a target language from SCXML. Returns the generated source files as text.', {
|
|
218
|
+
scxml: z.string().describe('SCXML XML content'),
|
|
219
|
+
target: z.enum(['java', 'js', 'javascript', 'csharp', 'c', 'python', 'go', 'st'])
|
|
220
|
+
.describe('Target language'),
|
|
221
|
+
options: z.object({
|
|
222
|
+
className: z.string().optional(),
|
|
223
|
+
packageName: z.string().optional(),
|
|
224
|
+
namespace: z.string().optional(),
|
|
225
|
+
codeOnly: z.boolean().optional(),
|
|
226
|
+
bundleAuto: z.boolean().optional(),
|
|
227
|
+
plcPlatform: z.string().optional(),
|
|
228
|
+
eventQueueDepth: z.number().optional(),
|
|
229
|
+
}).optional().describe('Target-specific generation options'),
|
|
230
|
+
}, async ({ scxml, target, options }) => {
|
|
231
|
+
const result = await generator.generate(scxml, target, options);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
server.tool('scxml_list_targets', 'List available code generation targets and their options.', {}, async () => {
|
|
237
|
+
const targets = await generator.listTargets();
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: 'text', text: JSON.stringify({ targets }, null, 2) }],
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
// ═══════════════════════════════════════════════
|
|
243
|
+
// Verification Tools
|
|
244
|
+
// ═══════════════════════════════════════════════
|
|
245
|
+
server.tool('scxml_compare_traces', 'Compare two execution traces (e.g., simulation trace vs generated-code trace) and report differences.', {
|
|
246
|
+
expected: z.array(z.record(z.unknown())).describe('Expected trace entries (e.g., from simulation)'),
|
|
247
|
+
actual: z.array(z.record(z.unknown())).describe('Actual trace entries (e.g., from running generated code)'),
|
|
248
|
+
ignoreTimestamps: z.boolean().optional().default(true).describe('Ignore timestamp differences'),
|
|
249
|
+
}, async ({ expected, actual, ignoreTimestamps }) => {
|
|
250
|
+
const result = compareTraces(expected, actual, ignoreTimestamps);
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
// ═══════════════════════════════════════════════
|
|
256
|
+
// Trace Management Tools
|
|
257
|
+
// ═══════════════════════════════════════════════
|
|
258
|
+
server.tool('scxml_trace_list', 'List all embedded traces in the current SCXML document. Requires an active simulation session.', {}, async () => {
|
|
259
|
+
if (!simulator.isConnected()) {
|
|
260
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
261
|
+
}
|
|
262
|
+
const traces = await simulator.listTraces();
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: 'text', text: JSON.stringify({ traces }, null, 2) }],
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
server.tool('scxml_trace_embed', 'Embed a trace into the SCXML document. Provide JSONL content to embed an external trace, or omit content to embed the current session execution history.', {
|
|
268
|
+
name: z.string().describe('Trace name (identifier for later retrieval)'),
|
|
269
|
+
description: z.string().optional().describe('Human-readable description of this trace'),
|
|
270
|
+
content: z.string().optional().describe('Raw JSONL trace content to embed. If omitted, the current session execution history is used.'),
|
|
271
|
+
}, async ({ name, description, content }) => {
|
|
272
|
+
if (!simulator.isConnected()) {
|
|
273
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
274
|
+
}
|
|
275
|
+
const result = await simulator.saveTrace(name, { description, content });
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: JSON.stringify({
|
|
280
|
+
embedded: name,
|
|
281
|
+
entryCount: result.entryCount,
|
|
282
|
+
}, null, 2),
|
|
283
|
+
}],
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
server.tool('scxml_trace_delete', 'Delete an embedded trace from the SCXML document by name.', {
|
|
287
|
+
name: z.string().describe('Trace name to delete'),
|
|
288
|
+
}, async ({ name }) => {
|
|
289
|
+
if (!simulator.isConnected()) {
|
|
290
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
291
|
+
}
|
|
292
|
+
await simulator.deleteTrace(name);
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: name }, null, 2) }],
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
server.tool('scxml_trace_get', 'Get an embedded trace by name. Returns the parsed trace entries for inspection, comparison, or export.', {
|
|
298
|
+
name: z.string().describe('Trace name to retrieve'),
|
|
299
|
+
}, async ({ name }) => {
|
|
300
|
+
if (!simulator.isConnected()) {
|
|
301
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
302
|
+
}
|
|
303
|
+
const result = await simulator.loadTrace(name);
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
// ═══════════════════════════════════════════════
|
|
309
|
+
// Trace Playback + Variable Tools
|
|
310
|
+
// ═══════════════════════════════════════════════
|
|
311
|
+
server.tool('scxml_trace_play', 'Start playback of an embedded trace by name. The simulator will replay the trace with state updates broadcast in real-time.', {
|
|
312
|
+
name: z.string().describe('Embedded trace name to play'),
|
|
313
|
+
speed: z.number().optional().describe('Playback speed multiplier (1.0 = real-time, 2.0 = 2x speed). Default: 1.0'),
|
|
314
|
+
}, async ({ name, speed }) => {
|
|
315
|
+
if (!simulator.isConnected()) {
|
|
316
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
317
|
+
}
|
|
318
|
+
const result = await simulator.playTrace(name, speed);
|
|
319
|
+
return {
|
|
320
|
+
content: [{
|
|
321
|
+
type: 'text',
|
|
322
|
+
text: JSON.stringify({
|
|
323
|
+
playing: name,
|
|
324
|
+
entryCount: result.entryCount,
|
|
325
|
+
speed: speed || 1.0,
|
|
326
|
+
}, null, 2),
|
|
327
|
+
}],
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
server.tool('scxml_trace_step', 'Step forward or backward through a loaded trace. Returns the current position and trace entry.', {
|
|
331
|
+
delta: z.number().describe('Steps to move: positive = forward, negative = backward'),
|
|
332
|
+
}, async ({ delta }) => {
|
|
333
|
+
if (!simulator.isConnected()) {
|
|
334
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
335
|
+
}
|
|
336
|
+
const result = await simulator.stepTrace(delta);
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
server.tool('scxml_sim_set_variable', 'Set a variable value during simulation. The variable must exist in the state machine datamodel.', {
|
|
342
|
+
name: z.string().describe('Variable name'),
|
|
343
|
+
value: z.unknown().describe('New value for the variable'),
|
|
344
|
+
}, async ({ name, value }) => {
|
|
345
|
+
if (!simulator.isConnected()) {
|
|
346
|
+
throw new Error('No active simulation session. Use scxml_sim_start first.');
|
|
347
|
+
}
|
|
348
|
+
await simulator.setVariable(name, value);
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: 'text', text: JSON.stringify({ set: name, value }, null, 2) }],
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
// ═══════════════════════════════════════════════
|
|
354
|
+
// Editor Interaction Tools
|
|
355
|
+
// ═══════════════════════════════════════════════
|
|
356
|
+
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
|
+
if (!editor.isConnected()) {
|
|
358
|
+
try {
|
|
359
|
+
await editor.connect();
|
|
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
|
+
}
|
|
367
|
+
const result = await editor.pushScxml(scxml);
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
server.tool('editor_get_scxml', 'Get the current SCXML content from the VSCXML-Generator editor (what the user is currently editing).', {}, async () => {
|
|
373
|
+
if (!editor.isConnected()) {
|
|
374
|
+
try {
|
|
375
|
+
await editor.connect();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected. Is VSCXML-Generator running?' }) }],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const result = await editor.getScxml();
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
server.tool('editor_show_notification', 'Show a toast notification in the VSCXML-Generator editor UI. Useful for status updates.', {
|
|
389
|
+
message: z.string().describe('Notification message'),
|
|
390
|
+
severity: z.enum(['info', 'warning', 'error', 'success']).optional().default('info'),
|
|
391
|
+
duration: z.number().optional().default(5000).describe('Duration in ms (0 = persistent)'),
|
|
392
|
+
}, async ({ message, severity, duration }) => {
|
|
393
|
+
if (!editor.isConnected()) {
|
|
394
|
+
try {
|
|
395
|
+
await editor.connect();
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
await editor.showNotification(message, severity, duration);
|
|
404
|
+
return {
|
|
405
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
server.tool('editor_highlight', 'Highlight specific states in the VSCXML-Editor canvas. Used to draw attention to states during explanation or debugging.', {
|
|
409
|
+
states: z.array(z.string()).describe('State IDs to highlight'),
|
|
410
|
+
color: z.string().optional().describe('Highlight color: hex (#ef4444) or preset (error, success, warning, info, active, danger). Default: blue.'),
|
|
411
|
+
duration: z.number().optional().describe('Duration in ms (0 = persistent until cleared)'),
|
|
412
|
+
clear: z.boolean().optional().describe('Clear existing highlights first'),
|
|
413
|
+
}, async ({ states, color, duration, clear }) => {
|
|
414
|
+
if (!editor.isConnected()) {
|
|
415
|
+
try {
|
|
416
|
+
await editor.connect();
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const resolvedColor = color ? (HIGHLIGHT_PRESETS[color.toLowerCase()] || color) : undefined;
|
|
423
|
+
await editor.highlight(states, { color: resolvedColor, duration, clear });
|
|
424
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
425
|
+
});
|
|
426
|
+
server.tool('editor_add_note', 'Add a note/annotation to the VSCXML-Editor canvas. Notes can be attached to states and conditionally visible.', {
|
|
427
|
+
content: z.string().describe('Note text content'),
|
|
428
|
+
attachedTo: z.string().optional().describe('State ID to attach the note to (moves with state)'),
|
|
429
|
+
color: z.string().optional().describe('Note background color (default: light blue)'),
|
|
430
|
+
visibleWhen: z.array(z.string()).optional().describe('Only show when these state IDs are active'),
|
|
431
|
+
}, async ({ content, attachedTo, color, visibleWhen }) => {
|
|
432
|
+
if (!editor.isConnected()) {
|
|
433
|
+
try {
|
|
434
|
+
await editor.connect();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const result = await editor.addNote(content, { attachedTo, color, visibleWhen });
|
|
441
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, noteId: result.noteId }) }] };
|
|
442
|
+
});
|
|
443
|
+
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
|
+
noteIds: z.array(z.string()).optional().describe('Specific note IDs to remove (omit to remove all MCP notes)'),
|
|
445
|
+
}, async ({ noteIds }) => {
|
|
446
|
+
if (!editor.isConnected()) {
|
|
447
|
+
try {
|
|
448
|
+
await editor.connect();
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await editor.removeNotes(noteIds);
|
|
455
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
456
|
+
});
|
|
457
|
+
server.tool('editor_navigate', 'Navigate the VSCXML-Editor canvas to show specific states or fit the entire diagram.', {
|
|
458
|
+
target: z.string().optional().describe('State ID to center on'),
|
|
459
|
+
fitStates: z.array(z.string()).optional().describe('Fit view to show all these states'),
|
|
460
|
+
fitAll: z.boolean().optional().describe('Fit entire diagram in view'),
|
|
461
|
+
zoom: z.number().optional().describe('Zoom level (default: 1.0)'),
|
|
462
|
+
}, async ({ target, fitStates, fitAll, zoom }) => {
|
|
463
|
+
if (!editor.isConnected()) {
|
|
464
|
+
try {
|
|
465
|
+
await editor.connect();
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
await editor.navigate({ target, fitStates, fitAll, zoom });
|
|
472
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
|
|
473
|
+
});
|
|
474
|
+
server.tool('editor_save_file', 'Save SCXML content to a file on disk. If content is omitted, saves the current editor content.', {
|
|
475
|
+
filePath: z.string().describe('Absolute file path to save to'),
|
|
476
|
+
content: z.string().optional().describe('SCXML content to save. If omitted, fetches current content from the editor.'),
|
|
477
|
+
}, async ({ filePath, content }) => {
|
|
478
|
+
if (!editor.isConnected()) {
|
|
479
|
+
try {
|
|
480
|
+
await editor.connect();
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// If no content provided, get it from the editor
|
|
487
|
+
let scxmlContent = content;
|
|
488
|
+
if (!scxmlContent) {
|
|
489
|
+
const editorContent = await editor.getScxml();
|
|
490
|
+
scxmlContent = editorContent.scxml;
|
|
491
|
+
}
|
|
492
|
+
const result = await editor.saveFile(filePath, scxmlContent);
|
|
493
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
494
|
+
});
|
|
495
|
+
server.tool('editor_load_file', 'Load an SCXML file from disk into the editor. The editor will display the loaded content.', {
|
|
496
|
+
filePath: z.string().describe('Absolute file path to load'),
|
|
497
|
+
}, async ({ filePath }) => {
|
|
498
|
+
if (!editor.isConnected()) {
|
|
499
|
+
try {
|
|
500
|
+
await editor.connect();
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const result = await editor.loadFile(filePath);
|
|
507
|
+
return {
|
|
508
|
+
content: [{
|
|
509
|
+
type: 'text',
|
|
510
|
+
text: JSON.stringify({
|
|
511
|
+
success: true,
|
|
512
|
+
filePath: result.filePath,
|
|
513
|
+
contentLength: result.content.length,
|
|
514
|
+
}),
|
|
515
|
+
}],
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
server.tool('editor_screenshot', 'Capture a screenshot of the VSCXML-Generator editor window. Returns a base64-encoded PNG image.', {}, async () => {
|
|
519
|
+
if (!editor.isConnected()) {
|
|
520
|
+
try {
|
|
521
|
+
await editor.connect();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const result = await editor.screenshot();
|
|
528
|
+
return {
|
|
529
|
+
content: [{
|
|
530
|
+
type: 'image',
|
|
531
|
+
data: result.data,
|
|
532
|
+
mimeType: 'image/png',
|
|
533
|
+
}],
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
server.tool('editor_connect_simulator', 'Connect the editor to the simulator backend so it shows live state highlights during MCP-driven simulation.', {
|
|
537
|
+
port: z.number().optional().describe('Simulator WebSocket port (default: 48621)'),
|
|
538
|
+
}, async ({ port }) => {
|
|
539
|
+
if (!editor.isConnected()) {
|
|
540
|
+
try {
|
|
541
|
+
await editor.connect();
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Editor not connected' }) }] };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const result = await editor.connectSimulator(port);
|
|
548
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
549
|
+
});
|
|
550
|
+
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
|
+
if (!editor.isConnected()) {
|
|
552
|
+
try {
|
|
553
|
+
await editor.connect();
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const result = await editor.getSelection();
|
|
560
|
+
const buffered = editor.getLastSelection();
|
|
561
|
+
return {
|
|
562
|
+
content: [{
|
|
563
|
+
type: 'text',
|
|
564
|
+
text: JSON.stringify({ ...result, lastSelectionEvent: buffered }, null, 2),
|
|
565
|
+
}],
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
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
|
+
if (!editor.isConnected()) {
|
|
570
|
+
try {
|
|
571
|
+
await editor.connect();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const viewport = await editor.getViewport();
|
|
578
|
+
return {
|
|
579
|
+
content: [{
|
|
580
|
+
type: 'text',
|
|
581
|
+
text: JSON.stringify({
|
|
582
|
+
...viewport,
|
|
583
|
+
lastSelection: editor.getLastSelection(),
|
|
584
|
+
lastDocumentChange: editor.getLastDocumentChange(),
|
|
585
|
+
}, null, 2),
|
|
586
|
+
}],
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
// ═══════════════════════════════════════════════
|
|
590
|
+
// Export Tools
|
|
591
|
+
// ═══════════════════════════════════════════════
|
|
592
|
+
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
|
+
if (!editor.isConnected()) {
|
|
594
|
+
try {
|
|
595
|
+
await editor.connect();
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const result = await editor.exportSvg();
|
|
602
|
+
return { content: [{ type: 'text', text: result.svg }] };
|
|
603
|
+
});
|
|
604
|
+
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
|
+
if (!editor.isConnected()) {
|
|
606
|
+
try {
|
|
607
|
+
await editor.connect();
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const result = await editor.exportPng();
|
|
614
|
+
return {
|
|
615
|
+
content: [{ type: 'image', data: result.data, mimeType: 'image/png' }],
|
|
616
|
+
};
|
|
617
|
+
});
|
|
618
|
+
server.tool('editor_export_player_html', 'Export a self-contained interactive HTML player for the current state machine. The HTML includes embedded SVG, SCXML, and JavaScript runtime for live simulation and trace playback. Opens in any browser with zero dependencies.', {
|
|
619
|
+
exportMode: z.enum(['interactive', 'traces', 'both']).optional().default('interactive')
|
|
620
|
+
.describe('interactive = live simulation, traces = recorded playback, both = switchable'),
|
|
621
|
+
selectedTraceIds: z.array(z.string()).optional()
|
|
622
|
+
.describe('Trace IDs to include when exportMode is "traces" or "both"'),
|
|
623
|
+
}, async ({ exportMode, selectedTraceIds }) => {
|
|
624
|
+
if (!editor.isConnected()) {
|
|
625
|
+
try {
|
|
626
|
+
await editor.connect();
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Editor not connected' }) }] };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const result = await editor.exportPlayerHtml({ exportMode, selectedTraceIds });
|
|
633
|
+
return { content: [{ type: 'text', text: result.html }] };
|
|
634
|
+
});
|
|
635
|
+
server.tool('editor_status', 'Check if the VSCXML-Generator editor is running and connected.', {}, async () => {
|
|
636
|
+
const status = await editor.getStatus();
|
|
637
|
+
return {
|
|
638
|
+
content: [{ type: 'text', text: JSON.stringify(status) }],
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
// ═══════════════════════════════════════════════
|
|
642
|
+
// Connection Status Tool
|
|
643
|
+
// ═══════════════════════════════════════════════
|
|
644
|
+
server.tool('scxml_status', 'Check the connection status of VSCXML backends (generator, simulator, editor).', {}, async () => {
|
|
645
|
+
const [genAvail, simAvail, editorStatus] = await Promise.all([
|
|
646
|
+
generator.isAvailable(),
|
|
647
|
+
simulator.isAvailable(),
|
|
648
|
+
editor.getStatus(),
|
|
649
|
+
]);
|
|
650
|
+
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
|
+
}],
|
|
659
|
+
};
|
|
660
|
+
});
|
|
661
|
+
return server;
|
|
662
|
+
}
|
|
663
|
+
// ═══════════════════════════════════════════════
|
|
664
|
+
// Trace comparison (pure logic, no backend call)
|
|
665
|
+
// ═══════════════════════════════════════════════
|
|
666
|
+
function compareTraces(expected, actual, ignoreTimestamps) {
|
|
667
|
+
const differences = [];
|
|
668
|
+
const maxLen = Math.max(expected.length, actual.length);
|
|
669
|
+
for (let i = 0; i < maxLen; i++) {
|
|
670
|
+
const exp = expected[i];
|
|
671
|
+
const act = actual[i];
|
|
672
|
+
if (!exp) {
|
|
673
|
+
differences.push({
|
|
674
|
+
index: i,
|
|
675
|
+
expected: exp,
|
|
676
|
+
actual: act,
|
|
677
|
+
description: `Extra actual entry: ${act.type}`,
|
|
678
|
+
});
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (!act) {
|
|
682
|
+
differences.push({
|
|
683
|
+
index: i,
|
|
684
|
+
expected: exp,
|
|
685
|
+
actual: act,
|
|
686
|
+
description: `Missing actual entry: ${exp.type}`,
|
|
687
|
+
});
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
// Compare entries
|
|
691
|
+
const expClean = ignoreTimestamps ? { ...exp, timestamp: undefined } : exp;
|
|
692
|
+
const actClean = ignoreTimestamps ? { ...act, timestamp: undefined } : act;
|
|
693
|
+
if (JSON.stringify(expClean) !== JSON.stringify(actClean)) {
|
|
694
|
+
const desc = [];
|
|
695
|
+
if (exp.type !== act.type)
|
|
696
|
+
desc.push(`type: ${exp.type} vs ${act.type}`);
|
|
697
|
+
if (exp.state !== act.state)
|
|
698
|
+
desc.push(`state: ${exp.state} vs ${act.state}`);
|
|
699
|
+
if (exp.event !== act.event)
|
|
700
|
+
desc.push(`event: ${exp.event} vs ${act.event}`);
|
|
701
|
+
if (exp.from !== act.from)
|
|
702
|
+
desc.push(`from: ${exp.from} vs ${act.from}`);
|
|
703
|
+
if (exp.to !== act.to)
|
|
704
|
+
desc.push(`to: ${exp.to} vs ${act.to}`);
|
|
705
|
+
differences.push({
|
|
706
|
+
index: i,
|
|
707
|
+
expected: exp,
|
|
708
|
+
actual: act,
|
|
709
|
+
description: desc.length > 0 ? desc.join(', ') : 'Values differ',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
match: differences.length === 0,
|
|
715
|
+
differences,
|
|
716
|
+
summary: differences.length === 0
|
|
717
|
+
? `Traces match (${expected.length} entries)`
|
|
718
|
+
: `${differences.length} difference(s) found in ${maxLen} entries`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=server.js.map
|