@nataliapc/mcp-openmsx 1.2.5 → 1.2.7

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
@@ -105,8 +105,8 @@ The MCP server translates high-level natural language commands from your Copilot
105
105
  ### Debugging Tools
106
106
  - `debug_run`: Control execution: _`break`, `isBreaked`, `continue`, `stepIn`, `stepOut`, `stepOver`, `stepBack`, `runTo`_.
107
107
  - `debug_cpu`: Read/write CPU registers, CPU info, Stack pile, and Disassemble code: _`getCpuRegisters`, `getRegister`, `setRegister`, `getStackPile`, `disassemble`, `getActiveCpu`_.
108
- - `debug_memory`: RAM memory operations: _`selectedSlots`, `getBlock`, `readByte`, `readWord`, `writeByte`, `writeWord`, `advanced_basic_listing`_.
109
- - `debug_vram`: VRAM operations: _`getBlock`, `readByte`, `writeByte`_.
108
+ - `debug_memory`: RAM memory operations: _`selectedSlots`, `getBlock`, `readByte`, `readWord`, `writeByte`, `writeWord`, `searchBytes`_.
109
+ - `debug_vram`: VRAM operations: _`getBlock`, `readByte`, `writeByte`, `searchBytes`_.
110
110
  - `debug_breakpoints`: Breakpoint management: _`create`, `remove`, `list`_.
111
111
 
112
112
  ### Automation Tools
@@ -241,7 +241,7 @@ Edit it to include the following JSON entry:
241
241
 
242
242
  | Variable | Description | Default Value | Example |
243
243
  |----------|-------------|---------------|---------|
244
- | `OPENMSX_EXECUTABLE` | Path or command to the openMSX executable | `openmsx` | `/usr/local/bin/openmsx` |
244
+ | `OPENMSX_EXECUTABLE` | Path or command to the openMSX executable | Auto-detected: `openmsx` (Linux), `/Applications/openMSX.app/Contents/MacOS/openmsx` (macOS), `openmsx.exe` (Windows) | `/usr/local/bin/openmsx` or `C:\Program Files\openMSX\openmsx.exe` |
245
245
  | `OPENMSX_SHARE_DIR` | Directory containing openMSX data files (machines, extensions, etc.) | System dependent | `/home/myuser/.openmsx/share` |
246
246
  | `OPENMSX_SCREENSHOT_DIR` | Directory where screenshots will be saved | Default for openmsx | `/myproject/screenshots` |
247
247
  | `OPENMSX_SCREENDUMP_DIR` | Directory where screen dumps will be saved | Default for openmsx | `/myproject/screendumps` |
@@ -256,7 +256,7 @@ Edit it to include the following JSON entry:
256
256
  > [!IMPORTANT]
257
257
  > This is not needed for using the MCP server, but if you want to install it manually, follow these steps.
258
258
 
259
- Currently, the MCP server requires Linux to be compiled. It has not been tested on Windows or macOS, although it will likely work on the latter as well.
259
+ The MCP server runs on Linux, macOS, and Windows. Building from source requires Node.js >= 18 and TypeScript.
260
260
 
261
261
  ### Manual installation
262
262
 
package/dist/openmsx.js CHANGED
@@ -7,16 +7,37 @@
7
7
  import fs from "fs/promises";
8
8
  import { extractDescriptionFromXML, decodeHtmlEntities, encodeHtmlEntities } from "./utils.js";
9
9
  import { spawn } from 'child_process';
10
+ import { createWriteStream } from 'fs';
10
11
  import path from 'path';
12
+ /** True when running on Windows. Evaluated once at module load. */
13
+ const IS_WINDOWS = process.platform === 'win32';
11
14
  /**
12
- * OpenMSX class for controlling the openMSX emulator via TCL commands over TCP socket
15
+ * OpenMSX class for controlling the openMSX emulator via TCL commands over stdio (Linux/macOS)
16
+ * or a Windows named pipe (Windows).
17
+ *
18
+ * Protocol summary (same XML protocol on all platforms):
19
+ * openMSX → stdout : XML output including <openmsx-output>, <reply>, <log>, <update>
20
+ * controller → openMSX : XML commands via stdin (stdio mode) or named pipe (pipe mode)
21
+ *
22
+ * On Linux/macOS: openmsx -control stdio
23
+ * Commands are sent via the child process stdin; replies come on stdout.
24
+ *
25
+ * On Windows: openmsx -control pipe:<pipename>
26
+ * openMSX reads commands from \\.\pipe\<pipename> (a Windows named pipe).
27
+ * Replies/output are still written to stdout (captured by Node's stdio pipes).
28
+ * We write commands to the named pipe using a WriteStream opened on the pipe path.
29
+ *
30
+ * Reference: https://openmsx.org/manual/openmsx-control.html
13
31
  */
14
32
  export class OpenMSX {
15
33
  lastMachine = null;
16
34
  process = null;
17
35
  isConnected = false;
36
+ // Windows named pipe write stream (only used on Windows with -control pipe)
37
+ pipeWriter = null;
18
38
  /**
19
- * Launch the openMSX emulator in stdio control mode
39
+ * Launch the openMSX emulator in stdio control mode (Linux/macOS)
40
+ * or pipe control mode (Windows).
20
41
  * @param machine - MSX machine to emulate (e.g., 'Panasonic_FS-A1GT', 'C-BIOS_MSX2+')
21
42
  * @param extensions - Array of extensions to load (e.g., ['fmpac', 'ide'])
22
43
  * @returns Promise that resolves when the emulator is ready
@@ -38,8 +59,21 @@ export class OpenMSX {
38
59
  safeResolve(`Error: openMSX emulator instance is already running (currrent machine: ${this.lastMachine}). Close it first before launching a new one.`);
39
60
  return;
40
61
  }
41
- // Build command line arguments
42
- const args = ['-control', 'stdio'];
62
+ // Clean up any leftover pipe writer from a previous session
63
+ // (e.g. if the process crashed and the exit handler didn't run)
64
+ this.closePipeWriter();
65
+ // Build command line arguments.
66
+ // On Windows, openMSX -control stdio has a known issue: the stdin reader
67
+ // thread blocks on read() and never unblocks cleanly when the pipe closes,
68
+ // causing openMSX to hang on exit. The correct mode for Windows is
69
+ // -control pipe:<name>, which uses a Windows named pipe for input and
70
+ // still writes replies/output to stdout.
71
+ // On Linux/macOS, -control stdio is correct and uses stdin/stdout directly.
72
+ // On Windows, use a unique pipe name based on PID to avoid collisions
73
+ // when multiple MCP server instances run simultaneously.
74
+ const pipeName = IS_WINDOWS ? `openmsx-mcp-${process.pid}` : '';
75
+ const controlArg = IS_WINDOWS ? `pipe:${pipeName}` : 'stdio';
76
+ const args = ['-control', controlArg];
43
77
  // Add machine parameter if specified
44
78
  if (machine) {
45
79
  this.lastMachine = machine; // Store last machine for future reference
@@ -51,14 +85,21 @@ export class OpenMSX {
51
85
  args.push('-ext', ext);
52
86
  });
53
87
  }
54
- // Launch openMSX with stdio control
88
+ // Launch openMSX.
89
+ // On both modes, stdout/stderr are piped so we can read replies and errors.
90
+ // On stdio mode, stdin is also piped (we write commands there).
91
+ // On pipe mode, stdin is ignored ('ignore') — openMSX reads from the named pipe.
55
92
  this.process = spawn(executable, args, {
56
- stdio: ['pipe', 'pipe', 'pipe']
93
+ stdio: IS_WINDOWS ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']
57
94
  });
58
- if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
95
+ if (!this.process.stdout || !this.process.stderr) {
59
96
  safeResolve('Error: Failed to create stdio pipes');
60
97
  return;
61
98
  }
99
+ if (!IS_WINDOWS && !this.process.stdin) {
100
+ safeResolve('Error: Failed to create stdin pipe');
101
+ return;
102
+ }
62
103
  // Check if process was launched successfully
63
104
  if (!this.process.pid || this.process.killed) {
64
105
  const stderrMessage = this.process.stderr.read()?.toString() || 'Failed to launch openMSX process';
@@ -70,11 +111,21 @@ export class OpenMSX {
70
111
  // Handle process events
71
112
  this.process.on('error', (error) => {
72
113
  console.error('openMSX process error:', error);
73
- safeResolve(`Error: ${error.message}`);
114
+ if (error.code === 'ENOENT') {
115
+ safeResolve(`Error: openMSX executable not found: "${executable}". ` +
116
+ `Set the OPENMSX_EXECUTABLE environment variable to the full path of the openMSX binary. ` +
117
+ `On macOS the standard path is /Applications/openMSX.app/Contents/MacOS/openmsx; ` +
118
+ `on Windows it is typically C:\\Program Files\\openMSX\\openmsx.exe; ` +
119
+ `on Linux it is usually 'openmsx' (in PATH after package install).`);
120
+ }
121
+ else {
122
+ safeResolve(`Error: ${error.message}`);
123
+ }
74
124
  });
75
125
  this.process.on('exit', (code, signal) => {
76
126
  this.isConnected = false;
77
127
  this.process = null;
128
+ this.closePipeWriter();
78
129
  });
79
130
  // Wait for the opening XML tag to confirm connection
80
131
  this.process.stdout.on('data', (data) => {
@@ -83,10 +134,16 @@ export class OpenMSX {
83
134
  this.isConnected = true;
84
135
  connectionTime = Date.now();
85
136
  // Don't resolve immediately, wait for potential fatal errors
86
- setTimeout(() => {
137
+ setTimeout(async () => {
87
138
  // Only resolve if no fatal error occurred during grace period
88
139
  if (!resolved) {
89
140
  try {
141
+ // On Windows, open the named pipe for writing commands.
142
+ // openMSX has already created the pipe server side by now.
143
+ if (IS_WINDOWS) {
144
+ const pipePath = `\\\\.\\pipe\\${pipeName}`;
145
+ await this.openWindowsPipe(pipePath);
146
+ }
90
147
  this.writeData('<openmsx-control>\n');
91
148
  // Set save settings on exit off
92
149
  this.sendCommand('set save_settings_on_exit off');
@@ -141,6 +198,43 @@ export class OpenMSX {
141
198
  }
142
199
  });
143
200
  }
201
+ /**
202
+ * Open a Windows named pipe for writing commands to openMSX.
203
+ * openMSX creates the pipe server when launched with -control pipe:<name>.
204
+ * We connect as a client (write-only) after openMSX signals it's ready.
205
+ * @param pipePath - Full Windows named pipe path, e.g. \\.\pipe\openmsx-mcp-1234
206
+ */
207
+ openWindowsPipe(pipePath) {
208
+ return new Promise((resolve, reject) => {
209
+ try {
210
+ // Node.js fs.createWriteStream supports Windows named pipe paths.
211
+ // The pipe must already exist (created by openMSX) at this point.
212
+ const writer = createWriteStream(pipePath, { flags: 'r+' });
213
+ writer.on('open', () => {
214
+ this.pipeWriter = writer;
215
+ resolve();
216
+ });
217
+ writer.on('error', (err) => {
218
+ reject(new Error(`Failed to open Windows named pipe "${pipePath}": ${err.message}`));
219
+ });
220
+ }
221
+ catch (err) {
222
+ reject(new Error(`Failed to create Windows named pipe writer: ${err instanceof Error ? err.message : err}`));
223
+ }
224
+ });
225
+ }
226
+ /**
227
+ * Close and destroy the Windows named pipe writer if open.
228
+ */
229
+ closePipeWriter() {
230
+ if (this.pipeWriter) {
231
+ try {
232
+ this.pipeWriter.destroy();
233
+ }
234
+ catch (_) { /* ignore */ }
235
+ this.pipeWriter = null;
236
+ }
237
+ }
144
238
  /**
145
239
  * Close the openMSX emulator process
146
240
  * @returns Promise that resolves when the process is closed
@@ -155,6 +249,7 @@ export class OpenMSX {
155
249
  this.lastMachine = null; // Clear last machine on exit
156
250
  this.isConnected = false;
157
251
  this.process = null;
252
+ this.closePipeWriter();
158
253
  resolve("Ok: Emulator process closed successfully");
159
254
  });
160
255
  this.process.on('error', (error) => {
@@ -166,8 +261,13 @@ export class OpenMSX {
166
261
  this.sendCommand('exit');
167
262
  }
168
263
  catch (error) {
169
- // If writing fails, force kill
170
- this.process.kill('SIGTERM');
264
+ // If writing fails, force kill.
265
+ // Use no-argument kill() for cross-platform safety:
266
+ // on POSIX it sends SIGTERM; on Windows it calls TerminateProcess().
267
+ try {
268
+ this.process.kill();
269
+ }
270
+ catch (_) { /* ignore */ }
171
271
  }
172
272
  }
173
273
  else {
@@ -306,14 +406,29 @@ export class OpenMSX {
306
406
  }
307
407
  }
308
408
  /**
309
- * Write data to the openMSX process stdin
409
+ * Write data to openMSX.
410
+ * On Linux/macOS: writes to the child process stdin.
411
+ * On Windows: writes to the named pipe (pipeWriter).
310
412
  * @param data - XML command or data to send
311
413
  */
312
414
  writeData(data) {
313
- if (!this.process || !this.process.stdin || !this.isConnected) {
415
+ if (!this.process || !this.isConnected) {
314
416
  throw new Error('openMSX process not running or not connected');
315
417
  }
316
- this.process.stdin.write(data);
418
+ if (IS_WINDOWS) {
419
+ // Windows: send via named pipe
420
+ if (!this.pipeWriter || this.pipeWriter.destroyed) {
421
+ throw new Error('Windows named pipe not open');
422
+ }
423
+ this.pipeWriter.write(data);
424
+ }
425
+ else {
426
+ // Linux/macOS: send via child process stdin
427
+ if (!this.process.stdin) {
428
+ throw new Error('openMSX stdin not available');
429
+ }
430
+ this.process.stdin.write(data);
431
+ }
317
432
  }
318
433
  /**
319
434
  * Read data from openMSX process stdout
@@ -348,6 +463,9 @@ export class OpenMSX {
348
463
  forceClose() {
349
464
  if (this.process && !this.process.killed) {
350
465
  try {
466
+ // 'SIGKILL' is accepted on Windows too (maps to TerminateProcess).
467
+ // No-argument kill() is also acceptable here, but SIGKILL makes
468
+ // the intent explicit: we want unconditional termination.
351
469
  this.process.kill('SIGKILL');
352
470
  }
353
471
  catch (error) {
@@ -356,6 +474,7 @@ export class OpenMSX {
356
474
  this.process = null;
357
475
  this.isConnected = false;
358
476
  }
477
+ this.closePipeWriter();
359
478
  }
360
479
  }
361
480
  /**
package/dist/server.js CHANGED
@@ -16,21 +16,23 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { randomUUID } from "node:crypto";
17
17
  import express from "express";
18
18
  import path from "path";
19
+ import { fileURLToPath } from 'node:url';
19
20
  import { createRequire } from 'module';
20
21
  import { openMSXInstance } from "./openmsx.js";
21
22
  import { VectorDB } from "./vectordb.js";
22
- import { detectOpenMSXShareDir } from "./utils.js";
23
+ import { detectOpenMSXExecutable, detectOpenMSXShareDir } from "./utils.js";
23
24
  import { registerTools } from "./server_tools.js";
24
25
  import { registerResources } from "./server_resources.js";
25
26
  import { registerPrompts } from "./server_prompts.js";
26
27
  // Dynamically obtain PACKAGE_VERSION from package.json at runtime
27
28
  const require = createRequire(import.meta.url);
28
29
  export const PACKAGE_VERSION = require('../package.json').version;
29
- const resourcesDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../resources");
30
- const vectorDbDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../vector-db");
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+ const resourcesDir = path.join(__dirname, "../resources");
32
+ const vectorDbDir = path.join(__dirname, "../vector-db");
31
33
  export const emuDirectories = {
32
34
  OPENMSX_SHARE_DIR: '',
33
- OPENMSX_EXECUTABLE: 'openmsx',
35
+ OPENMSX_EXECUTABLE: detectOpenMSXExecutable(),
34
36
  OPENMSX_REPLAYS_DIR: '',
35
37
  OPENMSX_SCREENSHOT_DIR: '',
36
38
  OPENMSX_SCREENDUMP_DIR: '',
@@ -129,14 +129,18 @@ export async function registerResources(server, resourcesDir) {
129
129
  let mimeType;
130
130
  // urldecode the instruction to avoid issues with special characters
131
131
  instruction = decodeURIComponent(instruction).replaceAll(' ', '_');
132
+ // Convert Windows-invalid characters to safe equivalents for the filesystem lookup.
133
+ // The RAG and MCP URIs keep the canonical command name (e.g. CLOAD?),
134
+ // but the file on disk uses a safe name (e.g. CLOAD_Q.md).
135
+ const instructionFile = instruction.replaceAll('?', '_Q');
132
136
  try {
133
137
  let resourceFile;
134
- [mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, 'programming', 'basic_wiki', instruction));
138
+ [mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, 'programming', 'basic_wiki', instructionFile));
135
139
  resourceContent = await fs.readFile(resourceFile, 'utf8');
136
140
  }
137
141
  catch (error) {
138
142
  // Throw exception (MCP protocol requirement)
139
- throw new Error(`Error reading resource programming/basic_wiki/${instruction}: ${error instanceof Error ? error.message : String(error)}`);
143
+ throw new Error(`Error reading resource programming/basic_wiki/${instruction} (file: ${instructionFile}): ${error instanceof Error ? error.message : String(error)}`);
140
144
  }
141
145
  return {
142
146
  contents: [{
@@ -10,6 +10,7 @@ import { resolveLaunchParams } from "./server_elicitations.js";
10
10
  // Tools available in the MCP server
11
11
  // https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
12
12
  export async function registerTools(server, emuDirectories) {
13
+ // emu_control
13
14
  server.registerTool(
14
15
  // Name of the tool (used to call it)
15
16
  "emu_control", {
@@ -193,6 +194,7 @@ export async function registerTools(server, emuDirectories) {
193
194
  isError: false,
194
195
  };
195
196
  });
197
+ // emu_info
196
198
  server.registerTool(
197
199
  // Name of the tool (used to call it)
198
200
  "emu_media", {
@@ -278,6 +280,7 @@ export async function registerTools(server, emuDirectories) {
278
280
  response
279
281
  ]);
280
282
  });
283
+ // emu_info
281
284
  server.registerTool(
282
285
  // Name of the tool (used to call it)
283
286
  "emu_info", {
@@ -345,6 +348,7 @@ export async function registerTools(server, emuDirectories) {
345
348
  isError: false,
346
349
  };
347
350
  });
351
+ // emu_vdp
348
352
  server.registerTool(
349
353
  // Name of the tool (used to call it)
350
354
  "emu_vdp", {
@@ -477,6 +481,7 @@ export async function registerTools(server, emuDirectories) {
477
481
  isError: false,
478
482
  };
479
483
  });
484
+ // debug_run
480
485
  server.registerTool(
481
486
  // Name of the tool (used to call it)
482
487
  "debug_run", {
@@ -549,6 +554,7 @@ export async function registerTools(server, emuDirectories) {
549
554
  response
550
555
  ]);
551
556
  });
557
+ // debug_cpu
552
558
  server.registerTool(
553
559
  // Name of the tool (used to call it)
554
560
  "debug_cpu", {
@@ -695,6 +701,7 @@ export async function registerTools(server, emuDirectories) {
695
701
  isError: false,
696
702
  };
697
703
  });
704
+ // debug_memory
698
705
  server.registerTool(
699
706
  // Name of the tool (used to call it)
700
707
  "debug_memory", {
@@ -703,7 +710,7 @@ export async function registerTools(server, emuDirectories) {
703
710
  description: "Slots info, and Read/write from/to memory in the openMSX emulator.",
704
711
  // Schema for the tool (input validation)
705
712
  inputSchema: {
706
- command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"])
713
+ command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord", "searchBytes"])
707
714
  .describe(`Available commands:
708
715
  'selectedSlots': to get a list of the currently selected memory slots.
709
716
  'getBlock <address> [lines]': to read a block of memory from the specified address.
@@ -711,6 +718,7 @@ export async function registerTools(server, emuDirectories) {
711
718
  'readWord <address>': to read a WORD from the specified address.
712
719
  'writeByte <address> <value8>': to write a BYTE to the specified address.
713
720
  'writeWord <address> <value16>': to write a WORD to the specified address.
721
+ 'searchBytes <address> <length> <values>': to search a sequence of bytes in RAM memory starting from the specified address and within the specified length.
714
722
  **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
715
723
  `),
716
724
  address: z.string()
@@ -731,6 +739,15 @@ export async function registerTools(server, emuDirectories) {
731
739
  .regex(/^0x[0-9a-fA-F]{4}$/, 'Must be a 4 digits hexadecimal number')
732
740
  .optional()
733
741
  .describe("4 hexadecimal digits for a word value (e.g. 0xa5b1). Used by [writeWord]"),
742
+ values: z.string()
743
+ .regex(/^(\s*0x[0-9a-fA-F]{2}\s*)+$/, "Values must be a space-separated string of 2 digits hexadecimal numbers (e.g. '0x1A 0xFF 0x00')")
744
+ .optional()
745
+ .describe("Space-separated string of 2 hexadecimal digits for byte values to search (e.g. '0x1A 0xFF 0x00'). Used by [searchBytes]"),
746
+ length: z.number()
747
+ .min(1, 'Minimum search length too low. Min: 1')
748
+ .max(65536, 'Maximum search length too high. Max: 65536')
749
+ .optional()
750
+ .describe("Decimal number of bytes to search within. Used by [searchBytes]"),
734
751
  },
735
752
  // Structured output schema (MCP protocol 2025-11-25)
736
753
  outputSchema: {
@@ -746,6 +763,10 @@ export async function registerTools(server, emuDirectories) {
746
763
  .describe("Hex dump block of memory. Present for 'getBlock'."),
747
764
  slots: z.string().optional()
748
765
  .describe("Currently selected memory slots info. Present for 'selectedSlots'."),
766
+ length: z.number().optional()
767
+ .describe("Length of bytes searched. Present for 'searchBytes'."),
768
+ values: z.string().optional()
769
+ .describe("Values searched for. Present for 'searchBytes'."),
749
770
  result: z.string().optional()
750
771
  .describe("Generic result or status message."),
751
772
  },
@@ -757,7 +778,7 @@ export async function registerTools(server, emuDirectories) {
757
778
  },
758
779
  },
759
780
  // Handler for the tool (function to be executed when the tool is called)
760
- async ({ command, address, lines, value8, value16 }) => {
781
+ async ({ command, address, lines, value8, value16, length, values }) => {
761
782
  let tclCommand;
762
783
  switch (command) {
763
784
  case "selectedSlots":
@@ -778,6 +799,23 @@ export async function registerTools(server, emuDirectories) {
778
799
  case "writeWord":
779
800
  tclCommand = `poke16 ${address} ${value16}`;
780
801
  break;
802
+ case "searchBytes":
803
+ length = parseInt(address, 16) + length > 0x10000 ? 0x10000 - parseInt(address, 16) : length;
804
+ tclCommand = `set pattern { ${values} }
805
+ set len [llength $pattern]
806
+ set results ""
807
+ for {set i ${address}} {$i \<= [expr {${address} + ${length} - $len}]} {incr i} {
808
+ set match 1
809
+ for {set j 0} {$j \< $len} {incr j} {
810
+ if {[peek [expr {$i + $j}]] != [lindex $pattern $j]} {
811
+ set match 0; break
812
+ }
813
+ }
814
+ if {$match} { append results [format "Found at 0x%04X\n" $i] }
815
+ }
816
+ if {$results eq ""} { return "No matches found" }
817
+ return $results`;
818
+ break;
781
819
  default:
782
820
  return { content: [{ type: "text", text: `Error: Unknown memory command "${command}".` }], isError: true };
783
821
  }
@@ -814,6 +852,10 @@ export async function registerTools(server, emuDirectories) {
814
852
  structuredContent = { command, address, result: response || "Ok" };
815
853
  break;
816
854
  }
855
+ case "searchBytes": {
856
+ structuredContent = { command, address, length, values, result: response };
857
+ break;
858
+ }
817
859
  default:
818
860
  structuredContent = { command, result: response };
819
861
  }
@@ -823,6 +865,7 @@ export async function registerTools(server, emuDirectories) {
823
865
  isError: false,
824
866
  };
825
867
  });
868
+ // debug_vram
826
869
  server.registerTool(
827
870
  // Name of the tool (used to call it)
828
871
  "debug_vram", {
@@ -831,11 +874,12 @@ export async function registerTools(server, emuDirectories) {
831
874
  description: "Read or write from/to VRAM video memory from the openMSX emulator.",
832
875
  // Schema for the tool (input validation)
833
876
  inputSchema: {
834
- command: z.enum(["getBlock", "readByte", "writeByte"])
877
+ command: z.enum(["getBlock", "readByte", "writeByte", "searchBytes"])
835
878
  .describe(`Available commands:
836
879
  'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
837
880
  'readByte <address>': to read a BYTE from the specified VRAM address.
838
881
  'writeByte <address> <value8>': to write a BYTE to the specified VRAM address.
882
+ 'searchBytes <address> <length> <values>': to search a sequence of bytes in VRAM memory starting from the specified address and within the specified length.
839
883
  **Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
840
884
  `),
841
885
  address: z.string()
@@ -849,6 +893,15 @@ export async function registerTools(server, emuDirectories) {
849
893
  .default(8)
850
894
  .describe("Number of lines to obtain. Used by [getBlock]"),
851
895
  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]"),
896
+ values: z.string()
897
+ .regex(/^(\s*0x[0-9a-fA-F]{2}\s*)+$/, "Values must be a space-separated string of 2 digits hexadecimal numbers (e.g. '0x1A 0xFF 0x00')")
898
+ .optional()
899
+ .describe("Space-separated string of 2 hexadecimal digits for byte values to search (e.g. '0x1A 0xFF 0x00'). Used by [searchBytes]"),
900
+ length: z.number()
901
+ .min(1, 'Minimum search length too low. Min: 1')
902
+ .max(65536, 'Maximum search length too high. Max: 65536')
903
+ .optional()
904
+ .describe("Decimal number of bytes to search within. Used by [searchBytes]"),
852
905
  },
853
906
  // Structured output schema (MCP protocol 2025-11-25)
854
907
  outputSchema: {
@@ -862,6 +915,10 @@ export async function registerTools(server, emuDirectories) {
862
915
  .describe("VRAM byte value in hexadecimal. Present for 'readByte'."),
863
916
  hexDump: z.string().optional()
864
917
  .describe("Hex dump block of VRAM. Present for 'getBlock'."),
918
+ length: z.number().optional()
919
+ .describe("Length of bytes searched. Present for 'searchBytes'."),
920
+ values: z.string().optional()
921
+ .describe("Values searched for. Present for 'searchBytes'."),
865
922
  result: z.string().optional()
866
923
  .describe("Generic result or status message."),
867
924
  },
@@ -873,7 +930,7 @@ export async function registerTools(server, emuDirectories) {
873
930
  },
874
931
  },
875
932
  // Handler for the tool (function to be executed when the tool is called)
876
- async ({ command, address, lines, value8 }) => {
933
+ async ({ command, address, lines, value8, values, length }) => {
877
934
  let tclCommand;
878
935
  switch (command) {
879
936
  case "getBlock":
@@ -885,6 +942,23 @@ export async function registerTools(server, emuDirectories) {
885
942
  case "writeByte":
886
943
  tclCommand = `vpoke ${address} ${value8}`;
887
944
  break;
945
+ case "searchBytes":
946
+ length = parseInt(address, 16) + length > 0x20000 ? 0x20000 - parseInt(address, 16) : length;
947
+ tclCommand = `set pattern { ${values} }
948
+ set len [llength $pattern]
949
+ set results ""
950
+ for {set i ${address}} {$i \<= [expr {${address} + ${length} - $len}]} {incr i} {
951
+ set match 1
952
+ for {set j 0} {$j \< $len} {incr j} {
953
+ if {[vpeek [expr {$i + $j}]] != [lindex $pattern $j]} {
954
+ set match 0; break
955
+ }
956
+ }
957
+ if {$match} { append results [format "Found at 0x%04X\n" $i] }
958
+ }
959
+ if {$results eq ""} { return "No matches found" }
960
+ return $results`;
961
+ break;
888
962
  default:
889
963
  return { content: [{ type: "text", text: `Error: Unknown video memory command "${command}".` }], isError: true };
890
964
  }
@@ -907,6 +981,10 @@ export async function registerTools(server, emuDirectories) {
907
981
  structuredContent = { command, address, result: response || "Ok" };
908
982
  break;
909
983
  }
984
+ case "searchBytes": {
985
+ structuredContent = { command, address, length, values, result: response };
986
+ break;
987
+ }
910
988
  default:
911
989
  structuredContent = { command, result: response };
912
990
  }
@@ -916,6 +994,7 @@ export async function registerTools(server, emuDirectories) {
916
994
  isError: false,
917
995
  };
918
996
  });
997
+ // debug_breakpoints
919
998
  server.registerTool(
920
999
  // Name of the tool (used to call it)
921
1000
  "debug_breakpoints", {
@@ -1010,6 +1089,7 @@ export async function registerTools(server, emuDirectories) {
1010
1089
  isError: false,
1011
1090
  };
1012
1091
  });
1092
+ // emu_savestates
1013
1093
  server.registerTool(
1014
1094
  // Name of the tool (used to call it)
1015
1095
  "emu_savestates", {
@@ -1066,6 +1146,7 @@ export async function registerTools(server, emuDirectories) {
1066
1146
  response
1067
1147
  ]);
1068
1148
  });
1149
+ // emu_replay
1069
1150
  server.registerTool(
1070
1151
  // Name of the tool (used to call it)
1071
1152
  "emu_replay", {
@@ -1210,6 +1291,7 @@ export async function registerTools(server, emuDirectories) {
1210
1291
  isError: false,
1211
1292
  };
1212
1293
  });
1294
+ // emu_keyboard
1213
1295
  server.registerTool(
1214
1296
  // Name of the tool (used to call it)
1215
1297
  "emu_keyboard", {
@@ -1288,6 +1370,7 @@ export async function registerTools(server, emuDirectories) {
1288
1370
  response
1289
1371
  ]);
1290
1372
  });
1373
+ // screen_shot
1291
1374
  server.registerTool(
1292
1375
  // Name of the tool (used to call it)
1293
1376
  "screen_shot", {
@@ -1352,6 +1435,7 @@ export async function registerTools(server, emuDirectories) {
1352
1435
  `Error: Unknown screen_shot command "${command}".`
1353
1436
  ]);
1354
1437
  });
1438
+ // screen_dump
1355
1439
  server.registerTool(
1356
1440
  // Name of the tool (used to call it)
1357
1441
  "screen_dump", {
@@ -1384,6 +1468,7 @@ The parameter scrbasename is the name of the filename (without path) to save the
1384
1468
  response
1385
1469
  ]);
1386
1470
  });
1471
+ // basic_programming
1387
1472
  server.registerTool(
1388
1473
  // Name of the tool (used to call it)
1389
1474
  "basic_programming", {
@@ -1549,6 +1634,7 @@ The parameter scrbasename is the name of the filename (without path) to save the
1549
1634
  isError: false,
1550
1635
  };
1551
1636
  });
1637
+ // vector_db_query
1552
1638
  server.registerTool(
1553
1639
  // Name of the tool (used to call it)
1554
1640
  "vector_db_query", {
@@ -1597,6 +1683,7 @@ The response is the list of the top 10 result resources that match the query, in
1597
1683
  // ============================================================================
1598
1684
  // Register a tool to get a specific MSX documentation resource
1599
1685
  // Retrieve MCP resources for MCP clients that don't support MCP resources.
1686
+ // msxdocs_resource_get
1600
1687
  server.registerTool(
1601
1688
  // Name of the tool (used to call it)
1602
1689
  "msxdocs_resource_get", {
package/dist/utils.js CHANGED
@@ -12,6 +12,32 @@ import { PACKAGE_VERSION } from "./server.js";
12
12
  import sanitizeHtml from 'sanitize-html';
13
13
  import { existsSync } from "fs";
14
14
  import os from "os";
15
+ /**
16
+ * Detect the openMSX executable path for the current platform.
17
+ *
18
+ * - Linux: 'openmsx' (expected in PATH after package install)
19
+ * - Windows: 'openmsx.exe' (Node spawn() needs the .exe extension on Windows)
20
+ * - macOS: probes the standard .app bundle path first; falls back to 'openmsx'
21
+ * in case the user has it in PATH (e.g. via Homebrew or manual install).
22
+ *
23
+ * The standard macOS bundle path is /Applications/openMSX.app/Contents/MacOS/openmsx.
24
+ * This is the path documented by the openMSX project (and its Catapult launcher).
25
+ */
26
+ export function detectOpenMSXExecutable() {
27
+ if (process.platform === 'win32') {
28
+ return 'openmsx.exe';
29
+ }
30
+ if (process.platform === 'darwin') {
31
+ const appBundlePath = '/Applications/openMSX.app/Contents/MacOS/openmsx';
32
+ if (existsSync(appBundlePath)) {
33
+ return appBundlePath;
34
+ }
35
+ // Fallback: user may have openMSX in PATH (e.g. via Homebrew)
36
+ return 'openmsx';
37
+ }
38
+ // Linux / other POSIX
39
+ return 'openmsx';
40
+ }
15
41
  /**
16
42
  * Detect the openMSX share directory by checking various methods
17
43
  * @returns string - The detected share directory or an empty string if not found
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",