@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 +1 -1
- package/dist/openmsx.js +123 -12
- package/dist/server.js +2 -4
- package/dist/utils.js +26 -0
- package/package.json +1 -1
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)
|
|
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
|
|
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
|
-
//
|
|
42
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
415
|
+
if (!this.process || !this.isConnected) {
|
|
319
416
|
throw new Error('openMSX process not running or not connected');
|
|
320
417
|
}
|
|
321
|
-
|
|
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
|
-
|
|
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
|