@nataliapc/mcp-openmsx 1.2.5 → 1.2.6

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
@@ -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`, `advanced_basic_listing`_.
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 | `openmsx` (Linux/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
- 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.
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
@@ -166,8 +166,13 @@ export class OpenMSX {
166
166
  this.sendCommand('exit');
167
167
  }
168
168
  catch (error) {
169
- // If writing fails, force kill
170
- this.process.kill('SIGTERM');
169
+ // If writing fails, force kill.
170
+ // Use no-argument kill() for cross-platform safety:
171
+ // on POSIX it sends SIGTERM; on Windows it calls TerminateProcess().
172
+ try {
173
+ this.process.kill();
174
+ }
175
+ catch (_) { /* ignore */ }
171
176
  }
172
177
  }
173
178
  else {
@@ -348,6 +353,9 @@ export class OpenMSX {
348
353
  forceClose() {
349
354
  if (this.process && !this.process.killed) {
350
355
  try {
356
+ // 'SIGKILL' is accepted on Windows too (maps to TerminateProcess).
357
+ // No-argument kill() is also acceptable here, but SIGKILL makes
358
+ // the intent explicit: we want unconditional termination.
351
359
  this.process.kill('SIGKILL');
352
360
  }
353
361
  catch (error) {
package/dist/server.js CHANGED
@@ -16,6 +16,7 @@ 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";
@@ -26,11 +27,14 @@ 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 resourcesDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../resources");
30
- const vectorDbDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../vector-db");
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: 'openmsx',
35
+ // On Windows, Node.js spawn() may not resolve 'openmsx' to 'openmsx.exe' unless it is in PATH.
36
+ // Using the platform-aware default reduces friction for Windows users who have openMSX in PATH.
37
+ OPENMSX_EXECUTABLE: process.platform === 'win32' ? 'openmsx.exe' : 'openmsx',
34
38
  OPENMSX_REPLAYS_DIR: '',
35
39
  OPENMSX_SCREENSHOT_DIR: '',
36
40
  OPENMSX_SCREENDUMP_DIR: '',
@@ -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', instruction));
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: [{
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Model context protocol server for openMSX automation and control",
5
5
  "main": "dist/server.js",
6
6
  "type": "module",
@@ -119,7 +119,7 @@
119
119
  },
120
120
  {
121
121
  "title": "CLOAD? - Verify Cassette Load",
122
- "uri": "msxdocs://basic_wiki/CLOAD?",
122
+ "uri": "msxdocs://basic_wiki/CLOAD_Q",
123
123
  "description": "Verifies a BASIC program on cassette tape against the program in memory."
124
124
  },
125
125
  {