@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 +4 -4
- package/dist/openmsx.js +133 -14
- package/dist/server.js +6 -4
- package/dist/server_resources.js +6 -2
- package/dist/server_tools.js +91 -4
- package/dist/utils.js +26 -0
- package/package.json +1 -1
- package/resources/programming/basic_wiki/_toc.json +1 -1
- package/resources/sdcc/lyx2md.py +745 -0
- package/resources/sdcc/sdccman.md +5557 -0
- /package/resources/programming/basic_wiki/{CLOAD?.md → CLOAD_Q.md} +0 -0
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`, `
|
|
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
|
-
|
|
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
|
|
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) => {
|
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
415
|
+
if (!this.process || !this.isConnected) {
|
|
314
416
|
throw new Error('openMSX process not running or not connected');
|
|
315
417
|
}
|
|
316
|
-
|
|
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
|
|
30
|
-
const
|
|
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:
|
|
35
|
+
OPENMSX_EXECUTABLE: detectOpenMSXExecutable(),
|
|
34
36
|
OPENMSX_REPLAYS_DIR: '',
|
|
35
37
|
OPENMSX_SCREENSHOT_DIR: '',
|
|
36
38
|
OPENMSX_SCREENDUMP_DIR: '',
|
package/dist/server_resources.js
CHANGED
|
@@ -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',
|
|
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: [{
|
package/dist/server_tools.js
CHANGED
|
@@ -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
|