@nataliapc/mcp-openmsx 1.0.1 → 1.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 CHANGED
@@ -12,13 +12,13 @@ Currently, the MCP server requires Linux to run. It has not been tested on Windo
12
12
 
13
13
  This project creates a bridge between modern AI-assisted development (e.g. GitHub Copilot, Claude Desktop) and retro computing (MSX systems) by providing:
14
14
 
15
- - **Emulator Control**: Launch, configure, and manage openMSX instances
16
- - **Media Management**: Handle ROM cartridges, floppy disks, and cassette tapes
17
- - **Debugging Tools**: Full CPU debugging with breakpoints, memory inspection, and step execution
18
- - **Video Control**: VDP register manipulation and screen capture
19
- - **Memory Operations**: Read/write RAM, VRAM, and I/O port access
20
- - **Automation**: Keyboard input simulation and savestate management
21
- - **Dual Transport**: Support for both stdio and HTTP transports
15
+ - **Emulator Control**: Launch, configure, manage openMSX instances, and replay timelines.
16
+ - **Media Management**: Handle ROM cartridges, floppy disks, and cassette tapes.
17
+ - **Debugging Tools**: Full CPU debugging with breakpoints, memory inspection, and step execution.
18
+ - **Video Control**: VDP register manipulation and screen capture.
19
+ - **Memory Operations**: Read/write RAM, VRAM, and I/O port access.
20
+ - **Automation**: Keyboard input simulation and savestate management.
21
+ - **Dual Transport**: Support for both stdio and HTTP transports.
22
22
 
23
23
  ## 🏗️ Architecture
24
24
 
@@ -44,7 +44,8 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
44
44
  ## 🛠️ Available MCP Tools
45
45
 
46
46
  ### Emulator Control Tools
47
- - `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`_.
47
+ - `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`, `wait`_.
48
+ - `emu_replay`: Controls emulation timeline: _`start`, `strop`, `status`, `goBack`, `absoluteGoto`, `truncate`, `saveReplay`, `loadReplay`_.
48
49
  - `emu_info`: Obtain informacion about the current emulated machine: _`getStatus`, `getSlotsMap`, `getIOPortsMap`_.
49
50
  - `emu_media`: Manage ROM, disk, and tape media: _`tapeInsert`, `tapeRewind`, `tapeEject`, `romInsert`, `romEject`, `diskInsert`, `diskInsertFolder`, `diskEject`_.
50
51
  - `emu_vdp`: Manage VDP (Video Display Processor): _`getPalette`, `getRegisters`, `getRegisterValue`, `setRegisterValue`, `screenGetMode`, `screenGetFullText`_.
@@ -62,49 +63,19 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
62
63
  - `screen_shot`: Capture emulator screen: _`as_image`, `to_file`_.
63
64
  - `screen_dump`: Export screen data as BASIC BSAVE.
64
65
 
65
- ## 🚀 Quick Start
66
-
67
- ### Manual installation
68
-
69
- ```bash
70
- npm install -g @nataliapc/mcp-openmsx
71
- ```
72
-
73
- ### Configuration
74
-
75
- Set optional environment variables to customize the server:
76
-
77
- ```bash
78
- export OPENMSX_EXECUTABLE="openmsx"
79
- export OPENMSX_SHARE_DIR="/usr/share/openmsx"
80
- export OPENMSX_SCREENSHOT_DIR="/my_project/screenshots"
81
- export OPENMSX_SCREENDUMP_DIR="/my_project/screendumps"
82
- export MCP_HTTP_PORT=3000
83
- ```
84
-
85
- ### Manual usage
86
-
87
- #### As MCP Server (stdio)
88
66
 
89
- ```bash
90
- mcp-openmsx
91
- ```
67
+ ## 🚀 Quick Start
92
68
 
93
- #### As HTTP Server
69
+ ### 🟢 Basic Usage with VSCode
94
70
 
95
- ```bash
96
- MCP_TRANSPORT=http mcp-openmsx
97
- # or
98
- mcp-openmsx http
99
- ```
100
-
101
- ### Basic Usage with Claude Desktop
71
+ * Install [Github Copilot extension](https://code.visualstudio.com/docs/copilot/overview)
72
+ * Add to your workspace a file `.vscode/mcp.json` with:
102
73
 
103
- Add to your `claude_desktop_config.json`:
74
+ ### STDIO mode (recommended)
104
75
 
105
76
  ```json
106
77
  {
107
- "mcpServers": {
78
+ "servers": {
108
79
  "mcp-openmsx": {
109
80
  "command": "npx",
110
81
  "args": ["@nataliapc/mcp-openmsx"],
@@ -115,44 +86,87 @@ Add to your `claude_desktop_config.json`:
115
86
  }
116
87
  }
117
88
  ```
89
+ **Note:** Environment variables are optional.
118
90
 
119
- ### Basic Usage with VSCode
120
-
121
- * Install [Github Copilot extension](https://code.visualstudio.com/docs/copilot/overview)
122
- * Add to your workspace a file `.vscode/mcp.json` with:
123
-
124
- #### stdio mode
91
+ ### Streamed HTTP mode (more advanced)
125
92
 
126
93
  ```json
127
94
  {
128
95
  "servers": {
129
96
  "mcp-openmsx": {
130
- "command": "npx",
131
- "args": ["@nataliapc/mcp-openmsx"],
132
- "env": {
133
- "OPENMSX_SHARE_DIR": "/usr/share/openmsx"
134
- }
97
+ "type": "http",
98
+ "url": "http://localhost:3000/mcp",
99
+ "headers": { }
135
100
  }
136
101
  }
137
102
  }
138
103
  ```
139
- **Note:** Environment variables are optional.
140
104
 
141
- #### http mode
105
+ **Note:** The MCP HTTP Server must be running standalone in the same computer or in another (`make run_http`).
106
+
107
+ ### 🟢 Basic Usage with Claude Desktop
108
+
109
+ Add to your `claude_desktop_config.json`:
142
110
 
143
111
  ```json
144
112
  {
145
- "servers": {
113
+ "mcpServers": {
146
114
  "mcp-openmsx": {
147
- "type": "http",
148
- "url": "http://localhost:3000/mcp",
149
- "headers": { }
115
+ "command": "npx",
116
+ "args": ["@nataliapc/mcp-openmsx"],
117
+ "env": {
118
+ "OPENMSX_SHARE_DIR": "/usr/share/openmsx"
119
+ }
150
120
  }
151
121
  }
152
122
  }
153
123
  ```
154
124
 
155
- **Note:** The MCP HTTP Server must be running standalone in the same computer or in another (`make run_http`).
125
+ ### 🟢 Environment Variables
126
+
127
+ | Variable | Description | Default Value | Example |
128
+ |----------|-------------|---------------|---------|
129
+ | `OPENMSX_EXECUTABLE` | Path or command to the openMSX executable | `openmsx` | `/usr/local/bin/openmsx` |
130
+ | `OPENMSX_SHARE_DIR` | Directory containing openMSX data files (machines, extensions, etc.) | System dependent | `/home/myuser/.openmsx/share` |
131
+ | `OPENMSX_SCREENSHOT_DIR` | Directory where screenshots will be saved | Default for openmsx | `/myproject/screenshots` |
132
+ | `OPENMSX_SCREENDUMP_DIR` | Directory where screen dumps will be saved | Default for openmsx | `/myproject/screendumps` |
133
+ | `MCP_TRANSPORT` | Transport mode (`stdio` or `http`) | `stdio` | `http` |
134
+ | `MCP_HTTP_PORT` | Port number for HTTP transport mode | `3000` | `8080` |
135
+
136
+
137
+ ## 🧑‍💻 Advanced Manual Usage
138
+
139
+ ### Manual installation
140
+
141
+ ```bash
142
+ npm install -g @nataliapc/mcp-openmsx
143
+ ```
144
+
145
+ ### Manual set of Environment Variables
146
+
147
+ Set optional environment variables to customize the server:
148
+
149
+ ```bash
150
+ export OPENMSX_EXECUTABLE="openmsx"
151
+ export OPENMSX_SHARE_DIR="/usr/share/openmsx"
152
+ export OPENMSX_SCREENSHOT_DIR="/my_project/screenshots"
153
+ export OPENMSX_SCREENDUMP_DIR="/my_project/screendumps"
154
+ export MCP_HTTP_PORT=3000
155
+ ```
156
+
157
+ ### As MCP Server (stdio)
158
+
159
+ ```bash
160
+ mcp-openmsx
161
+ ```
162
+
163
+ ### As HTTP Server
164
+
165
+ ```bash
166
+ MCP_TRANSPORT=http mcp-openmsx
167
+ # or
168
+ mcp-openmsx http
169
+ ```
156
170
 
157
171
 
158
172
  ## 💡 Development
@@ -178,16 +192,19 @@ npm run build
178
192
  npm run dev
179
193
  ```
180
194
 
181
- ## License
195
+
196
+ ## 🪪 License
182
197
 
183
198
  GPL2 License - see [LICENSE](LICENSE) file for details.
184
199
 
185
- ## Contributing
186
200
 
187
- Contributions are welcome! Please feel free to submit a Pull Request.
201
+ ## 🆘 Support
188
202
 
189
- ## Support
203
+ If you need help, or have questions or suggestions, please open an issue on the [GitHub Issues](https://github.com/nataliapc/mcp-openmsx/issues) page or check the project discussions.
190
204
 
191
- For issues and questions, please use the [GitHub Issues](https://github.com/nataliapc/mcp-openmsx/issues) page.
205
+
206
+ ## 🤝 Contributing
207
+
208
+ Contributions are welcome! Please feel free to submit a Pull Request.
192
209
 
193
210
  ---
package/dist/openmsx.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * @license GPL2
6
6
  */
7
7
  import fs from "fs/promises";
8
- import { extractDescriptionFromXML } from "./utils.js";
8
+ import { extractDescriptionFromXML, decodeHtmlEntities } from "./utils.js";
9
9
  import { spawn } from 'child_process';
10
10
  /**
11
11
  * OpenMSX class for controlling the openMSX emulator via TCL commands over TCP socket
@@ -89,8 +89,21 @@ export class OpenMSX {
89
89
  this.sendCommand('set renderer SDLGL-PP');
90
90
  // set machine on
91
91
  this.sendCommand('set power on');
92
+ // start reverse replay mode
93
+ this.sendCommand('reverse start');
92
94
  // Return success message
93
- safeResolve('Ok: openMSX emulator launched successfully');
95
+ let result = 'Ok: openMSX emulator launched successfully';
96
+ if (machine) {
97
+ result += ` with machine "${machine}"`;
98
+ }
99
+ if (extensions && extensions.length > 0) {
100
+ if (machine) {
101
+ result += ' and';
102
+ }
103
+ result += ` with extensions: "${extensions.join('", "')}"`;
104
+ }
105
+ result += ', is powered on, and replay mode is started.';
106
+ safeResolve(result);
94
107
  }
95
108
  catch (error) {
96
109
  safeResolve(`Error: Failed to send control commands - ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -179,7 +192,7 @@ export class OpenMSX {
179
192
  const trimmedLine = param.trim();
180
193
  if (trimmedLine) {
181
194
  const value = await this.sendCommand(`machine_info ${trimmedLine}`);
182
- machineInfo[trimmedLine] = value;
195
+ machineInfo[trimmedLine] = value.trim();
183
196
  }
184
197
  }
185
198
  return JSON.stringify(machineInfo, null, 2);
@@ -259,14 +272,16 @@ export class OpenMSX {
259
272
  // Look for reply tags in the output
260
273
  const replyMatch = output.match(/<reply result="(ok|nok)"[^>]*>(.*?)<\/reply>/s);
261
274
  if (replyMatch) {
275
+ const outputContent = decodeHtmlEntities(replyMatch[2].trim());
262
276
  if (replyMatch[1] === 'ok') {
263
- return replyMatch[2];
277
+ return outputContent;
264
278
  }
265
279
  else {
266
- return `Error: ${replyMatch[2].trim()}`;
280
+ return `Error: ${outputContent}`;
267
281
  }
268
282
  }
269
- return output.trim(); // Return raw output if no reply tag found
283
+ // Return raw output with HTML entities decoded
284
+ return decodeHtmlEntities(output.trim());
270
285
  }
271
286
  catch (error) {
272
287
  return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * through TCL commands via stdio.
7
7
  *
8
8
  * @package @nataliapc/mcp-openmsx
9
- * @version 1.0.1
9
+ * @version 1.1.0
10
10
  * @author Natalia Pujol Cremades (@nataliapc)
11
11
  * @license GPL2
12
12
  */
@@ -21,7 +21,7 @@ import fs from "fs/promises";
21
21
  import path from "path";
22
22
  import { openMSXInstance } from "./openmsx.js";
23
23
  // Version info for CLI
24
- const PACKAGE_VERSION = "1.0.1";
24
+ const PACKAGE_VERSION = "1.1.0";
25
25
  // Defaults for openMSX paths
26
26
  var OPENMSX_EXECUTABLE = 'openmsx';
27
27
  var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
@@ -39,41 +39,30 @@ function registerAllTools(server) {
39
39
  "emu_control",
40
40
  // Description of the tool (what it does)
41
41
  "Controls an openMSX emulator. Commands: " +
42
- "'launch [machine] [extensions]': opens a powered on openMSX emulator, machine and extensions parameters could be specified, always use 'machine_list' and 'extension_list' resources to obtain valid values. " +
42
+ "'launch [machine] [extensions]': opens a powered-on openMSX emulator; machine and extensions parameters can be specified; use 'machineList' and 'extensionList' tools to obtain valid values. " +
43
43
  "'close': closes the openMSX emulator. " +
44
44
  "'powerOn': powers on the openMSX emulator. " +
45
45
  "'powerOff': powers off the openMSX emulator. " +
46
46
  "'reset': resets the current machine. " +
47
- "'getEmulatorSpeed': get current emulator speed in percentage, default is 100. " +
48
- "'setEmulatorSpeed <emuspeed>': set the emulator speed in percentage, valid values are 1-10000, default is 100. " +
49
- "'machineList': get a list of all available MSX machines that can be emulated with openMSX. " +
50
- "'extensionList': get a list of all available MSX extensions that can be used with openMSX. ",
47
+ "'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100. " +
48
+ "'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100. " +
49
+ "'machineList': gets a list of all available MSX machines that can be emulated with openMSX. " +
50
+ "'extensionList': gets a list of all available MSX extensions that can be used with openMSX. " +
51
+ "'wait <seconds>': performs a wait for the specified number of seconds, default is 2. ",
51
52
  // Schema for the tool (input validation)
52
53
  {
53
- command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList"]),
54
+ command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]),
54
55
  machine: z.string().min(1).max(100).optional(),
55
56
  extensions: z.array(z.string().min(1).max(100)).optional(),
56
57
  emuspeed: z.number().min(1).max(10000).optional().default(100),
58
+ seconds: z.number().min(1).max(10).optional().default(2), // Seconds to wait
57
59
  },
58
60
  // Handler for the tool (function to be executed when the tool is called)
59
- async ({ command, machine, extensions, emuspeed }) => {
61
+ async ({ command, machine, extensions, emuspeed, seconds }) => {
60
62
  let result = "Error";
61
63
  switch (command) {
62
64
  case "launch":
63
65
  result = await openMSXInstance.emu_launch(OPENMSX_EXECUTABLE, machine || "", extensions || []);
64
- // Check if launch was successful
65
- if (result === "Ok") {
66
- result = "openMSX emulator launched";
67
- if (machine) {
68
- result += ` with machine "${machine}"`;
69
- }
70
- if (extensions && extensions.length > 0) {
71
- if (machine) {
72
- result += ' and ';
73
- }
74
- result += ` with extensions: ${extensions.join(', ')}`;
75
- }
76
- }
77
66
  break;
78
67
  case "close":
79
68
  result = await openMSXInstance.emu_close();
@@ -104,17 +93,18 @@ function registerAllTools(server) {
104
93
  case "extensionList":
105
94
  result = await openMSXInstance.getExtensionList(EXTENSIONS_DIR);
106
95
  break;
96
+ case "wait":
97
+ await new Promise(resolve => setTimeout(resolve, seconds * 1000));
98
+ result = `Waited for ${seconds} seconds.`;
99
+ break;
107
100
  default:
108
101
  result = `Error: Unknown command "${command}".`;
109
102
  break;
110
103
  }
111
104
  // Return result with proper format for MCP
112
- return {
113
- content: [{
114
- type: "text",
115
- text: result === '' ? 'Ok' : result,
116
- }],
117
- };
105
+ return getResponseContent([
106
+ result
107
+ ]);
118
108
  });
119
109
  server.tool(
120
110
  // Name of the tool (used to call it)
@@ -166,21 +156,15 @@ function registerAllTools(server) {
166
156
  tclCommand = "diska eject";
167
157
  break;
168
158
  default:
169
- return {
170
- content: [{
171
- type: "text",
172
- text: `Error: Unknown emulator media command "${command}".`,
173
- }],
174
- };
159
+ return getResponseContent([
160
+ `Error: Unknown emulator media command "${command}".`
161
+ ]);
175
162
  }
176
163
  const response = await openMSXInstance.sendCommand(tclCommand);
177
164
  // Return the response from openMSX
178
- return {
179
- content: [{
180
- type: "text",
181
- text: response === '' ? 'Ok' : response,
182
- }],
183
- };
165
+ return getResponseContent([
166
+ response
167
+ ]);
184
168
  });
185
169
  server.tool(
186
170
  // Name of the tool (used to call it)
@@ -199,12 +183,9 @@ function registerAllTools(server) {
199
183
  let tclCommand;
200
184
  switch (command) {
201
185
  case "getStatus":
202
- return {
203
- content: [{
204
- type: "text",
205
- text: await openMSXInstance.emu_status(),
206
- }],
207
- };
186
+ return getResponseContent([
187
+ await openMSXInstance.emu_status()
188
+ ]);
208
189
  case "getSlotsMap":
209
190
  tclCommand = "slotmap";
210
191
  break;
@@ -212,20 +193,14 @@ function registerAllTools(server) {
212
193
  tclCommand = "iomap";
213
194
  break;
214
195
  default:
215
- return {
216
- content: [{
217
- type: "text",
218
- text: `Error: Unknown emulator info command "${command}".`,
219
- }],
220
- };
196
+ return getResponseContent([
197
+ `Error: Unknown emulator info command "${command}".`
198
+ ]);
221
199
  }
222
200
  const response = await openMSXInstance.sendCommand(tclCommand);
223
- return {
224
- content: [{
225
- type: "text",
226
- text: response,
227
- }],
228
- };
201
+ return getResponseContent([
202
+ response
203
+ ]);
229
204
  });
230
205
  server.tool(
231
206
  // Name of the tool (used to call it)
@@ -265,35 +240,18 @@ function registerAllTools(server) {
265
240
  break;
266
241
  case "screenGetFullText":
267
242
  const response = await openMSXInstance.sendCommand('get_screen');
268
- return response.startsWith('Error:') ? {
269
- content: [{
270
- type: "text",
271
- text: response,
272
- }]
273
- } : {
274
- content: [{
275
- type: "text",
276
- text: "The screen text is:",
277
- }, {
278
- type: "text",
279
- text: response,
280
- }],
281
- };
243
+ return response.startsWith('Error:') ?
244
+ getResponseContent([response]) :
245
+ getResponseContent(["The screen text is:", response]);
282
246
  default:
283
- return {
284
- content: [{
285
- type: "text",
286
- text: `Error: Unknown emulator vdp command "${command}".`,
287
- }],
288
- };
247
+ return getResponseContent([
248
+ `Error: Unknown emulator vdp command "${command}".`
249
+ ]);
289
250
  }
290
251
  const response = await openMSXInstance.sendCommand(tclCommand);
291
- return {
292
- content: [{
293
- type: "text",
294
- text: response === "" ? "Ok" : response,
295
- }],
296
- };
252
+ return getResponseContent([
253
+ response
254
+ ]);
297
255
  });
298
256
  server.tool(
299
257
  // Name of the tool (used to call it)
@@ -343,20 +301,14 @@ function registerAllTools(server) {
343
301
  tclCommand = `run_to ${address}`;
344
302
  break;
345
303
  default:
346
- return {
347
- content: [{
348
- type: "text",
349
- text: `Error: Unknown debug command "${command}".`,
350
- }],
351
- };
304
+ return getResponseContent([
305
+ `Error: Unknown debug command "${command}".`
306
+ ]);
352
307
  }
353
308
  const response = await openMSXInstance.sendCommand(tclCommand);
354
- return {
355
- content: [{
356
- type: "text",
357
- text: response === '' ? 'Ok' : response,
358
- }],
359
- };
309
+ return getResponseContent([
310
+ response
311
+ ]);
360
312
  });
361
313
  server.tool(
362
314
  // Name of the tool (used to call it)
@@ -401,20 +353,14 @@ function registerAllTools(server) {
401
353
  tclCommand = "get_active_cpu";
402
354
  break;
403
355
  default:
404
- return {
405
- content: [{
406
- type: "text",
407
- text: `Error: Unknown memory command "${command}".`,
408
- }],
409
- };
356
+ return getResponseContent([
357
+ `Error: Unknown memory command "${command}".`
358
+ ]);
410
359
  }
411
360
  const response = await openMSXInstance.sendCommand(tclCommand);
412
- return {
413
- content: [{
414
- type: "text",
415
- text: response === '' ? 'Ok' : response,
416
- }],
417
- };
361
+ return getResponseContent([
362
+ response
363
+ ]);
418
364
  });
419
365
  server.tool(
420
366
  // Name of the tool (used to call it)
@@ -463,20 +409,14 @@ function registerAllTools(server) {
463
409
  tclCommand = "listing";
464
410
  break;
465
411
  default:
466
- return {
467
- content: [{
468
- type: "text",
469
- text: `Error: Unknown memory command "${command}".`,
470
- }],
471
- };
412
+ return getResponseContent([
413
+ `Error: Unknown memory command "${command}".`
414
+ ]);
472
415
  }
473
416
  const response = await openMSXInstance.sendCommand(tclCommand);
474
- return {
475
- content: [{
476
- type: "text",
477
- text: response === '' ? 'Ok' : response,
478
- }],
479
- };
417
+ return getResponseContent([
418
+ response
419
+ ]);
480
420
  });
481
421
  server.tool(
482
422
  // Name of the tool (used to call it)
@@ -508,20 +448,14 @@ function registerAllTools(server) {
508
448
  tclCommand = `vpoke ${address} ${value8}`;
509
449
  break;
510
450
  default:
511
- return {
512
- content: [{
513
- type: "text",
514
- text: `Error: Unknown video memory command "${command}".`,
515
- }],
516
- };
451
+ return getResponseContent([
452
+ `Error: Unknown video memory command "${command}".`
453
+ ]);
517
454
  }
518
455
  const response = await openMSXInstance.sendCommand(tclCommand);
519
- return {
520
- content: [{
521
- type: "text",
522
- text: response === '' ? 'Ok' : response,
523
- }],
524
- };
456
+ return getResponseContent([
457
+ response
458
+ ]);
525
459
  });
526
460
  server.tool(
527
461
  // Name of the tool (used to call it)
@@ -553,20 +487,14 @@ function registerAllTools(server) {
553
487
  tclCommand = 'debug list_bp';
554
488
  break;
555
489
  default:
556
- return {
557
- content: [{
558
- type: "text",
559
- text: `Error: Unknown breakpoint command "${command}".`,
560
- }],
561
- };
490
+ return getResponseContent([
491
+ `Error: Unknown breakpoint command "${command}".`
492
+ ]);
562
493
  }
563
494
  const response = await openMSXInstance.sendCommand(tclCommand);
564
- return {
565
- content: [{
566
- type: "text",
567
- text: response,
568
- }],
569
- };
495
+ return getResponseContent([
496
+ response
497
+ ]);
570
498
  });
571
499
  server.tool(
572
500
  // Name of the tool (used to call it)
@@ -600,23 +528,77 @@ function registerAllTools(server) {
600
528
  tclCommand = 'list_savestates';
601
529
  break;
602
530
  default:
603
- return {
604
- content: [{
605
- type: "text",
606
- text: `Error: Unknown savestate command "${command}".`,
607
- }],
608
- };
531
+ return getResponseContent([
532
+ `Error: Unknown savestate command "${command}".`
533
+ ]);
609
534
  }
610
535
  const response = await openMSXInstance.sendCommand(tclCommand);
611
- return {
612
- content: [{
613
- type: "text",
614
- text: textResponse,
615
- }, {
616
- type: "text",
617
- text: response,
618
- }],
619
- };
536
+ return getResponseContent([
537
+ textResponse,
538
+ response
539
+ ]);
540
+ });
541
+ server.tool(
542
+ // Name of the tool (used to call it)
543
+ "emu_replay",
544
+ // Description of the tool (what it does)
545
+ "When replay is enabled (the default) the emulator collect data while emulating, which enables you to go back and forward in MSX time; consider do a 'pause' to maintain the the timeline before a 'goBack' or 'absoluteGoto'. Commands: " +
546
+ "'start': starts the replay mode (enabled by default when emulator is launched). " +
547
+ "'stop': stops the replay mode. " +
548
+ "'status': gives information about the replay feature and the data that is collected. " +
549
+ "'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started. " +
550
+ "'absoluteGoto <time>': go to the indicated absolute time in seconds in the MSX timeline, if time is before replay started it will jump to the time when is started. " +
551
+ "'truncate': stop replaying and wipe all the future replay data after now. " +
552
+ "'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response. " +
553
+ "'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.",
554
+ // Schema for the tool (input validation)
555
+ {
556
+ command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "saveReplay", "loadReplay"]),
557
+ seconds: z.number().min(1).max(60).optional(), // Seconds to go back
558
+ time: z.string().regex(/^\d+$/).optional(), // Time in seconds to go to
559
+ filename: z.string().min(1).max(200).optional(), // Filename to save/load replay
560
+ },
561
+ // Handler for the tool (function to be executed when the tool is called)
562
+ async ({ command, seconds, time, filename }) => {
563
+ let tclCommand;
564
+ switch (command) {
565
+ case "start":
566
+ tclCommand = "reverse start";
567
+ break;
568
+ case "stop":
569
+ tclCommand = "reverse stop";
570
+ break;
571
+ case "status":
572
+ tclCommand = "reverse status";
573
+ break;
574
+ case "goBack":
575
+ tclCommand = `reverse goback ${seconds}`;
576
+ break;
577
+ case "absoluteGoto":
578
+ tclCommand = `reverse goto ${time}`;
579
+ break;
580
+ case "truncate":
581
+ tclCommand = "reverse truncatereplay";
582
+ break;
583
+ case "saveReplay":
584
+ if (filename)
585
+ filename = `"${filename}"`;
586
+ tclCommand = `reverse savereplay ${filename || ''}`;
587
+ break;
588
+ case "loadReplay":
589
+ if (filename)
590
+ filename = `"${filename}"`;
591
+ tclCommand = `reverse loadreplay ${filename}`;
592
+ break;
593
+ default:
594
+ return getResponseContent([
595
+ `Error: Unknown replay command "${command}".`
596
+ ]);
597
+ }
598
+ const response = await openMSXInstance.sendCommand(tclCommand);
599
+ return getResponseContent([
600
+ response
601
+ ]);
620
602
  });
621
603
  server.tool(
622
604
  // Name of the tool (used to call it)
@@ -637,20 +619,14 @@ function registerAllTools(server) {
637
619
  tclCommand = `type "${text}"`;
638
620
  break;
639
621
  default:
640
- return {
641
- content: [{
642
- type: "text",
643
- text: `Error: Unknown keyboard command "${command}".`,
644
- }],
645
- };
622
+ return getResponseContent([
623
+ `Error: Unknown keyboard command "${command}".`
624
+ ]);
646
625
  }
647
626
  const response = await openMSXInstance.sendCommand(tclCommand);
648
- return {
649
- content: [{
650
- type: "text",
651
- text: response === '' ? 'Ok' : response,
652
- }],
653
- };
627
+ return getResponseContent([
628
+ response
629
+ ]);
654
630
  });
655
631
  server.tool(
656
632
  // Name of the tool (used to call it)
@@ -684,30 +660,19 @@ function registerAllTools(server) {
684
660
  };
685
661
  }
686
662
  catch (error) {
687
- return {
688
- content: [{
689
- type: "text",
690
- text: 'Error creating screenshot: ' + response,
691
- }, {
692
- type: "text",
693
- text: error instanceof Error ? error.message : String(error),
694
- }],
695
- };
663
+ return getResponseContent([
664
+ 'Error creating screenshot: ' + response,
665
+ error instanceof Error ? error.message : String(error)
666
+ ]);
696
667
  }
697
668
  case "to_file":
698
- return {
699
- content: [{
700
- type: "text",
701
- text: response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response,
702
- }],
703
- };
669
+ return getResponseContent([
670
+ response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response
671
+ ]);
704
672
  }
705
- return {
706
- content: [{
707
- type: "text",
708
- text: `Error: Unknown screen_shot command "${command}".`,
709
- }],
710
- };
673
+ return getResponseContent([
674
+ `Error: Unknown screen_shot command "${command}".`
675
+ ]);
711
676
  });
712
677
  server.tool(
713
678
  // Name of the tool (used to call it)
@@ -722,17 +687,20 @@ function registerAllTools(server) {
722
687
  async ({ scrbasename }) => {
723
688
  const openmsxCommand = `save_msx_screen "${OPENMSX_SCREENDUMP_DIR + scrbasename}"`;
724
689
  const response = await openMSXInstance.sendCommand(openmsxCommand);
725
- return {
726
- content: [{
727
- type: "text",
728
- text: response.startsWith('Error:') ? 'Fail:' : 'Screendump file saved as:',
729
- }, {
730
- type: "text",
731
- text: response,
732
- }],
733
- };
690
+ return getResponseContent([
691
+ response.startsWith('Error:') ? 'Fail:' : 'Screendump file saved as:',
692
+ response
693
+ ]);
734
694
  });
735
695
  }
696
+ function getResponseContent(response) {
697
+ return {
698
+ content: response.map(line => ({
699
+ type: "text",
700
+ text: line == '' ? "Ok" : line,
701
+ })),
702
+ };
703
+ }
736
704
  // ============================================================================
737
705
  // Cleanup handlers for graceful shutdown of MCP server
738
706
  // Ensure openMSX emulator is closed when MCP server stops
package/dist/utils.js CHANGED
@@ -21,3 +21,34 @@ export async function extractDescriptionFromXML(filePath) {
21
21
  return 'Error reading description';
22
22
  }
23
23
  }
24
+ /**
25
+ * Decode HTML entities in a string to plain text
26
+ * @param text - String containing HTML entities
27
+ * @returns string - String with HTML entities decoded
28
+ */
29
+ export function decodeHtmlEntities(text) {
30
+ const htmlEntities = {
31
+ '&lt;': '<',
32
+ '&gt;': '>',
33
+ '&amp;': '&',
34
+ '&quot;': '"',
35
+ '&#x27;': "'",
36
+ '&#x2F;': '/',
37
+ '&#x60;': '`',
38
+ '&#x3D;': '=',
39
+ '&#39;': "'",
40
+ '&#47;': '/',
41
+ '&#96;': '`',
42
+ '&#61;': '=',
43
+ '&apos;': "'",
44
+ '&nbsp;': ' ',
45
+ '&#x0a;': '\n',
46
+ '&#x0A;': '\n',
47
+ '&#10;': '\n',
48
+ '&#13;': '\r',
49
+ '&#9;': '\t'
50
+ };
51
+ return text.replace(/&[#\w]+;/g, (entity) => {
52
+ return htmlEntities[entity] || entity;
53
+ });
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",