@nataliapc/mcp-openmsx 1.1.0 → 1.1.3
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 +6 -3
- package/dist/openmsx.js +20 -21
- package/dist/server.js +27 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,6 @@ A [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) s
|
|
|
6
6
|
|
|
7
7
|
This server provides comprehensive tools for MSX software development, testing, and automation through standardized MCP protocols.
|
|
8
8
|
|
|
9
|
-
Currently, the MCP server requires Linux to run. It has not been tested on Windows or macOS, although it will likely work on the latter as well.
|
|
10
|
-
|
|
11
9
|
## 🎯 Project Overview
|
|
12
10
|
|
|
13
11
|
This project creates a bridge between modern AI-assisted development (e.g. GitHub Copilot, Claude Desktop) and retro computing (MSX systems) by providing:
|
|
@@ -66,6 +64,8 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
|
|
|
66
64
|
|
|
67
65
|
## 🚀 Quick Start
|
|
68
66
|
|
|
67
|
+
You can use this MCP server in this basic way with the [precompiled NPM package](https://www.npmjs.com/package/@nataliapc/mcp-openmsx). You may need to have `nodejs` installed for this to work.
|
|
68
|
+
|
|
69
69
|
### 🟢 Basic Usage with VSCode
|
|
70
70
|
|
|
71
71
|
* Install [Github Copilot extension](https://code.visualstudio.com/docs/copilot/overview)
|
|
@@ -86,7 +86,8 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
**Note:** Environment variables are optional. Customize them as you need.
|
|
90
91
|
|
|
91
92
|
### Streamed HTTP mode (more advanced)
|
|
92
93
|
|
|
@@ -136,6 +137,8 @@ Add to your `claude_desktop_config.json`:
|
|
|
136
137
|
|
|
137
138
|
## 🧑💻 Advanced Manual Usage
|
|
138
139
|
|
|
140
|
+
Currently, the MCP server requires Linux to be compiled. It has not been tested on Windows or macOS, although it will likely work on the latter as well.
|
|
141
|
+
|
|
139
142
|
### Manual installation
|
|
140
143
|
|
|
141
144
|
```bash
|
package/dist/openmsx.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from "fs/promises";
|
|
8
8
|
import { extractDescriptionFromXML, decodeHtmlEntities } from "./utils.js";
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
10
11
|
/**
|
|
11
12
|
* OpenMSX class for controlling the openMSX emulator via TCL commands over TCP socket
|
|
12
13
|
*/
|
|
@@ -143,7 +144,7 @@ export class OpenMSX {
|
|
|
143
144
|
async emu_close() {
|
|
144
145
|
return new Promise((resolve) => {
|
|
145
146
|
if (!this.process) {
|
|
146
|
-
resolve("No emulator process running");
|
|
147
|
+
resolve("Error: No emulator process running");
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
this.process.on('exit', () => {
|
|
@@ -152,7 +153,7 @@ export class OpenMSX {
|
|
|
152
153
|
resolve("Ok: Emulator process closed successfully");
|
|
153
154
|
});
|
|
154
155
|
this.process.on('error', (error) => {
|
|
155
|
-
resolve(`Error closing emulator: ${error.message}`);
|
|
156
|
+
resolve(`Error: error closing emulator: ${error.message}`);
|
|
156
157
|
});
|
|
157
158
|
// Try graceful shutdown first
|
|
158
159
|
if (this.isConnected) {
|
|
@@ -183,7 +184,7 @@ export class OpenMSX {
|
|
|
183
184
|
try {
|
|
184
185
|
const response = await this.sendCommand('machine_info');
|
|
185
186
|
if (response.startsWith('Error:')) {
|
|
186
|
-
return
|
|
187
|
+
return response;
|
|
187
188
|
}
|
|
188
189
|
// Parse machine_info output into key-value pairs
|
|
189
190
|
const parameters = response.trim().split(' ');
|
|
@@ -198,9 +199,7 @@ export class OpenMSX {
|
|
|
198
199
|
return JSON.stringify(machineInfo, null, 2);
|
|
199
200
|
}
|
|
200
201
|
catch (error) {
|
|
201
|
-
return
|
|
202
|
-
error: `Failed to get machine status: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
203
|
-
});
|
|
202
|
+
return `Error: Failed to get machine status - ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
/**
|
|
@@ -210,7 +209,7 @@ export class OpenMSX {
|
|
|
210
209
|
async getMachineList(machinesDirectory) {
|
|
211
210
|
// Read the machines directory
|
|
212
211
|
let machines = [];
|
|
213
|
-
let machinesList = "No machines found.";
|
|
212
|
+
let machinesList = "Error: No machines found.";
|
|
214
213
|
try {
|
|
215
214
|
const allFiles = await fs.readdir(machinesDirectory);
|
|
216
215
|
machines = await Promise.all(allFiles
|
|
@@ -218,17 +217,17 @@ export class OpenMSX {
|
|
|
218
217
|
.map(async (file) => {
|
|
219
218
|
return {
|
|
220
219
|
name: file.replace('.xml', ''),
|
|
221
|
-
description: await extractDescriptionFromXML(
|
|
220
|
+
description: await extractDescriptionFromXML(path.join(machinesDirectory, file))
|
|
222
221
|
};
|
|
223
222
|
}));
|
|
223
|
+
if (machines.length !== 0) {
|
|
224
|
+
machinesList = JSON.stringify(machines, null, 2);
|
|
225
|
+
}
|
|
226
|
+
return machinesList;
|
|
224
227
|
}
|
|
225
228
|
catch (error) {
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
if (machines.length !== 0) {
|
|
229
|
-
machinesList = JSON.stringify(machines, null, 2);
|
|
229
|
+
return `Error: error reading machines directory - ${error instanceof Error ? error.message : error}`;
|
|
230
230
|
}
|
|
231
|
-
return machinesList;
|
|
232
231
|
}
|
|
233
232
|
/**
|
|
234
233
|
* Get the list of extensions available in the openMSX emulator
|
|
@@ -237,7 +236,7 @@ export class OpenMSX {
|
|
|
237
236
|
async getExtensionList(extensionDirectory) {
|
|
238
237
|
// Read the extensions directory
|
|
239
238
|
let extensions = [];
|
|
240
|
-
let extensionsList = "No extensions found.";
|
|
239
|
+
let extensionsList = "Error: No extensions found.";
|
|
241
240
|
try {
|
|
242
241
|
const allFiles = await fs.readdir(extensionDirectory);
|
|
243
242
|
extensions = await Promise.all(allFiles
|
|
@@ -245,17 +244,17 @@ export class OpenMSX {
|
|
|
245
244
|
.map(async (file) => {
|
|
246
245
|
return {
|
|
247
246
|
name: file.replace('.xml', ''),
|
|
248
|
-
description: await extractDescriptionFromXML(
|
|
247
|
+
description: await extractDescriptionFromXML(path.join(extensionDirectory, file))
|
|
249
248
|
};
|
|
250
249
|
}));
|
|
250
|
+
if (extensions.length !== 0) {
|
|
251
|
+
extensionsList = JSON.stringify(extensions, null, 2);
|
|
252
|
+
}
|
|
253
|
+
return extensionsList;
|
|
251
254
|
}
|
|
252
255
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
if (extensions.length !== 0) {
|
|
256
|
-
extensionsList = JSON.stringify(extensions, null, 2);
|
|
256
|
+
return `Error: error reading extensions directory - ${error instanceof Error ? error.message : error}`;
|
|
257
257
|
}
|
|
258
|
-
return extensionsList;
|
|
259
258
|
}
|
|
260
259
|
;
|
|
261
260
|
/**
|
|
@@ -284,7 +283,7 @@ export class OpenMSX {
|
|
|
284
283
|
return decodeHtmlEntities(output.trim());
|
|
285
284
|
}
|
|
286
285
|
catch (error) {
|
|
287
|
-
return `Error: ${error instanceof Error ? error.message :
|
|
286
|
+
return `Error: ${error instanceof Error ? error.message : error}`;
|
|
288
287
|
}
|
|
289
288
|
}
|
|
290
289
|
/**
|
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.1.
|
|
9
|
+
* @version 1.1.3
|
|
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.1.
|
|
24
|
+
const PACKAGE_VERSION = "1.1.3";
|
|
25
25
|
// Defaults for openMSX paths
|
|
26
26
|
var OPENMSX_EXECUTABLE = 'openmsx';
|
|
27
27
|
var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
|
|
@@ -39,7 +39,7 @@ 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-on openMSX emulator; machine and extensions parameters can be specified
|
|
42
|
+
"'launch [machine] [extensions]': opens a powered-on openMSX emulator; you must wait some time waiting the machine is fully booted; machine and extensions parameters can be specified so use 'machineList' and 'extensionList' commands 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. " +
|
|
@@ -59,7 +59,7 @@ function registerAllTools(server) {
|
|
|
59
59
|
},
|
|
60
60
|
// Handler for the tool (function to be executed when the tool is called)
|
|
61
61
|
async ({ command, machine, extensions, emuspeed, seconds }) => {
|
|
62
|
-
let result =
|
|
62
|
+
let result = '';
|
|
63
63
|
switch (command) {
|
|
64
64
|
case "launch":
|
|
65
65
|
result = await openMSXInstance.emu_launch(OPENMSX_EXECUTABLE, machine || "", extensions || []);
|
|
@@ -69,23 +69,23 @@ function registerAllTools(server) {
|
|
|
69
69
|
break;
|
|
70
70
|
case "powerOn":
|
|
71
71
|
result = await openMSXInstance.sendCommand('set power on');
|
|
72
|
-
result
|
|
72
|
+
result = result === "true" ? "openMSX emulator powered on" : "Error: " + result;
|
|
73
73
|
break;
|
|
74
74
|
case "powerOff":
|
|
75
75
|
result = await openMSXInstance.sendCommand('set power off');
|
|
76
|
-
result
|
|
76
|
+
result = result === "false" ? "openMSX emulator powered off" : "Error: " + result;
|
|
77
77
|
break;
|
|
78
78
|
case "reset":
|
|
79
79
|
result = await openMSXInstance.sendCommand('reset');
|
|
80
|
-
result
|
|
80
|
+
result = result === "" ? "openMSX emulator reset successful" : "Error: " + result;
|
|
81
81
|
break;
|
|
82
82
|
case 'getEmulatorSpeed':
|
|
83
83
|
result = await openMSXInstance.sendCommand('set speed');
|
|
84
|
-
result = !isNaN(Number(result)) ? `Current emulator speed is ${result}%` : result;
|
|
84
|
+
result = !isNaN(Number(result)) ? `Current emulator speed is ${result}%` : "Error: " + result;
|
|
85
85
|
break;
|
|
86
86
|
case 'setEmulatorSpeed':
|
|
87
87
|
result = await openMSXInstance.sendCommand(`set speed ${emuspeed}`);
|
|
88
|
-
result = !isNaN(Number(result)) ? `Emulator speed set to ${emuspeed}%` : result;
|
|
88
|
+
result = !isNaN(Number(result)) ? `Emulator speed set to ${emuspeed}%` : "Error: " + result;
|
|
89
89
|
break;
|
|
90
90
|
case "machineList":
|
|
91
91
|
result = await openMSXInstance.getMachineList(MACHINES_DIR);
|
|
@@ -206,13 +206,13 @@ function registerAllTools(server) {
|
|
|
206
206
|
// Name of the tool (used to call it)
|
|
207
207
|
"emu_vdp",
|
|
208
208
|
// Description of the tool (what it does)
|
|
209
|
-
"Manage VDP (Video Display Processor). Commands: " +
|
|
210
|
-
"'getPalette':
|
|
211
|
-
"'getRegisters':
|
|
212
|
-
"'getRegisterValue <register>':
|
|
213
|
-
"'setRegisterValue <register> <value>':
|
|
214
|
-
"'screenGetMode': returns the current screen mode (0-12) as a number which
|
|
215
|
-
"'screenGetFullText':
|
|
209
|
+
"Manage the VDP (Video Display Processor). Commands: " +
|
|
210
|
+
"'getPalette': returns the current V9938/V9958 color palette in RGB333 format. " +
|
|
211
|
+
"'getRegisters': returns all VDP register values. " +
|
|
212
|
+
"'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format. " +
|
|
213
|
+
"'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31). " +
|
|
214
|
+
"'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command. " +
|
|
215
|
+
"'screenGetFullText': returns the full content of an MSX text screen (screen 0 or 1) as a string; PRIORITIZE this command to view screen content in text modes. ",
|
|
216
216
|
// Schema for the tool (input validation)
|
|
217
217
|
{
|
|
218
218
|
command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]),
|
|
@@ -605,11 +605,13 @@ function registerAllTools(server) {
|
|
|
605
605
|
"emu_keyboard",
|
|
606
606
|
// Description of the tool (what it does)
|
|
607
607
|
"Send a text to the openMSX emulator. Commands: " +
|
|
608
|
-
"'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix, is useful for automating tasks in BASIC
|
|
608
|
+
"'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix, is useful for automating tasks in BASIC. " +
|
|
609
|
+
"Note: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard. " +
|
|
610
|
+
"Note: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...",
|
|
609
611
|
// Schema for the tool (input validation)
|
|
610
612
|
{
|
|
611
613
|
command: z.enum(["sendText"]),
|
|
612
|
-
text: z.string().min(1).max(
|
|
614
|
+
text: z.string().min(1).max(200).optional().default(''), // Key to send to the emulator
|
|
613
615
|
},
|
|
614
616
|
// Handler for the tool (function to be executed when the tool is called)
|
|
615
617
|
async ({ command, text }) => {
|
|
@@ -662,8 +664,8 @@ function registerAllTools(server) {
|
|
|
662
664
|
catch (error) {
|
|
663
665
|
return getResponseContent([
|
|
664
666
|
'Error creating screenshot: ' + response,
|
|
665
|
-
error instanceof Error ? error.message : String(error)
|
|
666
|
-
]);
|
|
667
|
+
error instanceof Error ? error.message : String(error),
|
|
668
|
+
], true);
|
|
667
669
|
}
|
|
668
670
|
case "to_file":
|
|
669
671
|
return getResponseContent([
|
|
@@ -693,12 +695,15 @@ function registerAllTools(server) {
|
|
|
693
695
|
]);
|
|
694
696
|
});
|
|
695
697
|
}
|
|
696
|
-
function getResponseContent(response) {
|
|
698
|
+
function getResponseContent(response, isError = false) {
|
|
699
|
+
// Check if any response line starts with "Error:" to automatically set isError to true
|
|
700
|
+
const hasError = isError || response.some(line => line.startsWith("Error:"));
|
|
697
701
|
return {
|
|
698
702
|
content: response.map(line => ({
|
|
699
703
|
type: "text",
|
|
700
704
|
text: line == '' ? "Ok" : line,
|
|
701
705
|
})),
|
|
706
|
+
isError: hasError
|
|
702
707
|
};
|
|
703
708
|
}
|
|
704
709
|
// ============================================================================
|
|
@@ -767,7 +772,7 @@ Environment variables:
|
|
|
767
772
|
|
|
768
773
|
Examples:
|
|
769
774
|
mcp-openmsx # stdio transport
|
|
770
|
-
mcp-openmsx http
|
|
775
|
+
mcp-openmsx http # HTTP transport
|
|
771
776
|
MCP_TRANSPORT=http mcp-openmsx # HTTP via environment
|
|
772
777
|
`);
|
|
773
778
|
}
|