@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.
@@ -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
- result = await openMSXInstance.emu_launch(emuDirectories.OPENMSX_EXECUTABLE, machine || "", extensions || []);
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
- await new Promise(resolve => setTimeout(resolve, seconds * 1000));
95
- result = `Waited for ${seconds} seconds.`;
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
- // Return result with proper format for MCP
102
- return getResponseContent([
103
- result
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 tclCommand;
314
+ let response;
203
315
  switch (command) {
204
316
  case "getStatus":
205
- return getResponseContent([
206
- await openMSXInstance.emu_status()
207
- ]);
317
+ response = await openMSXInstance.emu_status();
318
+ break;
208
319
  case "getSlotsMap":
209
- tclCommand = "slotmap";
320
+ response = await openMSXInstance.sendCommand("slotmap");
210
321
  break;
211
322
  case "getIOPortsMap":
212
- tclCommand = "iomap";
323
+ response = await openMSXInstance.sendCommand("iomap");
213
324
  break;
214
325
  default:
215
- return getResponseContent([
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
- const response = await openMSXInstance.sendCommand(tclCommand);
220
- return getResponseContent([
221
- response
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 response = await openMSXInstance.sendCommand('get_screen');
274
- return isErrorResponse(response) ?
275
- getResponseContent([response]) :
276
- getResponseContent(["The screen text is:", response]);
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 getResponseContent([
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
- return getResponseContent([
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 getResponseContent([
414
- `Error: Unknown memory command "${command}".`
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 getResponseContent([
419
- response
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 getResponseContent([
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
- return getResponseContent([
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 getResponseContent([
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
- return getResponseContent([
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 getResponseContent([
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
- return getResponseContent([
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 getResponseContent([
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
- return getResponseContent([
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 a text to the openMSX emulator.",
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
- .default('').describe("Text to send to the emulator via emulated keyboard"),
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
- return getResponseContent([
982
- response !== undefined ? response : `Error: No response for command "${command}".`
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
- // outputSchema: {
1003
- // results: z.array(z.object({
1004
- // score: z.number().describe("Proximity score of the result to the query, higher is better."),
1005
- // title: z.string().describe("Title of the resource."),
1006
- // uri: z.string().describe("URI of the resource, which can be used to access the resource."),
1007
- // document: z.string().describe("Document chunk of the resource, retrieved from the Vector DB."),
1008
- // id: z.string().describe("Unique resource chunk ID, used internally by the Vector DB."),
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
- results: results,
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) => {