@nataliapc/mcp-openmsx 1.0.1 → 1.1.0
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 +84 -67
- package/dist/openmsx.js +21 -6
- package/dist/server.js +169 -201
- package/dist/utils.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@ Currently, the MCP server requires Linux to run. It has not been tested on Windo
|
|
|
12
12
|
|
|
13
13
|
This project creates a bridge between modern AI-assisted development (e.g. GitHub Copilot, Claude Desktop) and retro computing (MSX systems) by providing:
|
|
14
14
|
|
|
15
|
-
- **Emulator Control**: Launch, configure,
|
|
16
|
-
- **Media Management**: Handle ROM cartridges, floppy disks, and cassette tapes
|
|
17
|
-
- **Debugging Tools**: Full CPU debugging with breakpoints, memory inspection, and step execution
|
|
18
|
-
- **Video Control**: VDP register manipulation and screen capture
|
|
19
|
-
- **Memory Operations**: Read/write RAM, VRAM, and I/O port access
|
|
20
|
-
- **Automation**: Keyboard input simulation and savestate management
|
|
21
|
-
- **Dual Transport**: Support for both stdio and HTTP transports
|
|
15
|
+
- **Emulator Control**: Launch, configure, manage openMSX instances, and replay timelines.
|
|
16
|
+
- **Media Management**: Handle ROM cartridges, floppy disks, and cassette tapes.
|
|
17
|
+
- **Debugging Tools**: Full CPU debugging with breakpoints, memory inspection, and step execution.
|
|
18
|
+
- **Video Control**: VDP register manipulation and screen capture.
|
|
19
|
+
- **Memory Operations**: Read/write RAM, VRAM, and I/O port access.
|
|
20
|
+
- **Automation**: Keyboard input simulation and savestate management.
|
|
21
|
+
- **Dual Transport**: Support for both stdio and HTTP transports.
|
|
22
22
|
|
|
23
23
|
## 🏗️ Architecture
|
|
24
24
|
|
|
@@ -44,7 +44,8 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
|
|
|
44
44
|
## 🛠️ Available MCP Tools
|
|
45
45
|
|
|
46
46
|
### Emulator Control Tools
|
|
47
|
-
- `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`_.
|
|
47
|
+
- `emu_control`: Controls an openMSX emulator: _`launch`, `close`, `powerOn`, `powerOff`, `reset`, `getEmulatorSpeed`, `setEmulatorSpeed`, `machineList`, `extensionList`, `wait`_.
|
|
48
|
+
- `emu_replay`: Controls emulation timeline: _`start`, `strop`, `status`, `goBack`, `absoluteGoto`, `truncate`, `saveReplay`, `loadReplay`_.
|
|
48
49
|
- `emu_info`: Obtain informacion about the current emulated machine: _`getStatus`, `getSlotsMap`, `getIOPortsMap`_.
|
|
49
50
|
- `emu_media`: Manage ROM, disk, and tape media: _`tapeInsert`, `tapeRewind`, `tapeEject`, `romInsert`, `romEject`, `diskInsert`, `diskInsertFolder`, `diskEject`_.
|
|
50
51
|
- `emu_vdp`: Manage VDP (Video Display Processor): _`getPalette`, `getRegisters`, `getRegisterValue`, `setRegisterValue`, `screenGetMode`, `screenGetFullText`_.
|
|
@@ -62,49 +63,19 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
|
|
|
62
63
|
- `screen_shot`: Capture emulator screen: _`as_image`, `to_file`_.
|
|
63
64
|
- `screen_dump`: Export screen data as BASIC BSAVE.
|
|
64
65
|
|
|
65
|
-
## 🚀 Quick Start
|
|
66
|
-
|
|
67
|
-
### Manual installation
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npm install -g @nataliapc/mcp-openmsx
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Configuration
|
|
74
|
-
|
|
75
|
-
Set optional environment variables to customize the server:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
export OPENMSX_EXECUTABLE="openmsx"
|
|
79
|
-
export OPENMSX_SHARE_DIR="/usr/share/openmsx"
|
|
80
|
-
export OPENMSX_SCREENSHOT_DIR="/my_project/screenshots"
|
|
81
|
-
export OPENMSX_SCREENDUMP_DIR="/my_project/screendumps"
|
|
82
|
-
export MCP_HTTP_PORT=3000
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Manual usage
|
|
86
|
-
|
|
87
|
-
#### As MCP Server (stdio)
|
|
88
66
|
|
|
89
|
-
|
|
90
|
-
mcp-openmsx
|
|
91
|
-
```
|
|
67
|
+
## 🚀 Quick Start
|
|
92
68
|
|
|
93
|
-
|
|
69
|
+
### 🟢 Basic Usage with VSCode
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# or
|
|
98
|
-
mcp-openmsx http
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Basic Usage with Claude Desktop
|
|
71
|
+
* Install [Github Copilot extension](https://code.visualstudio.com/docs/copilot/overview)
|
|
72
|
+
* Add to your workspace a file `.vscode/mcp.json` with:
|
|
102
73
|
|
|
103
|
-
|
|
74
|
+
### STDIO mode (recommended)
|
|
104
75
|
|
|
105
76
|
```json
|
|
106
77
|
{
|
|
107
|
-
"
|
|
78
|
+
"servers": {
|
|
108
79
|
"mcp-openmsx": {
|
|
109
80
|
"command": "npx",
|
|
110
81
|
"args": ["@nataliapc/mcp-openmsx"],
|
|
@@ -115,44 +86,87 @@ Add to your `claude_desktop_config.json`:
|
|
|
115
86
|
}
|
|
116
87
|
}
|
|
117
88
|
```
|
|
89
|
+
**Note:** Environment variables are optional.
|
|
118
90
|
|
|
119
|
-
###
|
|
120
|
-
|
|
121
|
-
* Install [Github Copilot extension](https://code.visualstudio.com/docs/copilot/overview)
|
|
122
|
-
* Add to your workspace a file `.vscode/mcp.json` with:
|
|
123
|
-
|
|
124
|
-
#### stdio mode
|
|
91
|
+
### Streamed HTTP mode (more advanced)
|
|
125
92
|
|
|
126
93
|
```json
|
|
127
94
|
{
|
|
128
95
|
"servers": {
|
|
129
96
|
"mcp-openmsx": {
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"OPENMSX_SHARE_DIR": "/usr/share/openmsx"
|
|
134
|
-
}
|
|
97
|
+
"type": "http",
|
|
98
|
+
"url": "http://localhost:3000/mcp",
|
|
99
|
+
"headers": { }
|
|
135
100
|
}
|
|
136
101
|
}
|
|
137
102
|
}
|
|
138
103
|
```
|
|
139
|
-
**Note:** Environment variables are optional.
|
|
140
104
|
|
|
141
|
-
|
|
105
|
+
**Note:** The MCP HTTP Server must be running standalone in the same computer or in another (`make run_http`).
|
|
106
|
+
|
|
107
|
+
### 🟢 Basic Usage with Claude Desktop
|
|
108
|
+
|
|
109
|
+
Add to your `claude_desktop_config.json`:
|
|
142
110
|
|
|
143
111
|
```json
|
|
144
112
|
{
|
|
145
|
-
"
|
|
113
|
+
"mcpServers": {
|
|
146
114
|
"mcp-openmsx": {
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
115
|
+
"command": "npx",
|
|
116
|
+
"args": ["@nataliapc/mcp-openmsx"],
|
|
117
|
+
"env": {
|
|
118
|
+
"OPENMSX_SHARE_DIR": "/usr/share/openmsx"
|
|
119
|
+
}
|
|
150
120
|
}
|
|
151
121
|
}
|
|
152
122
|
}
|
|
153
123
|
```
|
|
154
124
|
|
|
155
|
-
|
|
125
|
+
### 🟢 Environment Variables
|
|
126
|
+
|
|
127
|
+
| Variable | Description | Default Value | Example |
|
|
128
|
+
|----------|-------------|---------------|---------|
|
|
129
|
+
| `OPENMSX_EXECUTABLE` | Path or command to the openMSX executable | `openmsx` | `/usr/local/bin/openmsx` |
|
|
130
|
+
| `OPENMSX_SHARE_DIR` | Directory containing openMSX data files (machines, extensions, etc.) | System dependent | `/home/myuser/.openmsx/share` |
|
|
131
|
+
| `OPENMSX_SCREENSHOT_DIR` | Directory where screenshots will be saved | Default for openmsx | `/myproject/screenshots` |
|
|
132
|
+
| `OPENMSX_SCREENDUMP_DIR` | Directory where screen dumps will be saved | Default for openmsx | `/myproject/screendumps` |
|
|
133
|
+
| `MCP_TRANSPORT` | Transport mode (`stdio` or `http`) | `stdio` | `http` |
|
|
134
|
+
| `MCP_HTTP_PORT` | Port number for HTTP transport mode | `3000` | `8080` |
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## 🧑💻 Advanced Manual Usage
|
|
138
|
+
|
|
139
|
+
### Manual installation
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm install -g @nataliapc/mcp-openmsx
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Manual set of Environment Variables
|
|
146
|
+
|
|
147
|
+
Set optional environment variables to customize the server:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
export OPENMSX_EXECUTABLE="openmsx"
|
|
151
|
+
export OPENMSX_SHARE_DIR="/usr/share/openmsx"
|
|
152
|
+
export OPENMSX_SCREENSHOT_DIR="/my_project/screenshots"
|
|
153
|
+
export OPENMSX_SCREENDUMP_DIR="/my_project/screendumps"
|
|
154
|
+
export MCP_HTTP_PORT=3000
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### As MCP Server (stdio)
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
mcp-openmsx
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### As HTTP Server
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
MCP_TRANSPORT=http mcp-openmsx
|
|
167
|
+
# or
|
|
168
|
+
mcp-openmsx http
|
|
169
|
+
```
|
|
156
170
|
|
|
157
171
|
|
|
158
172
|
## 💡 Development
|
|
@@ -178,16 +192,19 @@ npm run build
|
|
|
178
192
|
npm run dev
|
|
179
193
|
```
|
|
180
194
|
|
|
181
|
-
|
|
195
|
+
|
|
196
|
+
## 🪪 License
|
|
182
197
|
|
|
183
198
|
GPL2 License - see [LICENSE](LICENSE) file for details.
|
|
184
199
|
|
|
185
|
-
## Contributing
|
|
186
200
|
|
|
187
|
-
|
|
201
|
+
## 🆘 Support
|
|
188
202
|
|
|
189
|
-
|
|
203
|
+
If you need help, or have questions or suggestions, please open an issue on the [GitHub Issues](https://github.com/nataliapc/mcp-openmsx/issues) page or check the project discussions.
|
|
190
204
|
|
|
191
|
-
|
|
205
|
+
|
|
206
|
+
## 🤝 Contributing
|
|
207
|
+
|
|
208
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
192
209
|
|
|
193
210
|
---
|
package/dist/openmsx.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @license GPL2
|
|
6
6
|
*/
|
|
7
7
|
import fs from "fs/promises";
|
|
8
|
-
import { extractDescriptionFromXML } from "./utils.js";
|
|
8
|
+
import { extractDescriptionFromXML, decodeHtmlEntities } from "./utils.js";
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
10
|
/**
|
|
11
11
|
* OpenMSX class for controlling the openMSX emulator via TCL commands over TCP socket
|
|
@@ -89,8 +89,21 @@ export class OpenMSX {
|
|
|
89
89
|
this.sendCommand('set renderer SDLGL-PP');
|
|
90
90
|
// set machine on
|
|
91
91
|
this.sendCommand('set power on');
|
|
92
|
+
// start reverse replay mode
|
|
93
|
+
this.sendCommand('reverse start');
|
|
92
94
|
// Return success message
|
|
93
|
-
|
|
95
|
+
let result = 'Ok: openMSX emulator launched successfully';
|
|
96
|
+
if (machine) {
|
|
97
|
+
result += ` with machine "${machine}"`;
|
|
98
|
+
}
|
|
99
|
+
if (extensions && extensions.length > 0) {
|
|
100
|
+
if (machine) {
|
|
101
|
+
result += ' and';
|
|
102
|
+
}
|
|
103
|
+
result += ` with extensions: "${extensions.join('", "')}"`;
|
|
104
|
+
}
|
|
105
|
+
result += ', is powered on, and replay mode is started.';
|
|
106
|
+
safeResolve(result);
|
|
94
107
|
}
|
|
95
108
|
catch (error) {
|
|
96
109
|
safeResolve(`Error: Failed to send control commands - ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -179,7 +192,7 @@ export class OpenMSX {
|
|
|
179
192
|
const trimmedLine = param.trim();
|
|
180
193
|
if (trimmedLine) {
|
|
181
194
|
const value = await this.sendCommand(`machine_info ${trimmedLine}`);
|
|
182
|
-
machineInfo[trimmedLine] = value;
|
|
195
|
+
machineInfo[trimmedLine] = value.trim();
|
|
183
196
|
}
|
|
184
197
|
}
|
|
185
198
|
return JSON.stringify(machineInfo, null, 2);
|
|
@@ -259,14 +272,16 @@ export class OpenMSX {
|
|
|
259
272
|
// Look for reply tags in the output
|
|
260
273
|
const replyMatch = output.match(/<reply result="(ok|nok)"[^>]*>(.*?)<\/reply>/s);
|
|
261
274
|
if (replyMatch) {
|
|
275
|
+
const outputContent = decodeHtmlEntities(replyMatch[2].trim());
|
|
262
276
|
if (replyMatch[1] === 'ok') {
|
|
263
|
-
return
|
|
277
|
+
return outputContent;
|
|
264
278
|
}
|
|
265
279
|
else {
|
|
266
|
-
return `Error: ${
|
|
280
|
+
return `Error: ${outputContent}`;
|
|
267
281
|
}
|
|
268
282
|
}
|
|
269
|
-
|
|
283
|
+
// Return raw output with HTML entities decoded
|
|
284
|
+
return decodeHtmlEntities(output.trim());
|
|
270
285
|
}
|
|
271
286
|
catch (error) {
|
|
272
287
|
return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* through TCL commands via stdio.
|
|
7
7
|
*
|
|
8
8
|
* @package @nataliapc/mcp-openmsx
|
|
9
|
-
* @version 1.0
|
|
9
|
+
* @version 1.1.0
|
|
10
10
|
* @author Natalia Pujol Cremades (@nataliapc)
|
|
11
11
|
* @license GPL2
|
|
12
12
|
*/
|
|
@@ -21,7 +21,7 @@ import fs from "fs/promises";
|
|
|
21
21
|
import path from "path";
|
|
22
22
|
import { openMSXInstance } from "./openmsx.js";
|
|
23
23
|
// Version info for CLI
|
|
24
|
-
const PACKAGE_VERSION = "1.0
|
|
24
|
+
const PACKAGE_VERSION = "1.1.0";
|
|
25
25
|
// Defaults for openMSX paths
|
|
26
26
|
var OPENMSX_EXECUTABLE = 'openmsx';
|
|
27
27
|
var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
|
|
@@ -39,41 +39,30 @@ function registerAllTools(server) {
|
|
|
39
39
|
"emu_control",
|
|
40
40
|
// Description of the tool (what it does)
|
|
41
41
|
"Controls an openMSX emulator. Commands: " +
|
|
42
|
-
"'launch [machine] [extensions]': opens a powered
|
|
42
|
+
"'launch [machine] [extensions]': opens a powered-on openMSX emulator; machine and extensions parameters can be specified; use 'machineList' and 'extensionList' tools to obtain valid values. " +
|
|
43
43
|
"'close': closes the openMSX emulator. " +
|
|
44
44
|
"'powerOn': powers on the openMSX emulator. " +
|
|
45
45
|
"'powerOff': powers off the openMSX emulator. " +
|
|
46
46
|
"'reset': resets the current machine. " +
|
|
47
|
-
"'getEmulatorSpeed':
|
|
48
|
-
"'setEmulatorSpeed <emuspeed>':
|
|
49
|
-
"'machineList':
|
|
50
|
-
"'extensionList':
|
|
47
|
+
"'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100. " +
|
|
48
|
+
"'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100. " +
|
|
49
|
+
"'machineList': gets a list of all available MSX machines that can be emulated with openMSX. " +
|
|
50
|
+
"'extensionList': gets a list of all available MSX extensions that can be used with openMSX. " +
|
|
51
|
+
"'wait <seconds>': performs a wait for the specified number of seconds, default is 2. ",
|
|
51
52
|
// Schema for the tool (input validation)
|
|
52
53
|
{
|
|
53
|
-
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList"]),
|
|
54
|
+
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList", "wait"]),
|
|
54
55
|
machine: z.string().min(1).max(100).optional(),
|
|
55
56
|
extensions: z.array(z.string().min(1).max(100)).optional(),
|
|
56
57
|
emuspeed: z.number().min(1).max(10000).optional().default(100),
|
|
58
|
+
seconds: z.number().min(1).max(10).optional().default(2), // Seconds to wait
|
|
57
59
|
},
|
|
58
60
|
// Handler for the tool (function to be executed when the tool is called)
|
|
59
|
-
async ({ command, machine, extensions, emuspeed }) => {
|
|
61
|
+
async ({ command, machine, extensions, emuspeed, seconds }) => {
|
|
60
62
|
let result = "Error";
|
|
61
63
|
switch (command) {
|
|
62
64
|
case "launch":
|
|
63
65
|
result = await openMSXInstance.emu_launch(OPENMSX_EXECUTABLE, machine || "", extensions || []);
|
|
64
|
-
// Check if launch was successful
|
|
65
|
-
if (result === "Ok") {
|
|
66
|
-
result = "openMSX emulator launched";
|
|
67
|
-
if (machine) {
|
|
68
|
-
result += ` with machine "${machine}"`;
|
|
69
|
-
}
|
|
70
|
-
if (extensions && extensions.length > 0) {
|
|
71
|
-
if (machine) {
|
|
72
|
-
result += ' and ';
|
|
73
|
-
}
|
|
74
|
-
result += ` with extensions: ${extensions.join(', ')}`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
66
|
break;
|
|
78
67
|
case "close":
|
|
79
68
|
result = await openMSXInstance.emu_close();
|
|
@@ -104,17 +93,18 @@ function registerAllTools(server) {
|
|
|
104
93
|
case "extensionList":
|
|
105
94
|
result = await openMSXInstance.getExtensionList(EXTENSIONS_DIR);
|
|
106
95
|
break;
|
|
96
|
+
case "wait":
|
|
97
|
+
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
|
98
|
+
result = `Waited for ${seconds} seconds.`;
|
|
99
|
+
break;
|
|
107
100
|
default:
|
|
108
101
|
result = `Error: Unknown command "${command}".`;
|
|
109
102
|
break;
|
|
110
103
|
}
|
|
111
104
|
// Return result with proper format for MCP
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
text: result === '' ? 'Ok' : result,
|
|
116
|
-
}],
|
|
117
|
-
};
|
|
105
|
+
return getResponseContent([
|
|
106
|
+
result
|
|
107
|
+
]);
|
|
118
108
|
});
|
|
119
109
|
server.tool(
|
|
120
110
|
// Name of the tool (used to call it)
|
|
@@ -166,21 +156,15 @@ function registerAllTools(server) {
|
|
|
166
156
|
tclCommand = "diska eject";
|
|
167
157
|
break;
|
|
168
158
|
default:
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
text: `Error: Unknown emulator media command "${command}".`,
|
|
173
|
-
}],
|
|
174
|
-
};
|
|
159
|
+
return getResponseContent([
|
|
160
|
+
`Error: Unknown emulator media command "${command}".`
|
|
161
|
+
]);
|
|
175
162
|
}
|
|
176
163
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
177
164
|
// Return the response from openMSX
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
text: response === '' ? 'Ok' : response,
|
|
182
|
-
}],
|
|
183
|
-
};
|
|
165
|
+
return getResponseContent([
|
|
166
|
+
response
|
|
167
|
+
]);
|
|
184
168
|
});
|
|
185
169
|
server.tool(
|
|
186
170
|
// Name of the tool (used to call it)
|
|
@@ -199,12 +183,9 @@ function registerAllTools(server) {
|
|
|
199
183
|
let tclCommand;
|
|
200
184
|
switch (command) {
|
|
201
185
|
case "getStatus":
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
text: await openMSXInstance.emu_status(),
|
|
206
|
-
}],
|
|
207
|
-
};
|
|
186
|
+
return getResponseContent([
|
|
187
|
+
await openMSXInstance.emu_status()
|
|
188
|
+
]);
|
|
208
189
|
case "getSlotsMap":
|
|
209
190
|
tclCommand = "slotmap";
|
|
210
191
|
break;
|
|
@@ -212,20 +193,14 @@ function registerAllTools(server) {
|
|
|
212
193
|
tclCommand = "iomap";
|
|
213
194
|
break;
|
|
214
195
|
default:
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
text: `Error: Unknown emulator info command "${command}".`,
|
|
219
|
-
}],
|
|
220
|
-
};
|
|
196
|
+
return getResponseContent([
|
|
197
|
+
`Error: Unknown emulator info command "${command}".`
|
|
198
|
+
]);
|
|
221
199
|
}
|
|
222
200
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
text: response,
|
|
227
|
-
}],
|
|
228
|
-
};
|
|
201
|
+
return getResponseContent([
|
|
202
|
+
response
|
|
203
|
+
]);
|
|
229
204
|
});
|
|
230
205
|
server.tool(
|
|
231
206
|
// Name of the tool (used to call it)
|
|
@@ -265,35 +240,18 @@ function registerAllTools(server) {
|
|
|
265
240
|
break;
|
|
266
241
|
case "screenGetFullText":
|
|
267
242
|
const response = await openMSXInstance.sendCommand('get_screen');
|
|
268
|
-
return response.startsWith('Error:') ?
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
text: response,
|
|
272
|
-
}]
|
|
273
|
-
} : {
|
|
274
|
-
content: [{
|
|
275
|
-
type: "text",
|
|
276
|
-
text: "The screen text is:",
|
|
277
|
-
}, {
|
|
278
|
-
type: "text",
|
|
279
|
-
text: response,
|
|
280
|
-
}],
|
|
281
|
-
};
|
|
243
|
+
return response.startsWith('Error:') ?
|
|
244
|
+
getResponseContent([response]) :
|
|
245
|
+
getResponseContent(["The screen text is:", response]);
|
|
282
246
|
default:
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
text: `Error: Unknown emulator vdp command "${command}".`,
|
|
287
|
-
}],
|
|
288
|
-
};
|
|
247
|
+
return getResponseContent([
|
|
248
|
+
`Error: Unknown emulator vdp command "${command}".`
|
|
249
|
+
]);
|
|
289
250
|
}
|
|
290
251
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
text: response === "" ? "Ok" : response,
|
|
295
|
-
}],
|
|
296
|
-
};
|
|
252
|
+
return getResponseContent([
|
|
253
|
+
response
|
|
254
|
+
]);
|
|
297
255
|
});
|
|
298
256
|
server.tool(
|
|
299
257
|
// Name of the tool (used to call it)
|
|
@@ -343,20 +301,14 @@ function registerAllTools(server) {
|
|
|
343
301
|
tclCommand = `run_to ${address}`;
|
|
344
302
|
break;
|
|
345
303
|
default:
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
text: `Error: Unknown debug command "${command}".`,
|
|
350
|
-
}],
|
|
351
|
-
};
|
|
304
|
+
return getResponseContent([
|
|
305
|
+
`Error: Unknown debug command "${command}".`
|
|
306
|
+
]);
|
|
352
307
|
}
|
|
353
308
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
text: response === '' ? 'Ok' : response,
|
|
358
|
-
}],
|
|
359
|
-
};
|
|
309
|
+
return getResponseContent([
|
|
310
|
+
response
|
|
311
|
+
]);
|
|
360
312
|
});
|
|
361
313
|
server.tool(
|
|
362
314
|
// Name of the tool (used to call it)
|
|
@@ -401,20 +353,14 @@ function registerAllTools(server) {
|
|
|
401
353
|
tclCommand = "get_active_cpu";
|
|
402
354
|
break;
|
|
403
355
|
default:
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
text: `Error: Unknown memory command "${command}".`,
|
|
408
|
-
}],
|
|
409
|
-
};
|
|
356
|
+
return getResponseContent([
|
|
357
|
+
`Error: Unknown memory command "${command}".`
|
|
358
|
+
]);
|
|
410
359
|
}
|
|
411
360
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
text: response === '' ? 'Ok' : response,
|
|
416
|
-
}],
|
|
417
|
-
};
|
|
361
|
+
return getResponseContent([
|
|
362
|
+
response
|
|
363
|
+
]);
|
|
418
364
|
});
|
|
419
365
|
server.tool(
|
|
420
366
|
// Name of the tool (used to call it)
|
|
@@ -463,20 +409,14 @@ function registerAllTools(server) {
|
|
|
463
409
|
tclCommand = "listing";
|
|
464
410
|
break;
|
|
465
411
|
default:
|
|
466
|
-
return
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
text: `Error: Unknown memory command "${command}".`,
|
|
470
|
-
}],
|
|
471
|
-
};
|
|
412
|
+
return getResponseContent([
|
|
413
|
+
`Error: Unknown memory command "${command}".`
|
|
414
|
+
]);
|
|
472
415
|
}
|
|
473
416
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
474
|
-
return
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
text: response === '' ? 'Ok' : response,
|
|
478
|
-
}],
|
|
479
|
-
};
|
|
417
|
+
return getResponseContent([
|
|
418
|
+
response
|
|
419
|
+
]);
|
|
480
420
|
});
|
|
481
421
|
server.tool(
|
|
482
422
|
// Name of the tool (used to call it)
|
|
@@ -508,20 +448,14 @@ function registerAllTools(server) {
|
|
|
508
448
|
tclCommand = `vpoke ${address} ${value8}`;
|
|
509
449
|
break;
|
|
510
450
|
default:
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
text: `Error: Unknown video memory command "${command}".`,
|
|
515
|
-
}],
|
|
516
|
-
};
|
|
451
|
+
return getResponseContent([
|
|
452
|
+
`Error: Unknown video memory command "${command}".`
|
|
453
|
+
]);
|
|
517
454
|
}
|
|
518
455
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
519
|
-
return
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
text: response === '' ? 'Ok' : response,
|
|
523
|
-
}],
|
|
524
|
-
};
|
|
456
|
+
return getResponseContent([
|
|
457
|
+
response
|
|
458
|
+
]);
|
|
525
459
|
});
|
|
526
460
|
server.tool(
|
|
527
461
|
// Name of the tool (used to call it)
|
|
@@ -553,20 +487,14 @@ function registerAllTools(server) {
|
|
|
553
487
|
tclCommand = 'debug list_bp';
|
|
554
488
|
break;
|
|
555
489
|
default:
|
|
556
|
-
return
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
text: `Error: Unknown breakpoint command "${command}".`,
|
|
560
|
-
}],
|
|
561
|
-
};
|
|
490
|
+
return getResponseContent([
|
|
491
|
+
`Error: Unknown breakpoint command "${command}".`
|
|
492
|
+
]);
|
|
562
493
|
}
|
|
563
494
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
text: response,
|
|
568
|
-
}],
|
|
569
|
-
};
|
|
495
|
+
return getResponseContent([
|
|
496
|
+
response
|
|
497
|
+
]);
|
|
570
498
|
});
|
|
571
499
|
server.tool(
|
|
572
500
|
// Name of the tool (used to call it)
|
|
@@ -600,23 +528,77 @@ function registerAllTools(server) {
|
|
|
600
528
|
tclCommand = 'list_savestates';
|
|
601
529
|
break;
|
|
602
530
|
default:
|
|
603
|
-
return
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
text: `Error: Unknown savestate command "${command}".`,
|
|
607
|
-
}],
|
|
608
|
-
};
|
|
531
|
+
return getResponseContent([
|
|
532
|
+
`Error: Unknown savestate command "${command}".`
|
|
533
|
+
]);
|
|
609
534
|
}
|
|
610
535
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
611
|
-
return
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
536
|
+
return getResponseContent([
|
|
537
|
+
textResponse,
|
|
538
|
+
response
|
|
539
|
+
]);
|
|
540
|
+
});
|
|
541
|
+
server.tool(
|
|
542
|
+
// Name of the tool (used to call it)
|
|
543
|
+
"emu_replay",
|
|
544
|
+
// Description of the tool (what it does)
|
|
545
|
+
"When replay is enabled (the default) the emulator collect data while emulating, which enables you to go back and forward in MSX time; consider do a 'pause' to maintain the the timeline before a 'goBack' or 'absoluteGoto'. Commands: " +
|
|
546
|
+
"'start': starts the replay mode (enabled by default when emulator is launched). " +
|
|
547
|
+
"'stop': stops the replay mode. " +
|
|
548
|
+
"'status': gives information about the replay feature and the data that is collected. " +
|
|
549
|
+
"'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started. " +
|
|
550
|
+
"'absoluteGoto <time>': go to the indicated absolute time in seconds in the MSX timeline, if time is before replay started it will jump to the time when is started. " +
|
|
551
|
+
"'truncate': stop replaying and wipe all the future replay data after now. " +
|
|
552
|
+
"'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response. " +
|
|
553
|
+
"'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.",
|
|
554
|
+
// Schema for the tool (input validation)
|
|
555
|
+
{
|
|
556
|
+
command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "saveReplay", "loadReplay"]),
|
|
557
|
+
seconds: z.number().min(1).max(60).optional(), // Seconds to go back
|
|
558
|
+
time: z.string().regex(/^\d+$/).optional(), // Time in seconds to go to
|
|
559
|
+
filename: z.string().min(1).max(200).optional(), // Filename to save/load replay
|
|
560
|
+
},
|
|
561
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
562
|
+
async ({ command, seconds, time, filename }) => {
|
|
563
|
+
let tclCommand;
|
|
564
|
+
switch (command) {
|
|
565
|
+
case "start":
|
|
566
|
+
tclCommand = "reverse start";
|
|
567
|
+
break;
|
|
568
|
+
case "stop":
|
|
569
|
+
tclCommand = "reverse stop";
|
|
570
|
+
break;
|
|
571
|
+
case "status":
|
|
572
|
+
tclCommand = "reverse status";
|
|
573
|
+
break;
|
|
574
|
+
case "goBack":
|
|
575
|
+
tclCommand = `reverse goback ${seconds}`;
|
|
576
|
+
break;
|
|
577
|
+
case "absoluteGoto":
|
|
578
|
+
tclCommand = `reverse goto ${time}`;
|
|
579
|
+
break;
|
|
580
|
+
case "truncate":
|
|
581
|
+
tclCommand = "reverse truncatereplay";
|
|
582
|
+
break;
|
|
583
|
+
case "saveReplay":
|
|
584
|
+
if (filename)
|
|
585
|
+
filename = `"${filename}"`;
|
|
586
|
+
tclCommand = `reverse savereplay ${filename || ''}`;
|
|
587
|
+
break;
|
|
588
|
+
case "loadReplay":
|
|
589
|
+
if (filename)
|
|
590
|
+
filename = `"${filename}"`;
|
|
591
|
+
tclCommand = `reverse loadreplay ${filename}`;
|
|
592
|
+
break;
|
|
593
|
+
default:
|
|
594
|
+
return getResponseContent([
|
|
595
|
+
`Error: Unknown replay command "${command}".`
|
|
596
|
+
]);
|
|
597
|
+
}
|
|
598
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
599
|
+
return getResponseContent([
|
|
600
|
+
response
|
|
601
|
+
]);
|
|
620
602
|
});
|
|
621
603
|
server.tool(
|
|
622
604
|
// Name of the tool (used to call it)
|
|
@@ -637,20 +619,14 @@ function registerAllTools(server) {
|
|
|
637
619
|
tclCommand = `type "${text}"`;
|
|
638
620
|
break;
|
|
639
621
|
default:
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
text: `Error: Unknown keyboard command "${command}".`,
|
|
644
|
-
}],
|
|
645
|
-
};
|
|
622
|
+
return getResponseContent([
|
|
623
|
+
`Error: Unknown keyboard command "${command}".`
|
|
624
|
+
]);
|
|
646
625
|
}
|
|
647
626
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
text: response === '' ? 'Ok' : response,
|
|
652
|
-
}],
|
|
653
|
-
};
|
|
627
|
+
return getResponseContent([
|
|
628
|
+
response
|
|
629
|
+
]);
|
|
654
630
|
});
|
|
655
631
|
server.tool(
|
|
656
632
|
// Name of the tool (used to call it)
|
|
@@ -684,30 +660,19 @@ function registerAllTools(server) {
|
|
|
684
660
|
};
|
|
685
661
|
}
|
|
686
662
|
catch (error) {
|
|
687
|
-
return
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}, {
|
|
692
|
-
type: "text",
|
|
693
|
-
text: error instanceof Error ? error.message : String(error),
|
|
694
|
-
}],
|
|
695
|
-
};
|
|
663
|
+
return getResponseContent([
|
|
664
|
+
'Error creating screenshot: ' + response,
|
|
665
|
+
error instanceof Error ? error.message : String(error)
|
|
666
|
+
]);
|
|
696
667
|
}
|
|
697
668
|
case "to_file":
|
|
698
|
-
return
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
text: response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response,
|
|
702
|
-
}],
|
|
703
|
-
};
|
|
669
|
+
return getResponseContent([
|
|
670
|
+
response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response
|
|
671
|
+
]);
|
|
704
672
|
}
|
|
705
|
-
return
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
text: `Error: Unknown screen_shot command "${command}".`,
|
|
709
|
-
}],
|
|
710
|
-
};
|
|
673
|
+
return getResponseContent([
|
|
674
|
+
`Error: Unknown screen_shot command "${command}".`
|
|
675
|
+
]);
|
|
711
676
|
});
|
|
712
677
|
server.tool(
|
|
713
678
|
// Name of the tool (used to call it)
|
|
@@ -722,17 +687,20 @@ function registerAllTools(server) {
|
|
|
722
687
|
async ({ scrbasename }) => {
|
|
723
688
|
const openmsxCommand = `save_msx_screen "${OPENMSX_SCREENDUMP_DIR + scrbasename}"`;
|
|
724
689
|
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
725
|
-
return
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}, {
|
|
730
|
-
type: "text",
|
|
731
|
-
text: response,
|
|
732
|
-
}],
|
|
733
|
-
};
|
|
690
|
+
return getResponseContent([
|
|
691
|
+
response.startsWith('Error:') ? 'Fail:' : 'Screendump file saved as:',
|
|
692
|
+
response
|
|
693
|
+
]);
|
|
734
694
|
});
|
|
735
695
|
}
|
|
696
|
+
function getResponseContent(response) {
|
|
697
|
+
return {
|
|
698
|
+
content: response.map(line => ({
|
|
699
|
+
type: "text",
|
|
700
|
+
text: line == '' ? "Ok" : line,
|
|
701
|
+
})),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
736
704
|
// ============================================================================
|
|
737
705
|
// Cleanup handlers for graceful shutdown of MCP server
|
|
738
706
|
// Ensure openMSX emulator is closed when MCP server stops
|
package/dist/utils.js
CHANGED
|
@@ -21,3 +21,34 @@ export async function extractDescriptionFromXML(filePath) {
|
|
|
21
21
|
return 'Error reading description';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Decode HTML entities in a string to plain text
|
|
26
|
+
* @param text - String containing HTML entities
|
|
27
|
+
* @returns string - String with HTML entities decoded
|
|
28
|
+
*/
|
|
29
|
+
export function decodeHtmlEntities(text) {
|
|
30
|
+
const htmlEntities = {
|
|
31
|
+
'<': '<',
|
|
32
|
+
'>': '>',
|
|
33
|
+
'&': '&',
|
|
34
|
+
'"': '"',
|
|
35
|
+
''': "'",
|
|
36
|
+
'/': '/',
|
|
37
|
+
'`': '`',
|
|
38
|
+
'=': '=',
|
|
39
|
+
''': "'",
|
|
40
|
+
'/': '/',
|
|
41
|
+
'`': '`',
|
|
42
|
+
'=': '=',
|
|
43
|
+
''': "'",
|
|
44
|
+
' ': ' ',
|
|
45
|
+
'
': '\n',
|
|
46
|
+
'
': '\n',
|
|
47
|
+
' ': '\n',
|
|
48
|
+
' ': '\r',
|
|
49
|
+
'	': '\t'
|
|
50
|
+
};
|
|
51
|
+
return text.replace(/&[#\w]+;/g, (entity) => {
|
|
52
|
+
return htmlEntities[entity] || entity;
|
|
53
|
+
});
|
|
54
|
+
}
|