@nataliapc/mcp-openmsx 1.1.4 → 1.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/README.md CHANGED
@@ -12,6 +12,7 @@ This project creates a bridge between modern AI-assisted development (e.g. GitHu
12
12
 
13
13
  - **Emulator Control**: Launch, configure, manage openMSX instances, and replay timelines.
14
14
  - **Media Management**: Handle ROM cartridges, floppy disks, and cassette tapes.
15
+ - **BASIC Programming Support**: Tools to facilitate BASIC language programming and development.
15
16
  - **Debugging Tools**: Full CPU debugging with breakpoints, memory inspection, and step execution.
16
17
  - **Video Control**: VDP register manipulation and screen capture.
17
18
  - **Memory Operations**: Read/write RAM, VRAM, and I/O port access.
@@ -43,11 +44,14 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
43
44
 
44
45
  ### Emulator Control Tools
45
46
  - `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`, `wait`_.
46
- - `emu_replay`: Controls emulation timeline: _`start`, `strop`, `status`, `goBack`, `absoluteGoto`, `truncate`, `saveReplay`, `loadReplay`_.
47
+ - `emu_replay`: Controls emulation timeline: _`start`, `stop`, `status`, `goBack`, `absoluteGoto`, `truncate`, `saveReplay`, `loadReplay`_.
47
48
  - `emu_info`: Obtain informacion about the current emulated machine: _`getStatus`, `getSlotsMap`, `getIOPortsMap`_.
48
49
  - `emu_media`: Manage ROM, disk, and tape media: _`tapeInsert`, `tapeRewind`, `tapeEject`, `romInsert`, `romEject`, `diskInsert`, `diskInsertFolder`, `diskEject`_.
49
50
  - `emu_vdp`: Manage VDP (Video Display Processor): _`getPalette`, `getRegisters`, `getRegisterValue`, `setRegisterValue`, `screenGetMode`, `screenGetFullText`_.
50
51
 
52
+ ### Programming Tools
53
+ - `basic_programming`: BASIC tools: _`newProgram`, `runProgram`, `setProgram`, `getFullProgram`, `getFullProgramAdvanced`, `listProgramLines`, `deleteProgramLines`_.
54
+
51
55
  ### Debugging Tools
52
56
  - `debug_run`: Control execution: _`break`, `isBreaked`, `continue`, `stepIn`, `stepOut`, `stepOver`, `stepBack`, `runTo`_.
53
57
  - `debug_cpu`: Read/write CPU registers, CPU info, Stack pile, and Disassemble code: _`getCpuRegisters`, `getRegister`, `setRegister`, `getStackPile`, `disassemble`, `getActiveCpu`_.
@@ -59,7 +63,7 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
59
63
  - `emu_keyboard`: Send text input to emulator: _`sendText`_.
60
64
  - `emu_savestates`: Save and restore machine states: _`load`, `save`, `list`_.
61
65
  - `screen_shot`: Capture emulator screen: _`as_image`, `to_file`_.
62
- - `screen_dump`: Export screen data as BASIC BSAVE.
66
+ - `screen_dump`: Export screen data as BASIC BSAVE instruction.
63
67
 
64
68
 
65
69
  ## 🚀 Quick Start
@@ -131,6 +135,7 @@ Add to your `claude_desktop_config.json`:
131
135
  | `OPENMSX_SHARE_DIR` | Directory containing openMSX data files (machines, extensions, etc.) | System dependent | `/home/myuser/.openmsx/share` |
132
136
  | `OPENMSX_SCREENSHOT_DIR` | Directory where screenshots will be saved | Default for openmsx | `/myproject/screenshots` |
133
137
  | `OPENMSX_SCREENDUMP_DIR` | Directory where screen dumps will be saved | Default for openmsx | `/myproject/screendumps` |
138
+ | `OPENMSX_REPLAYS_DIR` | Directory where replay files will be saved | Default for openmsx | `/myproject/replays` |
134
139
  | `MCP_TRANSPORT` | Transport mode (`stdio` or `http`) | `stdio` | `http` |
135
140
  | `MCP_HTTP_PORT` | Port number for HTTP transport mode | `3000` | `8080` |
136
141
 
@@ -154,6 +159,7 @@ export OPENMSX_EXECUTABLE="openmsx"
154
159
  export OPENMSX_SHARE_DIR="/usr/share/openmsx"
155
160
  export OPENMSX_SCREENSHOT_DIR="/my_project/screenshots"
156
161
  export OPENMSX_SCREENDUMP_DIR="/my_project/screendumps"
162
+ export OPENMSX_REPLAYS_DIR="/my_project/replays"
157
163
  export MCP_HTTP_PORT=3000
158
164
  ```
159
165
 
package/dist/openmsx.js CHANGED
@@ -12,6 +12,7 @@ import path from 'path';
12
12
  * OpenMSX class for controlling the openMSX emulator via TCL commands over TCP socket
13
13
  */
14
14
  export class OpenMSX {
15
+ lastMachine = null;
15
16
  process = null;
16
17
  isConnected = false;
17
18
  /**
@@ -34,13 +35,14 @@ export class OpenMSX {
34
35
  try {
35
36
  // Check if emulator is already running
36
37
  if (this.process && !this.process.killed) {
37
- safeResolve("Error: openMSX emulator is already running. Close it first before launching a new instance.");
38
+ safeResolve(`Error: openMSX emulator instance is already running (currrent machine: ${this.lastMachine}). Close it first before launching a new one.`);
38
39
  return;
39
40
  }
40
41
  // Build command line arguments
41
42
  const args = ['-control', 'stdio'];
42
43
  // Add machine parameter if specified
43
44
  if (machine) {
45
+ this.lastMachine = machine; // Store last machine for future reference
44
46
  args.push('-machine', machine);
45
47
  }
46
48
  // Add extensions if specified
@@ -86,6 +88,8 @@ export class OpenMSX {
86
88
  if (!resolved) {
87
89
  try {
88
90
  this.writeData('<openmsx-control>\n');
91
+ // Set save settings on exit off
92
+ this.sendCommand('set save_settings_on_exit off');
89
93
  // Set renderer to SDL
90
94
  this.sendCommand('set renderer SDLGL-PP');
91
95
  // set machine on
@@ -148,6 +152,7 @@ export class OpenMSX {
148
152
  return;
149
153
  }
150
154
  this.process.on('exit', () => {
155
+ this.lastMachine = null; // Clear last machine on exit
151
156
  this.isConnected = false;
152
157
  this.process = null;
153
158
  resolve("Ok: Emulator process closed successfully");
@@ -187,10 +192,15 @@ export class OpenMSX {
187
192
  return response;
188
193
  }
189
194
  // Parse machine_info output into key-value pairs
195
+ const skipInfo = ['issubslotted', 'input_port', 'slot', 'isexternalslot', 'output_port'];
190
196
  const parameters = response.trim().split(' ');
191
197
  const machineInfo = {};
192
198
  for (const param of parameters) {
193
199
  const trimmedLine = param.trim();
200
+ // Skip certain parameters that are not useful
201
+ if (skipInfo.includes(trimmedLine)) {
202
+ continue;
203
+ }
194
204
  if (trimmedLine) {
195
205
  const value = await this.sendCommand(`machine_info ${trimmedLine}`);
196
206
  machineInfo[trimmedLine] = value.trim();
package/dist/server.js CHANGED
@@ -20,11 +20,13 @@ import express from "express";
20
20
  import fs from "fs/promises";
21
21
  import path from "path";
22
22
  import { openMSXInstance } from "./openmsx.js";
23
+ import { encodeTypeText, isErrorResponse } from "./utils.js";
23
24
  // Version info for CLI
24
25
  const PACKAGE_VERSION = "1.1.4";
25
26
  // Defaults for openMSX paths
26
27
  var OPENMSX_EXECUTABLE = 'openmsx';
27
28
  var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
29
+ var OPENMSX_REPLAYS_DIR = '';
28
30
  var OPENMSX_SCREENSHOT_DIR = '';
29
31
  var OPENMSX_SCREENDUMP_DIR = '';
30
32
  var MACHINES_DIR = `${OPENMSX_SHARE_DIR}/machines`;
@@ -38,17 +40,19 @@ function registerAllTools(server) {
38
40
  // Name of the tool (used to call it)
39
41
  "emu_control",
40
42
  // Description of the tool (what it does)
41
- "Controls an openMSX emulator. Commands: " +
42
- "'launch [machine] [extensions]': opens a powered-on openMSX emulator; you must wait some time waiting the machine is fully booted; machine and extensions parameters can be specified so use 'machineList' and 'extensionList' commands to obtain valid values. " +
43
- "'close': closes the openMSX emulator. " +
44
- "'powerOn': powers on the openMSX emulator. " +
45
- "'powerOff': powers off the openMSX emulator. " +
46
- "'reset': resets the current machine. " +
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. ",
43
+ `Controls an openMSX emulator.
44
+ Commands:
45
+ 'launch [machine] [extensions]': opens a powered-on openMSX emulator; you must wait some time waiting the machine is fully booted; machine and extensions parameters can be specified so use 'machineList' and 'extensionList' commands to obtain valid values. " +
46
+ 'close': closes the openMSX emulator.
47
+ 'powerOn': powers on the openMSX emulator.
48
+ 'powerOff': powers off the openMSX emulator.
49
+ 'reset': resets the current machine.
50
+ 'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100.
51
+ 'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100.
52
+ 'machineList': gets a list of all available MSX machines that can be emulated with openMSX.
53
+ 'extensionList': gets a list of all available MSX extensions that can be used with openMSX.
54
+ 'wait <seconds>': performs a wait for the specified number of seconds, default is 2.
55
+ `,
52
56
  // Schema for the tool (input validation)
53
57
  {
54
58
  command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]),
@@ -110,15 +114,17 @@ function registerAllTools(server) {
110
114
  // Name of the tool (used to call it)
111
115
  "emu_media",
112
116
  // Description of the tool (what it does)
113
- "Manage tapes, rom cartridges, and floppy disks. Commands: " +
114
- "'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx). " +
115
- "'tapeRewind': rewind the current tape. " +
116
- "'tapeEject': remove tape from virtual cassette player. " +
117
- "'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A. " +
118
- "'romEject': remove the current ROM cartridge from cartridge slot A. " +
119
- "'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A. " +
120
- "'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory. " +
121
- "'diskEject': remove the current disk from floppy disk A. ",
117
+ `Manage tapes, rom cartridges, and floppy disks.
118
+ Commands:
119
+ 'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx).
120
+ 'tapeRewind': rewind the current tape.
121
+ 'tapeEject': remove tape from virtual cassette player.
122
+ 'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A.
123
+ 'romEject': remove the current ROM cartridge from cartridge slot A.
124
+ 'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A.
125
+ 'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory.
126
+ 'diskEject': remove the current disk from floppy disk A.
127
+ `,
122
128
  // Schema for the tool (input validation)
123
129
  {
124
130
  command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert", "diskInsertFolder", "diskEject"]),
@@ -170,10 +176,12 @@ function registerAllTools(server) {
170
176
  // Name of the tool (used to call it)
171
177
  "emu_info",
172
178
  // Description of the tool (what it does)
173
- "Obtain informacion about the current emulated machine. Commands: " +
174
- "'getStatus': returns the status of the openMSX emulator. " +
175
- "'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots. " +
176
- "'getIOPortsMap': shows an overview about the I/O mapped devices. ",
179
+ `Obtain informacion about the current emulated machine.
180
+ Commands:
181
+ 'getStatus': returns the status of the openMSX emulator.
182
+ 'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots.
183
+ 'getIOPortsMap': shows an overview about the I/O mapped devices.
184
+ `,
177
185
  // Schema for the tool (input validation)
178
186
  {
179
187
  command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]),
@@ -206,13 +214,15 @@ function registerAllTools(server) {
206
214
  // Name of the tool (used to call it)
207
215
  "emu_vdp",
208
216
  // Description of the tool (what it does)
209
- "Manage the VDP (Video Display Processor). Commands: " +
210
- "'getPalette': returns the current V9938/V9958 color palette in RGB333 format. " +
211
- "'getRegisters': returns all VDP register values. " +
212
- "'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format. " +
213
- "'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31). " +
214
- "'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command. " +
215
- "'screenGetFullText': returns the full content of an MSX text screen (screen 0 or 1) as a string; PRIORITIZE this command to view screen content in text modes. ",
217
+ `Manage the VDP (Video Display Processor).
218
+ Commands:
219
+ 'getPalette': returns the current V9938/V9958 color palette in RGB333 format.
220
+ 'getRegisters': returns all VDP register values.
221
+ 'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format.
222
+ 'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31).
223
+ 'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command.
224
+ 'screenGetFullText': returns the full content of an MSX text screen (screen 0 or 1) as a string; PRIORITIZE this command to view screen content in text modes.
225
+ `,
216
226
  // Schema for the tool (input validation)
217
227
  {
218
228
  command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]),
@@ -240,7 +250,7 @@ function registerAllTools(server) {
240
250
  break;
241
251
  case "screenGetFullText":
242
252
  const response = await openMSXInstance.sendCommand('get_screen');
243
- return response.startsWith('Error:') ?
253
+ return isErrorResponse(response) ?
244
254
  getResponseContent([response]) :
245
255
  getResponseContent(["The screen text is:", response]);
246
256
  default:
@@ -257,16 +267,18 @@ function registerAllTools(server) {
257
267
  // Name of the tool (used to call it)
258
268
  "debug_run",
259
269
  // Description of the tool (what it does)
260
- "Control execution (break, continue, step). Commands: " +
261
- "'break': to break CPU at current execution position. " +
262
- "'isBreaked': to check if the CPU is currently in break state (1) or not (0). " +
263
- "'continue': to continue execution after break. " +
264
- "'stepIn': to execute one CPU instruction, go into subroutines. " +
265
- "'stepOver': to execute one CPU instruction, but don't go into subroutines. " +
266
- "'stepOut': to step out of the current subroutine. " +
267
- "'stepBack': to step one instruction back in time. " +
268
- "'runTo <address>': to run the CPU until it reaches the specified address. " +
269
- "Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
270
+ `Control execution (break, continue, step).
271
+ Commands:
272
+ 'break': to break CPU at current execution position.
273
+ 'isBreaked': to check if the CPU is currently in break state (1) or not (0).
274
+ 'continue': to continue execution after break.
275
+ 'stepIn': to execute one CPU instruction, go into subroutines.
276
+ 'stepOver': to execute one CPU instruction, but don't go into subroutines.
277
+ 'stepOut': to step out of the current subroutine.
278
+ 'stepBack': to step one instruction back in time.
279
+ 'runTo <address>': to run the CPU until it reaches the specified address.
280
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
281
+ `,
270
282
  // Schema for the tool (input validation)
271
283
  {
272
284
  command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver", "stepBack", "runTo"]),
@@ -314,14 +326,16 @@ function registerAllTools(server) {
314
326
  // Name of the tool (used to call it)
315
327
  "debug_cpu",
316
328
  // Description of the tool (what it does)
317
- "Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory. Commands: " +
318
- "'getCpuRegisters': to get an overview of all the CPU registers. " +
319
- "'getRegister <register>': to get the decimal value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff). " +
320
- "'setRegister <register> <value>': to set the value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff). " +
321
- "'getStackPile': to get an overview of the CPU stack. " +
322
- "'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty. " +
323
- "'getActiveCpu': to return the active cpu: z80 or r800." +
324
- "Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
329
+ `Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory.
330
+ Commands:
331
+ 'getCpuRegisters': to get an overview of all the CPU registers.
332
+ 'getRegister <register>': to get the decimal value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff).
333
+ 'setRegister <register> <value>': to set the value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff).
334
+ 'getStackPile': to get an overview of the CPU stack.
335
+ 'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty.
336
+ 'getActiveCpu': to return the active cpu: z80 or r800.
337
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000)."
338
+ `,
325
339
  // Schema for the tool (input validation)
326
340
  {
327
341
  command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble", "getActiveCpu"]),
@@ -338,10 +352,10 @@ function registerAllTools(server) {
338
352
  tclCommand = "cpuregs";
339
353
  break;
340
354
  case "getRegister":
341
- tclCommand = "reg ${register}";
355
+ tclCommand = `reg ${register}`;
342
356
  break;
343
357
  case "setRegister":
344
- tclCommand = "reg ${register} ${value}";
358
+ tclCommand = `reg ${register} ${value}`;
345
359
  break;
346
360
  case "getStackPile":
347
361
  tclCommand = "stack";
@@ -366,18 +380,19 @@ function registerAllTools(server) {
366
380
  // Name of the tool (used to call it)
367
381
  "debug_memory",
368
382
  // Description of the tool (what it does)
369
- "Slots info, and Read/write from/to memory in the openMSX emulator. Commands: " +
370
- "'selectedSlots': to get a list of the currently selected memory slots. " +
371
- "'getBlock <address> [lines]': to read a block of memory from the specified address. " +
372
- "'readByte <address>': to read a BYTE from the specified address. " +
373
- "'readWord <address>': to read a WORD from the specified address. " +
374
- "'writeByte <address> <value8>': to write a BYTE to the specified address. " +
375
- "'writeWord <address> <value16>': to write a WORD to the specified address. " +
376
- "'advanced_basic_listing': to list the current BASIC program, with the ram address of each line listed. " +
377
- "Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
383
+ `Slots info, and Read/write from/to memory in the openMSX emulator.
384
+ Commands:
385
+ 'selectedSlots': to get a list of the currently selected memory slots.
386
+ 'getBlock <address> [lines]': to read a block of memory from the specified address.
387
+ 'readByte <address>': to read a BYTE from the specified address.
388
+ 'readWord <address>': to read a WORD from the specified address.
389
+ 'writeByte <address> <value8>': to write a BYTE to the specified address.
390
+ 'writeWord <address> <value16>': to write a WORD to the specified address.
391
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
392
+ `,
378
393
  // Schema for the tool (input validation)
379
394
  {
380
- command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord", "advanced_basic_listing"]),
395
+ command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"]),
381
396
  address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
382
397
  lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
383
398
  value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
@@ -405,9 +420,6 @@ function registerAllTools(server) {
405
420
  case "writeWord":
406
421
  tclCommand = `poke16 ${address} ${value16}`;
407
422
  break;
408
- case "advanced_basic_listing":
409
- tclCommand = "listing";
410
- break;
411
423
  default:
412
424
  return getResponseContent([
413
425
  `Error: Unknown memory command "${command}".`
@@ -422,11 +434,13 @@ function registerAllTools(server) {
422
434
  // Name of the tool (used to call it)
423
435
  "debug_vram",
424
436
  // Description of the tool (what it does)
425
- "Read or write from/to VRAM video memory in the openMSX emulator. Commands: " +
426
- "'getBlock <address> [lines]': to read a block of VRAM memory from the specified address. " +
427
- "'readByte <address>': to read a BYTE from the specified VRAM address. " +
428
- "'writeByte <address> <value8>': to write a BYTE to the specified VRAM address. " +
429
- "Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
437
+ `Read or write from/to VRAM video memory in the openMSX emulator.
438
+ Commands:
439
+ 'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
440
+ 'readByte <address>': to read a BYTE from the specified VRAM address.
441
+ 'writeByte <address> <value8>': to write a BYTE to the specified VRAM address.
442
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
443
+ `,
430
444
  // Schema for the tool (input validation)
431
445
  {
432
446
  command: z.enum(["getBlock", "readByte", "writeByte"]),
@@ -461,12 +475,14 @@ function registerAllTools(server) {
461
475
  // Name of the tool (used to call it)
462
476
  "debug_breakpoints",
463
477
  // Description of the tool (what it does)
464
- "Create, remove, and list breakpoints. Commands: " +
465
- "'create <address>': create a breakpoint at a specified address, and returns its name. " +
466
- "'remove <bpname>': remove a breakpoint by name (e.g. bp#1). " +
467
- "'list': enumerate the active breakpoints. " +
468
- "Note: Addresses and values are in hexadecimal format (e.g. 0x0000). " +
469
- "Note: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.",
478
+ `Create, remove, and list breakpoints.
479
+ Commands:
480
+ 'create <address>': create a breakpoint at a specified address, and returns its name.
481
+ 'remove <bpname>': remove a breakpoint by name (e.g. bp#1).
482
+ 'list': enumerate the active breakpoints.
483
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
484
+ "**Important Note**: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.
485
+ `,
470
486
  // Schema for the tool (input validation)
471
487
  {
472
488
  command: z.enum(["create", "remove", "list"]),
@@ -500,11 +516,13 @@ function registerAllTools(server) {
500
516
  // Name of the tool (used to call it)
501
517
  "emu_savestates",
502
518
  // Description of the tool (what it does)
503
- "Load, save, and list savestates. Commands: " +
504
- "'load <name>': restores a previously created savestate. " +
505
- "'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate. " +
506
- "'list': returns the names of all previously created savestates, separated by spaces. " +
507
- "Note: names with spaces are enclosed in {}.",
519
+ `Load, save, and list savestates.
520
+ Commands:
521
+ 'load <name>': restores a previously created savestate.
522
+ 'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate.
523
+ 'list': returns the names of all previously created savestates, separated by spaces.
524
+ **Important Note**: names with spaces are enclosed in {}.
525
+ `,
508
526
  // Schema for the tool (input validation)
509
527
  {
510
528
  command: z.enum(["load", "save", "list"]),
@@ -542,15 +560,19 @@ function registerAllTools(server) {
542
560
  // Name of the tool (used to call it)
543
561
  "emu_replay",
544
562
  // 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.",
563
+ `When replay is enabled (the default) the emulator collect data while emulating,
564
+ which enables you to go back and forward in MSX time.
565
+ Commands:
566
+ 'start': starts the replay mode (enabled by default when emulator is launched).
567
+ 'stop': stops the replay mode.
568
+ 'status': gives information about the replay feature and the data that is collected.
569
+ 'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started.
570
+ '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.
571
+ 'truncate': stop replaying and wipe all the future replay data after now.
572
+ 'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response.
573
+ 'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.
574
+ **Important Note**: consider do a #debug_run 'break' to maintain the timeline before a 'goBack' or 'absoluteGoto'.
575
+ `,
554
576
  // Schema for the tool (input validation)
555
577
  {
556
578
  command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "saveReplay", "loadReplay"]),
@@ -582,12 +604,12 @@ function registerAllTools(server) {
582
604
  break;
583
605
  case "saveReplay":
584
606
  if (filename)
585
- filename = `"${filename}"`;
607
+ filename = `"${OPENMSX_REPLAYS_DIR}${filename}"`;
586
608
  tclCommand = `reverse savereplay ${filename || ''}`;
587
609
  break;
588
610
  case "loadReplay":
589
611
  if (filename)
590
- filename = `"${filename}"`;
612
+ filename = `"${OPENMSX_REPLAYS_DIR}${filename}"`;
591
613
  tclCommand = `reverse loadreplay ${filename}`;
592
614
  break;
593
615
  default:
@@ -604,10 +626,12 @@ function registerAllTools(server) {
604
626
  // Name of the tool (used to call it)
605
627
  "emu_keyboard",
606
628
  // Description of the tool (what it does)
607
- "Send a text to the openMSX emulator. Commands: " +
608
- "'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix, is useful for automating tasks in BASIC. " +
609
- "Note: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard. " +
610
- "Note: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...",
629
+ `Send a text to the openMSX emulator.
630
+ Commands:
631
+ 'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix.
632
+ **Important Note**: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard.
633
+ **Important Note**: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...
634
+ `,
611
635
  // Schema for the tool (input validation)
612
636
  {
613
637
  command: z.enum(["sendText"]),
@@ -618,7 +642,7 @@ function registerAllTools(server) {
618
642
  let tclCommand;
619
643
  switch (command) {
620
644
  case "sendText":
621
- tclCommand = `type "${text}"`;
645
+ tclCommand = `type "${encodeTypeText(text)}"`;
622
646
  break;
623
647
  default:
624
648
  return getResponseContent([
@@ -634,9 +658,11 @@ function registerAllTools(server) {
634
658
  // Name of the tool (used to call it)
635
659
  "screen_shot",
636
660
  // Description of the tool (what it does)
637
- "Take a screenshot of the openMSX emulator screen. Commands: " +
638
- "'as_image': take a screenshot and the image is returned in the response. " +
639
- "'to_file': take a screenshot and save it to a file, the file name is returned in the response.",
661
+ `Take a screenshot of the openMSX emulator screen.
662
+ Commands:
663
+ 'as_image': take a screenshot and the image is returned in the response.
664
+ 'to_file': take a screenshot and save it to a file, the file name is returned in the response.
665
+ `,
640
666
  // Schema for the tool (input validation)
641
667
  {
642
668
  command: z.enum(["as_image", "to_file"]),
@@ -648,8 +674,16 @@ function registerAllTools(server) {
648
674
  switch (command) {
649
675
  case "as_image":
650
676
  try {
677
+ // Check if the response is a file path using fstat
678
+ if (!response || !response.startsWith(OPENMSX_SCREENSHOT_DIR) || !response.endsWith('.png')) {
679
+ throw new Error(`Invalid screenshot "${response}"`);
680
+ }
681
+ // Read the screenshot file
651
682
  const imageBuffer = await fs.readFile(response);
652
683
  const base64image = imageBuffer.toString('base64');
684
+ // Remove the file after reading it
685
+ await fs.unlink(response);
686
+ // Return the image in the response
653
687
  return {
654
688
  content: [{
655
689
  type: "text",
@@ -669,7 +703,7 @@ function registerAllTools(server) {
669
703
  }
670
704
  case "to_file":
671
705
  return getResponseContent([
672
- response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response
706
+ isErrorResponse(response) ? response : 'Screenshot taken in file: ' + response
673
707
  ]);
674
708
  }
675
709
  return getResponseContent([
@@ -680,7 +714,9 @@ function registerAllTools(server) {
680
714
  // Name of the tool (used to call it)
681
715
  "screen_dump",
682
716
  // Description of the tool (what it does)
683
- "Take a screendump of the openMSX emulator screen as SC?. The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'. ",
717
+ `Take a screendump of the openMSX emulator screen as SC?.
718
+ The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'.
719
+ `,
684
720
  // Schema for the tool (input validation)
685
721
  {
686
722
  scrbasename: z.string().min(1).max(100).default("screendump"),
@@ -690,14 +726,108 @@ function registerAllTools(server) {
690
726
  const openmsxCommand = `save_msx_screen "${OPENMSX_SCREENDUMP_DIR + scrbasename}"`;
691
727
  const response = await openMSXInstance.sendCommand(openmsxCommand);
692
728
  return getResponseContent([
693
- response.startsWith('Error:') ? 'Fail:' : 'Screendump file saved as:',
729
+ isErrorResponse(response) ? 'Fail:' : 'Screendump file saved as:',
694
730
  response
695
731
  ]);
696
732
  });
733
+ server.tool(
734
+ // Name of the tool (used to call it)
735
+ "basic_programming",
736
+ // Description of the tool (what it does)
737
+ `Tool helper to develop BASIC programs.
738
+ Commands:
739
+ 'newProgram': clears the current BASIC program.
740
+ 'setProgram <program>': sets/updates the current BASIC program to the specified string.
741
+ 'runProgram': runs the current BASIC program.
742
+ 'getFullProgram': returns the current BASIC program as a plain text string.
743
+ 'getFullProgramAdvanced': returns the current BASIC program with the ram address where each line is coded.
744
+ 'listProgramLines <startLine> [endLine]': lists at the emulator screen the selected range lines of the current BASIC program.
745
+ 'deleteProgramLines <startLine> [endline]': deletes a specific line range from the current BASIC program, if endline is not specified, only the startLine is deleted.
746
+ **Important Note**: priorize this tools to develop BASIC programs, it's more efficient than using the 'sendText' tool.
747
+ **Important Note**: all the program lines must be ended with a carriage return (\\r) to be correctly processed, even the last one.
748
+ `,
749
+ // Schema for the tool (input validation)
750
+ {
751
+ command: z.enum(["newProgram", "runProgram", "setProgram", "getFullProgram", "getFullProgramAdvanced", "listProgramLines", "deleteProgramLines"]),
752
+ program: z.string().min(1).max(10000).optional(), // BASIC program to set
753
+ startLine: z.number().min(0).max(9999).optional(),
754
+ endLine: z.number().min(0).max(9999).optional(),
755
+ },
756
+ // Handler for the tool (function to be executed when the tool is called)
757
+ async ({ command, program, startLine, endLine }) => {
758
+ const CTRL_L_TEMPLATE = 'keymatrixdown 6 2 ; keymatrixdown 4 2 ; after time 0.1 { keymatrixup 6 2 ; keymatrixup 4 2 ; type_via_keybuf "%s" }';
759
+ let tclCommand = undefined;
760
+ let response = undefined;
761
+ switch (command) {
762
+ case "newProgram":
763
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('new\r')));
764
+ if (response.startsWith('after#'))
765
+ response = '';
766
+ break;
767
+ case "runProgram":
768
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('run\r')));
769
+ if (response.startsWith('after#'))
770
+ response = '';
771
+ break;
772
+ case "deleteProgramLines":
773
+ if (startLine === undefined) {
774
+ response = 'Error: No startLine number provided to delete BASIC program lines.';
775
+ break;
776
+ }
777
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText(`delete ${startLine}-${endLine || startLine}\r`)));
778
+ break;
779
+ case "setProgram":
780
+ if (!program) {
781
+ response = 'Error: No BASIC program provided to set.';
782
+ break;
783
+ }
784
+ // Get current speed to restore it later
785
+ let speed = '100';
786
+ if (isErrorResponse(speed = await openMSXInstance.sendCommand('set speed'))) {
787
+ response = speed;
788
+ break;
789
+ }
790
+ // Set speed to fast for program input
791
+ if (isErrorResponse(response = await openMSXInstance.sendCommand('set speed 10000')))
792
+ break;
793
+ // Clear the screen and type the program
794
+ if (isErrorResponse(response = await openMSXInstance.sendCommand(`type_via_keybuf "${encodeTypeText(program)}"`)))
795
+ break;
796
+ // Restore the original speed
797
+ if (isErrorResponse(response = await openMSXInstance.sendCommand(`set speed ${speed}`)))
798
+ break;
799
+ // Success response
800
+ response = '';
801
+ break;
802
+ case "getFullProgram":
803
+ // Source: https://www.msx.org/forum/msx-talk/openmsx/export-basic-listing#comment-407392
804
+ tclCommand = 'regsub -all -line {^[0-9a-f]x[0-9a-f]{4} > } [ listing ] ""';
805
+ break;
806
+ case "getFullProgramAdvanced":
807
+ tclCommand = "listing";
808
+ break;
809
+ case "listProgramLines":
810
+ if (startLine === undefined) {
811
+ response = 'Error: No start line provided to list BASIC program lines.';
812
+ break;
813
+ }
814
+ tclCommand = `type_via_keybuf \"${encodeTypeText(`list ${startLine}-${endLine || startLine}\r`)}\"`;
815
+ break;
816
+ default:
817
+ response = `Error: Unknown command "${command}".`;
818
+ break;
819
+ }
820
+ if (response === undefined && tclCommand) {
821
+ response = await openMSXInstance.sendCommand(tclCommand);
822
+ }
823
+ return getResponseContent([
824
+ response !== undefined ? response : `Error: No response for command "${command}".`
825
+ ]);
826
+ });
697
827
  }
698
828
  function getResponseContent(response, isError = false) {
699
829
  // Check if any response line starts with "Error:" to automatically set isError to true
700
- const hasError = isError || response.some(line => line.startsWith("Error:"));
830
+ const hasError = isError || response.some(line => isErrorResponse(line));
701
831
  return {
702
832
  content: response.map(line => ({
703
833
  type: "text",
@@ -865,6 +995,9 @@ async function main() {
865
995
  if (process.env.OPENMSX_SCREENDUMP_DIR && process.env.OPENMSX_SCREENDUMP_DIR !== '') {
866
996
  OPENMSX_SCREENDUMP_DIR = process.env.OPENMSX_SCREENDUMP_DIR + path.sep;
867
997
  }
998
+ if (process.env.OPENMSX_REPLAYS_DIR && process.env.OPENMSX_REPLAYS_DIR !== '') {
999
+ OPENMSX_REPLAYS_DIR = process.env.OPENMSX_REPLAYS_DIR + path.sep;
1000
+ }
868
1001
  if (process.env.OPENMSX_SHARE_DIR) {
869
1002
  OPENMSX_SHARE_DIR = process.env.OPENMSX_SHARE_DIR + path.sep;
870
1003
  MACHINES_DIR = `${OPENMSX_SHARE_DIR}machines`;
package/dist/utils.js CHANGED
@@ -78,3 +78,29 @@ export function encodeHtmlEntities(text) {
78
78
  return char;
79
79
  });
80
80
  }
81
+ /**
82
+ * Encode a string for BASIC program input, escaping special characters
83
+ * @param text - String to encode
84
+ * @returns string - Encoded string with special characters escaped
85
+ */
86
+ export function encodeTypeText(text) {
87
+ const replacementMap = {
88
+ '\r': '\\r',
89
+ '\t': '\\t',
90
+ '"': '\\"',
91
+ };
92
+ return text.replace(/[\r\t"]/g, (char) => {
93
+ if (replacementMap[char]) {
94
+ return replacementMap[char];
95
+ }
96
+ return char;
97
+ });
98
+ }
99
+ /**
100
+ * Check if a response is an error response
101
+ * @param response - The response string to check
102
+ * @returns boolean - True if the response indicates an error, false otherwise
103
+ */
104
+ export function isErrorResponse(response) {
105
+ return response.startsWith('Error:') || response.startsWith('error:');
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",