@nataliapc/mcp-openmsx 1.2.4 → 1.2.5
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 +36 -12
- package/dist/server.js +2 -0
- package/dist/server_elicitations.js +178 -0
- package/dist/server_prompts.js +1 -1
- package/dist/server_resources.js +9 -1
- package/dist/server_sampling.js +35 -0
- package/dist/server_tools.js +663 -84
- package/dist/utils.js +229 -0
- package/package.json +12 -11
package/dist/server_tools.js
CHANGED
|
@@ -3,8 +3,9 @@ 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
|
|
@@ -20,7 +21,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
20
21
|
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed",
|
|
21
22
|
"machineList", "extensionList", "wait"])
|
|
22
23
|
.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. " +
|
|
24
|
+
'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
25
|
'close': closes the openMSX emulator.
|
|
25
26
|
'powerOn': powers on the openMSX emulator.
|
|
26
27
|
'powerOff': powers off the openMSX emulator.
|
|
@@ -53,14 +54,45 @@ export async function registerTools(server, emuDirectories) {
|
|
|
53
54
|
.default(3)
|
|
54
55
|
.describe("Number of seconds to wait; default is 3. Used by [wait]."),
|
|
55
56
|
},
|
|
57
|
+
outputSchema: {
|
|
58
|
+
command: z.string().describe("The command that was executed."),
|
|
59
|
+
speed: z.number().optional()
|
|
60
|
+
.describe("Emulator speed percentage. Present for 'getEmulatorSpeed' and 'setEmulatorSpeed'."),
|
|
61
|
+
machines: z.array(z.object({
|
|
62
|
+
name: z.string().describe("Machine name."),
|
|
63
|
+
description: z.string().describe("Machine description."),
|
|
64
|
+
})).optional()
|
|
65
|
+
.describe("List of available MSX machines. Present for 'machineList'."),
|
|
66
|
+
extensions: z.array(z.object({
|
|
67
|
+
name: z.string().describe("Extension name."),
|
|
68
|
+
description: z.string().describe("Extension description."),
|
|
69
|
+
})).optional()
|
|
70
|
+
.describe("List of available MSX extensions. Present for 'extensionList'."),
|
|
71
|
+
result: z.string().optional()
|
|
72
|
+
.describe("Generic result or status message."),
|
|
73
|
+
},
|
|
74
|
+
annotations: {
|
|
75
|
+
"readOnlyHint": true,
|
|
76
|
+
"destructiveHint": false,
|
|
77
|
+
"idempotentHint": false,
|
|
78
|
+
"openWorldHint": false,
|
|
79
|
+
},
|
|
56
80
|
},
|
|
57
81
|
// Handler for the tool (function to be executed when the tool is called)
|
|
58
|
-
async ({ command, machine, extensions, emuspeed, seconds }) => {
|
|
82
|
+
async ({ command, machine, extensions, emuspeed, seconds }, extra) => {
|
|
59
83
|
let result = '';
|
|
60
84
|
switch (command) {
|
|
61
|
-
case "launch":
|
|
62
|
-
|
|
85
|
+
case "launch": {
|
|
86
|
+
const resolved = await resolveLaunchParams(server, emuDirectories, machine, extensions);
|
|
87
|
+
if (resolved.cancelled) {
|
|
88
|
+
return { content: [{ type: "text", text: "Launch cancelled by user." }], isError: true };
|
|
89
|
+
}
|
|
90
|
+
if (resolved.error) {
|
|
91
|
+
return { content: [{ type: "text", text: resolved.error }], isError: true };
|
|
92
|
+
}
|
|
93
|
+
result = await openMSXInstance.emu_launch(emuDirectories.OPENMSX_EXECUTABLE, resolved.machine, resolved.extensions);
|
|
63
94
|
break;
|
|
95
|
+
}
|
|
64
96
|
case "close":
|
|
65
97
|
result = await openMSXInstance.emu_close();
|
|
66
98
|
break;
|
|
@@ -90,18 +122,76 @@ export async function registerTools(server, emuDirectories) {
|
|
|
90
122
|
case "extensionList":
|
|
91
123
|
result = await openMSXInstance.getExtensionList(emuDirectories.EXTENSIONS_DIR);
|
|
92
124
|
break;
|
|
93
|
-
case "wait":
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
case "wait": {
|
|
126
|
+
const total = seconds;
|
|
127
|
+
const progressToken = extra._meta?.progressToken;
|
|
128
|
+
let elapsed = 0;
|
|
129
|
+
try {
|
|
130
|
+
for (let i = 1; i <= total; i++) {
|
|
131
|
+
await sleepWithAbort(1000, extra.signal);
|
|
132
|
+
elapsed = i;
|
|
133
|
+
if (progressToken !== undefined) {
|
|
134
|
+
await extra.sendNotification({
|
|
135
|
+
method: "notifications/progress",
|
|
136
|
+
params: { progressToken, progress: i, total, message: `Waited ${i} of ${total} seconds` },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
result = `Waited for ${total} seconds.`;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
result = `Wait cancelled after ${elapsed} of ${total} seconds.`;
|
|
144
|
+
return { content: [{ type: "text", text: result }], isError: true };
|
|
145
|
+
}
|
|
96
146
|
break;
|
|
147
|
+
}
|
|
97
148
|
default:
|
|
98
149
|
result = `Error: Unknown command "${command}".`;
|
|
99
150
|
break;
|
|
100
151
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
if (isErrorResponse(result)) {
|
|
153
|
+
return { content: [{ type: "text", text: result }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
let structuredContent;
|
|
156
|
+
switch (command) {
|
|
157
|
+
case 'getEmulatorSpeed': {
|
|
158
|
+
const match = result.match(/(\d+)%/);
|
|
159
|
+
structuredContent = { command, speed: match ? parseInt(match[1]) : undefined, result };
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'setEmulatorSpeed': {
|
|
163
|
+
structuredContent = { command, speed: emuspeed, result };
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'machineList': {
|
|
167
|
+
try {
|
|
168
|
+
const machines = JSON.parse(result);
|
|
169
|
+
structuredContent = { command, machines };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
structuredContent = { command, result };
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'extensionList': {
|
|
177
|
+
try {
|
|
178
|
+
const extensions = JSON.parse(result);
|
|
179
|
+
structuredContent = { command, extensions };
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
structuredContent = { command, result };
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
default: {
|
|
187
|
+
structuredContent = { command, result };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: result }],
|
|
192
|
+
structuredContent,
|
|
193
|
+
isError: false,
|
|
194
|
+
};
|
|
105
195
|
});
|
|
106
196
|
server.registerTool(
|
|
107
197
|
// Name of the tool (used to call it)
|
|
@@ -142,6 +232,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
142
232
|
.optional()
|
|
143
233
|
.describe("Absolute Disk folder filename to insert. Used by [diskInsertFolder]"),
|
|
144
234
|
},
|
|
235
|
+
annotations: {
|
|
236
|
+
"readOnlyHint": true,
|
|
237
|
+
"destructiveHint": false,
|
|
238
|
+
"idempotentHint": false,
|
|
239
|
+
"openWorldHint": false,
|
|
240
|
+
},
|
|
145
241
|
},
|
|
146
242
|
// Handler for the tool (function to be executed when the tool is called)
|
|
147
243
|
async ({ command, tapefile, romfile, diskfile, diskfolder }) => {
|
|
@@ -196,30 +292,58 @@ export async function registerTools(server, emuDirectories) {
|
|
|
196
292
|
'getIOPortsMap': shows an overview about the I/O mapped devices.
|
|
197
293
|
`),
|
|
198
294
|
},
|
|
295
|
+
outputSchema: {
|
|
296
|
+
command: z.string().describe("The command that was executed."),
|
|
297
|
+
status: z.record(z.string()).optional()
|
|
298
|
+
.describe("Machine status key-value pairs (type, manufacturer, year, etc.). Present for 'getStatus'."),
|
|
299
|
+
result: z.string().optional()
|
|
300
|
+
.describe("Generic result text. Present for 'getSlotsMap' and 'getIOPortsMap'."),
|
|
301
|
+
},
|
|
302
|
+
annotations: {
|
|
303
|
+
"readOnlyHint": true,
|
|
304
|
+
"destructiveHint": false,
|
|
305
|
+
"idempotentHint": false,
|
|
306
|
+
"openWorldHint": false,
|
|
307
|
+
},
|
|
199
308
|
},
|
|
200
309
|
// Handler for the tool (function to be executed when the tool is called)
|
|
201
310
|
async ({ command }) => {
|
|
202
|
-
let
|
|
311
|
+
let response;
|
|
203
312
|
switch (command) {
|
|
204
313
|
case "getStatus":
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
]);
|
|
314
|
+
response = await openMSXInstance.emu_status();
|
|
315
|
+
break;
|
|
208
316
|
case "getSlotsMap":
|
|
209
|
-
|
|
317
|
+
response = await openMSXInstance.sendCommand("slotmap");
|
|
210
318
|
break;
|
|
211
319
|
case "getIOPortsMap":
|
|
212
|
-
|
|
320
|
+
response = await openMSXInstance.sendCommand("iomap");
|
|
213
321
|
break;
|
|
214
322
|
default:
|
|
215
|
-
return
|
|
216
|
-
`Error: Unknown emulator info command "${command}".`
|
|
217
|
-
]);
|
|
323
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator info command "${command}".` }], isError: true };
|
|
218
324
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
325
|
+
if (isErrorResponse(response)) {
|
|
326
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
327
|
+
}
|
|
328
|
+
let structuredContent;
|
|
329
|
+
if (command === "getStatus") {
|
|
330
|
+
try {
|
|
331
|
+
const status = JSON.parse(response);
|
|
332
|
+
structuredContent = { command, status };
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
structuredContent = { command, result: response };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
//TODO: parse the slotmap and iomap responses into structured content
|
|
340
|
+
structuredContent = { command, result: response };
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: response }],
|
|
344
|
+
structuredContent,
|
|
345
|
+
isError: false,
|
|
346
|
+
};
|
|
223
347
|
});
|
|
224
348
|
server.registerTool(
|
|
225
349
|
// Name of the tool (used to call it)
|
|
@@ -249,6 +373,37 @@ export async function registerTools(server, emuDirectories) {
|
|
|
249
373
|
.optional()
|
|
250
374
|
.describe("2 hexadecimal digits for a VDP register value (e.g. 0x1f). Used by [setRegisterValue]"),
|
|
251
375
|
},
|
|
376
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
377
|
+
outputSchema: {
|
|
378
|
+
command: z.string()
|
|
379
|
+
.describe("The executed command name."),
|
|
380
|
+
registers: z.record(z.string()).optional()
|
|
381
|
+
.describe("VDP register values as hex strings keyed by register number (0-31). Present for 'getRegisters'."),
|
|
382
|
+
register: z.number().optional()
|
|
383
|
+
.describe("VDP register number queried/modified. Present for 'getRegisterValue' and 'setRegisterValue'."),
|
|
384
|
+
decimalValue: z.number().optional()
|
|
385
|
+
.describe("Register value in decimal. Present for 'getRegisterValue'."),
|
|
386
|
+
hexValue: z.string().optional()
|
|
387
|
+
.describe("Register value in hexadecimal (e.g. '0x1F'). Present for 'getRegisterValue'."),
|
|
388
|
+
newValue: z.string().optional()
|
|
389
|
+
.describe("Value written to the register. Present for 'setRegisterValue'."),
|
|
390
|
+
palette: z.array(z.object({
|
|
391
|
+
index: z.number(), r: z.number(), g: z.number(), b: z.number(), rgb: z.string()
|
|
392
|
+
})).optional()
|
|
393
|
+
.describe("Color palette as array of 16 RGB333 entries. Present for 'getPalette'."),
|
|
394
|
+
screenMode: z.string().optional()
|
|
395
|
+
.describe("Current screen mode name (e.g. 'TEXT80', 'GRAPHIC2'). Present for 'screenGetMode'."),
|
|
396
|
+
screenText: z.string().optional()
|
|
397
|
+
.describe("Full text content of the MSX screen. Present for 'screenGetFullText'."),
|
|
398
|
+
result: z.string().optional()
|
|
399
|
+
.describe("Generic result or status message."),
|
|
400
|
+
},
|
|
401
|
+
annotations: {
|
|
402
|
+
"readOnlyHint": true,
|
|
403
|
+
"destructiveHint": false,
|
|
404
|
+
"idempotentHint": false,
|
|
405
|
+
"openWorldHint": false,
|
|
406
|
+
},
|
|
252
407
|
},
|
|
253
408
|
// Handler for the tool (function to be executed when the tool is called)
|
|
254
409
|
async ({ command, register, value }) => {
|
|
@@ -269,20 +424,58 @@ export async function registerTools(server, emuDirectories) {
|
|
|
269
424
|
case "screenGetMode":
|
|
270
425
|
tclCommand = "get_screen_mode";
|
|
271
426
|
break;
|
|
272
|
-
case "screenGetFullText":
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
427
|
+
case "screenGetFullText": {
|
|
428
|
+
const textResp = await openMSXInstance.sendCommand('get_screen');
|
|
429
|
+
if (isErrorResponse(textResp)) {
|
|
430
|
+
return { content: [{ type: "text", text: textResp }], isError: true };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
content: [{ type: "text", text: `The screen text is:\n${textResp}` }],
|
|
434
|
+
structuredContent: { command, screenText: textResp },
|
|
435
|
+
isError: false,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
277
438
|
default:
|
|
278
|
-
return
|
|
279
|
-
`Error: Unknown emulator vdp command "${command}".`
|
|
280
|
-
]);
|
|
439
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator vdp command "${command}".` }], isError: true };
|
|
281
440
|
}
|
|
282
441
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
283
|
-
|
|
284
|
-
response
|
|
285
|
-
|
|
442
|
+
if (isErrorResponse(response)) {
|
|
443
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
444
|
+
}
|
|
445
|
+
let structuredContent;
|
|
446
|
+
switch (command) {
|
|
447
|
+
case "getPalette": {
|
|
448
|
+
const pal = parsePalette(response);
|
|
449
|
+
structuredContent = { command, palette: pal };
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "getRegisters": {
|
|
453
|
+
const regs = parseVdpRegs(response);
|
|
454
|
+
structuredContent = { command, registers: regs };
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "getRegisterValue": {
|
|
458
|
+
const dec = parseInt(response.trim(), 10);
|
|
459
|
+
const hex = `0x${dec.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
460
|
+
structuredContent = { command, register, decimalValue: dec, hexValue: hex };
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case "setRegisterValue": {
|
|
464
|
+
structuredContent = { command, register, newValue: value, result: response || "Ok" };
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "screenGetMode": {
|
|
468
|
+
structuredContent = { command, screenMode: response.trim() };
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
default:
|
|
472
|
+
structuredContent = { command, result: response };
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
476
|
+
structuredContent,
|
|
477
|
+
isError: false,
|
|
478
|
+
};
|
|
286
479
|
});
|
|
287
480
|
server.registerTool(
|
|
288
481
|
// Name of the tool (used to call it)
|
|
@@ -310,6 +503,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
310
503
|
.optional()
|
|
311
504
|
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [runTo]"),
|
|
312
505
|
},
|
|
506
|
+
annotations: {
|
|
507
|
+
"readOnlyHint": true,
|
|
508
|
+
"destructiveHint": false,
|
|
509
|
+
"idempotentHint": false,
|
|
510
|
+
"openWorldHint": false,
|
|
511
|
+
},
|
|
313
512
|
},
|
|
314
513
|
// Handler for the tool (function to be executed when the tool is called)
|
|
315
514
|
async ({ command, address }) => {
|
|
@@ -345,6 +544,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
345
544
|
]);
|
|
346
545
|
}
|
|
347
546
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
547
|
+
//TODO: parse disassembly command response into structured content
|
|
348
548
|
return getResponseContent([
|
|
349
549
|
response
|
|
350
550
|
]);
|
|
@@ -369,6 +569,7 @@ export async function registerTools(server, emuDirectories) {
|
|
|
369
569
|
"**Important Note**: Addresses and values are in hexadecimal format (e.g. 0xd2 0x3af2)."
|
|
370
570
|
`),
|
|
371
571
|
register: z.enum(["pc", "sp", "ix", "iy", "af", "bc", "de", "hl", "ixh", "ixl", "iyh", "iyl",
|
|
572
|
+
"af'", "bc'", "de'", "hl'",
|
|
372
573
|
"a", "f", "b", "c", "d", "e", "h", "l", "i", "r", "im", "iff"])
|
|
373
574
|
.optional()
|
|
374
575
|
.describe("CPU register to read/write. Used by [getRegister, setRegister]"),
|
|
@@ -386,6 +587,35 @@ export async function registerTools(server, emuDirectories) {
|
|
|
386
587
|
.optional()
|
|
387
588
|
.describe("Number of bytes to disassemble. Used by [disassemble]"),
|
|
388
589
|
},
|
|
590
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
591
|
+
outputSchema: {
|
|
592
|
+
command: z.string()
|
|
593
|
+
.describe("The executed command name."),
|
|
594
|
+
registers: z.record(z.string()).optional()
|
|
595
|
+
.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'."),
|
|
596
|
+
register: z.string().optional()
|
|
597
|
+
.describe("CPU register name queried/modified. Present for 'getRegister' and 'setRegister'."),
|
|
598
|
+
decimalValue: z.number().optional()
|
|
599
|
+
.describe("Register value in decimal. Present for 'getRegister'."),
|
|
600
|
+
hexValue: z.string().optional()
|
|
601
|
+
.describe("Register value in hexadecimal (e.g. '0x1A3F'). Present for 'getRegister'."),
|
|
602
|
+
newValue: z.string().optional()
|
|
603
|
+
.describe("Value written to the register. Present for 'setRegister'."),
|
|
604
|
+
activeCpu: z.string().optional()
|
|
605
|
+
.describe("Active CPU type: 'z80' or 'r800'. Present for 'getActiveCpu'."),
|
|
606
|
+
stack: z.string().optional()
|
|
607
|
+
.describe("Stack pile dump content. Present for 'getStackPile'."),
|
|
608
|
+
disassembly: z.string().optional()
|
|
609
|
+
.describe("Disassembled code listing. Present for 'disassemble'."),
|
|
610
|
+
result: z.string().optional()
|
|
611
|
+
.describe("Generic result or status message."),
|
|
612
|
+
},
|
|
613
|
+
annotations: {
|
|
614
|
+
"readOnlyHint": true,
|
|
615
|
+
"destructiveHint": false,
|
|
616
|
+
"idempotentHint": false,
|
|
617
|
+
"openWorldHint": false,
|
|
618
|
+
},
|
|
389
619
|
},
|
|
390
620
|
// Handler for the tool (function to be executed when the tool is called)
|
|
391
621
|
async ({ command, address, register, value, size }) => {
|
|
@@ -410,14 +640,60 @@ export async function registerTools(server, emuDirectories) {
|
|
|
410
640
|
tclCommand = "get_active_cpu";
|
|
411
641
|
break;
|
|
412
642
|
default:
|
|
413
|
-
return
|
|
414
|
-
`Error: Unknown
|
|
415
|
-
|
|
643
|
+
return {
|
|
644
|
+
content: [{ type: "text", text: `Error: Unknown command "${command}".` }],
|
|
645
|
+
isError: true,
|
|
646
|
+
};
|
|
416
647
|
}
|
|
417
648
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
649
|
+
// On error, return unstructured content only (SDK skips outputSchema validation on errors)
|
|
650
|
+
if (isErrorResponse(response)) {
|
|
651
|
+
return {
|
|
652
|
+
content: [{ type: "text", text: response }],
|
|
653
|
+
isError: true,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// Build structuredContent based on the command
|
|
657
|
+
let structuredContent;
|
|
658
|
+
switch (command) {
|
|
659
|
+
case "getCpuRegisters": {
|
|
660
|
+
const regs = parseCpuRegs(response);
|
|
661
|
+
structuredContent = { command, registers: regs };
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case "getRegister": {
|
|
665
|
+
const decValue = parseInt(response.trim(), 10);
|
|
666
|
+
const padLen = is16bitRegister(register) ? 4 : 2;
|
|
667
|
+
const hexVal = `0x${decValue.toString(16).toUpperCase().padStart(padLen, '0')}`;
|
|
668
|
+
structuredContent = { command, register, decimalValue: decValue, hexValue: hexVal };
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case "setRegister": {
|
|
672
|
+
structuredContent = { command, register, newValue: value, result: response || "Ok" };
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case "getStackPile": {
|
|
676
|
+
//TODO: parse the stack pile response into structured content (e.g. an array of stack entries with address and value)
|
|
677
|
+
structuredContent = { command, stack: response };
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "disassemble": {
|
|
681
|
+
//TODO: parse the disassembly response into a structured format (e.g. an array of instructions with address, opcode, and assembly)
|
|
682
|
+
structuredContent = { command, disassembly: response };
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
case "getActiveCpu": {
|
|
686
|
+
structuredContent = { command, activeCpu: response.trim() };
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
default:
|
|
690
|
+
structuredContent = { command, result: response };
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
694
|
+
structuredContent,
|
|
695
|
+
isError: false,
|
|
696
|
+
};
|
|
421
697
|
});
|
|
422
698
|
server.registerTool(
|
|
423
699
|
// Name of the tool (used to call it)
|
|
@@ -456,6 +732,29 @@ export async function registerTools(server, emuDirectories) {
|
|
|
456
732
|
.optional()
|
|
457
733
|
.describe("4 hexadecimal digits for a word value (e.g. 0xa5b1). Used by [writeWord]"),
|
|
458
734
|
},
|
|
735
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
736
|
+
outputSchema: {
|
|
737
|
+
command: z.string()
|
|
738
|
+
.describe("The executed command name."),
|
|
739
|
+
address: z.string().optional()
|
|
740
|
+
.describe("Memory address queried/modified."),
|
|
741
|
+
decimalValue: z.number().optional()
|
|
742
|
+
.describe("Memory value in decimal. Present for 'readByte' and 'readWord'."),
|
|
743
|
+
hexValue: z.string().optional()
|
|
744
|
+
.describe("Memory value in hexadecimal. Present for 'readByte' and 'readWord'."),
|
|
745
|
+
hexDump: z.string().optional()
|
|
746
|
+
.describe("Hex dump block of memory. Present for 'getBlock'."),
|
|
747
|
+
slots: z.string().optional()
|
|
748
|
+
.describe("Currently selected memory slots info. Present for 'selectedSlots'."),
|
|
749
|
+
result: z.string().optional()
|
|
750
|
+
.describe("Generic result or status message."),
|
|
751
|
+
},
|
|
752
|
+
annotations: {
|
|
753
|
+
"readOnlyHint": true,
|
|
754
|
+
"destructiveHint": false,
|
|
755
|
+
"idempotentHint": false,
|
|
756
|
+
"openWorldHint": false,
|
|
757
|
+
},
|
|
459
758
|
},
|
|
460
759
|
// Handler for the tool (function to be executed when the tool is called)
|
|
461
760
|
async ({ command, address, lines, value8, value16 }) => {
|
|
@@ -480,14 +779,49 @@ export async function registerTools(server, emuDirectories) {
|
|
|
480
779
|
tclCommand = `poke16 ${address} ${value16}`;
|
|
481
780
|
break;
|
|
482
781
|
default:
|
|
483
|
-
return
|
|
484
|
-
`Error: Unknown memory command "${command}".`
|
|
485
|
-
]);
|
|
782
|
+
return { content: [{ type: "text", text: `Error: Unknown memory command "${command}".` }], isError: true };
|
|
486
783
|
}
|
|
487
784
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
488
|
-
|
|
489
|
-
response
|
|
490
|
-
|
|
785
|
+
if (isErrorResponse(response)) {
|
|
786
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
787
|
+
}
|
|
788
|
+
let structuredContent;
|
|
789
|
+
switch (command) {
|
|
790
|
+
case "selectedSlots": {
|
|
791
|
+
//TODO: parse the slotselect response into structured content (e.g. an array of selected slots with slot number and content info)
|
|
792
|
+
structuredContent = { command, slots: response };
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case "getBlock": {
|
|
796
|
+
structuredContent = { command, address, hexDump: response };
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case "readByte": {
|
|
800
|
+
const dec = parseInt(response.trim(), 10);
|
|
801
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(2, '0')}` };
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
case "readWord": {
|
|
805
|
+
const dec = parseInt(response.trim(), 10);
|
|
806
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(4, '0')}` };
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case "writeByte": {
|
|
810
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case "writeWord": {
|
|
814
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
default:
|
|
818
|
+
structuredContent = { command, result: response };
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
822
|
+
structuredContent,
|
|
823
|
+
isError: false,
|
|
824
|
+
};
|
|
491
825
|
});
|
|
492
826
|
server.registerTool(
|
|
493
827
|
// Name of the tool (used to call it)
|
|
@@ -516,6 +850,27 @@ export async function registerTools(server, emuDirectories) {
|
|
|
516
850
|
.describe("Number of lines to obtain. Used by [getBlock]"),
|
|
517
851
|
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]"),
|
|
518
852
|
},
|
|
853
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
854
|
+
outputSchema: {
|
|
855
|
+
command: z.string()
|
|
856
|
+
.describe("The executed command name."),
|
|
857
|
+
address: z.string().optional()
|
|
858
|
+
.describe("VRAM address queried/modified."),
|
|
859
|
+
decimalValue: z.number().optional()
|
|
860
|
+
.describe("VRAM byte value in decimal. Present for 'readByte'."),
|
|
861
|
+
hexValue: z.string().optional()
|
|
862
|
+
.describe("VRAM byte value in hexadecimal. Present for 'readByte'."),
|
|
863
|
+
hexDump: z.string().optional()
|
|
864
|
+
.describe("Hex dump block of VRAM. Present for 'getBlock'."),
|
|
865
|
+
result: z.string().optional()
|
|
866
|
+
.describe("Generic result or status message."),
|
|
867
|
+
},
|
|
868
|
+
annotations: {
|
|
869
|
+
"readOnlyHint": true,
|
|
870
|
+
"destructiveHint": false,
|
|
871
|
+
"idempotentHint": false,
|
|
872
|
+
"openWorldHint": false,
|
|
873
|
+
},
|
|
519
874
|
},
|
|
520
875
|
// Handler for the tool (function to be executed when the tool is called)
|
|
521
876
|
async ({ command, address, lines, value8 }) => {
|
|
@@ -531,14 +886,35 @@ export async function registerTools(server, emuDirectories) {
|
|
|
531
886
|
tclCommand = `vpoke ${address} ${value8}`;
|
|
532
887
|
break;
|
|
533
888
|
default:
|
|
534
|
-
return
|
|
535
|
-
`Error: Unknown video memory command "${command}".`
|
|
536
|
-
]);
|
|
889
|
+
return { content: [{ type: "text", text: `Error: Unknown video memory command "${command}".` }], isError: true };
|
|
537
890
|
}
|
|
538
891
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
539
|
-
|
|
540
|
-
response
|
|
541
|
-
|
|
892
|
+
if (isErrorResponse(response)) {
|
|
893
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
894
|
+
}
|
|
895
|
+
let structuredContent;
|
|
896
|
+
switch (command) {
|
|
897
|
+
case "getBlock": {
|
|
898
|
+
structuredContent = { command, address, hexDump: response };
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
case "readByte": {
|
|
902
|
+
const dec = parseInt(response.trim(), 10);
|
|
903
|
+
structuredContent = { command, address, decimalValue: dec, hexValue: `0x${dec.toString(16).toUpperCase().padStart(2, '0')}` };
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "writeByte": {
|
|
907
|
+
structuredContent = { command, address, result: response || "Ok" };
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
default:
|
|
911
|
+
structuredContent = { command, result: response };
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
915
|
+
structuredContent,
|
|
916
|
+
isError: false,
|
|
917
|
+
};
|
|
542
918
|
});
|
|
543
919
|
server.registerTool(
|
|
544
920
|
// Name of the tool (used to call it)
|
|
@@ -566,6 +942,29 @@ export async function registerTools(server, emuDirectories) {
|
|
|
566
942
|
.optional()
|
|
567
943
|
.describe("Breakpoint name (e.g. bp#1). Used by [remove]"),
|
|
568
944
|
},
|
|
945
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
946
|
+
outputSchema: {
|
|
947
|
+
command: z.string()
|
|
948
|
+
.describe("The executed command name."),
|
|
949
|
+
createdName: z.string().optional()
|
|
950
|
+
.describe("Name assigned to the newly created breakpoint (e.g. 'bp#1'). Present for 'create'."),
|
|
951
|
+
createdAddress: z.string().optional()
|
|
952
|
+
.describe("Address of the newly created breakpoint. Present for 'create'."),
|
|
953
|
+
removedName: z.string().optional()
|
|
954
|
+
.describe("Name of the removed breakpoint. Present for 'remove'."),
|
|
955
|
+
breakpoints: z.array(z.object({
|
|
956
|
+
name: z.string(), address: z.string(), condition: z.string(), command: z.string()
|
|
957
|
+
})).optional()
|
|
958
|
+
.describe("List of active breakpoints. Present for 'list'."),
|
|
959
|
+
result: z.string().optional()
|
|
960
|
+
.describe("Generic result or status message."),
|
|
961
|
+
},
|
|
962
|
+
annotations: {
|
|
963
|
+
"readOnlyHint": true,
|
|
964
|
+
"destructiveHint": false,
|
|
965
|
+
"idempotentHint": false,
|
|
966
|
+
"openWorldHint": false,
|
|
967
|
+
},
|
|
569
968
|
},
|
|
570
969
|
// Handler for the tool (function to be executed when the tool is called)
|
|
571
970
|
async ({ command, address, bpname }) => {
|
|
@@ -581,14 +980,35 @@ export async function registerTools(server, emuDirectories) {
|
|
|
581
980
|
tclCommand = 'debug list_bp';
|
|
582
981
|
break;
|
|
583
982
|
default:
|
|
584
|
-
return
|
|
585
|
-
`Error: Unknown breakpoint command "${command}".`
|
|
586
|
-
]);
|
|
983
|
+
return { content: [{ type: "text", text: `Error: Unknown breakpoint command "${command}".` }], isError: true };
|
|
587
984
|
}
|
|
588
985
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
589
|
-
|
|
590
|
-
response
|
|
591
|
-
|
|
986
|
+
if (isErrorResponse(response)) {
|
|
987
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
988
|
+
}
|
|
989
|
+
let structuredContent;
|
|
990
|
+
switch (command) {
|
|
991
|
+
case "create": {
|
|
992
|
+
structuredContent = { command, createdName: response.trim(), createdAddress: address };
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
case "remove": {
|
|
996
|
+
structuredContent = { command, removedName: bpname, result: response || "Ok" };
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
case "list": {
|
|
1000
|
+
const bps = parseBreakpoints(response);
|
|
1001
|
+
structuredContent = { command, breakpoints: bps };
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
default:
|
|
1005
|
+
structuredContent = { command, result: response };
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1009
|
+
structuredContent,
|
|
1010
|
+
isError: false,
|
|
1011
|
+
};
|
|
592
1012
|
});
|
|
593
1013
|
server.registerTool(
|
|
594
1014
|
// Name of the tool (used to call it)
|
|
@@ -611,6 +1031,12 @@ export async function registerTools(server, emuDirectories) {
|
|
|
611
1031
|
.optional()
|
|
612
1032
|
.describe("Name of the savestate to load/save. Used by [load, save]"),
|
|
613
1033
|
},
|
|
1034
|
+
annotations: {
|
|
1035
|
+
"readOnlyHint": false,
|
|
1036
|
+
"destructiveHint": true,
|
|
1037
|
+
"idempotentHint": false,
|
|
1038
|
+
"openWorldHint": false,
|
|
1039
|
+
},
|
|
614
1040
|
},
|
|
615
1041
|
// Handler for the tool (function to be executed when the tool is called)
|
|
616
1042
|
async ({ command, name }) => {
|
|
@@ -684,6 +1110,31 @@ export async function registerTools(server, emuDirectories) {
|
|
|
684
1110
|
.optional()
|
|
685
1111
|
.describe("Filename to save/load replay. Used by [saveReplay, loadReplay]"),
|
|
686
1112
|
},
|
|
1113
|
+
// Structured output schema (MCP protocol 2025-11-25)
|
|
1114
|
+
outputSchema: {
|
|
1115
|
+
command: z.string()
|
|
1116
|
+
.describe("The executed command name."),
|
|
1117
|
+
enabled: z.boolean().optional()
|
|
1118
|
+
.describe("Whether replay is currently enabled. Present for 'status'."),
|
|
1119
|
+
beginTime: z.number().optional()
|
|
1120
|
+
.describe("Replay begin time in seconds. Present for 'status'."),
|
|
1121
|
+
endTime: z.number().optional()
|
|
1122
|
+
.describe("Replay end time in seconds. Present for 'status'."),
|
|
1123
|
+
currentTime: z.number().optional()
|
|
1124
|
+
.describe("Current replay time in seconds. Present for 'status'."),
|
|
1125
|
+
snapshotCount: z.number().optional()
|
|
1126
|
+
.describe("Number of snapshots collected. Present for 'status'."),
|
|
1127
|
+
filename: z.string().optional()
|
|
1128
|
+
.describe("Replay filename saved/loaded. Present for 'saveReplay' and 'loadReplay'."),
|
|
1129
|
+
result: z.string().optional()
|
|
1130
|
+
.describe("Generic result or status message."),
|
|
1131
|
+
},
|
|
1132
|
+
annotations: {
|
|
1133
|
+
"readOnlyHint": false,
|
|
1134
|
+
"destructiveHint": true,
|
|
1135
|
+
"idempotentHint": false,
|
|
1136
|
+
"openWorldHint": false,
|
|
1137
|
+
},
|
|
687
1138
|
},
|
|
688
1139
|
// Handler for the tool (function to be executed when the tool is called)
|
|
689
1140
|
async ({ command, seconds, time, frames, filename }) => {
|
|
@@ -724,43 +1175,109 @@ export async function registerTools(server, emuDirectories) {
|
|
|
724
1175
|
tclCommand = `reverse loadreplay ${filename}`;
|
|
725
1176
|
break;
|
|
726
1177
|
default:
|
|
727
|
-
return
|
|
728
|
-
`Error: Unknown replay command "${command}".`
|
|
729
|
-
]);
|
|
1178
|
+
return { content: [{ type: "text", text: `Error: Unknown replay command "${command}".` }], isError: true };
|
|
730
1179
|
}
|
|
731
1180
|
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
732
|
-
|
|
733
|
-
response
|
|
734
|
-
|
|
1181
|
+
if (isErrorResponse(response)) {
|
|
1182
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
1183
|
+
}
|
|
1184
|
+
let structuredContent;
|
|
1185
|
+
switch (command) {
|
|
1186
|
+
case "status": {
|
|
1187
|
+
const status = parseReplayStatus(response);
|
|
1188
|
+
structuredContent = {
|
|
1189
|
+
command,
|
|
1190
|
+
enabled: status.enabled,
|
|
1191
|
+
beginTime: status.begin,
|
|
1192
|
+
endTime: status.end,
|
|
1193
|
+
currentTime: status.current,
|
|
1194
|
+
snapshotCount: status.snapshotCount,
|
|
1195
|
+
};
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
case "saveReplay":
|
|
1199
|
+
case "loadReplay": {
|
|
1200
|
+
structuredContent = { command, filename: filename || response.trim(), result: response || "Ok" };
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
default: {
|
|
1204
|
+
structuredContent = { command, result: response || "Ok" };
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1209
|
+
structuredContent,
|
|
1210
|
+
isError: false,
|
|
1211
|
+
};
|
|
735
1212
|
});
|
|
736
1213
|
server.registerTool(
|
|
737
1214
|
// Name of the tool (used to call it)
|
|
738
1215
|
"emu_keyboard", {
|
|
739
1216
|
title: "Keyboard tools",
|
|
740
1217
|
// Description of the tool (what it does)
|
|
741
|
-
description: "Send
|
|
1218
|
+
description: "Send text or key combinations to the openMSX emulator.",
|
|
742
1219
|
// Schema for the tool (input validation)
|
|
743
1220
|
inputSchema: {
|
|
744
|
-
command: z.enum(["sendText"])
|
|
1221
|
+
command: z.enum(["sendText", "sendKeyCombo"])
|
|
745
1222
|
.describe(`Available commands:
|
|
746
1223
|
'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix.
|
|
1224
|
+
'sendKeyCombo <keys> [holdTime]': press a combination of keys (e.g., CTRL+STOP to break a program).
|
|
747
1225
|
**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
1226
|
**Important Note**: escape keys that needs it as Return key (use \\r), double quotes (use \\\"), etc...
|
|
1227
|
+
**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.
|
|
1228
|
+
**Important Note**: MSX keyboards can experience key ghosting with 3+ simultaneous keys due to hardware limitations.
|
|
749
1229
|
`),
|
|
750
1230
|
text: z.string()
|
|
751
1231
|
.min(1, 'Text to send is too short')
|
|
752
1232
|
.max(200, 'Text to send is too long')
|
|
753
1233
|
.optional()
|
|
754
|
-
.
|
|
1234
|
+
.describe("Text to send to the emulator via emulated keyboard"),
|
|
1235
|
+
keys: z.array(z.string())
|
|
1236
|
+
.min(1, 'At least one key must be specified')
|
|
1237
|
+
.max(10, 'Too many keys (max 10)')
|
|
1238
|
+
.optional()
|
|
1239
|
+
.describe("Array of key names to press simultaneously (e.g., ['CTRL', 'STOP'])"),
|
|
1240
|
+
holdTime: z.number()
|
|
1241
|
+
.min(10, 'Hold time too short (min 10ms)')
|
|
1242
|
+
.max(5000, 'Hold time too long (max 5000ms)')
|
|
1243
|
+
.optional()
|
|
1244
|
+
.default(100)
|
|
1245
|
+
.describe("Time in milliseconds to hold keys down (default: 100)"),
|
|
1246
|
+
},
|
|
1247
|
+
annotations: {
|
|
1248
|
+
"readOnlyHint": false,
|
|
1249
|
+
"destructiveHint": true,
|
|
1250
|
+
"idempotentHint": false,
|
|
1251
|
+
"openWorldHint": false,
|
|
755
1252
|
},
|
|
756
1253
|
},
|
|
757
1254
|
// Handler for the tool (function to be executed when the tool is called)
|
|
758
|
-
async ({ command, text }) => {
|
|
1255
|
+
async ({ command, text, keys, holdTime }) => {
|
|
759
1256
|
let tclCommand;
|
|
760
1257
|
switch (command) {
|
|
761
1258
|
case "sendText":
|
|
1259
|
+
if (!text) {
|
|
1260
|
+
return getResponseContent([
|
|
1261
|
+
'Error: No text provided for sendText command.'
|
|
1262
|
+
]);
|
|
1263
|
+
}
|
|
762
1264
|
tclCommand = `type "${encodeTypeText(text)}"`;
|
|
763
1265
|
break;
|
|
1266
|
+
case "sendKeyCombo":
|
|
1267
|
+
if (!keys || keys.length === 0) {
|
|
1268
|
+
return getResponseContent([
|
|
1269
|
+
'Error: No keys provided for sendKeyCombo command.'
|
|
1270
|
+
]);
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
tclCommand = buildKeyComboCommand(keys, holdTime || 100);
|
|
1274
|
+
}
|
|
1275
|
+
catch (error) {
|
|
1276
|
+
return getResponseContent([
|
|
1277
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1278
|
+
]);
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
764
1281
|
default:
|
|
765
1282
|
return getResponseContent([
|
|
766
1283
|
`Error: Unknown keyboard command "${command}".`
|
|
@@ -785,10 +1302,16 @@ export async function registerTools(server, emuDirectories) {
|
|
|
785
1302
|
'to_file': take a screenshot and save it to a file, the file name is returned in the response.
|
|
786
1303
|
`),
|
|
787
1304
|
},
|
|
1305
|
+
annotations: {
|
|
1306
|
+
"readOnlyHint": false,
|
|
1307
|
+
"destructiveHint": false,
|
|
1308
|
+
"idempotentHint": false,
|
|
1309
|
+
"openWorldHint": false,
|
|
1310
|
+
},
|
|
788
1311
|
},
|
|
789
1312
|
// Handler for the tool (function to be executed when the tool is called)
|
|
790
1313
|
async ({ command }) => {
|
|
791
|
-
const openmsxCommand = `screenshot -raw -prefix "${path.join(emuDirectories.OPENMSX_SCREENSHOT_DIR, 'mcp_')}"`;
|
|
1314
|
+
const openmsxCommand = `screenshot -raw -doublesize -prefix "${path.join(emuDirectories.OPENMSX_SCREENSHOT_DIR, 'mcp_')}"`;
|
|
792
1315
|
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
793
1316
|
switch (command) {
|
|
794
1317
|
case "as_image":
|
|
@@ -845,6 +1368,12 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
845
1368
|
.default("screendump")
|
|
846
1369
|
.describe("Screendump filename (without path nor extension) to save the screendump; default is 'screendump'"),
|
|
847
1370
|
},
|
|
1371
|
+
annotations: {
|
|
1372
|
+
"readOnlyHint": false,
|
|
1373
|
+
"destructiveHint": true,
|
|
1374
|
+
"idempotentHint": false,
|
|
1375
|
+
"openWorldHint": false,
|
|
1376
|
+
},
|
|
848
1377
|
},
|
|
849
1378
|
// Handler for the tool (function to be executed when the tool is called)
|
|
850
1379
|
async ({ scrbasename }) => {
|
|
@@ -893,6 +1422,21 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
893
1422
|
.optional()
|
|
894
1423
|
.describe("End line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
|
|
895
1424
|
},
|
|
1425
|
+
outputSchema: {
|
|
1426
|
+
command: z.string().describe("The command that was executed."),
|
|
1427
|
+
available: z.boolean().optional()
|
|
1428
|
+
.describe("Whether BASIC mode is available. Present for 'isBasicAvailable'."),
|
|
1429
|
+
program: z.string().optional()
|
|
1430
|
+
.describe("BASIC program text. Present for 'getFullProgram' and 'getFullProgramAdvanced'."),
|
|
1431
|
+
result: z.string().optional()
|
|
1432
|
+
.describe("Generic result or status message."),
|
|
1433
|
+
},
|
|
1434
|
+
annotations: {
|
|
1435
|
+
"readOnlyHint": false,
|
|
1436
|
+
"destructiveHint": true,
|
|
1437
|
+
"idempotentHint": false,
|
|
1438
|
+
"openWorldHint": false,
|
|
1439
|
+
},
|
|
896
1440
|
},
|
|
897
1441
|
// Handler for the tool (function to be executed when the tool is called)
|
|
898
1442
|
async ({ command, program, startLine, endLine }) => {
|
|
@@ -978,9 +1522,32 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
978
1522
|
if (response === undefined && tclCommand) {
|
|
979
1523
|
response = await openMSXInstance.sendCommand(tclCommand);
|
|
980
1524
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1525
|
+
if (response === undefined) {
|
|
1526
|
+
return { content: [{ type: "text", text: `Error: No response for command "${command}".` }], isError: true };
|
|
1527
|
+
}
|
|
1528
|
+
if (isErrorResponse(response)) {
|
|
1529
|
+
return { content: [{ type: "text", text: response }], isError: true };
|
|
1530
|
+
}
|
|
1531
|
+
let structuredContent;
|
|
1532
|
+
switch (command) {
|
|
1533
|
+
case "isBasicAvailable": {
|
|
1534
|
+
structuredContent = { command, available: response === "true" };
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
case "getFullProgram":
|
|
1538
|
+
case "getFullProgramAdvanced": {
|
|
1539
|
+
structuredContent = { command, program: response };
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1542
|
+
default: {
|
|
1543
|
+
structuredContent = { command, result: response || "Ok" };
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
content: [{ type: "text", text: response || "Ok" }],
|
|
1548
|
+
structuredContent,
|
|
1549
|
+
isError: false,
|
|
1550
|
+
};
|
|
984
1551
|
});
|
|
985
1552
|
server.registerTool(
|
|
986
1553
|
// Name of the tool (used to call it)
|
|
@@ -999,15 +1566,21 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
999
1566
|
.max(100, 'Query string too long')
|
|
1000
1567
|
.describe("Query string to search in the Vector DB resources, case-insensitive and may contain spaces."),
|
|
1001
1568
|
},
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1569
|
+
outputSchema: {
|
|
1570
|
+
results: z.array(z.object({
|
|
1571
|
+
score: z.string().describe("Proximity score of the result to the query, higher is better."),
|
|
1572
|
+
title: z.string().describe("Title of the resource."),
|
|
1573
|
+
uri: z.string().describe("URI of the resource, which can be used to access the resource."),
|
|
1574
|
+
document: z.string().describe("Document chunk of the resource, retrieved from the Vector DB."),
|
|
1575
|
+
id: z.string().describe("Unique resource chunk ID, used internally by the Vector DB."),
|
|
1576
|
+
}))
|
|
1577
|
+
},
|
|
1578
|
+
annotations: {
|
|
1579
|
+
"readOnlyHint": true,
|
|
1580
|
+
"destructiveHint": false,
|
|
1581
|
+
"idempotentHint": true,
|
|
1582
|
+
"openWorldHint": false,
|
|
1583
|
+
},
|
|
1011
1584
|
},
|
|
1012
1585
|
// Handler for the tool (function to be executed when the tool is called)
|
|
1013
1586
|
async ({ query }) => {
|
|
@@ -1017,7 +1590,7 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
1017
1590
|
type: "text",
|
|
1018
1591
|
text: JSON.stringify(results),
|
|
1019
1592
|
}],
|
|
1020
|
-
|
|
1593
|
+
structuredContent: { results },
|
|
1021
1594
|
isError: false,
|
|
1022
1595
|
};
|
|
1023
1596
|
});
|
|
@@ -1035,6 +1608,12 @@ The response is the list of the top 10 result resources that match the query, in
|
|
|
1035
1608
|
resourceName: z.enum(getRegisteredResourcesList().map(res => res.resource.name))
|
|
1036
1609
|
.describe("Name of the resource to obtain, e.g. 'msxdocs_programming_interrupts'"),
|
|
1037
1610
|
},
|
|
1611
|
+
annotations: {
|
|
1612
|
+
"readOnlyHint": true,
|
|
1613
|
+
"destructiveHint": false,
|
|
1614
|
+
"idempotentHint": true,
|
|
1615
|
+
"openWorldHint": true,
|
|
1616
|
+
},
|
|
1038
1617
|
},
|
|
1039
1618
|
// Handler for the tool (function to be executed when the tool is called)
|
|
1040
1619
|
async ({ resourceName }, extra) => {
|