@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/README.md +40 -16
- package/dist/openmsx.js +10 -2
- package/dist/server.js +9 -3
- package/dist/server_elicitations.js +178 -0
- package/dist/server_prompts.js +1 -1
- package/dist/server_resources.js +15 -3
- package/dist/server_sampling.js +35 -0
- package/dist/server_tools.js +754 -88
- package/dist/utils.js +229 -0
- package/package.json +12 -11
- package/resources/programming/basic_wiki/_toc.json +1 -1
- package/resources/sdcc/lyx2md.py +745 -0
- package/resources/sdcc/sdccman.md +5557 -0
- /package/resources/programming/basic_wiki/{CLOAD?.md → CLOAD_Q.md} +0 -0
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.
|
|
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.
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
42
42
|
"@themaximalist/embeddings.js": "^0.1.3",
|
|
43
|
-
"@types/express": "^5.0.
|
|
43
|
+
"@types/express": "^5.0.6",
|
|
44
|
+
"@types/mime-types": "^3.0.1",
|
|
44
45
|
"@xenova/transformers": "^2.17.2",
|
|
45
|
-
"debug": "^4.4.
|
|
46
|
-
"express": "^5.1
|
|
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.
|
|
50
|
+
"tsx": "^4.21.0",
|
|
49
51
|
"vectra": "^0.11.1",
|
|
50
|
-
"zod": "^3.
|
|
52
|
+
"zod": "^3.25.76"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
|
-
"@modelcontextprotocol/inspector": "^0.
|
|
54
|
-
"@types/
|
|
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.
|
|
59
|
+
"typescript": "^5.9.3"
|
|
59
60
|
},
|
|
60
61
|
"files": [
|
|
61
62
|
"dist/**/*",
|