@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/server_tools.js
CHANGED
|
@@ -3,12 +3,14 @@ import fs from "fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { openMSXInstance } from "./openmsx.js";
|
|
5
5
|
import { VectorDB } from "./vectordb.js";
|
|
6
|
-
import { encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
|
|
6
|
+
import { encodeTypeText, buildKeyComboCommand, isErrorResponse, getResponseContent, parseCpuRegs, is16bitRegister, parseVdpRegs, parsePalette, parseBreakpoints, parseReplayStatus, sleepWithAbort } from "./utils.js";
|
|
7
7
|
import { getRegisteredResourcesList } from "./server_resources.js";
|
|
8
|
+
import { resolveLaunchParams } from "./server_elicitations.js";
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Tools available in the MCP server
|
|
10
11
|
// https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
|
|
11
12
|
export async function registerTools(server, emuDirectories) {
|
|
13
|
+
// emu_control
|
|
12
14
|
server.registerTool(
|
|
13
15
|
// Name of the tool (used to call it)
|
|
14
16
|
"emu_control", {
|
|
@@ -20,7 +22,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
20
22
|
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed",
|
|
21
23
|
"machineList", "extensionList", "wait"])
|
|
22
24
|
.describe(`Available commands:
|
|
23
|
-
'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. " +
|
|
25
|
+
'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, or let them ambiguous and use elicitation. " +
|
|
24
26
|
'close': closes the openMSX emulator.
|
|
25
27
|
'powerOn': powers on the openMSX emulator.
|
|
26
28
|
'powerOff': powers off the openMSX emulator.
|
|
@@ -53,14 +55,45 @@ export async function registerTools(server, emuDirectories) {
|
|
|
53
55
|
.default(3)
|
|
54
56
|
.describe("Number of seconds to wait; default is 3. Used by [wait]."),
|
|
55
57
|
},
|
|
58
|
+
outputSchema: {
|
|
59
|
+
command: z.string().describe("The command that was executed."),
|
|
60
|
+
speed: z.number().optional()
|
|
61
|
+
.describe("Emulator speed percentage. Present for 'getEmulatorSpeed' and 'setEmulatorSpeed'."),
|
|
62
|
+
machines: z.array(z.object({
|
|
63
|
+
name: z.string().describe("Machine name."),
|
|
64
|
+
description: z.string().describe("Machine description."),
|
|
65
|
+
})).optional()
|
|
66
|
+
.describe("List of available MSX machines. Present for 'machineList'."),
|
|
67
|
+
extensions: z.array(z.object({
|
|
68
|
+
name: z.string().describe("Extension name."),
|
|
69
|
+
description: z.string().describe("Extension description."),
|
|
70
|
+
})).optional()
|
|
71
|
+
.describe("List of available MSX extensions. Present for 'extensionList'."),
|
|
72
|
+
result: z.string().optional()
|
|
73
|
+
.describe("Generic result or status message."),
|
|
74
|
+
},
|
|
75
|
+
annotations: {
|
|
76
|
+
"readOnlyHint": true,
|
|
77
|
+
"destructiveHint": false,
|
|
78
|
+
"idempotentHint": false,
|
|
79
|
+
"openWorldHint": false,
|
|
80
|
+
},
|
|
56
81
|
},
|
|
57
82
|
// Handler for the tool (function to be executed when the tool is called)
|
|
58
|
-
async ({ command, machine, extensions, emuspeed, seconds }) => {
|
|
83
|
+
async ({ command, machine, extensions, emuspeed, seconds }, extra) => {
|
|
59
84
|
let result = '';
|
|
60
85
|
switch (command) {
|
|
61
|
-
case "launch":
|
|
62
|
-
|
|
86
|
+
case "launch": {
|
|
87
|
+
const resolved = await resolveLaunchParams(server, emuDirectories, machine, extensions);
|
|
88
|
+
if (resolved.cancelled) {
|
|
89
|
+
return { content: [{ type: "text", text: "Launch cancelled by user." }], isError: true };
|
|
90
|
+
}
|
|
91
|
+
if (resolved.error) {
|
|
92
|
+
return { content: [{ type: "text", text: resolved.error }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
result = await openMSXInstance.emu_launch(emuDirectories.OPENMSX_EXECUTABLE, resolved.machine, resolved.extensions);
|
|
63
95
|
break;
|
|
96
|
+
}
|
|
64
97
|
case "close":
|
|
65
98
|
result = await openMSXInstance.emu_close();
|
|
66
99
|
break;
|
|
@@ -90,19 +123,78 @@ export async function registerTools(server, emuDirectories) {
|
|
|
90
123
|
case "extensionList":
|
|
91
124
|
result = await openMSXInstance.getExtensionList(emuDirectories.EXTENSIONS_DIR);
|
|
92
125
|
break;
|
|
93
|
-
case "wait":
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
case "wait": {
|
|
127
|
+
const total = seconds;
|
|
128
|
+
const progressToken = extra._meta?.progressToken;
|
|
129
|
+
let elapsed = 0;
|
|
130
|
+
try {
|
|
131
|
+
for (let i = 1; i <= total; i++) {
|
|
132
|
+
await sleepWithAbort(1000, extra.signal);
|
|
133
|
+
elapsed = i;
|
|
134
|
+
if (progressToken !== undefined) {
|
|
135
|
+
await extra.sendNotification({
|
|
136
|
+
method: "notifications/progress",
|
|
137
|
+
params: { progressToken, progress: i, total, message: `Waited ${i} of ${total} seconds` },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
result = `Waited for ${total} seconds.`;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
result = `Wait cancelled after ${elapsed} of ${total} seconds.`;
|
|
145
|
+
return { content: [{ type: "text", text: result }], isError: true };
|
|
146
|
+
}
|
|
96
147
|
break;
|
|
148
|
+
}
|
|
97
149
|
default:
|
|
98
150
|
result = `Error: Unknown command "${command}".`;
|
|
99
151
|
break;
|
|
100
152
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
if (isErrorResponse(result)) {
|
|
154
|
+
return { content: [{ type: "text", text: result }], isError: true };
|
|
155
|
+
}
|
|
156
|
+
let structuredContent;
|
|
157
|
+
switch (command) {
|
|
158
|
+
case 'getEmulatorSpeed': {
|
|
159
|
+
const match = result.match(/(\d+)%/);
|
|
160
|
+
structuredContent = { command, speed: match ? parseInt(match[1]) : undefined, result };
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case 'setEmulatorSpeed': {
|
|
164
|
+
structuredContent = { command, speed: emuspeed, result };
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'machineList': {
|
|
168
|
+
try {
|
|
169
|
+
const machines = JSON.parse(result);
|
|
170
|
+
structuredContent = { command, machines };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
structuredContent = { command, result };
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case 'extensionList': {
|
|
178
|
+
try {
|
|
179
|
+
const extensions = JSON.parse(result);
|
|
180
|
+
structuredContent = { command, extensions };
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
structuredContent = { command, result };
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default: {
|
|
188
|
+
structuredContent = { command, result };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text", text: result }],
|
|
193
|
+
structuredContent,
|
|
194
|
+
isError: false,
|
|
195
|
+
};
|
|
105
196
|
});
|
|
197
|
+
// emu_info
|
|
106
198
|
server.registerTool(
|
|
107
199
|
// Name of the tool (used to call it)
|
|
108
200
|
"emu_media", {
|
|
@@ -142,6 +234,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
142
234
|
.optional()
|
|
143
235
|
.describe("Absolute Disk folder filename to insert. Used by [diskInsertFolder]"),
|
|
144
236
|
},
|
|
237
|
+
annotations: {
|
|
238
|
+
"readOnlyHint": true,
|
|
239
|
+
"destructiveHint": false,
|
|
240
|
+
"idempotentHint": false,
|
|
241
|
+
"openWorldHint": false,
|
|
242
|
+
},
|
|
145
243
|
},
|
|
146
244
|
// Handler for the tool (function to be executed when the tool is called)
|
|
147
245
|
async ({ command, tapefile, romfile, diskfile, diskfolder }) => {
|
|
@@ -182,6 +280,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
182
280
|
response
|
|
183
281
|
]);
|
|
184
282
|
});
|
|
283
|
+
// emu_info
|
|
185
284
|
server.registerTool(
|
|
186
285
|
// Name of the tool (used to call it)
|
|
187
286
|
"emu_info", {
|
|
@@ -196,31 +295,60 @@ export async function registerTools(server, emuDirectories) {
|
|
|
196
295
|
'getIOPortsMap': shows an overview about the I/O mapped devices.
|
|
197
296
|
`),
|
|
198
297
|
},
|
|
298
|
+
outputSchema: {
|
|
299
|
+
command: z.string().describe("The command that was executed."),
|
|
300
|
+
status: z.record(z.string()).optional()
|
|
301
|
+
.describe("Machine status key-value pairs (type, manufacturer, year, etc.). Present for 'getStatus'."),
|
|
302
|
+
result: z.string().optional()
|
|
303
|
+
.describe("Generic result text. Present for 'getSlotsMap' and 'getIOPortsMap'."),
|
|
304
|
+
},
|
|
305
|
+
annotations: {
|
|
306
|
+
"readOnlyHint": true,
|
|
307
|
+
"destructiveHint": false,
|
|
308
|
+
"idempotentHint": false,
|
|
309
|
+
"openWorldHint": false,
|
|
310
|
+
},
|
|
199
311
|
},
|
|
200
312
|
// Handler for the tool (function to be executed when the tool is called)
|
|
201
313
|
async ({ command }) => {
|
|
202
|
-
let
|
|
314
|
+
let response;
|
|
203
315
|
switch (command) {
|
|
204
316
|
case "getStatus":
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
]);
|
|
317
|
+
response = await openMSXInstance.emu_status();
|
|
318
|
+
break;
|
|
208
319
|
case "getSlotsMap":
|
|
209
|
-
|
|
320
|
+
response = await openMSXInstance.sendCommand("slotmap");
|
|
210
321
|
break;
|
|
211
322
|
case "getIOPortsMap":
|
|
212
|
-
|
|
323
|
+
response = await openMSXInstance.sendCommand("iomap");
|
|
213
324
|
break;
|
|
214
325
|
default:
|
|
215
|
-
return
|
|
216
|
-
`Error: Unknown emulator info command "${command}".`
|
|
217
|
-
]);
|
|
326
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator info command "${command}".` }], isError: true };
|
|
218
327
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
328
|
+
if (isErrorResponse(response)) {
|
|
329
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
330
|
+
}
|
|
331
|
+
let structuredContent;
|
|
332
|
+
if (command === "getStatus") {
|
|
333
|
+
try {
|
|
334
|
+
const status = JSON.parse(response);
|
|
335
|
+
structuredContent = { command, status };
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
structuredContent = { command, result: response };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
//TODO: parse the slotmap and iomap responses into structured content
|
|
343
|
+
structuredContent = { command, result: response };
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: response }],
|
|
347
|
+
structuredContent,
|
|
348
|
+
isError: false,
|
|
349
|
+
};
|
|
223
350
|
});
|
|
351
|
+
// emu_vdp
|
|
224
352
|
server.registerTool(
|
|
225
353
|
// Name of the tool (used to call it)
|
|
226
354
|
"emu_vdp", {
|
|
@@ -249,6 +377,37 @@ export async function registerTools(server, emuDirectories) {
|
|
|
249
377
|
.optional()
|
|
250
378
|
.describe("2 hexadecimal digits for a VDP register value (e.g. 0x1f). Used by [setRegisterValue]"),
|
|
251
379
|
},
|
|
380
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
381
|
+
outputSchema: {
|
|
382
|
+
command: z.string()
|
|
383
|
+
.describe("The executed command name."),
|
|
384
|
+
registers: z.record(z.string()).optional()
|
|
385
|
+
.describe("VDP register values as hex strings keyed by register number (0-31). Present for 'getRegisters'."),
|
|
386
|
+
register: z.number().optional()
|
|
387
|
+
.describe("VDP register number queried/modified. Present for 'getRegisterValue' and 'setRegisterValue'."),
|
|
388
|
+
decimalValue: z.number().optional()
|
|
389
|
+
.describe("Register value in decimal. Present for 'getRegisterValue'."),
|
|
390
|
+
hexValue: z.string().optional()
|
|
391
|
+
.describe("Register value in hexadecimal (e.g. '0x1F'). Present for 'getRegisterValue'."),
|
|
392
|
+
newValue: z.string().optional()
|
|
393
|
+
.describe("Value written to the register. Present for 'setRegisterValue'."),
|
|
394
|
+
palette: z.array(z.object({
|
|
395
|
+
index: z.number(), r: z.number(), g: z.number(), b: z.number(), rgb: z.string()
|
|
396
|
+
})).optional()
|
|
397
|
+
.describe("Color palette as array of 16 RGB333 entries. Present for 'getPalette'."),
|
|
398
|
+
screenMode: z.string().optional()
|
|
399
|
+
.describe("Current screen mode name (e.g. 'TEXT80', 'GRAPHIC2'). Present for 'screenGetMode'."),
|
|
400
|
+
screenText: z.string().optional()
|
|
401
|
+
.describe("Full text content of the MSX screen. Present for 'screenGetFullText'."),
|
|
402
|
+
result: z.string().optional()
|
|
403
|
+
.describe("Generic result or status message."),
|
|
404
|
+
},
|
|
405
|
+
annotations: {
|
|
406
|
+
"readOnlyHint": true,
|
|
407
|
+
"destructiveHint": false,
|
|
408
|
+
"idempotentHint": false,
|
|
409
|
+
"openWorldHint": false,
|
|
410
|
+
},
|
|
252
411
|
},
|
|
253
412
|
// Handler for the tool (function to be executed when the tool is called)
|
|
254
413
|
async ({ command, register, value }) => {
|
|
@@ -269,21 +428,60 @@ export async function registerTools(server, emuDirectories) {
|
|
|
269
428
|
case "screenGetMode":
|
|
270
429
|
tclCommand = "get_screen_mode";
|
|
271
430
|
break;
|
|
272
|
-
case "screenGetFullText":
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
431
|
+
case "screenGetFullText": {
|
|
432
|
+
const textResp = await openMSXInstance.sendCommand('get_screen');
|
|
433
|
+
if (isErrorResponse(textResp)) {
|
|
434
|
+
return { content: [{ type: "text", text: textResp }], isError: true };
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
content: [{ type: "text", text: `The screen text is:\n${textResp}` }],
|
|
438
|
+
structuredContent: { command, screenText: textResp },
|
|
439
|
+
isError: false,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
277
442
|
default:
|
|
278
|
-
return
|
|
279
|
-
`Error: Unknown emulator vdp command "${command}".`
|
|
280
|
-
]);
|
|
443
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator vdp command "${command}".` }], isError: true };
|
|
281
444
|
}
|
|
282
445
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
283
|
-
|
|
284
|
-
response
|
|
285
|
-
|
|
446
|
+
if (isErrorResponse(response)) {
|
|
447
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
448
|
+
}
|
|
449
|
+
let structuredContent;
|
|
450
|
+
switch (command) {
|
|
451
|
+
case "getPalette": {
|
|
452
|
+
const pal = parsePalette(response);
|
|
453
|
+
structuredContent = { command, palette: pal };
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case "getRegisters": {
|
|
457
|
+
const regs = parseVdpRegs(response);
|
|
458
|
+
structuredContent = { command, registers: regs };
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "getRegisterValue": {
|
|
462
|
+
const dec = parseInt(response.trim(), 10);
|
|
463
|
+
const hex = `0x${dec.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
464
|
+
structuredContent = { command, register, decimalValue: dec, hexValue: hex };
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "setRegisterValue": {
|
|
468
|
+
structuredContent = { command, register, newValue: value, result: response || "Ok" };
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "screenGetMode": {
|
|
472
|
+
structuredContent = { command, screenMode: response.trim() };
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
default:
|
|
476
|
+
structuredContent = { command, result: response };
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
480
|
+
structuredContent,
|
|
481
|
+
isError: false,
|
|
482
|
+
};
|
|
286
483
|
});
|
|
484
|
+
// debug_run
|
|
287
485
|
server.registerTool(
|
|
288
486
|
// Name of the tool (used to call it)
|
|
289
487
|
"debug_run", {
|
|
@@ -310,6 +508,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
310
508
|
.optional()
|
|
311
509
|
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [runTo]"),
|
|
312
510
|
},
|
|
511
|
+
annotations: {
|
|
512
|
+
"readOnlyHint": true,
|
|
513
|
+
"destructiveHint": false,
|
|
514
|
+
"idempotentHint": false,
|
|
515
|
+
"openWorldHint": false,
|
|
516
|
+
},
|
|
313
517
|
},
|
|
314
518
|
// Handler for the tool (function to be executed when the tool is called)
|
|
315
519
|
async ({ command, address }) => {
|
|
@@ -345,10 +549,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
345
549
|
]);
|
|
346
550
|
}
|
|
347
551
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
552
|
+
//TODO: parse disassembly command response into structured content
|
|
348
553
|
return getResponseContent([
|
|
349
554
|
response
|
|
350
555
|
]);
|
|
351
556
|
});
|
|
557
|
+
// debug_cpu
|
|
352
558
|
server.registerTool(
|
|
353
559
|
// Name of the tool (used to call it)
|
|
354
560
|
"debug_cpu", {
|
|
@@ -369,6 +575,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
369
575
|
"**Important Note**: Addresses and values are in hexadecimal format (e.g. 0xd2 0x3af2)."
|
|
370
576
|
`),
|
|
371
577
|
register: z.enum(["pc", "sp", "ix", "iy", "af", "bc", "de", "hl", "ixh", "ixl", "iyh", "iyl",
|
|
578
|
+
"af'", "bc'", "de'", "hl'",
|
|
372
579
|
"a", "f", "b", "c", "d", "e", "h", "l", "i", "r", "im", "iff"])
|
|
373
580
|
.optional()
|
|
374
581
|
.describe("CPU register to read/write. Used by [getRegister, setRegister]"),
|
|
@@ -386,6 +593,35 @@ export async function registerTools(server, emuDirectories) {
|
|
|
386
593
|
.optional()
|
|
387
594
|
.describe("Number of bytes to disassemble. Used by [disassemble]"),
|
|
388
595
|
},
|
|
596
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
597
|
+
outputSchema: {
|
|
598
|
+
command: z.string()
|
|
599
|
+
.describe("The executed command name."),
|
|
600
|
+
registers: z.record(z.string()).optional()
|
|
601
|
+
.describe("All CPU register values as hex strings, keyed by register name (AF, BC, DE, HL, AF', BC', DE', HL', IX, IY, PC, SP, I, R, IM, IFF). Present for 'getCpuRegisters'."),
|
|
602
|
+
register: z.string().optional()
|
|
603
|
+
.describe("CPU register name queried/modified. Present for 'getRegister' and 'setRegister'."),
|
|
604
|
+
decimalValue: z.number().optional()
|
|
605
|
+
.describe("Register value in decimal. Present for 'getRegister'."),
|
|
606
|
+
hexValue: z.string().optional()
|
|
607
|
+
.describe("Register value in hexadecimal (e.g. '0x1A3F'). Present for 'getRegister'."),
|
|
608
|
+
newValue: z.string().optional()
|
|
609
|
+
.describe("Value written to the register. Present for 'setRegister'."),
|
|
610
|
+
activeCpu: z.string().optional()
|
|
611
|
+
.describe("Active CPU type: 'z80' or 'r800'. Present for 'getActiveCpu'."),
|
|
612
|
+
stack: z.string().optional()
|
|
613
|
+
.describe("Stack pile dump content. Present for 'getStackPile'."),
|
|
614
|
+
disassembly: z.string().optional()
|
|
615
|
+
.describe("Disassembled code listing. Present for 'disassemble'."),
|
|
616
|
+
result: z.string().optional()
|
|
617
|
+
.describe("Generic result or status message."),
|
|
618
|
+
},
|
|
619
|
+
annotations: {
|
|
620
|
+
"readOnlyHint": true,
|
|
621
|
+
"destructiveHint": false,
|
|
622
|
+
"idempotentHint": false,
|
|
623
|
+
"openWorldHint": false,
|
|
624
|
+
},
|
|
389
625
|
},
|
|
390
626
|
// Handler for the tool (function to be executed when the tool is called)
|
|
391
627
|
async ({ command, address, register, value, size }) => {
|
|
@@ -410,15 +646,62 @@ export async function registerTools(server, emuDirectories) {
|
|
|
410
646
|
tclCommand = "get_active_cpu";
|
|
411
647
|
break;
|
|
412
648
|
default:
|
|
413
|
-
return
|
|
414
|
-
`Error: Unknown
|
|
415
|
-
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: "text", text: `Error: Unknown command "${command}".` }],
|
|
651
|
+
isError: true,
|
|
652
|
+
};
|
|
416
653
|
}
|
|
417
654
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
655
|
+
// On error, return unstructured content only (SDK skips outputSchema validation on errors)
|
|
656
|
+
if (isErrorResponse(response)) {
|
|
657
|
+
return {
|
|
658
|
+
content: [{ type: "text", text: response }],
|
|
659
|
+
isError: true,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
// Build structuredContent based on the command
|
|
663
|
+
let structuredContent;
|
|
664
|
+
switch (command) {
|
|
665
|
+
case "getCpuRegisters": {
|
|
666
|
+
const regs = parseCpuRegs(response);
|
|
667
|
+
structuredContent = { command, registers: regs };
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "getRegister": {
|
|
671
|
+
const decValue = parseInt(response.trim(), 10);
|
|
672
|
+
const padLen = is16bitRegister(register) ? 4 : 2;
|
|
673
|
+
const hexVal = `0x${decValue.toString(16).toUpperCase().padStart(padLen, '0')}`;
|
|
674
|
+
structuredContent = { command, register, decimalValue: decValue, hexValue: hexVal };
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case "setRegister": {
|
|
678
|
+
structuredContent = { command, register, newValue: value, result: response || "Ok" };
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case "getStackPile": {
|
|
682
|
+
//TODO: parse the stack pile response into structured content (e.g. an array of stack entries with address and value)
|
|
683
|
+
structuredContent = { command, stack: response };
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "disassemble": {
|
|
687
|
+
//TODO: parse the disassembly response into a structured format (e.g. an array of instructions with address, opcode, and assembly)
|
|
688
|
+
structuredContent = { command, disassembly: response };
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case "getActiveCpu": {
|
|
692
|
+
structuredContent = { command, activeCpu: response.trim() };
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
default:
|
|
696
|
+
structuredContent = { command, result: response };
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
700
|
+
structuredContent,
|
|
701
|
+
isError: false,
|
|
702
|
+
};
|
|
421
703
|
});
|
|
704
|
+
// debug_memory
|
|
422
705
|
server.registerTool(
|
|
423
706
|
// Name of the tool (used to call it)
|
|
424
707
|
"debug_memory", {
|
|
@@ -427,7 +710,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
427
710
|
description: "Slots info, and Read/write from/to memory in the openMSX emulator.",
|
|
428
711
|
// Schema for the tool (input validation)
|
|
429
712
|
inputSchema: {
|
|
430
|
-
command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"])
|
|
713
|
+
command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord", "searchBytes"])
|
|
431
714
|
.describe(`Available commands:
|
|
432
715
|
'selectedSlots': to get a list of the currently selected memory slots.
|
|
433
716
|
'getBlock <address> [lines]': to read a block of memory from the specified address.
|
|
@@ -435,6 +718,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
435
718
|
'readWord <address>': to read a WORD from the specified address.
|
|
436
719
|
'writeByte <address> <value8>': to write a BYTE to the specified address.
|
|
437
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.
|
|
438
722
|
**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
|
|
439
723
|
`),
|
|
440
724
|
address: z.string()
|
|
@@ -455,10 +739,46 @@ export async function registerTools(server, emuDirectories) {
|
|
|
455
739
|
.regex(/^0x[0-9a-fA-F]{4}$/, 'Must be a 4 digits hexadecimal number')
|
|
456
740
|
.optional()
|
|
457
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]"),
|
|
751
|
+
},
|
|
752
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
753
|
+
outputSchema: {
|
|
754
|
+
command: z.string()
|
|
755
|
+
.describe("The executed command name."),
|
|
756
|
+
address: z.string().optional()
|
|
757
|
+
.describe("Memory address queried/modified."),
|
|
758
|
+
decimalValue: z.number().optional()
|
|
759
|
+
.describe("Memory value in decimal. Present for 'readByte' and 'readWord'."),
|
|
760
|
+
hexValue: z.string().optional()
|
|
761
|
+
.describe("Memory value in hexadecimal. Present for 'readByte' and 'readWord'."),
|
|
762
|
+
hexDump: z.string().optional()
|
|
763
|
+
.describe("Hex dump block of memory. Present for 'getBlock'."),
|
|
764
|
+
slots: z.string().optional()
|
|
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'."),
|
|
770
|
+
result: z.string().optional()
|
|
771
|
+
.describe("Generic result or status message."),
|
|
772
|
+
},
|
|
773
|
+
annotations: {
|
|
774
|
+
"readOnlyHint": true,
|
|
775
|
+
"destructiveHint": false,
|
|
776
|
+
"idempotentHint": false,
|
|
777
|
+
"openWorldHint": false,
|
|
458
778
|
},
|
|
459
779
|
},
|
|
460
780
|
// Handler for the tool (function to be executed when the tool is called)
|
|
461
|
-
async ({ command, address, lines, value8, value16 }) => {
|
|
781
|
+
async ({ command, address, lines, value8, value16, length, values }) => {
|
|
462
782
|
let tclCommand;
|
|
463
783
|
switch (command) {
|
|
464
784
|
case "selectedSlots":
|
|
@@ -479,16 +799,73 @@ export async function registerTools(server, emuDirectories) {
|
|
|
479
799
|
case "writeWord":
|
|
480
800
|
tclCommand = `poke16 ${address} ${value16}`;
|
|
481
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;
|
|
482
819
|
default:
|
|
483
|
-
return
|
|
484
|
-
`Error: Unknown memory command "${command}".`
|
|
485
|
-
]);
|
|
820
|
+
return { content: [{ type: "text", text: `Error: Unknown memory command "${command}".` }], isError: true };
|
|
486
821
|
}
|
|
487
822
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
488
|
-
|
|
489
|
-
response
|
|
490
|
-
|
|
823
|
+
if (isErrorResponse(response)) {
|
|
824
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
825
|
+
}
|
|
826
|
+
let structuredContent;
|
|
827
|
+
switch (command) {
|
|
828
|
+
case "selectedSlots": {
|
|
829
|
+
//TODO: parse the slotselect response into structured content (e.g. an array of selected slots with slot number and content info)
|
|
830
|
+
structuredContent = { command, slots: response };
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case "getBlock": {
|
|
834
|
+
structuredContent = { command, address, hexDump: response };
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case "readByte": {
|
|
838
|
+
const dec = parseInt(response.trim(), 10);
|
|
839
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(2, '0')}` };
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case "readWord": {
|
|
843
|
+
const dec = parseInt(response.trim(), 10);
|
|
844
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(4, '0')}` };
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
case "writeByte": {
|
|
848
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case "writeWord": {
|
|
852
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
case "searchBytes": {
|
|
856
|
+
structuredContent = { command, address, length, values, result: response };
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
default:
|
|
860
|
+
structuredContent = { command, result: response };
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
864
|
+
structuredContent,
|
|
865
|
+
isError: false,
|
|
866
|
+
};
|
|
491
867
|
});
|
|
868
|
+
// debug_vram
|
|
492
869
|
server.registerTool(
|
|
493
870
|
// Name of the tool (used to call it)
|
|
494
871
|
"debug_vram", {
|
|
@@ -497,11 +874,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
497
874
|
description: "Read or write from/to VRAM video memory from the openMSX emulator.",
|
|
498
875
|
// Schema for the tool (input validation)
|
|
499
876
|
inputSchema: {
|
|
500
|
-
command: z.enum(["getBlock", "readByte", "writeByte"])
|
|
877
|
+
command: z.enum(["getBlock", "readByte", "writeByte", "searchBytes"])
|
|
501
878
|
.describe(`Available commands:
|
|
502
879
|
'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
|
|
503
880
|
'readByte <address>': to read a BYTE from the specified VRAM address.
|
|
504
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.
|
|
505
883
|
**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
|
|
506
884
|
`),
|
|
507
885
|
address: z.string()
|
|
@@ -515,10 +893,44 @@ export async function registerTools(server, emuDirectories) {
|
|
|
515
893
|
.default(8)
|
|
516
894
|
.describe("Number of lines to obtain. Used by [getBlock]"),
|
|
517
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]"),
|
|
905
|
+
},
|
|
906
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
907
|
+
outputSchema: {
|
|
908
|
+
command: z.string()
|
|
909
|
+
.describe("The executed command name."),
|
|
910
|
+
address: z.string().optional()
|
|
911
|
+
.describe("VRAM address queried/modified."),
|
|
912
|
+
decimalValue: z.number().optional()
|
|
913
|
+
.describe("VRAM byte value in decimal. Present for 'readByte'."),
|
|
914
|
+
hexValue: z.string().optional()
|
|
915
|
+
.describe("VRAM byte value in hexadecimal. Present for 'readByte'."),
|
|
916
|
+
hexDump: z.string().optional()
|
|
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'."),
|
|
922
|
+
result: z.string().optional()
|
|
923
|
+
.describe("Generic result or status message."),
|
|
924
|
+
},
|
|
925
|
+
annotations: {
|
|
926
|
+
"readOnlyHint": true,
|
|
927
|
+
"destructiveHint": false,
|
|
928
|
+
"idempotentHint": false,
|
|
929
|
+
"openWorldHint": false,
|
|
518
930
|
},
|
|
519
931
|
},
|
|
520
932
|
// Handler for the tool (function to be executed when the tool is called)
|
|
521
|
-
async ({ command, address, lines, value8 }) => {
|
|
933
|
+
async ({ command, address, lines, value8, values, length }) => {
|
|
522
934
|
let tclCommand;
|
|
523
935
|
switch (command) {
|
|
524
936
|
case "getBlock":
|
|
@@ -530,16 +942,59 @@ export async function registerTools(server, emuDirectories) {
|
|
|
530
942
|
case "writeByte":
|
|
531
943
|
tclCommand = `vpoke ${address} ${value8}`;
|
|
532
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;
|
|
533
962
|
default:
|
|
534
|
-
return
|
|
535
|
-
`Error: Unknown video memory command "${command}".`
|
|
536
|
-
]);
|
|
963
|
+
return { content: [{ type: "text", text: `Error: Unknown video memory command "${command}".` }], isError: true };
|
|
537
964
|
}
|
|
538
965
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
539
|
-
|
|
540
|
-
response
|
|
541
|
-
|
|
966
|
+
if (isErrorResponse(response)) {
|
|
967
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
968
|
+
}
|
|
969
|
+
let structuredContent;
|
|
970
|
+
switch (command) {
|
|
971
|
+
case "getBlock": {
|
|
972
|
+
structuredContent = { command, address, hexDump: response };
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
case "readByte": {
|
|
976
|
+
const dec = parseInt(response.trim(), 10);
|
|
977
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(2, '0')}` };
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case "writeByte": {
|
|
981
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
case "searchBytes": {
|
|
985
|
+
structuredContent = { command, address, length, values, result: response };
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
default:
|
|
989
|
+
structuredContent = { command, result: response };
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
993
|
+
structuredContent,
|
|
994
|
+
isError: false,
|
|
995
|
+
};
|
|
542
996
|
});
|
|
997
|
+
// debug_breakpoints
|
|
543
998
|
server.registerTool(
|
|
544
999
|
// Name of the tool (used to call it)
|
|
545
1000
|
"debug_breakpoints", {
|
|
@@ -566,6 +1021,29 @@ export async function registerTools(server, emuDirectories) {
|
|
|
566
1021
|
.optional()
|
|
567
1022
|
.describe("Breakpoint name (e.g. bp#1). Used by [remove]"),
|
|
568
1023
|
},
|
|
1024
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
1025
|
+
outputSchema: {
|
|
1026
|
+
command: z.string()
|
|
1027
|
+
.describe("The executed command name."),
|
|
1028
|
+
createdName: z.string().optional()
|
|
1029
|
+
.describe("Name assigned to the newly created breakpoint (e.g. 'bp#1'). Present for 'create'."),
|
|
1030
|
+
createdAddress: z.string().optional()
|
|
1031
|
+
.describe("Address of the newly created breakpoint. Present for 'create'."),
|
|
1032
|
+
removedName: z.string().optional()
|
|
1033
|
+
.describe("Name of the removed breakpoint. Present for 'remove'."),
|
|
1034
|
+
breakpoints: z.array(z.object({
|
|
1035
|
+
name: z.string(), address: z.string(), condition: z.string(), command: z.string()
|
|
1036
|
+
})).optional()
|
|
1037
|
+
.describe("List of active breakpoints. Present for 'list'."),
|
|
1038
|
+
result: z.string().optional()
|
|
1039
|
+
.describe("Generic result or status message."),
|
|
1040
|
+
},
|
|
1041
|
+
annotations: {
|
|
1042
|
+
"readOnlyHint": true,
|
|
1043
|
+
"destructiveHint": false,
|
|
1044
|
+
"idempotentHint": false,
|
|
1045
|
+
"openWorldHint": false,
|
|
1046
|
+
},
|
|
569
1047
|
},
|
|
570
1048
|
// Handler for the tool (function to be executed when the tool is called)
|
|
571
1049
|
async ({ command, address, bpname }) => {
|
|
@@ -581,15 +1059,37 @@ export async function registerTools(server, emuDirectories) {
|
|
|
581
1059
|
tclCommand = 'debug list_bp';
|
|
582
1060
|
break;
|
|
583
1061
|
default:
|
|
584
|
-
return
|
|
585
|
-
`Error: Unknown breakpoint command "${command}".`
|
|
586
|
-
]);
|
|
1062
|
+
return { content: [{ type: "text", text: `Error: Unknown breakpoint command "${command}".` }], isError: true };
|
|
587
1063
|
}
|
|
588
1064
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
589
|
-
|
|
590
|
-
response
|
|
591
|
-
|
|
1065
|
+
if (isErrorResponse(response)) {
|
|
1066
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
1067
|
+
}
|
|
1068
|
+
let structuredContent;
|
|
1069
|
+
switch (command) {
|
|
1070
|
+
case "create": {
|
|
1071
|
+
structuredContent = { command, createdName: response.trim(), createdAddress: address };
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "remove": {
|
|
1075
|
+
structuredContent = { command, removedName: bpname, result: response || "Ok" };
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "list": {
|
|
1079
|
+
const bps = parseBreakpoints(response);
|
|
1080
|
+
structuredContent = { command, breakpoints: bps };
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
default:
|
|
1084
|
+
structuredContent = { command, result: response };
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1088
|
+
structuredContent,
|
|
1089
|
+
isError: false,
|
|
1090
|
+
};
|
|
592
1091
|
});
|
|
1092
|
+
// emu_savestates
|
|
593
1093
|
server.registerTool(
|
|
594
1094
|
// Name of the tool (used to call it)
|
|
595
1095
|
"emu_savestates", {
|
|
@@ -611,6 +1111,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
611
1111
|
.optional()
|
|
612
1112
|
.describe("Name of the savestate to load/save. Used by [load, save]"),
|
|
613
1113
|
},
|
|
1114
|
+
annotations: {
|
|
1115
|
+
"readOnlyHint": false,
|
|
1116
|
+
"destructiveHint": true,
|
|
1117
|
+
"idempotentHint": false,
|
|
1118
|
+
"openWorldHint": false,
|
|
1119
|
+
},
|
|
614
1120
|
},
|
|
615
1121
|
// Handler for the tool (function to be executed when the tool is called)
|
|
616
1122
|
async ({ command, name }) => {
|
|
@@ -640,6 +1146,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
640
1146
|
response
|
|
641
1147
|
]);
|
|
642
1148
|
});
|
|
1149
|
+
// emu_replay
|
|
643
1150
|
server.registerTool(
|
|
644
1151
|
// Name of the tool (used to call it)
|
|
645
1152
|
"emu_replay", {
|
|
@@ -684,6 +1191,31 @@ export async function registerTools(server, emuDirectories) {
|
|
|
684
1191
|
.optional()
|
|
685
1192
|
.describe("Filename to save/load replay. Used by [saveReplay, loadReplay]"),
|
|
686
1193
|
},
|
|
1194
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
1195
|
+
outputSchema: {
|
|
1196
|
+
command: z.string()
|
|
1197
|
+
.describe("The executed command name."),
|
|
1198
|
+
enabled: z.boolean().optional()
|
|
1199
|
+
.describe("Whether replay is currently enabled. Present for 'status'."),
|
|
1200
|
+
beginTime: z.number().optional()
|
|
1201
|
+
.describe("Replay begin time in seconds. Present for 'status'."),
|
|
1202
|
+
endTime: z.number().optional()
|
|
1203
|
+
.describe("Replay end time in seconds. Present for 'status'."),
|
|
1204
|
+
currentTime: z.number().optional()
|
|
1205
|
+
.describe("Current replay time in seconds. Present for 'status'."),
|
|
1206
|
+
snapshotCount: z.number().optional()
|
|
1207
|
+
.describe("Number of snapshots collected. Present for 'status'."),
|
|
1208
|
+
filename: z.string().optional()
|
|
1209
|
+
.describe("Replay filename saved/loaded. Present for 'saveReplay' and 'loadReplay'."),
|
|
1210
|
+
result: z.string().optional()
|
|
1211
|
+
.describe("Generic result or status message."),
|
|
1212
|
+
},
|
|
1213
|
+
annotations: {
|
|
1214
|
+
"readOnlyHint": false,
|
|
1215
|
+
"destructiveHint": true,
|
|
1216
|
+
"idempotentHint": false,
|
|
1217
|
+
"openWorldHint": false,
|
|
1218
|
+
},
|
|
687
1219
|
},
|
|
688
1220
|
// Handler for the tool (function to be executed when the tool is called)
|
|
689
1221
|
async ({ command, seconds, time, frames, filename }) => {
|
|
@@ -724,43 +1256,110 @@ export async function registerTools(server, emuDirectories) {
|
|
|
724
1256
|
tclCommand = `reverse loadreplay ${filename}`;
|
|
725
1257
|
break;
|
|
726
1258
|
default:
|
|
727
|
-
return
|
|
728
|
-
`Error: Unknown replay command "${command}".`
|
|
729
|
-
]);
|
|
1259
|
+
return { content: [{ type: "text", text: `Error: Unknown replay command "${command}".` }], isError: true };
|
|
730
1260
|
}
|
|
731
1261
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
732
|
-
|
|
733
|
-
response
|
|
734
|
-
|
|
1262
|
+
if (isErrorResponse(response)) {
|
|
1263
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
1264
|
+
}
|
|
1265
|
+
let structuredContent;
|
|
1266
|
+
switch (command) {
|
|
1267
|
+
case "status": {
|
|
1268
|
+
const status = parseReplayStatus(response);
|
|
1269
|
+
structuredContent = {
|
|
1270
|
+
command,
|
|
1271
|
+
enabled: status.enabled,
|
|
1272
|
+
beginTime: status.begin,
|
|
1273
|
+
endTime: status.end,
|
|
1274
|
+
currentTime: status.current,
|
|
1275
|
+
snapshotCount: status.snapshotCount,
|
|
1276
|
+
};
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
case "saveReplay":
|
|
1280
|
+
case "loadReplay": {
|
|
1281
|
+
structuredContent = { command, filename: filename || response.trim(), result: response || "Ok" };
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
default: {
|
|
1285
|
+
structuredContent = { command, result: response || "Ok" };
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1290
|
+
structuredContent,
|
|
1291
|
+
isError: false,
|
|
1292
|
+
};
|
|
735
1293
|
});
|
|
1294
|
+
// emu_keyboard
|
|
736
1295
|
server.registerTool(
|
|
737
1296
|
// Name of the tool (used to call it)
|
|
738
1297
|
"emu_keyboard", {
|
|
739
1298
|
title: "Keyboard tools",
|
|
740
1299
|
// Description of the tool (what it does)
|
|
741
|
-
description: "Send
|
|
1300
|
+
description: "Send text or key combinations to the openMSX emulator.",
|
|
742
1301
|
// Schema for the tool (input validation)
|
|
743
1302
|
inputSchema: {
|
|
744
|
-
command: z.enum(["sendText"])
|
|
1303
|
+
command: z.enum(["sendText", "sendKeyCombo"])
|
|
745
1304
|
.describe(`Available commands:
|
|
746
1305
|
'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix.
|
|
1306
|
+
'sendKeyCombo <keys> [holdTime]': press a combination of keys (e.g., CTRL+STOP to break a program).
|
|
747
1307
|
**Important Note**: each 'text' sent is limited to 200 characters, and the 'text' is sent as if it was typed in the MSX keyboard.
|
|
748
1308
|
**Important Note**: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...
|
|
1309
|
+
**Important Note**: valid key names for sendKeyCombo include: SHIFT, CTRL, GRAPH, CAPS, CODE, F1-F5, ESC, TAB, STOP, BS, SELECT, RETURN/ENTER, SPACE, HOME, INS, DEL, LEFT, UP, DOWN, RIGHT.
|
|
1310
|
+
**Important Note**: MSX keyboards can experience key ghosting with 3+ simultaneous keys due to hardware limitations.
|
|
749
1311
|
`),
|
|
750
1312
|
text: z.string()
|
|
751
1313
|
.min(1, 'Text to send is too short')
|
|
752
1314
|
.max(200, 'Text to send is too long')
|
|
753
1315
|
.optional()
|
|
754
|
-
.
|
|
1316
|
+
.describe("Text to send to the emulator via emulated keyboard"),
|
|
1317
|
+
keys: z.array(z.string())
|
|
1318
|
+
.min(1, 'At least one key must be specified')
|
|
1319
|
+
.max(10, 'Too many keys (max 10)')
|
|
1320
|
+
.optional()
|
|
1321
|
+
.describe("Array of key names to press simultaneously (e.g., ['CTRL', 'STOP'])"),
|
|
1322
|
+
holdTime: z.number()
|
|
1323
|
+
.min(10, 'Hold time too short (min 10ms)')
|
|
1324
|
+
.max(5000, 'Hold time too long (max 5000ms)')
|
|
1325
|
+
.optional()
|
|
1326
|
+
.default(100)
|
|
1327
|
+
.describe("Time in milliseconds to hold keys down (default: 100)"),
|
|
1328
|
+
},
|
|
1329
|
+
annotations: {
|
|
1330
|
+
"readOnlyHint": false,
|
|
1331
|
+
"destructiveHint": true,
|
|
1332
|
+
"idempotentHint": false,
|
|
1333
|
+
"openWorldHint": false,
|
|
755
1334
|
},
|
|
756
1335
|
},
|
|
757
1336
|
// Handler for the tool (function to be executed when the tool is called)
|
|
758
|
-
async ({ command, text }) => {
|
|
1337
|
+
async ({ command, text, keys, holdTime }) => {
|
|
759
1338
|
let tclCommand;
|
|
760
1339
|
switch (command) {
|
|
761
1340
|
case "sendText":
|
|
1341
|
+
if (!text) {
|
|
1342
|
+
return getResponseContent([
|
|
1343
|
+
'Error: No text provided for sendText command.'
|
|
1344
|
+
]);
|
|
1345
|
+
}
|
|
762
1346
|
tclCommand = `type "${encodeTypeText(text)}"`;
|
|
763
1347
|
break;
|
|
1348
|
+
case "sendKeyCombo":
|
|
1349
|
+
if (!keys || keys.length === 0) {
|
|
1350
|
+
return getResponseContent([
|
|
1351
|
+
'Error: No keys provided for sendKeyCombo command.'
|
|
1352
|
+
]);
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
tclCommand = buildKeyComboCommand(keys, holdTime || 100);
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
return getResponseContent([
|
|
1359
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1360
|
+
]);
|
|
1361
|
+
}
|
|
1362
|
+
break;
|
|
764
1363
|
default:
|
|
765
1364
|
return getResponseContent([
|
|
766
1365
|
`Error: Unknown keyboard command "${command}".`
|
|
@@ -771,6 +1370,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
771
1370
|
response
|
|
772
1371
|
]);
|
|
773
1372
|
});
|
|
1373
|
+
// screen_shot
|
|
774
1374
|
server.registerTool(
|
|
775
1375
|
// Name of the tool (used to call it)
|
|
776
1376
|
"screen_shot", {
|
|
@@ -785,10 +1385,16 @@ export async function registerTools(server, emuDirectories) {
|
|
|
785
1385
|
'to_file': take a screenshot and save it to a file, the file name is returned in the response.
|
|
786
1386
|
`),
|
|
787
1387
|
},
|
|
1388
|
+
annotations: {
|
|
1389
|
+
"readOnlyHint": false,
|
|
1390
|
+
"destructiveHint": false,
|
|
1391
|
+
"idempotentHint": false,
|
|
1392
|
+
"openWorldHint": false,
|
|
1393
|
+
},
|
|
788
1394
|
},
|
|
789
1395
|
// Handler for the tool (function to be executed when the tool is called)
|
|
790
1396
|
async ({ command }) => {
|
|
791
|
-
const openmsxCommand = `screenshot -raw -prefix "${path.join(emuDirectories.OPENMSX_SCREENSHOT_DIR, 'mcp_')}"`;
|
|
1397
|
+
const openmsxCommand = `screenshot -raw -doublesize -prefix "${path.join(emuDirectories.OPENMSX_SCREENSHOT_DIR, 'mcp_')}"`;
|
|
792
1398
|
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
793
1399
|
switch (command) {
|
|
794
1400
|
case "as_image":
|
|
@@ -829,6 +1435,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
829
1435
|
`Error: Unknown screen_shot command "${command}".`
|
|
830
1436
|
]);
|
|
831
1437
|
});
|
|
1438
|
+
// screen_dump
|
|
832
1439
|
server.registerTool(
|
|
833
1440
|
// Name of the tool (used to call it)
|
|
834
1441
|
"screen_dump", {
|
|
@@ -845,6 +1452,12 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
845
1452
|
.default("screendump")
|
|
846
1453
|
.describe("Screendump filename (without path nor extension) to save the screendump; default is 'screendump'"),
|
|
847
1454
|
},
|
|
1455
|
+
annotations: {
|
|
1456
|
+
"readOnlyHint": false,
|
|
1457
|
+
"destructiveHint": true,
|
|
1458
|
+
"idempotentHint": false,
|
|
1459
|
+
"openWorldHint": false,
|
|
1460
|
+
},
|
|
848
1461
|
},
|
|
849
1462
|
// Handler for the tool (function to be executed when the tool is called)
|
|
850
1463
|
async ({ scrbasename }) => {
|
|
@@ -855,6 +1468,7 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
855
1468
|
response
|
|
856
1469
|
]);
|
|
857
1470
|
});
|
|
1471
|
+
// basic_programming
|
|
858
1472
|
server.registerTool(
|
|
859
1473
|
// Name of the tool (used to call it)
|
|
860
1474
|
"basic_programming", {
|
|
@@ -893,6 +1507,21 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
893
1507
|
.optional()
|
|
894
1508
|
.describe("End line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
|
|
895
1509
|
},
|
|
1510
|
+
outputSchema: {
|
|
1511
|
+
command: z.string().describe("The command that was executed."),
|
|
1512
|
+
available: z.boolean().optional()
|
|
1513
|
+
.describe("Whether BASIC mode is available. Present for 'isBasicAvailable'."),
|
|
1514
|
+
program: z.string().optional()
|
|
1515
|
+
.describe("BASIC program text. Present for 'getFullProgram' and 'getFullProgramAdvanced'."),
|
|
1516
|
+
result: z.string().optional()
|
|
1517
|
+
.describe("Generic result or status message."),
|
|
1518
|
+
},
|
|
1519
|
+
annotations: {
|
|
1520
|
+
"readOnlyHint": false,
|
|
1521
|
+
"destructiveHint": true,
|
|
1522
|
+
"idempotentHint": false,
|
|
1523
|
+
"openWorldHint": false,
|
|
1524
|
+
},
|
|
896
1525
|
},
|
|
897
1526
|
// Handler for the tool (function to be executed when the tool is called)
|
|
898
1527
|
async ({ command, program, startLine, endLine }) => {
|
|
@@ -978,10 +1607,34 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
978
1607
|
if (response === undefined && tclCommand) {
|
|
979
1608
|
response = await openMSXInstance.sendCommand(tclCommand);
|
|
980
1609
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1610
|
+
if (response === undefined) {
|
|
1611
|
+
return { content: [{ type: "text", text: `Error: No response for command "${command}".` }], isError: true };
|
|
1612
|
+
}
|
|
1613
|
+
if (isErrorResponse(response)) {
|
|
1614
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
1615
|
+
}
|
|
1616
|
+
let structuredContent;
|
|
1617
|
+
switch (command) {
|
|
1618
|
+
case "isBasicAvailable": {
|
|
1619
|
+
structuredContent = { command, available: response === "true" };
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
case "getFullProgram":
|
|
1623
|
+
case "getFullProgramAdvanced": {
|
|
1624
|
+
structuredContent = { command, program: response };
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
default: {
|
|
1628
|
+
structuredContent = { command, result: response || "Ok" };
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1633
|
+
structuredContent,
|
|
1634
|
+
isError: false,
|
|
1635
|
+
};
|
|
984
1636
|
});
|
|
1637
|
+
// vector_db_query
|
|
985
1638
|
server.registerTool(
|
|
986
1639
|
// Name of the tool (used to call it)
|
|
987
1640
|
"vector_db_query", {
|
|
@@ -999,15 +1652,21 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
999
1652
|
.max(100, 'Query string too long')
|
|
1000
1653
|
.describe("Query string to search in the Vector DB resources, case-insensitive and may contain spaces."),
|
|
1001
1654
|
},
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1655
|
+
outputSchema: {
|
|
1656
|
+
results: z.array(z.object({
|
|
1657
|
+
score: z.string().describe("Proximity score of the result to the query, higher is better."),
|
|
1658
|
+
title: z.string().describe("Title of the resource."),
|
|
1659
|
+
uri: z.string().describe("URI of the resource, which can be used to access the resource."),
|
|
1660
|
+
document: z.string().describe("Document chunk of the resource, retrieved from the Vector DB."),
|
|
1661
|
+
id: z.string().describe("Unique resource chunk ID, used internally by the Vector DB."),
|
|
1662
|
+
}))
|
|
1663
|
+
},
|
|
1664
|
+
annotations: {
|
|
1665
|
+
"readOnlyHint": true,
|
|
1666
|
+
"destructiveHint": false,
|
|
1667
|
+
"idempotentHint": true,
|
|
1668
|
+
"openWorldHint": false,
|
|
1669
|
+
},
|
|
1011
1670
|
},
|
|
1012
1671
|
// Handler for the tool (function to be executed when the tool is called)
|
|
1013
1672
|
async ({ query }) => {
|
|
@@ -1017,13 +1676,14 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
1017
1676
|
type: "text",
|
|
1018
1677
|
text: JSON.stringify(results),
|
|
1019
1678
|
}],
|
|
1020
|
-
|
|
1679
|
+
structuredContent: { results },
|
|
1021
1680
|
isError: false,
|
|
1022
1681
|
};
|
|
1023
1682
|
});
|
|
1024
1683
|
// ============================================================================
|
|
1025
1684
|
// Register a tool to get a specific MSX documentation resource
|
|
1026
1685
|
// Retrieve MCP resources for MCP clients that don't support MCP resources.
|
|
1686
|
+
// msxdocs_resource_get
|
|
1027
1687
|
server.registerTool(
|
|
1028
1688
|
// Name of the tool (used to call it)
|
|
1029
1689
|
"msxdocs_resource_get", {
|
|
@@ -1035,6 +1695,12 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
1035
1695
|
resourceName: z.enum(getRegisteredResourcesList().map(res => res.resource.name))
|
|
1036
1696
|
.describe("Name of the resource to obtain, e.g. 'msxdocs_programming_interrupts'"),
|
|
1037
1697
|
},
|
|
1698
|
+
annotations: {
|
|
1699
|
+
"readOnlyHint": true,
|
|
1700
|
+
"destructiveHint": false,
|
|
1701
|
+
"idempotentHint": true,
|
|
1702
|
+
"openWorldHint": true,
|
|
1703
|
+
},
|
|
1038
1704
|
},
|
|
1039
1705
|
// Handler for the tool (function to be executed when the tool is called)
|
|
1040
1706
|
async ({ resourceName }, extra) => {
|