@nataliapc/mcp-openmsx 1.1.4 → 1.1.8

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.
Files changed (49) hide show
  1. package/README.md +46 -2
  2. package/dist/openmsx.js +11 -1
  3. package/dist/server.js +343 -123
  4. package/dist/utils.js +43 -0
  5. package/package.json +4 -1
  6. package/resources/audio/toc.json +31 -0
  7. package/resources/bios/Calling_BIOS_from_MSX-DOS.md +75 -0
  8. package/resources/bios/MSX2_SUBROM_BIOS_calls.md +734 -0
  9. package/resources/bios/MSX_BIOS_calls.md +1046 -0
  10. package/resources/bios/toc.json +24 -0
  11. package/resources/book--msx2-technical-handbook/Appendix1__BIOS_Listing.md +1464 -0
  12. package/resources/book--msx2-technical-handbook/Appendix2__Math-Pack.md +427 -0
  13. package/resources/book--msx2-technical-handbook/Appendix3__Bit_Block_Transfer.md +182 -0
  14. package/resources/book--msx2-technical-handbook/Appendix4__Work_Area_Listing.md +1637 -0
  15. package/resources/book--msx2-technical-handbook/Appendix5__VRAM_Map.md +145 -0
  16. package/resources/book--msx2-technical-handbook/Appendix6__IO_Map.md +128 -0
  17. package/resources/book--msx2-technical-handbook/Appendix8_10__Control_Codes_and_Escape_Sequences.md +76 -0
  18. package/resources/book--msx2-technical-handbook/Chapter1__MSX_System_Overview.md +402 -0
  19. package/resources/book--msx2-technical-handbook/Chapter2__BASIC.md +2148 -0
  20. package/resources/book--msx2-technical-handbook/Chapter3__MSX-DOS.md +2577 -0
  21. package/resources/book--msx2-technical-handbook/Chapter4a__VDP_and_Display_Screen.md +2052 -0
  22. package/resources/book--msx2-technical-handbook/Chapter4b__VDP_and_Display_Screen.md +3311 -0
  23. package/resources/book--msx2-technical-handbook/Chapter5a__Access_to_Peripherals_through_BIOS.md +2714 -0
  24. package/resources/book--msx2-technical-handbook/Chapter5b__Access_to_Peripherals_through_BIOS.md +1263 -0
  25. package/resources/book--msx2-technical-handbook/MSX_Kun_BASIC_Compiler.md +220 -0
  26. package/resources/book--msx2-technical-handbook/toc.json +82 -0
  27. package/resources/book--the-msx-red-book/the_msx_red_book.md +10349 -0
  28. package/resources/book--the-msx-red-book/toc.json +12 -0
  29. package/resources/msx-dos/MSX-DOS_2_Function_Specifications.md +1366 -0
  30. package/resources/msx-dos/MSX-DOS_2_Program_Interface_Specification.md +963 -0
  31. package/resources/msx-dos/toc.json +18 -0
  32. package/resources/msx-unapi/Ethernet_UNAPI_specification_1.1.md +369 -0
  33. package/resources/msx-unapi/Introduction_to_MSX-UNAPI.md +132 -0
  34. package/resources/msx-unapi/MSX_UNAPI_specification_1.1.md +679 -0
  35. package/resources/msx-unapi/TCP-IP_UNAPI_specification.md +2361 -0
  36. package/resources/msx-unapi/toc.json +27 -0
  37. package/resources/others/toc.json +11 -0
  38. package/resources/processors/Z80_R800_instruction_set.md +482 -0
  39. package/resources/processors/toc.json +24 -0
  40. package/resources/processors/z80-undocumented.tex +5617 -0
  41. package/resources/processors/z80_detailed_instruction_set.md +2025 -0
  42. package/resources/programming/toc.json +121 -0
  43. package/resources/system/MSX_IO_ports_overview.md +554 -0
  44. package/resources/system/toc.json +18 -0
  45. package/resources/video/V9938_Technical_Data_Book.md +3623 -0
  46. package/resources/video/V9958_Technical_Data_Book.md +417 -0
  47. package/resources/video/V9990_Programmers_Manual_Banzai.html +1582 -0
  48. package/resources/video/VDP_TMS9918A.txt +709 -0
  49. package/resources/video/toc.json +28 -0
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.1.4
9
+ * @version 1.1.8
10
10
  * @author Natalia Pujol Cremades (@nataliapc)
11
11
  * @license GPL2
12
12
  */
@@ -18,13 +18,17 @@ import { randomUUID } from "node:crypto";
18
18
  import { z } from "zod";
19
19
  import express from "express";
20
20
  import fs from "fs/promises";
21
+ import mime from "mime-types";
21
22
  import path from "path";
22
23
  import { openMSXInstance } from "./openmsx.js";
24
+ import { encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
23
25
  // Version info for CLI
24
- const PACKAGE_VERSION = "1.1.4";
26
+ const PACKAGE_VERSION = "1.1.8";
27
+ const resourcesDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../resources");
25
28
  // Defaults for openMSX paths
26
29
  var OPENMSX_EXECUTABLE = 'openmsx';
27
30
  var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
31
+ var OPENMSX_REPLAYS_DIR = '';
28
32
  var OPENMSX_SCREENSHOT_DIR = '';
29
33
  var OPENMSX_SCREENDUMP_DIR = '';
30
34
  var MACHINES_DIR = `${OPENMSX_SHARE_DIR}/machines`;
@@ -33,22 +37,24 @@ var EXTENSIONS_DIR = `${OPENMSX_SHARE_DIR}/extensions`;
33
37
  // Tools available in the MCP server
34
38
  // https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
35
39
  //
36
- function registerAllTools(server) {
40
+ async function registerAllTools(server) {
37
41
  server.tool(
38
42
  // Name of the tool (used to call it)
39
43
  "emu_control",
40
44
  // 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. ",
45
+ `Controls an openMSX emulator.
46
+ Commands:
47
+ '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. " +
48
+ 'close': closes the openMSX emulator.
49
+ 'powerOn': powers on the openMSX emulator.
50
+ 'powerOff': powers off the openMSX emulator.
51
+ 'reset': resets the current machine.
52
+ 'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100.
53
+ 'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100.
54
+ 'machineList': gets a list of all available MSX machines that can be emulated with openMSX.
55
+ 'extensionList': gets a list of all available MSX extensions that can be used with openMSX.
56
+ 'wait <seconds>': performs a wait for the specified number of seconds, default is 2.
57
+ `,
52
58
  // Schema for the tool (input validation)
53
59
  {
54
60
  command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]),
@@ -110,15 +116,17 @@ function registerAllTools(server) {
110
116
  // Name of the tool (used to call it)
111
117
  "emu_media",
112
118
  // 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. ",
119
+ `Manage tapes, rom cartridges, and floppy disks.
120
+ Commands:
121
+ 'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx).
122
+ 'tapeRewind': rewind the current tape.
123
+ 'tapeEject': remove tape from virtual cassette player.
124
+ 'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A.
125
+ 'romEject': remove the current ROM cartridge from cartridge slot A.
126
+ 'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A.
127
+ 'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory.
128
+ 'diskEject': remove the current disk from floppy disk A.
129
+ `,
122
130
  // Schema for the tool (input validation)
123
131
  {
124
132
  command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert", "diskInsertFolder", "diskEject"]),
@@ -170,10 +178,12 @@ function registerAllTools(server) {
170
178
  // Name of the tool (used to call it)
171
179
  "emu_info",
172
180
  // 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. ",
181
+ `Obtain informacion about the current emulated machine.
182
+ Commands:
183
+ 'getStatus': returns the status of the openMSX emulator.
184
+ 'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots.
185
+ 'getIOPortsMap': shows an overview about the I/O mapped devices.
186
+ `,
177
187
  // Schema for the tool (input validation)
178
188
  {
179
189
  command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]),
@@ -206,13 +216,15 @@ function registerAllTools(server) {
206
216
  // Name of the tool (used to call it)
207
217
  "emu_vdp",
208
218
  // 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. ",
219
+ `Manage the VDP (Video Display Processor).
220
+ Commands:
221
+ 'getPalette': returns the current V9938/V9958 color palette in RGB333 format.
222
+ 'getRegisters': returns all VDP register values.
223
+ 'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format.
224
+ 'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31).
225
+ 'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command.
226
+ '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.
227
+ `,
216
228
  // Schema for the tool (input validation)
217
229
  {
218
230
  command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]),
@@ -240,7 +252,7 @@ function registerAllTools(server) {
240
252
  break;
241
253
  case "screenGetFullText":
242
254
  const response = await openMSXInstance.sendCommand('get_screen');
243
- return response.startsWith('Error:') ?
255
+ return isErrorResponse(response) ?
244
256
  getResponseContent([response]) :
245
257
  getResponseContent(["The screen text is:", response]);
246
258
  default:
@@ -257,16 +269,18 @@ function registerAllTools(server) {
257
269
  // Name of the tool (used to call it)
258
270
  "debug_run",
259
271
  // 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).",
272
+ `Control execution (break, continue, step).
273
+ Commands:
274
+ 'break': to break CPU at current execution position.
275
+ 'isBreaked': to check if the CPU is currently in break state (1) or not (0).
276
+ 'continue': to continue execution after break.
277
+ 'stepIn': to execute one CPU instruction, go into subroutines.
278
+ 'stepOver': to execute one CPU instruction, but don't go into subroutines.
279
+ 'stepOut': to step out of the current subroutine.
280
+ 'stepBack': to step one instruction back in time.
281
+ 'runTo <address>': to run the CPU until it reaches the specified address.
282
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
283
+ `,
270
284
  // Schema for the tool (input validation)
271
285
  {
272
286
  command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver", "stepBack", "runTo"]),
@@ -314,21 +328,23 @@ function registerAllTools(server) {
314
328
  // Name of the tool (used to call it)
315
329
  "debug_cpu",
316
330
  // 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).",
331
+ `Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory.
332
+ Commands:
333
+ 'getCpuRegisters': to get an overview of all the CPU registers.
334
+ '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).
335
+ '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).
336
+ 'getStackPile': to get an overview of the CPU stack.
337
+ 'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty.
338
+ 'getActiveCpu': to return the active cpu: z80 or r800.
339
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0xd2 0x3af2)."
340
+ `,
325
341
  // Schema for the tool (input validation)
326
342
  {
327
343
  command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble", "getActiveCpu"]),
328
344
  register: z.enum(["pc", "sp", "ix", "iy", "af", "bc", "de", "hl", "ixh", "ixl", "iyh", "iyl", "a", "f", "b", "c", "d", "e", "h", "l", "i", "r", "im", "iff"]).optional(),
329
- address: z.string().regex(/^0x[0-9a-fA-F]{4}$/), // 4 hex digits for MSX memory address
330
- value: z.string().regex(/^0x[0-9a-fA-F]{2-4}$/).optional(), // 2-4 hex digits for byte value for writeByte command
331
- size: z.number().min(1).max(50).optional().default(8), // Number of bytes for disassemble command
345
+ address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
346
+ value: z.string().regex(/^0x[0-9a-fA-F]{2,4}$/).optional(), // 2-4 hex digits for byte value for writeByte command
347
+ size: z.number().min(8).max(50).optional(), // Number of bytes for disassemble command
332
348
  },
333
349
  // Handler for the tool (function to be executed when the tool is called)
334
350
  async ({ command, address, register, value, size }) => {
@@ -338,10 +354,10 @@ function registerAllTools(server) {
338
354
  tclCommand = "cpuregs";
339
355
  break;
340
356
  case "getRegister":
341
- tclCommand = "reg ${register}";
357
+ tclCommand = `reg ${register}`;
342
358
  break;
343
359
  case "setRegister":
344
- tclCommand = "reg ${register} ${value}";
360
+ tclCommand = `reg ${register} ${value}`;
345
361
  break;
346
362
  case "getStackPile":
347
363
  tclCommand = "stack";
@@ -366,18 +382,19 @@ function registerAllTools(server) {
366
382
  // Name of the tool (used to call it)
367
383
  "debug_memory",
368
384
  // 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).",
385
+ `Slots info, and Read/write from/to memory in the openMSX emulator.
386
+ Commands:
387
+ 'selectedSlots': to get a list of the currently selected memory slots.
388
+ 'getBlock <address> [lines]': to read a block of memory from the specified address.
389
+ 'readByte <address>': to read a BYTE from the specified address.
390
+ 'readWord <address>': to read a WORD from the specified address.
391
+ 'writeByte <address> <value8>': to write a BYTE to the specified address.
392
+ 'writeWord <address> <value16>': to write a WORD to the specified address.
393
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
394
+ `,
378
395
  // Schema for the tool (input validation)
379
396
  {
380
- command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord", "advanced_basic_listing"]),
397
+ command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"]),
381
398
  address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
382
399
  lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
383
400
  value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
@@ -405,9 +422,6 @@ function registerAllTools(server) {
405
422
  case "writeWord":
406
423
  tclCommand = `poke16 ${address} ${value16}`;
407
424
  break;
408
- case "advanced_basic_listing":
409
- tclCommand = "listing";
410
- break;
411
425
  default:
412
426
  return getResponseContent([
413
427
  `Error: Unknown memory command "${command}".`
@@ -422,11 +436,13 @@ function registerAllTools(server) {
422
436
  // Name of the tool (used to call it)
423
437
  "debug_vram",
424
438
  // 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).",
439
+ `Read or write from/to VRAM video memory in the openMSX emulator.
440
+ Commands:
441
+ 'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
442
+ 'readByte <address>': to read a BYTE from the specified VRAM address.
443
+ 'writeByte <address> <value8>': to write a BYTE to the specified VRAM address.
444
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
445
+ `,
430
446
  // Schema for the tool (input validation)
431
447
  {
432
448
  command: z.enum(["getBlock", "readByte", "writeByte"]),
@@ -461,12 +477,14 @@ function registerAllTools(server) {
461
477
  // Name of the tool (used to call it)
462
478
  "debug_breakpoints",
463
479
  // 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.",
480
+ `Create, remove, and list breakpoints.
481
+ Commands:
482
+ 'create <address>': create a breakpoint at a specified address, and returns its name.
483
+ 'remove <bpname>': remove a breakpoint by name (e.g. bp#1).
484
+ 'list': enumerate the active breakpoints.
485
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
486
+ "**Important Note**: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.
487
+ `,
470
488
  // Schema for the tool (input validation)
471
489
  {
472
490
  command: z.enum(["create", "remove", "list"]),
@@ -500,11 +518,13 @@ function registerAllTools(server) {
500
518
  // Name of the tool (used to call it)
501
519
  "emu_savestates",
502
520
  // 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 {}.",
521
+ `Load, save, and list savestates.
522
+ Commands:
523
+ 'load <name>': restores a previously created savestate.
524
+ 'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate.
525
+ 'list': returns the names of all previously created savestates, separated by spaces.
526
+ **Important Note**: names with spaces are enclosed in {}.
527
+ `,
508
528
  // Schema for the tool (input validation)
509
529
  {
510
530
  command: z.enum(["load", "save", "list"]),
@@ -542,15 +562,19 @@ function registerAllTools(server) {
542
562
  // Name of the tool (used to call it)
543
563
  "emu_replay",
544
564
  // 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.",
565
+ `When replay is enabled (the default) the emulator collect data while emulating,
566
+ which enables you to go back and forward in MSX time.
567
+ Commands:
568
+ 'start': starts the replay mode (enabled by default when emulator is launched).
569
+ 'stop': stops the replay mode.
570
+ 'status': gives information about the replay feature and the data that is collected.
571
+ 'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started.
572
+ '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.
573
+ 'truncate': stop replaying and wipe all the future replay data after now.
574
+ 'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response.
575
+ 'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.
576
+ **Important Note**: consider do a #debug_run 'break' to maintain the timeline before a 'goBack' or 'absoluteGoto'.
577
+ `,
554
578
  // Schema for the tool (input validation)
555
579
  {
556
580
  command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "saveReplay", "loadReplay"]),
@@ -582,12 +606,12 @@ function registerAllTools(server) {
582
606
  break;
583
607
  case "saveReplay":
584
608
  if (filename)
585
- filename = `"${filename}"`;
609
+ filename = `"${OPENMSX_REPLAYS_DIR}${filename}"`;
586
610
  tclCommand = `reverse savereplay ${filename || ''}`;
587
611
  break;
588
612
  case "loadReplay":
589
613
  if (filename)
590
- filename = `"${filename}"`;
614
+ filename = `"${OPENMSX_REPLAYS_DIR}${filename}"`;
591
615
  tclCommand = `reverse loadreplay ${filename}`;
592
616
  break;
593
617
  default:
@@ -604,10 +628,12 @@ function registerAllTools(server) {
604
628
  // Name of the tool (used to call it)
605
629
  "emu_keyboard",
606
630
  // 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...",
631
+ `Send a text to the openMSX emulator.
632
+ Commands:
633
+ 'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix.
634
+ **Important Note**: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard.
635
+ **Important Note**: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...
636
+ `,
611
637
  // Schema for the tool (input validation)
612
638
  {
613
639
  command: z.enum(["sendText"]),
@@ -618,7 +644,7 @@ function registerAllTools(server) {
618
644
  let tclCommand;
619
645
  switch (command) {
620
646
  case "sendText":
621
- tclCommand = `type "${text}"`;
647
+ tclCommand = `type "${encodeTypeText(text)}"`;
622
648
  break;
623
649
  default:
624
650
  return getResponseContent([
@@ -634,9 +660,11 @@ function registerAllTools(server) {
634
660
  // Name of the tool (used to call it)
635
661
  "screen_shot",
636
662
  // 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.",
663
+ `Take a screenshot of the openMSX emulator screen.
664
+ Commands:
665
+ 'as_image': take a screenshot and the image is returned in the response.
666
+ 'to_file': take a screenshot and save it to a file, the file name is returned in the response.
667
+ `,
640
668
  // Schema for the tool (input validation)
641
669
  {
642
670
  command: z.enum(["as_image", "to_file"]),
@@ -648,8 +676,16 @@ function registerAllTools(server) {
648
676
  switch (command) {
649
677
  case "as_image":
650
678
  try {
679
+ // Check if the response is a file path
680
+ if (!response || !response.startsWith(OPENMSX_SCREENSHOT_DIR) || !response.endsWith('.png')) {
681
+ throw new Error(`Invalid screenshot "${response}"`);
682
+ }
683
+ // Read the screenshot file
651
684
  const imageBuffer = await fs.readFile(response);
652
685
  const base64image = imageBuffer.toString('base64');
686
+ // Remove the file after reading it
687
+ await fs.unlink(response);
688
+ // Return the image in the response
653
689
  return {
654
690
  content: [{
655
691
  type: "text",
@@ -669,7 +705,7 @@ function registerAllTools(server) {
669
705
  }
670
706
  case "to_file":
671
707
  return getResponseContent([
672
- response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response
708
+ isErrorResponse(response) ? response : 'Screenshot taken in file: ' + response
673
709
  ]);
674
710
  }
675
711
  return getResponseContent([
@@ -680,7 +716,9 @@ function registerAllTools(server) {
680
716
  // Name of the tool (used to call it)
681
717
  "screen_dump",
682
718
  // 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'. ",
719
+ `Take a screendump of the openMSX emulator screen as SC?.
720
+ The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'.
721
+ `,
684
722
  // Schema for the tool (input validation)
685
723
  {
686
724
  scrbasename: z.string().min(1).max(100).default("screendump"),
@@ -690,21 +728,200 @@ function registerAllTools(server) {
690
728
  const openmsxCommand = `save_msx_screen "${OPENMSX_SCREENDUMP_DIR + scrbasename}"`;
691
729
  const response = await openMSXInstance.sendCommand(openmsxCommand);
692
730
  return getResponseContent([
693
- response.startsWith('Error:') ? 'Fail:' : 'Screendump file saved as:',
731
+ isErrorResponse(response) ? 'Fail:' : 'Screendump file saved as:',
694
732
  response
695
733
  ]);
696
734
  });
735
+ server.tool(
736
+ // Name of the tool (used to call it)
737
+ "basic_programming",
738
+ // Description of the tool (what it does)
739
+ `Tool helper to develop BASIC programs.
740
+ Commands:
741
+ 'newProgram': clears the current BASIC program.
742
+ 'setProgram <program>': sets/updates the current BASIC program to the specified string.
743
+ 'runProgram': runs the current BASIC program.
744
+ 'getFullProgram': returns the current BASIC program as a plain text string.
745
+ 'getFullProgramAdvanced': returns the current BASIC program with the ram address where each line is coded.
746
+ 'listProgramLines <startLine> [endLine]': lists at the emulator screen the selected range lines of the current BASIC program.
747
+ 'deleteProgramLines <startLine> [endline]': deletes a specific line range from the current BASIC program, if endline is not specified, only the startLine is deleted.
748
+ **Important Note**: priorize this tools to develop BASIC programs, it's more efficient than using the 'sendText' tool.
749
+ **Important Note**: all the program lines must be ended with a carriage return (\\r) to be correctly processed, even the last one.
750
+ **Important Note**: if you have doubts about MSX BASIC, use the resources provided by this MCP.
751
+ `,
752
+ // Schema for the tool (input validation)
753
+ {
754
+ command: z.enum(["newProgram", "runProgram", "setProgram", "getFullProgram", "getFullProgramAdvanced", "listProgramLines", "deleteProgramLines"]),
755
+ program: z.string().min(1).max(10000).optional(), // BASIC program to set
756
+ startLine: z.number().min(0).max(9999).optional(),
757
+ endLine: z.number().min(0).max(9999).optional(),
758
+ },
759
+ // Handler for the tool (function to be executed when the tool is called)
760
+ async ({ command, program, startLine, endLine }) => {
761
+ const CTRL_L_TEMPLATE = 'keymatrixdown 6 2 ; keymatrixdown 4 2 ; after time 0.1 { keymatrixup 6 2 ; keymatrixup 4 2 ; type_via_keybuf "%s" }';
762
+ let tclCommand = undefined;
763
+ let response = undefined;
764
+ switch (command) {
765
+ case "newProgram":
766
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('new\r')));
767
+ if (response.startsWith('after#'))
768
+ response = '';
769
+ break;
770
+ case "runProgram":
771
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('run\r')));
772
+ if (response.startsWith('after#'))
773
+ response = '';
774
+ break;
775
+ case "deleteProgramLines":
776
+ if (startLine === undefined) {
777
+ response = 'Error: No startLine number provided to delete BASIC program lines.';
778
+ break;
779
+ }
780
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText(`delete ${startLine}-${endLine || startLine}\r`)));
781
+ break;
782
+ case "setProgram":
783
+ if (!program) {
784
+ response = 'Error: No BASIC program provided to set.';
785
+ break;
786
+ }
787
+ // Get current speed to restore it later
788
+ let speed = '100';
789
+ if (isErrorResponse(speed = await openMSXInstance.sendCommand('set speed'))) {
790
+ response = speed;
791
+ break;
792
+ }
793
+ // Set speed to fast for program input
794
+ if (isErrorResponse(response = await openMSXInstance.sendCommand('set speed 10000')))
795
+ break;
796
+ // Clear the screen and type the program
797
+ if (isErrorResponse(response = await openMSXInstance.sendCommand(`type_via_keybuf "${encodeTypeText(program)}"`)))
798
+ break;
799
+ // Restore the original speed
800
+ if (isErrorResponse(response = await openMSXInstance.sendCommand(`set speed ${speed}`)))
801
+ break;
802
+ // Success response
803
+ response = '';
804
+ break;
805
+ case "getFullProgram":
806
+ // Source: https://www.msx.org/forum/msx-talk/openmsx/export-basic-listing#comment-407392
807
+ tclCommand = 'regsub -all -line {^[0-9a-f]x[0-9a-f]{4} > } [ listing ] ""';
808
+ break;
809
+ case "getFullProgramAdvanced":
810
+ tclCommand = "listing";
811
+ break;
812
+ case "listProgramLines":
813
+ if (startLine === undefined) {
814
+ response = 'Error: No start line provided to list BASIC program lines.';
815
+ break;
816
+ }
817
+ tclCommand = `type_via_keybuf \"${encodeTypeText(`list ${startLine}-${endLine || startLine}\r`)}\"`;
818
+ break;
819
+ default:
820
+ response = `Error: Unknown command "${command}".`;
821
+ break;
822
+ }
823
+ if (response === undefined && tclCommand) {
824
+ response = await openMSXInstance.sendCommand(tclCommand);
825
+ }
826
+ return getResponseContent([
827
+ response !== undefined ? response : `Error: No response for command "${command}".`
828
+ ]);
829
+ });
830
+ // ============================================================================
831
+ // MSX Documentation resources
832
+ const resdocs = (await listResourcesDirectory()).sort();
833
+ for (let index = 0; index < resdocs.length; index++) {
834
+ const sectionName = resdocs[index];
835
+ const tocFile = path.join(resourcesDir, `${sectionName}/toc.json`);
836
+ const tocContent = JSON.parse(await fs.readFile(tocFile, 'utf8'));
837
+ tocContent.toc.forEach((item, itemIndex) => {
838
+ const itemName = item.uri.split('/').pop() || '';
839
+ server.resource(
840
+ // Name of the resource (used to call it)
841
+ `msxdocs_${sectionName}_${itemName}`,
842
+ // Resource URI template
843
+ item.uri,
844
+ // Metadata for the resource
845
+ {
846
+ title: item.title || `MSX Documentation '${sectionName}' - ${itemName}`,
847
+ description: item.description || `Documentation for MSX resource '${sectionName}' - ${itemName}`,
848
+ mimeType: item.mimeType || 'text/markdown',
849
+ },
850
+ // Handler for the resource (function to be executed when the resource is called)
851
+ async (uri) => {
852
+ let resourceContent;
853
+ let mimeType;
854
+ if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
855
+ // Fetch the resource from the URL
856
+ try {
857
+ resourceContent = await fetch(uri.href).then(response => {
858
+ mimeType = response.headers.get('content-type') || 'text/plain';
859
+ return response.text();
860
+ }) || 'Error downloading resource content';
861
+ }
862
+ catch (error) {
863
+ // Throw exception (MCP protocol requirement)
864
+ throw new Error(`Error fetching resource from ${uri.href}: ${error instanceof Error ? error.message : String(error)}`);
865
+ }
866
+ }
867
+ else {
868
+ // Read the resource from the local MCP server resources directory
869
+ try {
870
+ let resourceFile;
871
+ [mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, `${sectionName}/${itemName}`));
872
+ resourceContent = await fs.readFile(resourceFile, 'utf8');
873
+ }
874
+ catch (error) {
875
+ // Throw exception (MCP protocol requirement)
876
+ throw new Error(`Error reading resource ${sectionName}/${item.uri}: ${error instanceof Error ? error.message : String(error)}`);
877
+ }
878
+ }
879
+ return {
880
+ contents: [{
881
+ uri: uri.href,
882
+ text: resourceContent,
883
+ mimeType: mimeType || 'text/plain',
884
+ }],
885
+ };
886
+ });
887
+ });
888
+ }
889
+ ;
890
+ }
891
+ async function addFileExtension(filePath) {
892
+ // Get directory and filename
893
+ const directory = path.dirname(filePath);
894
+ const filename = path.basename(filePath);
895
+ try {
896
+ // Get all files in directory that start with our filename
897
+ const files = await fs.readdir(directory);
898
+ const matchingFiles = files.filter(file => file.startsWith(filename));
899
+ if (matchingFiles.length > 0) {
900
+ const fileFound = path.join(directory, matchingFiles[0]);
901
+ return [
902
+ mime.lookup(fileFound) || 'text/plain',
903
+ fileFound
904
+ ];
905
+ }
906
+ }
907
+ catch (error) {
908
+ console.error('Error reading directory:', error);
909
+ }
910
+ // Return original if no matches found
911
+ return ['text/plain', filePath];
697
912
  }
698
- function getResponseContent(response, isError = false) {
699
- // Check if any response line starts with "Error:" to automatically set isError to true
700
- const hasError = isError || response.some(line => line.startsWith("Error:"));
701
- return {
702
- content: response.map(line => ({
703
- type: "text",
704
- text: line == '' ? "Ok" : line,
705
- })),
706
- isError: hasError
707
- };
913
+ async function listResourcesDirectory() {
914
+ try {
915
+ const directories = await fs.readdir(resourcesDir, { withFileTypes: true });
916
+ const folderNames = directories
917
+ .filter(dirent => dirent.isDirectory())
918
+ .map(dirent => dirent.name);
919
+ return folderNames;
920
+ }
921
+ catch (error) {
922
+ console.error("Error reading resources directory:", error);
923
+ return [];
924
+ }
708
925
  }
709
926
  // ============================================================================
710
927
  // Cleanup handlers for graceful shutdown of MCP server
@@ -803,7 +1020,7 @@ async function startHttpServer() {
803
1020
  }
804
1021
  };
805
1022
  // Create a new server instance for this session
806
- const httpServer = createServerInstance();
1023
+ const httpServer = await createServerInstance();
807
1024
  await httpServer.connect(transport);
808
1025
  }
809
1026
  else {
@@ -831,14 +1048,14 @@ async function startHttpServer() {
831
1048
  console.log(`MCP Server listening on port ${port}`);
832
1049
  });
833
1050
  }
834
- function createServerInstance() {
1051
+ async function createServerInstance() {
835
1052
  // Create a new server instance (you might want to extract server creation logic)
836
1053
  const newServer = new McpServer({
837
1054
  name: "mcp-openmsx",
838
1055
  version: PACKAGE_VERSION,
839
1056
  });
840
1057
  // Re-register all tools (you might want to extract this to a separate function)
841
- registerAllTools(newServer);
1058
+ await registerAllTools(newServer);
842
1059
  return newServer;
843
1060
  }
844
1061
  // ============================================================================
@@ -865,6 +1082,9 @@ async function main() {
865
1082
  if (process.env.OPENMSX_SCREENDUMP_DIR && process.env.OPENMSX_SCREENDUMP_DIR !== '') {
866
1083
  OPENMSX_SCREENDUMP_DIR = process.env.OPENMSX_SCREENDUMP_DIR + path.sep;
867
1084
  }
1085
+ if (process.env.OPENMSX_REPLAYS_DIR && process.env.OPENMSX_REPLAYS_DIR !== '') {
1086
+ OPENMSX_REPLAYS_DIR = process.env.OPENMSX_REPLAYS_DIR + path.sep;
1087
+ }
868
1088
  if (process.env.OPENMSX_SHARE_DIR) {
869
1089
  OPENMSX_SHARE_DIR = process.env.OPENMSX_SHARE_DIR + path.sep;
870
1090
  MACHINES_DIR = `${OPENMSX_SHARE_DIR}machines`;
@@ -879,7 +1099,7 @@ async function main() {
879
1099
  else {
880
1100
  // Default to stdio
881
1101
  const transport = new StdioServerTransport();
882
- await createServerInstance().connect(transport);
1102
+ (await createServerInstance()).connect(transport);
883
1103
  }
884
1104
  }
885
1105
  main().catch((error) => {