@nataliapc/mcp-openmsx 1.1.8 → 1.1.14

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
@@ -44,13 +44,13 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
44
44
 
45
45
  ### Emulator Control Tools
46
46
  - `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`, `wait`_.
47
- - `emu_replay`: Controls emulation timeline: _`start`, `stop`, `status`, `goBack`, `absoluteGoto`, `truncate`, `saveReplay`, `loadReplay`_.
47
+ - `emu_replay`: Controls emulation timeline: _`start`, `stop`, `status`, `goBack`, `absoluteGoto`, `advanceFrame`, `reverseFrame`, `truncate`, `saveReplay`, `loadReplay`_.
48
48
  - `emu_info`: Obtain informacion about the current emulated machine: _`getStatus`, `getSlotsMap`, `getIOPortsMap`_.
49
49
  - `emu_media`: Manage ROM, disk, and tape media: _`tapeInsert`, `tapeRewind`, `tapeEject`, `romInsert`, `romEject`, `diskInsert`, `diskInsertFolder`, `diskEject`_.
50
50
  - `emu_vdp`: Manage VDP (Video Display Processor): _`getPalette`, `getRegisters`, `getRegisterValue`, `setRegisterValue`, `screenGetMode`, `screenGetFullText`_.
51
51
 
52
52
  ### Programming Tools
53
- - `basic_programming`: BASIC tools: _`newProgram`, `runProgram`, `setProgram`, `getFullProgram`, `getFullProgramAdvanced`, `listProgramLines`, `deleteProgramLines`_.
53
+ - `basic_programming`: BASIC tools: _`isBasicAvailable`, `newProgram`, `runProgram`, `setProgram`, `getFullProgram`, `getFullProgramAdvanced`, `listProgramLines`, `deleteProgramLines`_.
54
54
 
55
55
  ### Debugging Tools
56
56
  - `debug_run`: Control execution: _`break`, `isBreaked`, `continue`, `stepIn`, `stepOut`, `stepOver`, `stepBack`, `runTo`_.
@@ -83,6 +83,7 @@ There are more than 60 resources available, some included directly in the MCP an
83
83
  - `Programming` (ASM, BASIC, ...)
84
84
  - `MSX-DOS`
85
85
  - `MSX-UNAPI`
86
+ - `MSX BASIC`
86
87
 
87
88
  And two books:
88
89
 
package/dist/openmsx.js CHANGED
@@ -212,6 +212,15 @@ export class OpenMSX {
212
212
  return `Error: Failed to get machine status - ${error instanceof Error ? error.message : 'Unknown error'}`;
213
213
  }
214
214
  }
215
+ async emu_isInBasic() {
216
+ try {
217
+ const response = await this.sendCommand('slotselect');
218
+ return response.includes('0000: slot 0') && response.includes('4000: slot 0');
219
+ }
220
+ catch (error) {
221
+ return false;
222
+ }
223
+ }
215
224
  /**
216
225
  * Get the list of machines available in the openMSX emulator
217
226
  * @returns Promise<object> - object with machine names and descriptions or error message
package/dist/server.js CHANGED
@@ -6,11 +6,11 @@
6
6
  * through TCL commands via stdio.
7
7
  *
8
8
  * @package @nataliapc/mcp-openmsx
9
- * @version 1.1.8
9
+ * @version 1.1.14
10
10
  * @author Natalia Pujol Cremades (@nataliapc)
11
11
  * @license GPL2
12
12
  */
13
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16
16
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
@@ -18,12 +18,11 @@ 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";
22
21
  import path from "path";
23
22
  import { openMSXInstance } from "./openmsx.js";
24
- import { encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
23
+ import { fetchCleanWebpage, addFileExtension, listResourcesDirectory, encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
25
24
  // Version info for CLI
26
- const PACKAGE_VERSION = "1.1.8";
25
+ const PACKAGE_VERSION = "1.1.14";
27
26
  const resourcesDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../resources");
28
27
  // Defaults for openMSX paths
29
28
  var OPENMSX_EXECUTABLE = 'openmsx';
@@ -38,30 +37,31 @@ var EXTENSIONS_DIR = `${OPENMSX_SHARE_DIR}/extensions`;
38
37
  // https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
39
38
  //
40
39
  async function registerAllTools(server) {
41
- server.tool(
40
+ server.registerTool(
42
41
  // Name of the tool (used to call it)
43
- "emu_control",
44
- // Description of the tool (what it does)
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
- `,
58
- // Schema for the tool (input validation)
59
- {
60
- command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]),
61
- machine: z.string().min(1).max(100).optional(),
62
- extensions: z.array(z.string().min(1).max(100)).optional(),
63
- emuspeed: z.number().min(1).max(10000).optional().default(100),
64
- seconds: z.number().min(1).max(10).optional().default(2), // Seconds to wait
42
+ "emu_control", {
43
+ title: "Emulator control tools",
44
+ // Description of the tool (what it does)
45
+ description: "Tools to control an openMSX emulator.",
46
+ // Schema for the tool (input validation)
47
+ inputSchema: {
48
+ command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]).describe(`Available commands:
49
+ '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. " +
50
+ 'close': closes the openMSX emulator.
51
+ 'powerOn': powers on the openMSX emulator.
52
+ 'powerOff': powers off the openMSX emulator.
53
+ 'reset': resets the current machine.
54
+ 'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100.
55
+ 'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100.
56
+ 'machineList': gets a list of all available MSX machines that can be emulated with openMSX.
57
+ 'extensionList': gets a list of all available MSX extensions that can be used with openMSX.
58
+ 'wait <seconds>': performs a wait for the specified number of seconds, default is 3.
59
+ `),
60
+ machine: z.string().min(1).max(100).optional().describe("Machine name to launch; valid names can be obtained using [machineList]. Used by [launch]."),
61
+ extensions: z.array(z.string().min(1).max(100)).optional().describe("List of extensions to use; valid extensions can be obtained using [extensionList]. Used by [launch]."),
62
+ emuspeed: z.number().min(1).max(10000).optional().default(100).describe("Emulator speed as a percentage (1-10000); default is 100. Used by [setEmulatorSpeed]."),
63
+ seconds: z.number().min(1).max(10).optional().default(3).describe("Number of seconds to wait; default is 3. Used by [wait]."),
64
+ },
65
65
  },
66
66
  // Handler for the tool (function to be executed when the tool is called)
67
67
  async ({ command, machine, extensions, emuspeed, seconds }) => {
@@ -112,28 +112,29 @@ async function registerAllTools(server) {
112
112
  result
113
113
  ]);
114
114
  });
115
- server.tool(
115
+ server.registerTool(
116
116
  // Name of the tool (used to call it)
117
- "emu_media",
118
- // Description of the tool (what it does)
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
- `,
130
- // Schema for the tool (input validation)
131
- {
132
- command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert", "diskInsertFolder", "diskEject"]),
133
- tapefile: z.string().min(1).max(200).optional(), // Tape file to insert
134
- romfile: z.string().min(1).max(200).optional(), // ROM file to insert
135
- diskfile: z.string().min(1).max(200).optional(), // Disk file to insert
136
- diskfolder: z.string().min(1).max(200).optional(), // Disk folder to insert
117
+ "emu_media", {
118
+ title: "Emulator media tools",
119
+ // Description of the tool (what it does)
120
+ description: "Manage tapes, rom cartridges, and floppy disks.",
121
+ // Schema for the tool (input validation)
122
+ inputSchema: {
123
+ command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert", "diskInsertFolder", "diskEject"]).describe(`Available commands:
124
+ 'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx).
125
+ 'tapeRewind': rewind the current tape.
126
+ 'tapeEject': remove tape from virtual cassette player.
127
+ 'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A.
128
+ 'romEject': remove the current ROM cartridge from cartridge slot A.
129
+ 'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A.
130
+ 'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory.
131
+ 'diskEject': remove the current disk from floppy disk A.
132
+ `),
133
+ tapefile: z.string().min(1).max(200).optional().describe("Absolute Tape filename to insert. Used by [tapeInsert]"),
134
+ romfile: z.string().min(1).max(200).optional().describe("Absolute ROM filename to insert. Used by [romInsert]"),
135
+ diskfile: z.string().min(1).max(200).optional().describe("Absolute Disk filename to insert. Used by [diskInsert]"),
136
+ diskfolder: z.string().min(1).max(200).optional().describe("Absolute Disk folder filename to insert. Used by [diskInsertFolder]"),
137
+ },
137
138
  },
138
139
  // Handler for the tool (function to be executed when the tool is called)
139
140
  async ({ command, tapefile, romfile, diskfile, diskfolder }) => {
@@ -174,19 +175,20 @@ async function registerAllTools(server) {
174
175
  response
175
176
  ]);
176
177
  });
177
- server.tool(
178
+ server.registerTool(
178
179
  // Name of the tool (used to call it)
179
- "emu_info",
180
- // Description of the tool (what it does)
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
- `,
187
- // Schema for the tool (input validation)
188
- {
189
- command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]),
180
+ "emu_info", {
181
+ title: "Emulator info tools",
182
+ // Description of the tool (what it does)
183
+ description: "Obtain informacion about the current emulated machine.",
184
+ // Schema for the tool (input validation)
185
+ inputSchema: {
186
+ command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]).describe(`Available commands:
187
+ 'getStatus': returns the status of the openMSX emulator.
188
+ 'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots.
189
+ 'getIOPortsMap': shows an overview about the I/O mapped devices.
190
+ `),
191
+ },
190
192
  },
191
193
  // Handler for the tool (function to be executed when the tool is called)
192
194
  async ({ command }) => {
@@ -212,24 +214,25 @@ async function registerAllTools(server) {
212
214
  response
213
215
  ]);
214
216
  });
215
- server.tool(
217
+ server.registerTool(
216
218
  // Name of the tool (used to call it)
217
- "emu_vdp",
218
- // Description of the tool (what it does)
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
- `,
228
- // Schema for the tool (input validation)
229
- {
230
- command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]),
231
- register: z.number().min(0).max(31).optional(), // Register to read/write
232
- value: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(),
219
+ "emu_vdp", {
220
+ title: "VDP tools",
221
+ // Description of the tool (what it does)
222
+ description: "Manage the VDP (Video Display Processor).",
223
+ // Schema for the tool (input validation)
224
+ inputSchema: {
225
+ command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]).describe(`Available commands:
226
+ 'getPalette': returns the current V9938/V9958 color palette in RGB333 format.
227
+ 'getRegisters': returns all VDP register values.
228
+ 'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format.
229
+ 'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31).
230
+ 'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command.
231
+ '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.
232
+ `),
233
+ register: z.number().min(0).max(31).optional().describe("VDP register number (0-31) to read/write. Used by [getRegisterValue, setRegisterValue]"),
234
+ value: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional().describe("2 hexadecimal digits for a VDP register value (e.g. 0x1f). Used by [setRegisterValue]"),
235
+ },
233
236
  },
234
237
  // Handler for the tool (function to be executed when the tool is called)
235
238
  async ({ command, register, value }) => {
@@ -265,26 +268,27 @@ async function registerAllTools(server) {
265
268
  response
266
269
  ]);
267
270
  });
268
- server.tool(
271
+ server.registerTool(
269
272
  // Name of the tool (used to call it)
270
- "debug_run",
271
- // Description of the tool (what it does)
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
- `,
284
- // Schema for the tool (input validation)
285
- {
286
- command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver", "stepBack", "runTo"]),
287
- address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional()
273
+ "debug_run", {
274
+ title: "CPU Runtime Debugger tools",
275
+ // Description of the tool (what it does)
276
+ description: "Control execution (break, continue, step).",
277
+ // Schema for the tool (input validation)
278
+ inputSchema: {
279
+ command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver", "stepBack", "runTo"]).describe(`Available commands:
280
+ 'break': to break CPU (pause emulation) at current execution position.
281
+ 'isBreaked': to check if the CPU is currently in break state (1) or not (0).
282
+ 'continue': to continue execution after break.
283
+ 'stepIn': to execute one CPU instruction, go into subroutines.
284
+ 'stepOver': to execute one CPU instruction, but don't go into subroutines.
285
+ 'stepOut': to step out of the current subroutine.
286
+ 'stepBack': to step one instruction back in time.
287
+ 'runTo <address>': to run the CPU until it reaches the specified address.
288
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
289
+ `),
290
+ address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional().describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [runTo]"),
291
+ },
288
292
  },
289
293
  // Handler for the tool (function to be executed when the tool is called)
290
294
  async ({ command, address }) => {
@@ -324,27 +328,29 @@ async function registerAllTools(server) {
324
328
  response
325
329
  ]);
326
330
  });
327
- server.tool(
331
+ server.registerTool(
328
332
  // Name of the tool (used to call it)
329
- "debug_cpu",
330
- // Description of the tool (what it does)
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
- `,
341
- // Schema for the tool (input validation)
342
- {
343
- command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble", "getActiveCpu"]),
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(),
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
333
+ "debug_cpu", {
334
+ title: "CPU tools",
335
+ // Description of the tool (what it does)
336
+ description: "Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory.",
337
+ // Schema for the tool (input validation)
338
+ inputSchema: {
339
+ command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble", "getActiveCpu"]).describe(`Available commands:
340
+ 'getCpuRegisters': to get an overview of all the CPU registers.
341
+ '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).
342
+ '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).
343
+ 'getStackPile': to get an overview of the CPU stack.
344
+ 'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty.
345
+ 'getActiveCpu': to return the active cpu: z80 or r800.
346
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0xd2 0x3af2)."
347
+ `),
348
+ 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()
349
+ .describe("CPU register to read/write. Used by [getRegister, setRegister]"),
350
+ address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional().describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [disassemble]"),
351
+ value: z.string().regex(/^0x[0-9a-fA-F]{2,4}$/).optional().describe("2-4 hexadecimal digits for a byte value (e.g. 0xa5 or 0xa5b1). Used by [setRegister]"),
352
+ size: z.number().min(8).max(50).optional().describe("Number of bytes to disassemble. Used by [disassemble]"),
353
+ },
348
354
  },
349
355
  // Handler for the tool (function to be executed when the tool is called)
350
356
  async ({ command, address, register, value, size }) => {
@@ -378,27 +384,28 @@ async function registerAllTools(server) {
378
384
  response
379
385
  ]);
380
386
  });
381
- server.tool(
387
+ server.registerTool(
382
388
  // Name of the tool (used to call it)
383
- "debug_memory",
384
- // Description of the tool (what it does)
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
- `,
395
- // Schema for the tool (input validation)
396
- {
397
- command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"]),
398
- address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
399
- lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
400
- value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
401
- value16: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for byte value for writeByte command
389
+ "debug_memory", {
390
+ title: "Memory tools",
391
+ // Description of the tool (what it does)
392
+ description: "Slots info, and Read/write from/to memory in the openMSX emulator.",
393
+ // Schema for the tool (input validation)
394
+ inputSchema: {
395
+ command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"]).describe(`Available commands:
396
+ 'selectedSlots': to get a list of the currently selected memory slots.
397
+ 'getBlock <address> [lines]': to read a block of memory from the specified address.
398
+ 'readByte <address>': to read a BYTE from the specified address.
399
+ 'readWord <address>': to read a WORD from the specified address.
400
+ 'writeByte <address> <value8>': to write a BYTE to the specified address.
401
+ 'writeWord <address> <value16>': to write a WORD to the specified address.
402
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
403
+ `),
404
+ address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional().describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [getBlock, readByte, writeByte, readWord, writeWord]"),
405
+ lines: z.number().min(1).max(50).optional().default(8).describe("Number of lines to obtain. Used by [getBlock]"),
406
+ value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional().describe("2 hexadecimal digits for a byte value (e.g. 0xa5). Used by [writeByte]"),
407
+ value16: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional().describe("4 hexadecimal digits for a word value (e.g. 0xa5b1). Used by [writeWord]"),
408
+ },
402
409
  },
403
410
  // Handler for the tool (function to be executed when the tool is called)
404
411
  async ({ command, address, lines, value8, value16 }) => {
@@ -432,23 +439,24 @@ async function registerAllTools(server) {
432
439
  response
433
440
  ]);
434
441
  });
435
- server.tool(
442
+ server.registerTool(
436
443
  // Name of the tool (used to call it)
437
- "debug_vram",
438
- // Description of the tool (what it does)
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
- `,
446
- // Schema for the tool (input validation)
447
- {
448
- command: z.enum(["getBlock", "readByte", "writeByte"]),
449
- address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
450
- lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
451
- value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
444
+ "debug_vram", {
445
+ title: "VRAM tools",
446
+ // Description of the tool (what it does)
447
+ description: "Read or write from/to VRAM video memory from the openMSX emulator.",
448
+ // Schema for the tool (input validation)
449
+ inputSchema: {
450
+ command: z.enum(["getBlock", "readByte", "writeByte"]).describe(`Available commands:
451
+ 'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
452
+ 'readByte <address>': to read a BYTE from the specified VRAM address.
453
+ 'writeByte <address> <value8>': to write a BYTE to the specified VRAM address.
454
+ **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
455
+ `),
456
+ address: z.string().regex(/^0x[0-9a-fA-F]{5}$/).optional().describe("5 hexadecimal digits for a VRAM address (e.g. 0x04af3). Used by [getBlock, readByte, writeByte]"),
457
+ lines: z.number().min(1).max(50).optional().default(8).describe("Number of lines to obtain. Used by [getBlock]"),
458
+ value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional().describe("2 hexadecimal digits for a byte value (e.g. 0xa5). Used by [writeByte]"),
459
+ },
452
460
  },
453
461
  // Handler for the tool (function to be executed when the tool is called)
454
462
  async ({ command, address, lines, value8 }) => {
@@ -473,23 +481,24 @@ async function registerAllTools(server) {
473
481
  response
474
482
  ]);
475
483
  });
476
- server.tool(
484
+ server.registerTool(
477
485
  // Name of the tool (used to call it)
478
- "debug_breakpoints",
479
- // Description of the tool (what it does)
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
- `,
488
- // Schema for the tool (input validation)
489
- {
490
- command: z.enum(["create", "remove", "list"]),
491
- address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
492
- bpname: z.string().min(3).max(10).optional(), // breakpoint name (e.g. bp#1)
486
+ "debug_breakpoints", {
487
+ title: "Breakpoints tools",
488
+ // Description of the tool (what it does)
489
+ description: "Create, remove, and list breakpoints.",
490
+ // Schema for the tool (input validation)
491
+ inputSchema: {
492
+ command: z.enum(["create", "remove", "list"]).describe(`Available commands:
493
+ 'create <address>': create a breakpoint at a specified address, and returns its name.
494
+ 'remove <bpname>': remove a breakpoint by name (e.g. bp#1).
495
+ 'list': enumerate the active breakpoints.
496
+ "**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x4af3).
497
+ "**Important Note**: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.
498
+ `),
499
+ address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional().describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [create]"),
500
+ bpname: z.string().min(3).max(10).optional().describe("Breakpoint name (e.g. bp#1). Used by [remove]"),
501
+ },
493
502
  },
494
503
  // Handler for the tool (function to be executed when the tool is called)
495
504
  async ({ command, address, bpname }) => {
@@ -514,21 +523,22 @@ async function registerAllTools(server) {
514
523
  response
515
524
  ]);
516
525
  });
517
- server.tool(
526
+ server.registerTool(
518
527
  // Name of the tool (used to call it)
519
- "emu_savestates",
520
- // Description of the tool (what it does)
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
- `,
528
- // Schema for the tool (input validation)
529
- {
530
- command: z.enum(["load", "save", "list"]),
531
- name: z.string().min(1).max(20).optional(), // breakpoint name (e.g. bp#1)
528
+ "emu_savestates", {
529
+ title: "Save states tools",
530
+ // Description of the tool (what it does)
531
+ description: "Load, save, and list savestates.",
532
+ // Schema for the tool (input validation)
533
+ inputSchema: {
534
+ command: z.enum(["load", "save", "list"]).describe(`Available commands:
535
+ 'load <name>': restores a previously created savestate.
536
+ 'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate.
537
+ 'list': returns the names of all previously created savestates, separated by spaces.
538
+ **Important Note**: names with spaces are enclosed in {}.
539
+ `),
540
+ name: z.string().min(1).max(20).optional().describe("Name of the savestate to load/save. Used by [load, save]"),
541
+ },
532
542
  },
533
543
  // Handler for the tool (function to be executed when the tool is called)
534
544
  async ({ command, name }) => {
@@ -558,32 +568,35 @@ async function registerAllTools(server) {
558
568
  response
559
569
  ]);
560
570
  });
561
- server.tool(
571
+ server.registerTool(
562
572
  // Name of the tool (used to call it)
563
- "emu_replay",
564
- // Description of the tool (what it does)
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
- `,
578
- // Schema for the tool (input validation)
579
- {
580
- command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "saveReplay", "loadReplay"]),
581
- seconds: z.number().min(1).max(60).optional(), // Seconds to go back
582
- time: z.string().regex(/^\d+$/).optional(), // Time in seconds to go to
583
- filename: z.string().min(1).max(200).optional(), // Filename to save/load replay
573
+ "emu_replay", {
574
+ title: "Replay tools",
575
+ // Description of the tool (what it does)
576
+ description: "When replay is enabled (the default) the emulator collect data while emulating, which enables you to go back and forward in the emulated MSX time.",
577
+ // Schema for the tool (input validation)
578
+ inputSchema: {
579
+ command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "advanceFrame", "reverseFrame", "saveReplay", "loadReplay"]).describe(`Available commands:
580
+ 'start': starts the replay mode (enabled by default when emulator is launched).
581
+ 'stop': stops the replay mode.
582
+ 'status': gives information about the replay feature and the data that is collected.
583
+ 'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started.
584
+ '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.
585
+ 'truncate': stop replaying and wipe all the future replay data after now.
586
+ 'advanceFrame' [frames]: advances a number of frames in the replay timeline, useful to advance the timeline while debugging.
587
+ 'reverseFrame' [frames]: reverses a number of frames in the replay timeline, useful to reverse the timeline while debugging.
588
+ 'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response.
589
+ 'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.
590
+ **Important Note**: consider do a #debug_run 'break' to maintain the timeline before a 'goBack' or 'absoluteGoto'.
591
+ `),
592
+ seconds: z.number().min(1).max(60).optional().describe("Seconds to go back. Used by [goBack]"),
593
+ time: z.string().regex(/^\d+$/).optional().describe("Absolute time in seconds to go to. Used by [absoluteGoto]"),
594
+ frames: z.number().min(1).max(1000).optional().default(1).describe("Number of frames to advance/reverse. Used by [advanceFrame, reverseFrame]"),
595
+ filename: z.string().min(1).max(200).optional().describe("Filename to save/load replay. Used by [saveReplay, loadReplay]"),
596
+ },
584
597
  },
585
598
  // Handler for the tool (function to be executed when the tool is called)
586
- async ({ command, seconds, time, filename }) => {
599
+ async ({ command, seconds, time, frames, filename }) => {
587
600
  let tclCommand;
588
601
  switch (command) {
589
602
  case "start":
@@ -604,6 +617,12 @@ async function registerAllTools(server) {
604
617
  case "truncate":
605
618
  tclCommand = "reverse truncatereplay";
606
619
  break;
620
+ case "advanceFrame":
621
+ tclCommand = `advance_frame ${frames}`;
622
+ break;
623
+ case "reverseFrame":
624
+ tclCommand = `reverse_frame ${frames}`;
625
+ break;
607
626
  case "saveReplay":
608
627
  if (filename)
609
628
  filename = `"${OPENMSX_REPLAYS_DIR}${filename}"`;
@@ -624,20 +643,21 @@ async function registerAllTools(server) {
624
643
  response
625
644
  ]);
626
645
  });
627
- server.tool(
646
+ server.registerTool(
628
647
  // Name of the tool (used to call it)
629
- "emu_keyboard",
630
- // Description of the tool (what it does)
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
- `,
637
- // Schema for the tool (input validation)
638
- {
639
- command: z.enum(["sendText"]),
640
- text: z.string().min(1).max(200).optional().default(''), // Key to send to the emulator
648
+ "emu_keyboard", {
649
+ title: "Keyboard tools",
650
+ // Description of the tool (what it does)
651
+ description: "Send a text to the openMSX emulator.",
652
+ // Schema for the tool (input validation)
653
+ inputSchema: {
654
+ command: z.enum(["sendText"]).describe(`Available commands:
655
+ 'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix.
656
+ **Important Note**: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard.
657
+ **Important Note**: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...
658
+ `),
659
+ text: z.string().min(1).max(200).optional().default('').describe("Text to send to the emulator via emulated keyboard"),
660
+ },
641
661
  },
642
662
  // Handler for the tool (function to be executed when the tool is called)
643
663
  async ({ command, text }) => {
@@ -656,18 +676,19 @@ async function registerAllTools(server) {
656
676
  response
657
677
  ]);
658
678
  });
659
- server.tool(
679
+ server.registerTool(
660
680
  // Name of the tool (used to call it)
661
- "screen_shot",
662
- // Description of the tool (what it does)
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
- `,
668
- // Schema for the tool (input validation)
669
- {
670
- command: z.enum(["as_image", "to_file"]),
681
+ "screen_shot", {
682
+ title: "Screenshot tools",
683
+ // Description of the tool (what it does)
684
+ description: "Take a screenshot of the openMSX emulator screen.",
685
+ // Schema for the tool (input validation)
686
+ inputSchema: {
687
+ command: z.enum(["as_image", "to_file"]).describe(`Available commands:
688
+ 'as_image': take a screenshot and the image is returned in the response.
689
+ 'to_file': take a screenshot and save it to a file, the file name is returned in the response.
690
+ `),
691
+ },
671
692
  },
672
693
  // Handler for the tool (function to be executed when the tool is called)
673
694
  async ({ command }) => {
@@ -712,16 +733,18 @@ async function registerAllTools(server) {
712
733
  `Error: Unknown screen_shot command "${command}".`
713
734
  ]);
714
735
  });
715
- server.tool(
736
+ server.registerTool(
716
737
  // Name of the tool (used to call it)
717
- "screen_dump",
718
- // Description of the tool (what it does)
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
- `,
722
- // Schema for the tool (input validation)
723
- {
724
- scrbasename: z.string().min(1).max(100).default("screendump"),
738
+ "screen_dump", {
739
+ title: "Screen dump tools",
740
+ // Description of the tool (what it does)
741
+ description: `Take a screendump of the openMSX emulator screen as SC?.
742
+ The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'.
743
+ `,
744
+ // Schema for the tool (input validation)
745
+ inputSchema: {
746
+ scrbasename: z.string().min(1).max(100).default("screendump").describe("Screendump filename (without path or extension) to save the screendump; default is 'screendump'"),
747
+ },
725
748
  },
726
749
  // Handler for the tool (function to be executed when the tool is called)
727
750
  async ({ scrbasename }) => {
@@ -732,94 +755,105 @@ async function registerAllTools(server) {
732
755
  response
733
756
  ]);
734
757
  });
735
- server.tool(
758
+ server.registerTool(
736
759
  // 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(),
760
+ "basic_programming", {
761
+ title: "BASIC programming tools",
762
+ // Description of the tool (what it does)
763
+ description: "Helper tool for developing BASIC programs.",
764
+ // Schema for the tool (input validation)
765
+ inputSchema: {
766
+ command: z.enum(["isBasicAvailable", "newProgram", "runProgram", "setProgram", "getFullProgram", "getFullProgramAdvanced", "listProgramLines", "deleteProgramLines"]).describe(`Available commands:
767
+ 'isBasicAvailable': checks if the current machine is ready to manage BASIC programs (true), or not (false).
768
+ 'newProgram': clears the current BASIC program.
769
+ 'setProgram <program>': sets a full BASIC program or updates part of the current BASIC program with the specified string.
770
+ 'runProgram': runs the current BASIC program.
771
+ 'getFullProgram': retrieves the current BASIC program as plain text; very useful in text screen modes.
772
+ 'getFullProgramAdvanced': retrieves the current BASIC program along with the RAM address where each line is stored.
773
+ 'listProgramLines <startLine> [endLine]': lists the selected range of lines from the current BASIC program on the emulator screen.
774
+ 'deleteProgramLines <startLine> [endLine]': deletes a specific range of lines from the current BASIC program; if endLine is not specified, only the startLine is deleted.
775
+ **Important Note**: if error 'not in BASIC mode' then use the command 'isBasicAvailable' to wait for a ready state.
776
+ **Important Note**: prioritize these tools for developing BASIC programs, as they are more efficient than using the 'sendText' tool.
777
+ **Important Note**: all program lines must end with a carriage return (\\r) to be processed correctly, including the last line.
778
+ **Important Note**: if you have questions about MSX BASIC, use the resources provided by this MCP server.
779
+ `),
780
+ program: z.string().min(1).max(10000).optional().describe("Basic program to set; use only \\r for line endings, even the last one. Used by [setProgram]"),
781
+ startLine: z.number().min(0).max(9999).optional().describe("Start line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
782
+ endLine: z.number().min(0).max(9999).optional().describe("End line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
783
+ },
758
784
  },
759
785
  // Handler for the tool (function to be executed when the tool is called)
760
786
  async ({ command, program, startLine, endLine }) => {
761
787
  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
788
  let tclCommand = undefined;
763
789
  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.';
790
+ const inBasic = await openMSXInstance.emu_isInBasic();
791
+ if (command !== "isBasicAvailable" && !inBasic) {
792
+ response = 'Error: The current MSX machine is not in BASIC mode.';
793
+ }
794
+ else
795
+ switch (command) {
796
+ case "isBasicAvailable":
797
+ response = inBasic.toString();
778
798
  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.';
799
+ case "newProgram":
800
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('new\r')));
801
+ if (response.startsWith('after#'))
802
+ response = '';
785
803
  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;
804
+ case "runProgram":
805
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('run\r')));
806
+ if (response.startsWith('after#'))
807
+ response = '';
791
808
  break;
792
- }
793
- // Set speed to fast for program input
794
- if (isErrorResponse(response = await openMSXInstance.sendCommand('set speed 10000')))
809
+ case "deleteProgramLines":
810
+ if (startLine === undefined) {
811
+ response = 'Error: No startLine number provided to delete BASIC program lines.';
812
+ break;
813
+ }
814
+ response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText(`delete ${startLine}-${endLine || startLine}\r`)));
795
815
  break;
796
- // Clear the screen and type the program
797
- if (isErrorResponse(response = await openMSXInstance.sendCommand(`type_via_keybuf "${encodeTypeText(program)}"`)))
816
+ case "setProgram":
817
+ if (!program) {
818
+ response = 'Error: no BASIC program provided to set.';
819
+ break;
820
+ }
821
+ if (program.includes('\n') || program.includes('\\n')) {
822
+ response = 'Error: you cannot use \\n for line endings, use only \\r instead.';
823
+ break;
824
+ }
825
+ // Escape '$' characters if '(' is the next character and is not escaped yet (openMSX variable substitutions)
826
+ program = program.replace(/([^\\])(\$\()/g, '$1\\$2');
827
+ // Get current speed to restore it later
828
+ let speed = '100';
829
+ if (isErrorResponse(speed = await openMSXInstance.sendCommand('set speed'))) {
830
+ response = speed;
831
+ break;
832
+ }
833
+ // Set speed to fast, type de program, wait to end, and restore speed
834
+ if (isErrorResponse(response = await openMSXInstance.sendCommand(`set speed 10000 ; type_via_keybuf "${encodeTypeText(program)}" ; after idle 20 { set speed ${speed} }`)))
835
+ break;
836
+ // Success response
837
+ response = '';
798
838
  break;
799
- // Restore the original speed
800
- if (isErrorResponse(response = await openMSXInstance.sendCommand(`set speed ${speed}`)))
839
+ case "getFullProgram":
840
+ // Source: https://www.msx.org/forum/msx-talk/openmsx/export-basic-listing#comment-407392
841
+ tclCommand = 'regsub -all -line {^[0-9a-f]x[0-9a-f]{4} > } [ listing ] ""';
801
842
  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.';
843
+ case "getFullProgramAdvanced":
844
+ tclCommand = "listing";
815
845
  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
- }
846
+ case "listProgramLines":
847
+ if (startLine === undefined) {
848
+ response = 'Error: No start line provided to list BASIC program lines.';
849
+ break;
850
+ }
851
+ tclCommand = `type_via_keybuf \"${encodeTypeText(`list ${startLine}-${endLine || startLine}\r`)}\"`;
852
+ break;
853
+ default:
854
+ response = `Error: Unknown command "${command}".`;
855
+ break;
856
+ }
823
857
  if (response === undefined && tclCommand) {
824
858
  response = await openMSXInstance.sendCommand(tclCommand);
825
859
  }
@@ -829,22 +863,22 @@ async function registerAllTools(server) {
829
863
  });
830
864
  // ============================================================================
831
865
  // MSX Documentation resources
832
- const resdocs = (await listResourcesDirectory()).sort();
866
+ const resdocs = (await listResourcesDirectory(resourcesDir)).sort();
833
867
  for (let index = 0; index < resdocs.length; index++) {
834
868
  const sectionName = resdocs[index];
835
869
  const tocFile = path.join(resourcesDir, `${sectionName}/toc.json`);
836
870
  const tocContent = JSON.parse(await fs.readFile(tocFile, 'utf8'));
837
871
  tocContent.toc.forEach((item, itemIndex) => {
838
- const itemName = item.uri.split('/').pop() || '';
839
- server.resource(
872
+ const itemName = path.parse(item.uri.split('/').pop()).name || '';
873
+ server.registerResource(
840
874
  // Name of the resource (used to call it)
841
- `msxdocs_${sectionName}_${itemName}`,
875
+ `msxdocs_${sectionName}_${item.title.replace(/[^a-z0-9]+/gi, '_').toLowerCase()}`,
842
876
  // Resource URI template
843
877
  item.uri,
844
878
  // Metadata for the resource
845
879
  {
846
- title: item.title || `MSX Documentation '${sectionName}' - ${itemName}`,
847
- description: item.description || `Documentation for MSX resource '${sectionName}' - ${itemName}`,
880
+ title: item.title || `MSX Documentation '${sectionName}': ${itemName}`,
881
+ description: item.description || `Documentation for MSX resource '${sectionName}': ${itemName}`,
848
882
  mimeType: item.mimeType || 'text/markdown',
849
883
  },
850
884
  // Handler for the resource (function to be executed when the resource is called)
@@ -854,14 +888,11 @@ async function registerAllTools(server) {
854
888
  if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
855
889
  // Fetch the resource from the URL
856
890
  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';
891
+ [resourceContent, mimeType] = await fetchCleanWebpage(uri.href);
861
892
  }
862
893
  catch (error) {
863
894
  // Throw exception (MCP protocol requirement)
864
- throw new Error(`Error fetching resource from ${uri.href}: ${error instanceof Error ? error.message : String(error)}`);
895
+ throw error;
865
896
  }
866
897
  }
867
898
  else {
@@ -887,41 +918,48 @@ async function registerAllTools(server) {
887
918
  });
888
919
  }
889
920
  ;
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
- ];
921
+ server.resource("msxdocs_basic_wiki", new ResourceTemplate("msxdocs://basic_wiki/{instruction}", {
922
+ list: undefined,
923
+ complete: {
924
+ instruction: (value) => [
925
+ "ABS()", "AND", "ASC()", "ATN()", "AUTO", "BASE()", "BEEP", "BIN$()", "BLOAD", "BSAVE", "CALL", "CALL ADJUST", "CALL PAUSE", "CALL PCMPLAY", "CALL PCMREC",
926
+ "CDBL()", "CHR$()", "CINT()", "CIRCLE", "CLEAR", "CLOAD", "CLOAD?", "CLOSE", "CLS", "COLOR", "COLOR=", "COLOR", "COLOR", "CONT", "COPY", "COPY", "COS()", "CSAVE",
927
+ "CSNG()", "CSRLIN", "DATA", "DEFDBL", "DEF FN", "DEFINT", "DEFSNG", "DEFSTR", "DEF USR", "DELETE", "DIM", "DRAW", "ELSE", "END", "EOF()", "EQV", "ERASE", "ERL", "ERR",
928
+ "ERROR", "EXP()", "FIX()", "FN", "FOR...NEXT", "FRE()", "GET DATE", "GET TIME", "GOSUB", "GOTO", "HEX$()", "IF...GOTO...ELSE", "IF...THEN...ELSE", "IMP",
929
+ "INKEY$", "INP()", "INPUT", "INPUT$()", "INSTR()", "INT()", "INTERVAL", "KEY", "KEY()", "LEFT$()", "LEN()", "LET", "LINE", "LINE INPUT", "LIST", "LLIST",
930
+ "LOAD", "LOCATE", "LOG()", "LPOS()", "LPRINT", "MAXFILES", "MERGE", "MID$()", "MOD", "MOTOR", "NEW", "NOT", "OCT$()", "ON...GOSUB", "ON...GOTO",
931
+ "ON ERROR GOTO", "ON INTERVAL GOSUB", "ON KEY GOSUB", "ON SPRITE GOSUB", "ON STOP GOSUB", "ON STRIG GOSUB", "OPEN", "OR", "OUT", "PAD()", "PAINT", "PDL()",
932
+ "PEEK()", "PLAY", "PLAY()", "POINT", "POKE", "POS()", "PRESET", "PRINT", "PSET", "PUT KANJI", "PUT SPRITE", "READ", "REM", "RENUM", "RESTORE", "RESUME",
933
+ "RETURN", "RIGHT$()", "RND()", "RUN", "SAVE", "SCREEN", "SET ADJUST", "SET BEEP", "SET DATE", "SET PAGE", "SET PASSWORD", "SET PROMPT", "SET SCREEN",
934
+ "SET SCROLL", "SET TIME", "SET TITLE", "SET VIDEO", "SGN()", "SIN()", "SOUND", "SPACE$()", "SPC()", "SPRITE", "SPRITE$()", "SQR()", "STICK()", "STOP",
935
+ "STR$()", "STRIG()", "STRING$()", "SWAP", "TAB()", "TAN()", "TIME", "TROFF", "TRON", "USR()", "VAL()", "VARPTR()", "VDP()", "VPEEK()", "VPOKE", "WAIT",
936
+ "WIDTH", "XOR"
937
+ ],
938
+ },
939
+ }), {
940
+ title: "MSX BASIC Instructions Documentation",
941
+ description: "Documentation about all the standard MSX BASIC instructions from www.msx.org",
942
+ mimeType: "text/html",
943
+ }, async (uri, variables) => {
944
+ const instruction = variables.instruction.replace(/ /g, '_').replace(/\?/g, '%3F').replace(/=/g, '%3D');
945
+ const url = `https://www.msx.org/wiki/${instruction}`;
946
+ let resourceContent;
947
+ let mimeType;
948
+ try {
949
+ [resourceContent, mimeType] = await fetchCleanWebpage(url);
905
950
  }
906
- }
907
- catch (error) {
908
- console.error('Error reading directory:', error);
909
- }
910
- // Return original if no matches found
911
- return ['text/plain', filePath];
912
- }
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
- }
951
+ catch (error) {
952
+ // Throw exception (MCP protocol requirement)
953
+ throw error;
954
+ }
955
+ return {
956
+ contents: [{
957
+ uri: uri.href,
958
+ text: resourceContent,
959
+ mimeType: mimeType || 'text/plain',
960
+ }],
961
+ };
962
+ });
925
963
  }
926
964
  // ============================================================================
927
965
  // Cleanup handlers for graceful shutdown of MCP server
@@ -1055,7 +1093,13 @@ async function createServerInstance() {
1055
1093
  version: PACKAGE_VERSION,
1056
1094
  });
1057
1095
  // Re-register all tools (you might want to extract this to a separate function)
1058
- await registerAllTools(newServer);
1096
+ try {
1097
+ await registerAllTools(newServer);
1098
+ }
1099
+ catch (error) {
1100
+ console.error("Error registering tools/resources:", error);
1101
+ throw error;
1102
+ }
1059
1103
  return newServer;
1060
1104
  }
1061
1105
  // ============================================================================
package/dist/utils.js CHANGED
@@ -5,6 +5,8 @@
5
5
  * @license GPL2
6
6
  */
7
7
  import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import mime from 'mime-types';
8
10
  /**
9
11
  * Extract description from XML file
10
12
  * @param filePath - Full path to the XML file
@@ -21,6 +23,73 @@ export async function extractDescriptionFromXML(filePath) {
21
23
  return 'Error reading description';
22
24
  }
23
25
  }
26
+ /**
27
+ * Add file extension to a file path if it exists in the same directory and determine its MIME type
28
+ * @param filePath - Full path to the file excluding extension
29
+ * @returns Promise<string[]> - An array containing the MIME type and the full file path with extension
30
+ */
31
+ export async function addFileExtension(filePath) {
32
+ // Get directory and filename
33
+ const directory = path.dirname(filePath);
34
+ const filename = path.basename(filePath);
35
+ try {
36
+ // Get all files in directory that start with our filename
37
+ const files = await fs.readdir(directory);
38
+ const matchingFiles = files.filter(file => file.startsWith(filename));
39
+ if (matchingFiles.length > 0) {
40
+ const fileFound = path.join(directory, matchingFiles[0]);
41
+ return [
42
+ mime.lookup(fileFound) || 'text/plain',
43
+ fileFound
44
+ ];
45
+ }
46
+ }
47
+ catch (error) {
48
+ console.error('Error reading directory:', error);
49
+ }
50
+ // Return original if no matches found
51
+ return ['text/plain', filePath];
52
+ }
53
+ /**
54
+ * List all folders in the resources directory
55
+ * @param resourcesDir - Path to the resources directory
56
+ * @returns Promise<string[]> - List of folder names in the resources directory
57
+ */
58
+ export async function listResourcesDirectory(resourcesDir) {
59
+ try {
60
+ const directories = await fs.readdir(resourcesDir, { withFileTypes: true });
61
+ const folderNames = directories
62
+ .filter(dirent => dirent.isDirectory())
63
+ .map(dirent => dirent.name);
64
+ return folderNames;
65
+ }
66
+ catch (error) {
67
+ console.error("Error reading resources directory:", error);
68
+ return [];
69
+ }
70
+ }
71
+ /**
72
+ * Fetch a webpage and return its content (without scripts, styles, or links) and its MIME type
73
+ * @param url - URL of the webpage to fetch
74
+ * @returns Promise<[string, string]> - A tuple containing the webpage content and its MIME type
75
+ */
76
+ export async function fetchCleanWebpage(url) {
77
+ let resourceContent;
78
+ let mimeType = 'text/plain';
79
+ try {
80
+ resourceContent = await fetch(url).then(response => {
81
+ mimeType = response.headers.get('content-type') || 'text/plain';
82
+ return response.text();
83
+ }) || 'Error downloading content';
84
+ // Remove script, style, and link tags from the content
85
+ resourceContent = resourceContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>|<link\b[^>]*\/?>/gi, '');
86
+ }
87
+ catch (error) {
88
+ // Throw exception (MCP protocol requirement)
89
+ throw new Error(`Error fetching resource from ${url}: ${error instanceof Error ? error.message : String(error)}`);
90
+ }
91
+ return [resourceContent, mimeType];
92
+ }
24
93
  /**
25
94
  * Decode HTML entities in a string to plain text
26
95
  * @param text - String containing HTML entities
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.1.8",
3
+ "version": "1.1.14",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
- "title": "More MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
3
- "description": "MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
2
+ "title": "MSX Technical about BIOS",
3
+ "description": "MSX Technical Info about BIOS calls, including references for MSX BIOS, MSX2 SUBROM BIOS, and how to call BIOS from MSX-DOS.",
4
4
  "toc": [
5
5
  {
6
6
  "title": "MSX BIOS calls reference",
@@ -1,6 +1,6 @@
1
1
  {
2
- "title": "More MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
3
- "description": "MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
2
+ "title": "MSX-DOS Technical Info",
3
+ "description": "MSX-DOS Technical Info including MSX-DOS 2 Program Interface Specification and MSX-DOS 2 Function Specifications.",
4
4
  "toc": [
5
5
  {
6
6
  "title": "MSX-DOS 2 Program Interface Specification",
@@ -1,6 +1,6 @@
1
1
  {
2
- "title": "More MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
3
- "description": "MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
2
+ "title": "CPU Technical Info",
3
+ "description": "Technical Info about the Z80, R800, and others processors, including detailed instruction sets, undocumented instructions, and timing information.",
4
4
  "toc": [
5
5
  {
6
6
  "title": "Z80 Detailed Instruction Set (Z80 Heaven)",
@@ -1,6 +1,6 @@
1
1
  {
2
- "title": "More MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
3
- "description": "MSX Technical Info from http://bifi.msxnet.org/msxnet/tech/",
2
+ "title": "MSX System Technical Info",
3
+ "description": "MSX System Technical Info including I/O ports, system variables, and hardware interfacing.",
4
4
  "toc": [
5
5
  {
6
6
  "title": "MSX I/O ports overview",