@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.
- package/README.md +46 -2
- package/dist/openmsx.js +11 -1
- package/dist/server.js +343 -123
- package/dist/utils.js +43 -0
- package/package.json +4 -1
- package/resources/audio/toc.json +31 -0
- package/resources/bios/Calling_BIOS_from_MSX-DOS.md +75 -0
- package/resources/bios/MSX2_SUBROM_BIOS_calls.md +734 -0
- package/resources/bios/MSX_BIOS_calls.md +1046 -0
- package/resources/bios/toc.json +24 -0
- package/resources/book--msx2-technical-handbook/Appendix1__BIOS_Listing.md +1464 -0
- package/resources/book--msx2-technical-handbook/Appendix2__Math-Pack.md +427 -0
- package/resources/book--msx2-technical-handbook/Appendix3__Bit_Block_Transfer.md +182 -0
- package/resources/book--msx2-technical-handbook/Appendix4__Work_Area_Listing.md +1637 -0
- package/resources/book--msx2-technical-handbook/Appendix5__VRAM_Map.md +145 -0
- package/resources/book--msx2-technical-handbook/Appendix6__IO_Map.md +128 -0
- package/resources/book--msx2-technical-handbook/Appendix8_10__Control_Codes_and_Escape_Sequences.md +76 -0
- package/resources/book--msx2-technical-handbook/Chapter1__MSX_System_Overview.md +402 -0
- package/resources/book--msx2-technical-handbook/Chapter2__BASIC.md +2148 -0
- package/resources/book--msx2-technical-handbook/Chapter3__MSX-DOS.md +2577 -0
- package/resources/book--msx2-technical-handbook/Chapter4a__VDP_and_Display_Screen.md +2052 -0
- package/resources/book--msx2-technical-handbook/Chapter4b__VDP_and_Display_Screen.md +3311 -0
- package/resources/book--msx2-technical-handbook/Chapter5a__Access_to_Peripherals_through_BIOS.md +2714 -0
- package/resources/book--msx2-technical-handbook/Chapter5b__Access_to_Peripherals_through_BIOS.md +1263 -0
- package/resources/book--msx2-technical-handbook/MSX_Kun_BASIC_Compiler.md +220 -0
- package/resources/book--msx2-technical-handbook/toc.json +82 -0
- package/resources/book--the-msx-red-book/the_msx_red_book.md +10349 -0
- package/resources/book--the-msx-red-book/toc.json +12 -0
- package/resources/msx-dos/MSX-DOS_2_Function_Specifications.md +1366 -0
- package/resources/msx-dos/MSX-DOS_2_Program_Interface_Specification.md +963 -0
- package/resources/msx-dos/toc.json +18 -0
- package/resources/msx-unapi/Ethernet_UNAPI_specification_1.1.md +369 -0
- package/resources/msx-unapi/Introduction_to_MSX-UNAPI.md +132 -0
- package/resources/msx-unapi/MSX_UNAPI_specification_1.1.md +679 -0
- package/resources/msx-unapi/TCP-IP_UNAPI_specification.md +2361 -0
- package/resources/msx-unapi/toc.json +27 -0
- package/resources/others/toc.json +11 -0
- package/resources/processors/Z80_R800_instruction_set.md +482 -0
- package/resources/processors/toc.json +24 -0
- package/resources/processors/z80-undocumented.tex +5617 -0
- package/resources/processors/z80_detailed_instruction_set.md +2025 -0
- package/resources/programming/toc.json +121 -0
- package/resources/system/MSX_IO_ports_overview.md +554 -0
- package/resources/system/toc.json +18 -0
- package/resources/video/V9938_Technical_Data_Book.md +3623 -0
- package/resources/video/V9958_Technical_Data_Book.md +417 -0
- package/resources/video/V9990_Programmers_Manual_Banzai.html +1582 -0
- package/resources/video/VDP_TMS9918A.txt +709 -0
- 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.
|
|
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.
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
331
|
-
size: z.number().min(
|
|
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 =
|
|
357
|
+
tclCommand = `reg ${register}`;
|
|
342
358
|
break;
|
|
343
359
|
case "setRegister":
|
|
344
|
-
tclCommand =
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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"
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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) => {
|