@nataliapc/mcp-openmsx 1.2.6 → 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
@@ -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` (Linux/macOS) / `openmsx.exe` (Windows) | `/usr/local/bin/openmsx` or `C:\Program Files\openMSX\openmsx.exe` |
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` |
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) => {
@@ -311,14 +406,29 @@ export class OpenMSX {
311
406
  }
312
407
  }
313
408
  /**
314
- * 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).
315
412
  * @param data - XML command or data to send
316
413
  */
317
414
  writeData(data) {
318
- if (!this.process || !this.process.stdin || !this.isConnected) {
415
+ if (!this.process || !this.isConnected) {
319
416
  throw new Error('openMSX process not running or not connected');
320
417
  }
321
- 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
+ }
322
432
  }
323
433
  /**
324
434
  * Read data from openMSX process stdout
@@ -364,6 +474,7 @@ export class OpenMSX {
364
474
  this.process = null;
365
475
  this.isConnected = false;
366
476
  }
477
+ this.closePipeWriter();
367
478
  }
368
479
  }
369
480
  /**
package/dist/server.js CHANGED
@@ -20,7 +20,7 @@ import { fileURLToPath } from 'node:url';
20
20
  import { createRequire } from 'module';
21
21
  import { openMSXInstance } from "./openmsx.js";
22
22
  import { VectorDB } from "./vectordb.js";
23
- import { detectOpenMSXShareDir } from "./utils.js";
23
+ import { detectOpenMSXExecutable, detectOpenMSXShareDir } from "./utils.js";
24
24
  import { registerTools } from "./server_tools.js";
25
25
  import { registerResources } from "./server_resources.js";
26
26
  import { registerPrompts } from "./server_prompts.js";
@@ -32,9 +32,7 @@ const resourcesDir = path.join(__dirname, "../resources");
32
32
  const vectorDbDir = path.join(__dirname, "../vector-db");
33
33
  export const emuDirectories = {
34
34
  OPENMSX_SHARE_DIR: '',
35
- // On Windows, Node.js spawn() may not resolve 'openmsx' to 'openmsx.exe' unless it is in PATH.
36
- // Using the platform-aware default reduces friction for Windows users who have openMSX in PATH.
37
- OPENMSX_EXECUTABLE: process.platform === 'win32' ? 'openmsx.exe' : 'openmsx',
35
+ OPENMSX_EXECUTABLE: detectOpenMSXExecutable(),
38
36
  OPENMSX_REPLAYS_DIR: '',
39
37
  OPENMSX_SCREENSHOT_DIR: '',
40
38
  OPENMSX_SCREENDUMP_DIR: '',
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.6",
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",