@nataliapc/mcp-openmsx 1.2.4 → 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/dist/utils.js CHANGED
@@ -236,6 +236,93 @@ export function encodeTypeText(text) {
236
236
  return char;
237
237
  });
238
238
  }
239
+ /**
240
+ * MSX keyboard matrix mapping for International (QWERTY) layout.
241
+ * Maps key names to [row, mask] coordinates in the MSX keyboard matrix.
242
+ *
243
+ * The MSX keyboard matrix is 11 rows × 8 bits. To press a key:
244
+ * - Use `keymatrixdown <row> <mask>` in openMSX TCL
245
+ * - Use `keymatrixup <row> <mask>` to release
246
+ *
247
+ * Example: CTRL is at row 6, bit 1 (mask 0x02)
248
+ * - Press: `keymatrixdown 6 0x02`
249
+ * - Release: `keymatrixup 6 0x02`
250
+ *
251
+ * Note: Rows 6-8 (modifier keys, special keys, navigation) are consistent
252
+ * across all MSX models. Other rows may vary by keyboard layout.
253
+ *
254
+ * Reference: mcp-server/resources/others/keyboard_matrices.md
255
+ */
256
+ export const MSX_KEY_MATRIX = {
257
+ // Row 6: Modifier and function keys
258
+ 'SHIFT': [6, 0x01], // bit 0
259
+ 'CTRL': [6, 0x02], // bit 1
260
+ 'GRAPH': [6, 0x04], // bit 2
261
+ 'CAPS': [6, 0x08], // bit 3
262
+ 'CODE': [6, 0x10], // bit 4
263
+ 'F1': [6, 0x20], // bit 5
264
+ 'F2': [6, 0x40], // bit 6
265
+ 'F3': [6, 0x80], // bit 7
266
+ // Row 7: Special keys
267
+ 'F4': [7, 0x01], // bit 0
268
+ 'F5': [7, 0x02], // bit 1
269
+ 'ESC': [7, 0x04], // bit 2
270
+ 'TAB': [7, 0x08], // bit 3
271
+ 'STOP': [7, 0x10], // bit 4
272
+ 'BS': [7, 0x20], // bit 5
273
+ 'SELECT': [7, 0x40], // bit 6
274
+ 'RETURN': [7, 0x80], // bit 7
275
+ 'ENTER': [7, 0x80], // bit 7 (alias for RETURN)
276
+ // Row 8: Navigation and editing keys
277
+ 'SPACE': [8, 0x01], // bit 0
278
+ 'HOME': [8, 0x02], // bit 1
279
+ 'INS': [8, 0x04], // bit 2
280
+ 'DEL': [8, 0x08], // bit 3
281
+ 'LEFT': [8, 0x10], // bit 4
282
+ 'UP': [8, 0x20], // bit 5
283
+ 'DOWN': [8, 0x40], // bit 6
284
+ 'RIGHT': [8, 0x80], // bit 7
285
+ };
286
+ /**
287
+ * Build a TCL command to press and release a combination of keys on the MSX keyboard.
288
+ *
289
+ * @param keys - Array of key names (e.g., ["CTRL", "STOP"])
290
+ * @param holdTimeMs - Time in milliseconds to hold keys down (default: 100)
291
+ * @returns TCL command string for openMSX
292
+ * @throws Error if any key name is not recognized
293
+ *
294
+ * @example
295
+ * // Press CTRL+STOP for 100ms
296
+ * buildKeyComboCommand(["CTRL", "STOP"], 100)
297
+ * // Returns: "keymatrixdown 6 0x02 ; keymatrixdown 7 0x10 ; after time 0.1 { keymatrixup 6 0x02 ; keymatrixup 7 0x10 }"
298
+ */
299
+ export function buildKeyComboCommand(keys, holdTimeMs = 100) {
300
+ if (!keys || keys.length === 0) {
301
+ throw new Error('No keys provided for key combination');
302
+ }
303
+ // Validate all keys exist and collect their matrix coordinates
304
+ const keyCoords = [];
305
+ for (const key of keys) {
306
+ const keyUpper = key.toUpperCase();
307
+ if (!MSX_KEY_MATRIX[keyUpper]) {
308
+ const validKeys = Object.keys(MSX_KEY_MATRIX).join(', ');
309
+ throw new Error(`Unknown key "${key}". Valid keys: ${validKeys}`);
310
+ }
311
+ keyCoords.push(MSX_KEY_MATRIX[keyUpper]);
312
+ }
313
+ // Build press commands: keymatrixdown <row> <mask> for each key
314
+ const pressCommands = keyCoords
315
+ .map(([row, mask]) => `keymatrixdown ${row} ${mask}`)
316
+ .join(' ; ');
317
+ // Build release commands: keymatrixup <row> <mask> for each key
318
+ const releaseCommands = keyCoords
319
+ .map(([row, mask]) => `keymatrixup ${row} ${mask}`)
320
+ .join(' ; ');
321
+ // Convert milliseconds to seconds for openMSX 'after time' command
322
+ const holdTimeSec = holdTimeMs / 1000;
323
+ // Build full TCL command: press all keys, wait, then release all keys
324
+ return `${pressCommands} ; after time ${holdTimeSec} { ${releaseCommands} }`;
325
+ }
239
326
  /**
240
327
  * Check if a response is an error response
241
328
  * @param response - The response string to check
@@ -261,6 +348,124 @@ export function getResponseContent(response, isError = false) {
261
348
  isError: hasError
262
349
  };
263
350
  }
351
+ /**
352
+ * Parse the output of the openMSX 'cpuregs' TCL command into a structured object.
353
+ * The output format is:
354
+ * AF =0044 BC =0000 DE =0000 HL =F380
355
+ * AF'=0000 BC'=0000 DE'=0000 HL'=0000
356
+ * IX =0000 IY =0000 PC =632F SP =F37E
357
+ * I =00 R =5D IM =01 IFF=01
358
+ * @param response - Raw text response from the cpuregs command
359
+ * @returns Record mapping register names to hex values
360
+ */
361
+ export function parseCpuRegs(response) {
362
+ const registers = {};
363
+ const regex = /(\w+'?)\s*=\s*([0-9a-fA-F]+)/g;
364
+ let match;
365
+ while ((match = regex.exec(response)) !== null) {
366
+ registers[match[1]] = match[2];
367
+ }
368
+ return registers;
369
+ }
370
+ /**
371
+ * Check if a CPU register name corresponds to a 16-bit register.
372
+ * @param register - Register name (e.g. 'pc', 'a', 'ix')
373
+ * @returns true if the register is 16-bit
374
+ */
375
+ export function is16bitRegister(register) {
376
+ return ["pc", "sp", "ix", "iy", "af", "bc", "de", "hl"].includes(register.toLowerCase());
377
+ }
378
+ /**
379
+ * Parse the output of the openMSX 'vdpregs' TCL command into a structured object.
380
+ * Output format:
381
+ * 0 : 0x04 8 : 0x08 16 : 0x00 24 : 0x00
382
+ * 1 : 0x70 9 : 0x02 17 : 0x18 25 : 0x00
383
+ * ...
384
+ * @param response - Raw text response from the vdpregs command
385
+ * @returns Record mapping register number (string) to hex value string
386
+ */
387
+ export function parseVdpRegs(response) {
388
+ const registers = {};
389
+ const regex = /(\d+)\s*:\s*(0x[0-9a-fA-F]{2})/g;
390
+ let match;
391
+ while ((match = regex.exec(response)) !== null) {
392
+ registers[match[1]] = match[2];
393
+ }
394
+ return registers;
395
+ }
396
+ /**
397
+ * Parse the output of the openMSX 'palette' TCL command into a structured object.
398
+ * Output format:
399
+ * 0:000 4:117 8:711 c:141
400
+ * 1:000 5:237 9:733 d:625
401
+ * ...
402
+ * @param response - Raw text response from the palette command
403
+ * @returns Array of 16 palette entries with index, r, g, b values
404
+ */
405
+ export function parsePalette(response) {
406
+ const palette = [];
407
+ const regex = /([0-9a-fA-F]):([0-7])([0-7])([0-7])/g;
408
+ let match;
409
+ while ((match = regex.exec(response)) !== null) {
410
+ palette.push({
411
+ index: parseInt(match[1], 16),
412
+ r: parseInt(match[2]),
413
+ g: parseInt(match[3]),
414
+ b: parseInt(match[4]),
415
+ rgb: match[2] + match[3] + match[4],
416
+ });
417
+ }
418
+ palette.sort((a, b) => a.index - b.index);
419
+ return palette;
420
+ }
421
+ /**
422
+ * Parse the output of the openMSX 'debug list_bp' TCL command into a structured array.
423
+ * Output format:
424
+ * bp#1 0x4000 {} {debug break}
425
+ * bp#2 0x8000 {} {debug break}
426
+ * @param response - Raw text response from the debug list_bp command
427
+ * @returns Array of breakpoint objects
428
+ */
429
+ export function parseBreakpoints(response) {
430
+ if (!response.trim())
431
+ return [];
432
+ const breakpoints = [];
433
+ const lines = response.trim().split('\n');
434
+ for (const line of lines) {
435
+ const match = line.match(/^(\S+)\s+(0x[0-9a-fA-F]{4})\s+\{([^}]*)\}\s+\{([^}]*)\}/);
436
+ if (match) {
437
+ breakpoints.push({
438
+ name: match[1],
439
+ address: match[2],
440
+ condition: match[3],
441
+ command: match[4],
442
+ });
443
+ }
444
+ }
445
+ return breakpoints;
446
+ }
447
+ /**
448
+ * Parse the output of the openMSX 'reverse status' TCL command into a structured object.
449
+ * Output format:
450
+ * status enabled begin 0.0 end 294.08 current 294.08 snapshots {...} last_event 0.0
451
+ * @param response - Raw text response from the reverse status command
452
+ * @returns Structured replay status object
453
+ */
454
+ export function parseReplayStatus(response) {
455
+ const statusMatch = response.match(/status\s+(\w+)/);
456
+ const beginMatch = response.match(/begin\s+([\d.]+)/);
457
+ const endMatch = response.match(/end\s+([\d.]+)/);
458
+ const currentMatch = response.match(/current\s+([\d.]+)/);
459
+ const snapshotsMatch = response.match(/snapshots\s+\{([^}]*)\}/);
460
+ const snapshotCount = snapshotsMatch ? snapshotsMatch[1].trim().split(/\s+/).filter(s => s).length : 0;
461
+ return {
462
+ enabled: statusMatch ? statusMatch[1] === 'enabled' : false,
463
+ begin: beginMatch ? parseFloat(beginMatch[1]) : 0,
464
+ end: endMatch ? parseFloat(endMatch[1]) : 0,
465
+ current: currentMatch ? parseFloat(currentMatch[1]) : 0,
466
+ snapshotCount,
467
+ };
468
+ }
264
469
  /*
265
470
  * Sleep for a specified number of milliseconds
266
471
  * @param ms - Number of milliseconds to sleep
@@ -269,3 +474,27 @@ export function getResponseContent(response, isError = false) {
269
474
  export function sleep(ms) {
270
475
  return new Promise(resolve => setTimeout(resolve, ms));
271
476
  }
477
+ /**
478
+ * Sleep for a specified number of milliseconds, with support for cancellation via AbortSignal.
479
+ * @param ms - Number of milliseconds to sleep
480
+ * @param signal - AbortSignal to cancel the sleep early
481
+ * @returns Promise that resolves after the specified time, or rejects if aborted
482
+ * @throws {DOMException} If the signal is aborted (with name 'AbortError')
483
+ */
484
+ export function sleepWithAbort(ms, signal) {
485
+ return new Promise((resolve, reject) => {
486
+ if (signal.aborted) {
487
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
488
+ return;
489
+ }
490
+ const timer = setTimeout(() => {
491
+ signal.removeEventListener('abort', onAbort);
492
+ resolve();
493
+ }, ms);
494
+ const onAbort = () => {
495
+ clearTimeout(timer);
496
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
497
+ };
498
+ signal.addEventListener('abort', onAbort, { once: true });
499
+ });
500
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nataliapc/mcp-openmsx",
3
- "version": "1.2.4",
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",
@@ -38,24 +38,25 @@
38
38
  "test": "echo \"Error: no test specified\" && exit 1"
39
39
  },
40
40
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.16.0",
41
+ "@modelcontextprotocol/sdk": "^1.26.0",
42
42
  "@themaximalist/embeddings.js": "^0.1.3",
43
- "@types/express": "^5.0.2",
43
+ "@types/express": "^5.0.6",
44
+ "@types/mime-types": "^3.0.1",
44
45
  "@xenova/transformers": "^2.17.2",
45
- "debug": "^4.4.1",
46
- "express": "^5.1.0",
46
+ "debug": "^4.4.3",
47
+ "express": "^5.2.1",
48
+ "mime-types": "^3.0.2",
47
49
  "sanitize-html": "^2.17.0",
48
- "tsx": "^4.7.1",
50
+ "tsx": "^4.21.0",
49
51
  "vectra": "^0.11.1",
50
- "zod": "^3.24.4"
52
+ "zod": "^3.25.76"
51
53
  },
52
54
  "devDependencies": {
53
- "@modelcontextprotocol/inspector": "^0.15.0",
54
- "@types/mime-types": "^3.0.1",
55
- "@types/node": "^24.0.15",
55
+ "@modelcontextprotocol/inspector": "^0.20.0",
56
+ "@types/node": "^25.2.3",
56
57
  "@types/sanitize-html": "^2.16.0",
57
58
  "shx": "^0.4.0",
58
- "typescript": "^5.8.3"
59
+ "typescript": "^5.9.3"
59
60
  },
60
61
  "files": [
61
62
  "dist/**/*",
@@ -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
  {