@nataliapc/mcp-openmsx 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- **Note:** Environment variables are optional.
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
@@ -5,7 +5,7 @@
5
5
  * @license GPL2
6
6
  */
7
7
  import fs from "fs/promises";
8
- import { extractDescriptionFromXML, decodeHtmlEntities } from "./utils.js";
8
+ import { extractDescriptionFromXML, decodeHtmlEntities, encodeHtmlEntities } from "./utils.js";
9
9
  import { spawn } from 'child_process';
10
10
  import path from 'path';
11
11
  /**
@@ -144,7 +144,7 @@ export class OpenMSX {
144
144
  async emu_close() {
145
145
  return new Promise((resolve) => {
146
146
  if (!this.process) {
147
- resolve("No emulator process running");
147
+ resolve("Error: No emulator process running");
148
148
  return;
149
149
  }
150
150
  this.process.on('exit', () => {
@@ -153,7 +153,7 @@ export class OpenMSX {
153
153
  resolve("Ok: Emulator process closed successfully");
154
154
  });
155
155
  this.process.on('error', (error) => {
156
- resolve(`Error closing emulator: ${error.message}`);
156
+ resolve(`Error: error closing emulator: ${error.message}`);
157
157
  });
158
158
  // Try graceful shutdown first
159
159
  if (this.isConnected) {
@@ -184,7 +184,7 @@ export class OpenMSX {
184
184
  try {
185
185
  const response = await this.sendCommand('machine_info');
186
186
  if (response.startsWith('Error:')) {
187
- return JSON.stringify({ error: response });
187
+ return response;
188
188
  }
189
189
  // Parse machine_info output into key-value pairs
190
190
  const parameters = response.trim().split(' ');
@@ -199,9 +199,7 @@ export class OpenMSX {
199
199
  return JSON.stringify(machineInfo, null, 2);
200
200
  }
201
201
  catch (error) {
202
- return JSON.stringify({
203
- error: `Failed to get machine status: ${error instanceof Error ? error.message : 'Unknown error'}`
204
- });
202
+ return `Error: Failed to get machine status - ${error instanceof Error ? error.message : 'Unknown error'}`;
205
203
  }
206
204
  }
207
205
  /**
@@ -211,7 +209,7 @@ export class OpenMSX {
211
209
  async getMachineList(machinesDirectory) {
212
210
  // Read the machines directory
213
211
  let machines = [];
214
- let machinesList = "No machines found.";
212
+ let machinesList = "Error: No machines found.";
215
213
  try {
216
214
  const allFiles = await fs.readdir(machinesDirectory);
217
215
  machines = await Promise.all(allFiles
@@ -222,14 +220,14 @@ export class OpenMSX {
222
220
  description: await extractDescriptionFromXML(path.join(machinesDirectory, file))
223
221
  };
224
222
  }));
223
+ if (machines.length !== 0) {
224
+ machinesList = JSON.stringify(machines, null, 2);
225
+ }
226
+ return machinesList;
225
227
  }
226
228
  catch (error) {
227
- machinesList = 'Error reading machines directory: ' + error;
228
- }
229
- if (machines.length !== 0) {
230
- machinesList = JSON.stringify(machines, null, 2);
229
+ return `Error: error reading machines directory - ${error instanceof Error ? error.message : error}`;
231
230
  }
232
- return machinesList;
233
231
  }
234
232
  /**
235
233
  * Get the list of extensions available in the openMSX emulator
@@ -238,7 +236,7 @@ export class OpenMSX {
238
236
  async getExtensionList(extensionDirectory) {
239
237
  // Read the extensions directory
240
238
  let extensions = [];
241
- let extensionsList = "No extensions found.";
239
+ let extensionsList = "Error: No extensions found.";
242
240
  try {
243
241
  const allFiles = await fs.readdir(extensionDirectory);
244
242
  extensions = await Promise.all(allFiles
@@ -249,14 +247,14 @@ export class OpenMSX {
249
247
  description: await extractDescriptionFromXML(path.join(extensionDirectory, file))
250
248
  };
251
249
  }));
250
+ if (extensions.length !== 0) {
251
+ extensionsList = JSON.stringify(extensions, null, 2);
252
+ }
253
+ return extensionsList;
252
254
  }
253
255
  catch (error) {
254
- extensionsList = 'Error reading extensions directory: ' + error;
255
- }
256
- if (extensions.length !== 0) {
257
- extensionsList = JSON.stringify(extensions, null, 2);
256
+ return `Error: error reading extensions directory - ${error instanceof Error ? error.message : error}`;
258
257
  }
259
- return extensionsList;
260
258
  }
261
259
  ;
262
260
  /**
@@ -267,7 +265,7 @@ export class OpenMSX {
267
265
  async sendCommand(command) {
268
266
  try {
269
267
  // Send command
270
- this.writeData(`<command>${command}</command>\n`);
268
+ this.writeData(`<command>${encodeHtmlEntities(command)}</command>\n`);
271
269
  // Read response using readData()
272
270
  const output = (await this.readData()).trim();
273
271
  // Look for reply tags in the output
@@ -285,7 +283,7 @@ export class OpenMSX {
285
283
  return decodeHtmlEntities(output.trim());
286
284
  }
287
285
  catch (error) {
288
- return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
286
+ return `Error: ${error instanceof Error ? error.message : error}`;
289
287
  }
290
288
  }
291
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.2
9
+ * @version 1.1.4
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.2";
24
+ const PACKAGE_VERSION = "1.1.4";
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; use 'machineList' and 'extensionList' tools to obtain valid values. " +
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 = "Error";
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 += result === "Ok" ? ": openMSX emulator powered on" : "";
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 += result === "Ok" ? ": openMSX emulator powered off" : "";
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 += result === "Ok" ? ": openMSX emulator reset" : "";
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': return the current V9938/V9958 colors palette in format RGB333. " +
211
- "'getRegisters': return all the VDP register values. " +
212
- "'getRegisterValue <register>': return the value of a specific VDP register (0-31) in decimal format. " +
213
- "'setRegisterValue <register> <value>': set a hexadecimal value to a specific VDP register (0-31). " +
214
- "'screenGetMode': returns the current screen mode (0-12) as a number which would also be used for the basic command SCREEN. " +
215
- "'screenGetFullText': return the full content of an MSX text screen (screen 0 or 1) as a string, useful for debugging. ",
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, use \\r for Return key. ",
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(100).optional().default(''), // Key to send to the emulator
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 # HTTP transport
775
+ mcp-openmsx http # HTTP transport
771
776
  MCP_TRANSPORT=http mcp-openmsx # HTTP via environment
772
777
  `);
773
778
  }
package/dist/utils.js CHANGED
@@ -32,16 +32,16 @@ export function decodeHtmlEntities(text) {
32
32
  '&gt;': '>',
33
33
  '&amp;': '&',
34
34
  '&quot;': '"',
35
+ '&nbsp;': ' ',
35
36
  '&#x27;': "'",
36
37
  '&#x2F;': '/',
37
38
  '&#x60;': '`',
38
39
  '&#x3D;': '=',
40
+ '&apos;': "'",
39
41
  '&#39;': "'",
40
42
  '&#47;': '/',
41
43
  '&#96;': '`',
42
44
  '&#61;': '=',
43
- '&apos;': "'",
44
- '&nbsp;': ' ',
45
45
  '&#x0a;': '\n',
46
46
  '&#x0A;': '\n',
47
47
  '&#10;': '\n',
@@ -52,3 +52,29 @@ export function decodeHtmlEntities(text) {
52
52
  return htmlEntities[entity] || entity;
53
53
  });
54
54
  }
55
+ /**
56
+ * Encode a plain text to HTML entities, also escaping characters with ASCII >= 127
57
+ * @param text - String to encode
58
+ * @returns string - String with HTML entities encoded
59
+ */
60
+ export function encodeHtmlEntities(text) {
61
+ const htmlEntities = {
62
+ '<': '&lt;',
63
+ '>': '&gt;',
64
+ '&': '&amp;',
65
+ '"': '&quot;',
66
+ "'": '&apos;',
67
+ '=': '&#61;',
68
+ '/': '&#47;',
69
+ };
70
+ return text.replace(/[\u00A0-\uFFFF<>&"'`=\/]/g, (char) => {
71
+ if (htmlEntities[char]) {
72
+ return htmlEntities[char];
73
+ }
74
+ const code = char.charCodeAt(0);
75
+ if (code >= 127) {
76
+ return `&#${code};`;
77
+ }
78
+ return char;
79
+ });
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",