@nataliapc/mcp-openmsx 1.2.3 → 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 +29 -1075
- package/dist/server_elicitations.js +178 -0
- package/dist/server_prompts.js +69 -0
- package/dist/server_resources.js +149 -0
- package/dist/server_sampling.js +35 -0
- package/dist/server_tools.js +1674 -0
- package/dist/utils.js +229 -0
- package/package.json +12 -11
|
@@ -0,0 +1,1674 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { openMSXInstance } from "./openmsx.js";
|
|
5
|
+
import { VectorDB } from "./vectordb.js";
|
|
6
|
+
import { encodeTypeText, buildKeyComboCommand, isErrorResponse, getResponseContent, parseCpuRegs, is16bitRegister, parseVdpRegs, parsePalette, parseBreakpoints, parseReplayStatus, sleepWithAbort } from "./utils.js";
|
|
7
|
+
import { getRegisteredResourcesList } from "./server_resources.js";
|
|
8
|
+
import { resolveLaunchParams } from "./server_elicitations.js";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Tools available in the MCP server
|
|
11
|
+
// https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
|
|
12
|
+
export async function registerTools(server, emuDirectories) {
|
|
13
|
+
server.registerTool(
|
|
14
|
+
// Name of the tool (used to call it)
|
|
15
|
+
"emu_control", {
|
|
16
|
+
title: "Emulator control tools",
|
|
17
|
+
// Description of the tool (what it does)
|
|
18
|
+
description: "Tools to control an openMSX emulator.",
|
|
19
|
+
// Schema for the tool (input validation)
|
|
20
|
+
inputSchema: {
|
|
21
|
+
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed",
|
|
22
|
+
"machineList", "extensionList", "wait"])
|
|
23
|
+
.describe(`Available commands:
|
|
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. " +
|
|
25
|
+
'close': closes the openMSX emulator.
|
|
26
|
+
'powerOn': powers on the openMSX emulator.
|
|
27
|
+
'powerOff': powers off the openMSX emulator.
|
|
28
|
+
'reset': resets the current machine.
|
|
29
|
+
'getEmulatorSpeed': gets the current emulator speed as a percentage, default is 100.
|
|
30
|
+
'setEmulatorSpeed <emuspeed>': sets the emulator speed as a percentage, valid values are 1-10000, default is 100.
|
|
31
|
+
'machineList': gets a list of all available MSX machines that can be emulated with openMSX.
|
|
32
|
+
'extensionList': gets a list of all available MSX extensions that can be used with openMSX.
|
|
33
|
+
'wait <seconds>': performs a wait for the specified number of seconds, default is 3.
|
|
34
|
+
`),
|
|
35
|
+
machine: z.string()
|
|
36
|
+
.max(100, 'Machine name too long')
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Machine name to launch; valid names can be obtained using [machineList]. Used by [launch]."),
|
|
39
|
+
extensions: z.array(z.string()
|
|
40
|
+
.min(1, 'Extension name cannot be empy')
|
|
41
|
+
.max(100, 'Extension name too long'))
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("List of extensions to use; valid extensions can be obtained using [extensionList]. Used by [launch]."),
|
|
44
|
+
emuspeed: z.number()
|
|
45
|
+
.min(1, 'Emulator speed too low')
|
|
46
|
+
.max(10000, 'Emulator speed too high')
|
|
47
|
+
.optional()
|
|
48
|
+
.default(100)
|
|
49
|
+
.describe("Emulator speed as a percentage (1-10000); default is 100. Used by [setEmulatorSpeed]."),
|
|
50
|
+
seconds: z.number()
|
|
51
|
+
.min(1, 'Minimum wait time too short')
|
|
52
|
+
.max(10, 'Maximum wait time too long')
|
|
53
|
+
.optional()
|
|
54
|
+
.default(3)
|
|
55
|
+
.describe("Number of seconds to wait; default is 3. Used by [wait]."),
|
|
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
|
+
},
|
|
80
|
+
},
|
|
81
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
82
|
+
async ({ command, machine, extensions, emuspeed, seconds }, extra) => {
|
|
83
|
+
let result = '';
|
|
84
|
+
switch (command) {
|
|
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);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "close":
|
|
97
|
+
result = await openMSXInstance.emu_close();
|
|
98
|
+
break;
|
|
99
|
+
case "powerOn":
|
|
100
|
+
result = await openMSXInstance.sendCommand('set power on');
|
|
101
|
+
result = result === "true" ? "openMSX emulator powered on" : "Error: " + result;
|
|
102
|
+
break;
|
|
103
|
+
case "powerOff":
|
|
104
|
+
result = await openMSXInstance.sendCommand('set power off');
|
|
105
|
+
result = result === "false" ? "openMSX emulator powered off" : "Error: " + result;
|
|
106
|
+
break;
|
|
107
|
+
case "reset":
|
|
108
|
+
result = await openMSXInstance.sendCommand('reset');
|
|
109
|
+
result = result === "" ? "openMSX emulator reset successful" : "Error: " + result;
|
|
110
|
+
break;
|
|
111
|
+
case 'getEmulatorSpeed':
|
|
112
|
+
result = await openMSXInstance.sendCommand('set speed');
|
|
113
|
+
result = !isNaN(Number(result)) ? `Current emulator speed is ${result}%` : "Error: " + result;
|
|
114
|
+
break;
|
|
115
|
+
case 'setEmulatorSpeed':
|
|
116
|
+
result = await openMSXInstance.sendCommand(`set speed ${emuspeed}`);
|
|
117
|
+
result = !isNaN(Number(result)) ? `Emulator speed set to ${emuspeed}%` : "Error: " + result;
|
|
118
|
+
break;
|
|
119
|
+
case "machineList":
|
|
120
|
+
result = await openMSXInstance.getMachineList(emuDirectories.MACHINES_DIR);
|
|
121
|
+
break;
|
|
122
|
+
case "extensionList":
|
|
123
|
+
result = await openMSXInstance.getExtensionList(emuDirectories.EXTENSIONS_DIR);
|
|
124
|
+
break;
|
|
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
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
result = `Error: Unknown command "${command}".`;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
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
|
+
};
|
|
195
|
+
});
|
|
196
|
+
server.registerTool(
|
|
197
|
+
// Name of the tool (used to call it)
|
|
198
|
+
"emu_media", {
|
|
199
|
+
title: "Emulator media tools",
|
|
200
|
+
// Description of the tool (what it does)
|
|
201
|
+
description: "Manage tapes, rom cartridges, and floppy disks.",
|
|
202
|
+
// Schema for the tool (input validation)
|
|
203
|
+
inputSchema: {
|
|
204
|
+
command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert",
|
|
205
|
+
"diskInsertFolder", "diskEject"])
|
|
206
|
+
.describe(`Available commands:
|
|
207
|
+
'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx).
|
|
208
|
+
'tapeRewind': rewind the current tape.
|
|
209
|
+
'tapeEject': remove tape from virtual cassette player.
|
|
210
|
+
'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A.
|
|
211
|
+
'romEject': remove the current ROM cartridge from cartridge slot A.
|
|
212
|
+
'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A.
|
|
213
|
+
'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory.
|
|
214
|
+
'diskEject': remove the current disk from floppy disk A.
|
|
215
|
+
`),
|
|
216
|
+
tapefile: z.string()
|
|
217
|
+
.max(200, 'Tape filename too long')
|
|
218
|
+
.regex(/^.*(\.cas|\.wav|\.tsx)$/gi, 'Tape filename must end with .cas, .wav, or .tsx')
|
|
219
|
+
// check also if command is 'tapeInsert'
|
|
220
|
+
.optional()
|
|
221
|
+
.describe("Absolute Tape filename to insert. Used by [tapeInsert]"),
|
|
222
|
+
romfile: z.string()
|
|
223
|
+
.max(200, 'ROM filename too long')
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("Absolute ROM filename to insert. Used by [romInsert]"),
|
|
226
|
+
diskfile: z.string()
|
|
227
|
+
.max(200, 'Disk filename too long')
|
|
228
|
+
.optional()
|
|
229
|
+
.describe("Absolute Disk filename to insert. Used by [diskInsert]"),
|
|
230
|
+
diskfolder: z.string()
|
|
231
|
+
.max(200, 'Disk folder path too long')
|
|
232
|
+
.optional()
|
|
233
|
+
.describe("Absolute Disk folder filename to insert. Used by [diskInsertFolder]"),
|
|
234
|
+
},
|
|
235
|
+
annotations: {
|
|
236
|
+
"readOnlyHint": true,
|
|
237
|
+
"destructiveHint": false,
|
|
238
|
+
"idempotentHint": false,
|
|
239
|
+
"openWorldHint": false,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
243
|
+
async ({ command, tapefile, romfile, diskfile, diskfolder }) => {
|
|
244
|
+
let tclCommand;
|
|
245
|
+
switch (command) {
|
|
246
|
+
case "tapeInsert":
|
|
247
|
+
tclCommand = `cassetteplayer insert "${tapefile}"`;
|
|
248
|
+
break;
|
|
249
|
+
case "tapeRewind":
|
|
250
|
+
tclCommand = "cassetteplayer rewind";
|
|
251
|
+
break;
|
|
252
|
+
case "tapeEject":
|
|
253
|
+
tclCommand = "cassetteplayer eject";
|
|
254
|
+
break;
|
|
255
|
+
case "romInsert":
|
|
256
|
+
tclCommand = `carta insert "${romfile}"`;
|
|
257
|
+
break;
|
|
258
|
+
case "romEject":
|
|
259
|
+
tclCommand = "carta eject";
|
|
260
|
+
break;
|
|
261
|
+
case "diskInsert":
|
|
262
|
+
tclCommand = `diska insert "${diskfile}"`;
|
|
263
|
+
break;
|
|
264
|
+
case "diskInsertFolder":
|
|
265
|
+
tclCommand = `diska insert "${diskfolder}"`;
|
|
266
|
+
break;
|
|
267
|
+
case "diskEject":
|
|
268
|
+
tclCommand = "diska eject";
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
return getResponseContent([
|
|
272
|
+
`Error: Unknown emulator media command "${command}".`
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
276
|
+
// Return the response from openMSX
|
|
277
|
+
return getResponseContent([
|
|
278
|
+
response
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
server.registerTool(
|
|
282
|
+
// Name of the tool (used to call it)
|
|
283
|
+
"emu_info", {
|
|
284
|
+
title: "Emulator info tools",
|
|
285
|
+
// Description of the tool (what it does)
|
|
286
|
+
description: "Obtain informacion about the current emulated machine.",
|
|
287
|
+
// Schema for the tool (input validation)
|
|
288
|
+
inputSchema: {
|
|
289
|
+
command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]).describe(`Available commands:
|
|
290
|
+
'getStatus': returns the status of the openMSX emulator.
|
|
291
|
+
'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots.
|
|
292
|
+
'getIOPortsMap': shows an overview about the I/O mapped devices.
|
|
293
|
+
`),
|
|
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
|
+
},
|
|
308
|
+
},
|
|
309
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
310
|
+
async ({ command }) => {
|
|
311
|
+
let response;
|
|
312
|
+
switch (command) {
|
|
313
|
+
case "getStatus":
|
|
314
|
+
response = await openMSXInstance.emu_status();
|
|
315
|
+
break;
|
|
316
|
+
case "getSlotsMap":
|
|
317
|
+
response = await openMSXInstance.sendCommand("slotmap");
|
|
318
|
+
break;
|
|
319
|
+
case "getIOPortsMap":
|
|
320
|
+
response = await openMSXInstance.sendCommand("iomap");
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator info command "${command}".` }], isError: true };
|
|
324
|
+
}
|
|
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
|
+
};
|
|
347
|
+
});
|
|
348
|
+
server.registerTool(
|
|
349
|
+
// Name of the tool (used to call it)
|
|
350
|
+
"emu_vdp", {
|
|
351
|
+
title: "VDP tools",
|
|
352
|
+
// Description of the tool (what it does)
|
|
353
|
+
description: "Manage the VDP (Video Display Processor).",
|
|
354
|
+
// Schema for the tool (input validation)
|
|
355
|
+
inputSchema: {
|
|
356
|
+
command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode",
|
|
357
|
+
"screenGetFullText"])
|
|
358
|
+
.describe(`Available commands:
|
|
359
|
+
'getPalette': returns the current V9938/V9958 color palette in RGB333 format.
|
|
360
|
+
'getRegisters': returns all VDP register values.
|
|
361
|
+
'getRegisterValue <register>': returns the value of a specific VDP register (0-31) in decimal format.
|
|
362
|
+
'setRegisterValue <register> <value>': sets a hexadecimal value to a specific VDP register (0-31).
|
|
363
|
+
'screenGetMode': returns the current screen mode (0-12) as a number, which matches the BASIC SCREEN command.
|
|
364
|
+
'screenGetFullText': returns the full content of an MSX text screen (screen 0 or 1) as a string; PRIORITIZE this command to view screen content in text modes.
|
|
365
|
+
`),
|
|
366
|
+
register: z.number()
|
|
367
|
+
.min(0, 'VDP register number too low')
|
|
368
|
+
.max(31, 'VDP register number too high')
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("VDP register number (0-31) to read/write. Used by [getRegisterValue, setRegisterValue]"),
|
|
371
|
+
value: z.string()
|
|
372
|
+
.regex(/^0x[0-9a-fA-F]{2}$/, 'Value must be a 2 digits hexadecimal number')
|
|
373
|
+
.optional()
|
|
374
|
+
.describe("2 hexadecimal digits for a VDP register value (e.g. 0x1f). Used by [setRegisterValue]"),
|
|
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
|
+
},
|
|
407
|
+
},
|
|
408
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
409
|
+
async ({ command, register, value }) => {
|
|
410
|
+
let tclCommand;
|
|
411
|
+
switch (command) {
|
|
412
|
+
case "getPalette":
|
|
413
|
+
tclCommand = "palette";
|
|
414
|
+
break;
|
|
415
|
+
case "getRegisters":
|
|
416
|
+
tclCommand = "vdpregs";
|
|
417
|
+
break;
|
|
418
|
+
case "getRegisterValue":
|
|
419
|
+
tclCommand = `vdpreg ${register}`;
|
|
420
|
+
break;
|
|
421
|
+
case "setRegisterValue":
|
|
422
|
+
tclCommand = `vdpreg ${register} ${value}`;
|
|
423
|
+
break;
|
|
424
|
+
case "screenGetMode":
|
|
425
|
+
tclCommand = "get_screen_mode";
|
|
426
|
+
break;
|
|
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
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
return { content: [{ type: "text", text: `Error: Unknown emulator vdp command "${command}".` }], isError: true };
|
|
440
|
+
}
|
|
441
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
479
|
+
});
|
|
480
|
+
server.registerTool(
|
|
481
|
+
// Name of the tool (used to call it)
|
|
482
|
+
"debug_run", {
|
|
483
|
+
title: "CPU Runtime Debugger tools",
|
|
484
|
+
// Description of the tool (what it does)
|
|
485
|
+
description: "Control execution (break, continue, step).",
|
|
486
|
+
// Schema for the tool (input validation)
|
|
487
|
+
inputSchema: {
|
|
488
|
+
command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver",
|
|
489
|
+
"stepBack", "runTo"])
|
|
490
|
+
.describe(`Available commands:
|
|
491
|
+
'break': to break CPU (pause emulation) at current execution position.
|
|
492
|
+
'isBreaked': to check if the CPU is currently in break state (1) or not (0).
|
|
493
|
+
'continue': to continue execution after break.
|
|
494
|
+
'stepIn': to execute one CPU instruction, go into subroutines.
|
|
495
|
+
'stepOver': to execute one CPU instruction, but don't go into subroutines.
|
|
496
|
+
'stepOut': to step out of the current subroutine.
|
|
497
|
+
'stepBack': to step one instruction back in time.
|
|
498
|
+
'runTo <address>': to run the CPU until it reaches the specified address.
|
|
499
|
+
**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
|
|
500
|
+
`),
|
|
501
|
+
address: z.string()
|
|
502
|
+
.regex(/^0x[0-9a-fA-F]{4}$/, 'Address must be a 4 digits hexadecimal number')
|
|
503
|
+
.optional()
|
|
504
|
+
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [runTo]"),
|
|
505
|
+
},
|
|
506
|
+
annotations: {
|
|
507
|
+
"readOnlyHint": true,
|
|
508
|
+
"destructiveHint": false,
|
|
509
|
+
"idempotentHint": false,
|
|
510
|
+
"openWorldHint": false,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
514
|
+
async ({ command, address }) => {
|
|
515
|
+
let tclCommand;
|
|
516
|
+
switch (command) {
|
|
517
|
+
case "break":
|
|
518
|
+
tclCommand = "debug break";
|
|
519
|
+
break;
|
|
520
|
+
case "isBreaked":
|
|
521
|
+
tclCommand = "debug breaked";
|
|
522
|
+
break;
|
|
523
|
+
case "continue":
|
|
524
|
+
tclCommand = "debug cont";
|
|
525
|
+
break;
|
|
526
|
+
case "stepIn":
|
|
527
|
+
tclCommand = "step_in";
|
|
528
|
+
break;
|
|
529
|
+
case "stepOver":
|
|
530
|
+
tclCommand = "step_over";
|
|
531
|
+
break;
|
|
532
|
+
case "stepOut":
|
|
533
|
+
tclCommand = "step_out";
|
|
534
|
+
break;
|
|
535
|
+
case "stepBack":
|
|
536
|
+
tclCommand = "step_back";
|
|
537
|
+
break;
|
|
538
|
+
case "runTo":
|
|
539
|
+
tclCommand = `run_to ${address}`;
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
return getResponseContent([
|
|
543
|
+
`Error: Unknown debug command "${command}".`
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
547
|
+
//TODO: parse disassembly command response into structured content
|
|
548
|
+
return getResponseContent([
|
|
549
|
+
response
|
|
550
|
+
]);
|
|
551
|
+
});
|
|
552
|
+
server.registerTool(
|
|
553
|
+
// Name of the tool (used to call it)
|
|
554
|
+
"debug_cpu", {
|
|
555
|
+
title: "CPU tools",
|
|
556
|
+
// Description of the tool (what it does)
|
|
557
|
+
description: "Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory.",
|
|
558
|
+
// Schema for the tool (input validation)
|
|
559
|
+
inputSchema: {
|
|
560
|
+
command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble",
|
|
561
|
+
"getActiveCpu"])
|
|
562
|
+
.describe(`Available commands:
|
|
563
|
+
'getCpuRegisters': to get an overview of all the CPU registers.
|
|
564
|
+
'getRegister <register>': to get the decimal value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff).
|
|
565
|
+
'setRegister <register> <value>': to set the value of a specific CPU register (pc, sp, ix, iy, af, bc, de, hl, ixh, ixl, iyh, iyl, a, f, b, c, d, e, h, l, i, r, im, iff).
|
|
566
|
+
'getStackPile': to get an overview of the CPU stack.
|
|
567
|
+
'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty.
|
|
568
|
+
'getActiveCpu': to return the active cpu: z80 or r800.
|
|
569
|
+
"**Important Note**: Addresses and values are in hexadecimal format (e.g. 0xd2 0x3af2)."
|
|
570
|
+
`),
|
|
571
|
+
register: z.enum(["pc", "sp", "ix", "iy", "af", "bc", "de", "hl", "ixh", "ixl", "iyh", "iyl",
|
|
572
|
+
"af'", "bc'", "de'", "hl'",
|
|
573
|
+
"a", "f", "b", "c", "d", "e", "h", "l", "i", "r", "im", "iff"])
|
|
574
|
+
.optional()
|
|
575
|
+
.describe("CPU register to read/write. Used by [getRegister, setRegister]"),
|
|
576
|
+
address: z.string()
|
|
577
|
+
.regex(/^0x[0-9a-fA-F]{4}$/, 'Address must be a 4 digits hexadecimal number')
|
|
578
|
+
.optional()
|
|
579
|
+
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [disassemble]"),
|
|
580
|
+
value: z.string()
|
|
581
|
+
.regex(/^0x[0-9a-fA-F]{2,4}$/, 'Value must be a 2-4 digits hexadecimal number')
|
|
582
|
+
.optional()
|
|
583
|
+
.describe("2-4 hexadecimal digits for a byte value (e.g. 0xa5 or 0xa5b1). Used by [setRegister]"),
|
|
584
|
+
size: z.number()
|
|
585
|
+
.min(8, 'Minimum disassemble size too small')
|
|
586
|
+
.max(50, 'Maximum disassemble size too large')
|
|
587
|
+
.optional()
|
|
588
|
+
.describe("Number of bytes to disassemble. Used by [disassemble]"),
|
|
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
|
+
},
|
|
619
|
+
},
|
|
620
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
621
|
+
async ({ command, address, register, value, size }) => {
|
|
622
|
+
let tclCommand;
|
|
623
|
+
switch (command) {
|
|
624
|
+
case "getCpuRegisters":
|
|
625
|
+
tclCommand = "cpuregs";
|
|
626
|
+
break;
|
|
627
|
+
case "getRegister":
|
|
628
|
+
tclCommand = `reg ${register}`;
|
|
629
|
+
break;
|
|
630
|
+
case "setRegister":
|
|
631
|
+
tclCommand = `reg ${register} ${value}`;
|
|
632
|
+
break;
|
|
633
|
+
case "getStackPile":
|
|
634
|
+
tclCommand = "stack";
|
|
635
|
+
break;
|
|
636
|
+
case "disassemble":
|
|
637
|
+
tclCommand = `disasm ${address || ""} ${size || ""}`;
|
|
638
|
+
break;
|
|
639
|
+
case "getActiveCpu":
|
|
640
|
+
tclCommand = "get_active_cpu";
|
|
641
|
+
break;
|
|
642
|
+
default:
|
|
643
|
+
return {
|
|
644
|
+
content: [{ type: "text", text: `Error: Unknown command "${command}".` }],
|
|
645
|
+
isError: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
697
|
+
});
|
|
698
|
+
server.registerTool(
|
|
699
|
+
// Name of the tool (used to call it)
|
|
700
|
+
"debug_memory", {
|
|
701
|
+
title: "Memory tools",
|
|
702
|
+
// Description of the tool (what it does)
|
|
703
|
+
description: "Slots info, and Read/write from/to memory in the openMSX emulator.",
|
|
704
|
+
// Schema for the tool (input validation)
|
|
705
|
+
inputSchema: {
|
|
706
|
+
command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord"])
|
|
707
|
+
.describe(`Available commands:
|
|
708
|
+
'selectedSlots': to get a list of the currently selected memory slots.
|
|
709
|
+
'getBlock <address> [lines]': to read a block of memory from the specified address.
|
|
710
|
+
'readByte <address>': to read a BYTE from the specified address.
|
|
711
|
+
'readWord <address>': to read a WORD from the specified address.
|
|
712
|
+
'writeByte <address> <value8>': to write a BYTE to the specified address.
|
|
713
|
+
'writeWord <address> <value16>': to write a WORD to the specified address.
|
|
714
|
+
**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
|
|
715
|
+
`),
|
|
716
|
+
address: z.string()
|
|
717
|
+
.regex(/^0x[0-9a-fA-F]{4}$/, 'Address must be a 4 digits hexadecimal number')
|
|
718
|
+
.optional()
|
|
719
|
+
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [getBlock, readByte, writeByte, readWord, writeWord]"),
|
|
720
|
+
lines: z.number()
|
|
721
|
+
.min(1, 'Minimum number of lines too low')
|
|
722
|
+
.max(50, 'Maximum number of lines too high')
|
|
723
|
+
.optional()
|
|
724
|
+
.default(8)
|
|
725
|
+
.describe("Number of lines to obtain. Used by [getBlock]"),
|
|
726
|
+
value8: z.string()
|
|
727
|
+
.regex(/^0x[0-9a-fA-F]{2}$/, 'Must be a 2 digits hexadecimal number')
|
|
728
|
+
.optional()
|
|
729
|
+
.describe("2 hexadecimal digits for a byte value (e.g. 0xa5). Used by [writeByte]"),
|
|
730
|
+
value16: z.string()
|
|
731
|
+
.regex(/^0x[0-9a-fA-F]{4}$/, 'Must be a 4 digits hexadecimal number')
|
|
732
|
+
.optional()
|
|
733
|
+
.describe("4 hexadecimal digits for a word value (e.g. 0xa5b1). Used by [writeWord]"),
|
|
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
|
+
},
|
|
758
|
+
},
|
|
759
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
760
|
+
async ({ command, address, lines, value8, value16 }) => {
|
|
761
|
+
let tclCommand;
|
|
762
|
+
switch (command) {
|
|
763
|
+
case "selectedSlots":
|
|
764
|
+
tclCommand = "slotselect";
|
|
765
|
+
break;
|
|
766
|
+
case "getBlock":
|
|
767
|
+
tclCommand = `showmem ${address} ${lines}`;
|
|
768
|
+
break;
|
|
769
|
+
case "readByte":
|
|
770
|
+
tclCommand = `peek ${address}`;
|
|
771
|
+
break;
|
|
772
|
+
case "readWord":
|
|
773
|
+
tclCommand = `peek16 ${address}`;
|
|
774
|
+
break;
|
|
775
|
+
case "writeByte":
|
|
776
|
+
tclCommand = `poke ${address} ${value8}`;
|
|
777
|
+
break;
|
|
778
|
+
case "writeWord":
|
|
779
|
+
tclCommand = `poke16 ${address} ${value16}`;
|
|
780
|
+
break;
|
|
781
|
+
default:
|
|
782
|
+
return { content: [{ type: "text", text: `Error: Unknown memory command "${command}".` }], isError: true };
|
|
783
|
+
}
|
|
784
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
825
|
+
});
|
|
826
|
+
server.registerTool(
|
|
827
|
+
// Name of the tool (used to call it)
|
|
828
|
+
"debug_vram", {
|
|
829
|
+
title: "VRAM tools",
|
|
830
|
+
// Description of the tool (what it does)
|
|
831
|
+
description: "Read or write from/to VRAM video memory from the openMSX emulator.",
|
|
832
|
+
// Schema for the tool (input validation)
|
|
833
|
+
inputSchema: {
|
|
834
|
+
command: z.enum(["getBlock", "readByte", "writeByte"])
|
|
835
|
+
.describe(`Available commands:
|
|
836
|
+
'getBlock <address> [lines]': to read a block of VRAM memory from the specified address.
|
|
837
|
+
'readByte <address>': to read a BYTE from the specified VRAM address.
|
|
838
|
+
'writeByte <address> <value8>': to write a BYTE to the specified VRAM address.
|
|
839
|
+
**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x0000).
|
|
840
|
+
`),
|
|
841
|
+
address: z.string()
|
|
842
|
+
.regex(/^0x[0-9a-fA-F]{5}$/, 'Address must be a 5 digits hexadecimal number')
|
|
843
|
+
.optional()
|
|
844
|
+
.describe("5 hexadecimal digits for a VRAM address (e.g. 0x04af3). Used by [getBlock, readByte, writeByte]"),
|
|
845
|
+
lines: z.number()
|
|
846
|
+
.min(1, 'Minimum number of lines too low')
|
|
847
|
+
.max(50, 'Maximum number of lines too high')
|
|
848
|
+
.optional()
|
|
849
|
+
.default(8)
|
|
850
|
+
.describe("Number of lines to obtain. Used by [getBlock]"),
|
|
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]"),
|
|
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
|
+
},
|
|
874
|
+
},
|
|
875
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
876
|
+
async ({ command, address, lines, value8 }) => {
|
|
877
|
+
let tclCommand;
|
|
878
|
+
switch (command) {
|
|
879
|
+
case "getBlock":
|
|
880
|
+
tclCommand = `showdebuggable VRAM ${address} ${lines}`;
|
|
881
|
+
break;
|
|
882
|
+
case "readByte":
|
|
883
|
+
tclCommand = `vpeek ${address}`;
|
|
884
|
+
break;
|
|
885
|
+
case "writeByte":
|
|
886
|
+
tclCommand = `vpoke ${address} ${value8}`;
|
|
887
|
+
break;
|
|
888
|
+
default:
|
|
889
|
+
return { content: [{ type: "text", text: `Error: Unknown video memory command "${command}".` }], isError: true };
|
|
890
|
+
}
|
|
891
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
918
|
+
});
|
|
919
|
+
server.registerTool(
|
|
920
|
+
// Name of the tool (used to call it)
|
|
921
|
+
"debug_breakpoints", {
|
|
922
|
+
title: "Breakpoints tools",
|
|
923
|
+
// Description of the tool (what it does)
|
|
924
|
+
description: "Create, remove, and list breakpoints.",
|
|
925
|
+
// Schema for the tool (input validation)
|
|
926
|
+
inputSchema: {
|
|
927
|
+
command: z.enum(["create", "remove", "list"])
|
|
928
|
+
.describe(`Available commands:
|
|
929
|
+
'create <address>': create a breakpoint at a specified address, and returns its name.
|
|
930
|
+
'remove <bpname>': remove a breakpoint by name (e.g. bp#1).
|
|
931
|
+
'list': enumerate the active breakpoints.
|
|
932
|
+
"**Important Note**: Addresses and values are in hexadecimal format (e.g. 0x4af3).
|
|
933
|
+
"**Important Note**: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.
|
|
934
|
+
`),
|
|
935
|
+
address: z.string()
|
|
936
|
+
.regex(/^0x[0-9a-fA-F]{4}$/, 'Address must be a 4 digits hexadecimal number')
|
|
937
|
+
.optional()
|
|
938
|
+
.describe("4 hexadecimal digits for a memory address (e.g. 0x4af3). Used by [create]"),
|
|
939
|
+
bpname: z.string()
|
|
940
|
+
.min(3, 'Breakpoint name too short')
|
|
941
|
+
.max(10, 'Breakpoint name too long')
|
|
942
|
+
.optional()
|
|
943
|
+
.describe("Breakpoint name (e.g. bp#1). Used by [remove]"),
|
|
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
|
+
},
|
|
968
|
+
},
|
|
969
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
970
|
+
async ({ command, address, bpname }) => {
|
|
971
|
+
let tclCommand;
|
|
972
|
+
switch (command) {
|
|
973
|
+
case "create":
|
|
974
|
+
tclCommand = `debug set_bp ${address}`;
|
|
975
|
+
break;
|
|
976
|
+
case "remove":
|
|
977
|
+
tclCommand = `debug remove_bp ${bpname}`;
|
|
978
|
+
break;
|
|
979
|
+
case "list":
|
|
980
|
+
tclCommand = 'debug list_bp';
|
|
981
|
+
break;
|
|
982
|
+
default:
|
|
983
|
+
return { content: [{ type: "text", text: `Error: Unknown breakpoint command "${command}".` }], isError: true };
|
|
984
|
+
}
|
|
985
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
1012
|
+
});
|
|
1013
|
+
server.registerTool(
|
|
1014
|
+
// Name of the tool (used to call it)
|
|
1015
|
+
"emu_savestates", {
|
|
1016
|
+
title: "Save states tools",
|
|
1017
|
+
// Description of the tool (what it does)
|
|
1018
|
+
description: "Load, save, and list savestates.",
|
|
1019
|
+
// Schema for the tool (input validation)
|
|
1020
|
+
inputSchema: {
|
|
1021
|
+
command: z.enum(["load", "save", "list"])
|
|
1022
|
+
.describe(`Available commands:
|
|
1023
|
+
'load <name>': restores a previously created savestate.
|
|
1024
|
+
'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate.
|
|
1025
|
+
'list': returns the names of all previously created savestates, separated by spaces.
|
|
1026
|
+
**Important Note**: names with spaces are enclosed in {}.
|
|
1027
|
+
`),
|
|
1028
|
+
name: z.string()
|
|
1029
|
+
.min(1, 'Savestate name too short')
|
|
1030
|
+
.max(50, 'Savestate name too long')
|
|
1031
|
+
.optional()
|
|
1032
|
+
.describe("Name of the savestate to load/save. Used by [load, save]"),
|
|
1033
|
+
},
|
|
1034
|
+
annotations: {
|
|
1035
|
+
"readOnlyHint": false,
|
|
1036
|
+
"destructiveHint": true,
|
|
1037
|
+
"idempotentHint": false,
|
|
1038
|
+
"openWorldHint": false,
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1042
|
+
async ({ command, name }) => {
|
|
1043
|
+
let tclCommand;
|
|
1044
|
+
let textResponse = "Error:";
|
|
1045
|
+
switch (command) {
|
|
1046
|
+
case "load":
|
|
1047
|
+
textResponse = "Loaded savestate: ";
|
|
1048
|
+
tclCommand = `loadstate ${name}`;
|
|
1049
|
+
break;
|
|
1050
|
+
case "save":
|
|
1051
|
+
textResponse = "Saved savestate: ";
|
|
1052
|
+
tclCommand = `savestate ${name}`;
|
|
1053
|
+
break;
|
|
1054
|
+
case "list":
|
|
1055
|
+
textResponse = "Savestate names: ";
|
|
1056
|
+
tclCommand = 'list_savestates';
|
|
1057
|
+
break;
|
|
1058
|
+
default:
|
|
1059
|
+
return getResponseContent([
|
|
1060
|
+
`Error: Unknown savestate command "${command}".`
|
|
1061
|
+
]);
|
|
1062
|
+
}
|
|
1063
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
1064
|
+
return getResponseContent([
|
|
1065
|
+
textResponse,
|
|
1066
|
+
response
|
|
1067
|
+
]);
|
|
1068
|
+
});
|
|
1069
|
+
server.registerTool(
|
|
1070
|
+
// Name of the tool (used to call it)
|
|
1071
|
+
"emu_replay", {
|
|
1072
|
+
title: "Replay tools",
|
|
1073
|
+
// Description of the tool (what it does)
|
|
1074
|
+
description: "When replay is enabled (the default) the emulator collect data while emulating, which enables you to go back and forward in the emulated MSX time.",
|
|
1075
|
+
// Schema for the tool (input validation)
|
|
1076
|
+
inputSchema: {
|
|
1077
|
+
command: z.enum(["start", "stop", "status", "goBack", "absoluteGoto", "truncate", "advanceFrame",
|
|
1078
|
+
"reverseFrame", "saveReplay", "loadReplay"])
|
|
1079
|
+
.describe(`Available commands:
|
|
1080
|
+
'start': starts the replay mode (enabled by default when emulator is launched).
|
|
1081
|
+
'stop': stops the replay mode.
|
|
1082
|
+
'status': gives information about the replay feature and the data that is collected.
|
|
1083
|
+
'goBack <seconds>': go back specified seconds (1-60) in the timeline, you cannot go back to a time before the time the replay started.
|
|
1084
|
+
'absoluteGoto <time>': go to the indicated absolute time in seconds in the MSX timeline, if time is before replay started it will jump to the time when is started.
|
|
1085
|
+
'truncate': stop replaying and wipe all the future replay data after now.
|
|
1086
|
+
'advanceFrame' [frames]: advances a number of frames in the replay timeline, useful to advance the timeline while debugging.
|
|
1087
|
+
'reverseFrame' [frames]: reverses a number of frames in the replay timeline, useful to reverse the timeline while debugging.
|
|
1088
|
+
'saveReplay [filename]': saves the current replay data to a file (extension .omr), filename is returned in the response.
|
|
1089
|
+
'loadReplay <filename>': loads a previously saved replay file (extension .omr), starts replaying from the begin, and starts replay mode.
|
|
1090
|
+
**Important Note**: consider do a #debug_run 'break' to maintain the timeline before a 'goBack' or 'absoluteGoto'.
|
|
1091
|
+
`),
|
|
1092
|
+
seconds: z.number()
|
|
1093
|
+
.min(1, 'Minimum seconds too low')
|
|
1094
|
+
.max(60, 'Maximum seconds too high')
|
|
1095
|
+
.optional()
|
|
1096
|
+
.describe("Seconds to go back. Used by [goBack]"),
|
|
1097
|
+
time: z.string()
|
|
1098
|
+
.regex(/^\d+$/, 'Time must be an absolute time in seconds')
|
|
1099
|
+
.optional()
|
|
1100
|
+
.describe("Absolute time in seconds to go to. Used by [absoluteGoto]"),
|
|
1101
|
+
frames: z.number()
|
|
1102
|
+
.min(1, 'Minimum frames too low')
|
|
1103
|
+
.max(1000, 'Maximum frames too high')
|
|
1104
|
+
.optional()
|
|
1105
|
+
.default(1)
|
|
1106
|
+
.describe("Number of frames to advance/reverse. Used by [advanceFrame, reverseFrame]"),
|
|
1107
|
+
filename: z.string()
|
|
1108
|
+
.min(1, 'Filename too short')
|
|
1109
|
+
.max(200, 'Filename too long')
|
|
1110
|
+
.optional()
|
|
1111
|
+
.describe("Filename to save/load replay. Used by [saveReplay, loadReplay]"),
|
|
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
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1140
|
+
async ({ command, seconds, time, frames, filename }) => {
|
|
1141
|
+
let tclCommand;
|
|
1142
|
+
switch (command) {
|
|
1143
|
+
case "start":
|
|
1144
|
+
tclCommand = "reverse start";
|
|
1145
|
+
break;
|
|
1146
|
+
case "stop":
|
|
1147
|
+
tclCommand = "reverse stop";
|
|
1148
|
+
break;
|
|
1149
|
+
case "status":
|
|
1150
|
+
tclCommand = "reverse status";
|
|
1151
|
+
break;
|
|
1152
|
+
case "goBack":
|
|
1153
|
+
tclCommand = `reverse goback ${seconds}`;
|
|
1154
|
+
break;
|
|
1155
|
+
case "absoluteGoto":
|
|
1156
|
+
tclCommand = `reverse goto ${time}`;
|
|
1157
|
+
break;
|
|
1158
|
+
case "truncate":
|
|
1159
|
+
tclCommand = "reverse truncatereplay";
|
|
1160
|
+
break;
|
|
1161
|
+
case "advanceFrame":
|
|
1162
|
+
tclCommand = `advance_frame ${frames}`;
|
|
1163
|
+
break;
|
|
1164
|
+
case "reverseFrame":
|
|
1165
|
+
tclCommand = `reverse_frame ${frames}`;
|
|
1166
|
+
break;
|
|
1167
|
+
case "saveReplay":
|
|
1168
|
+
if (filename)
|
|
1169
|
+
filename = path.join(emuDirectories.OPENMSX_REPLAYS_DIR, filename);
|
|
1170
|
+
tclCommand = `reverse savereplay ${filename || ''}`;
|
|
1171
|
+
break;
|
|
1172
|
+
case "loadReplay":
|
|
1173
|
+
if (filename)
|
|
1174
|
+
filename = path.join(emuDirectories.OPENMSX_REPLAYS_DIR, filename);
|
|
1175
|
+
tclCommand = `reverse loadreplay ${filename}`;
|
|
1176
|
+
break;
|
|
1177
|
+
default:
|
|
1178
|
+
return { content: [{ type: "text", text: `Error: Unknown replay command "${command}".` }], isError: true };
|
|
1179
|
+
}
|
|
1180
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
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
|
+
};
|
|
1212
|
+
});
|
|
1213
|
+
server.registerTool(
|
|
1214
|
+
// Name of the tool (used to call it)
|
|
1215
|
+
"emu_keyboard", {
|
|
1216
|
+
title: "Keyboard tools",
|
|
1217
|
+
// Description of the tool (what it does)
|
|
1218
|
+
description: "Send text or key combinations to the openMSX emulator.",
|
|
1219
|
+
// Schema for the tool (input validation)
|
|
1220
|
+
inputSchema: {
|
|
1221
|
+
command: z.enum(["sendText", "sendKeyCombo"])
|
|
1222
|
+
.describe(`Available commands:
|
|
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).
|
|
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.
|
|
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.
|
|
1229
|
+
`),
|
|
1230
|
+
text: z.string()
|
|
1231
|
+
.min(1, 'Text to send is too short')
|
|
1232
|
+
.max(200, 'Text to send is too long')
|
|
1233
|
+
.optional()
|
|
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,
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1255
|
+
async ({ command, text, keys, holdTime }) => {
|
|
1256
|
+
let tclCommand;
|
|
1257
|
+
switch (command) {
|
|
1258
|
+
case "sendText":
|
|
1259
|
+
if (!text) {
|
|
1260
|
+
return getResponseContent([
|
|
1261
|
+
'Error: No text provided for sendText command.'
|
|
1262
|
+
]);
|
|
1263
|
+
}
|
|
1264
|
+
tclCommand = `type "${encodeTypeText(text)}"`;
|
|
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;
|
|
1281
|
+
default:
|
|
1282
|
+
return getResponseContent([
|
|
1283
|
+
`Error: Unknown keyboard command "${command}".`
|
|
1284
|
+
]);
|
|
1285
|
+
}
|
|
1286
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
1287
|
+
return getResponseContent([
|
|
1288
|
+
response
|
|
1289
|
+
]);
|
|
1290
|
+
});
|
|
1291
|
+
server.registerTool(
|
|
1292
|
+
// Name of the tool (used to call it)
|
|
1293
|
+
"screen_shot", {
|
|
1294
|
+
title: "Screenshot tools",
|
|
1295
|
+
// Description of the tool (what it does)
|
|
1296
|
+
description: "Take a screenshot of the openMSX emulator screen.",
|
|
1297
|
+
// Schema for the tool (input validation)
|
|
1298
|
+
inputSchema: {
|
|
1299
|
+
command: z.enum(["as_image", "to_file"])
|
|
1300
|
+
.describe(`Available commands:
|
|
1301
|
+
'as_image': take a screenshot and the image is returned in the response.
|
|
1302
|
+
'to_file': take a screenshot and save it to a file, the file name is returned in the response.
|
|
1303
|
+
`),
|
|
1304
|
+
},
|
|
1305
|
+
annotations: {
|
|
1306
|
+
"readOnlyHint": false,
|
|
1307
|
+
"destructiveHint": false,
|
|
1308
|
+
"idempotentHint": false,
|
|
1309
|
+
"openWorldHint": false,
|
|
1310
|
+
},
|
|
1311
|
+
},
|
|
1312
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1313
|
+
async ({ command }) => {
|
|
1314
|
+
const openmsxCommand = `screenshot -raw -doublesize -prefix "${path.join(emuDirectories.OPENMSX_SCREENSHOT_DIR, 'mcp_')}"`;
|
|
1315
|
+
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
1316
|
+
switch (command) {
|
|
1317
|
+
case "as_image":
|
|
1318
|
+
try {
|
|
1319
|
+
// Check if the response is a file path
|
|
1320
|
+
if (!response || !response.startsWith(emuDirectories.OPENMSX_SCREENSHOT_DIR) || !response.endsWith('.png')) {
|
|
1321
|
+
throw new Error(`Invalid screenshot "${response}"`);
|
|
1322
|
+
}
|
|
1323
|
+
// Read the screenshot file
|
|
1324
|
+
const imageBuffer = await fs.readFile(response);
|
|
1325
|
+
const base64image = imageBuffer.toString('base64');
|
|
1326
|
+
// Remove the file after reading it
|
|
1327
|
+
await fs.unlink(response);
|
|
1328
|
+
// Return the image in the response
|
|
1329
|
+
return {
|
|
1330
|
+
content: [{
|
|
1331
|
+
type: "text",
|
|
1332
|
+
text: "Screenshot taken successfully:",
|
|
1333
|
+
}, {
|
|
1334
|
+
type: "image",
|
|
1335
|
+
data: base64image,
|
|
1336
|
+
mimeType: "image/png",
|
|
1337
|
+
}],
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
return getResponseContent([
|
|
1342
|
+
'Error creating screenshot: ' + response,
|
|
1343
|
+
error instanceof Error ? error.message : String(error),
|
|
1344
|
+
], true);
|
|
1345
|
+
}
|
|
1346
|
+
case "to_file":
|
|
1347
|
+
return getResponseContent([
|
|
1348
|
+
isErrorResponse(response) ? response : 'Screenshot taken in file: ' + response
|
|
1349
|
+
]);
|
|
1350
|
+
}
|
|
1351
|
+
return getResponseContent([
|
|
1352
|
+
`Error: Unknown screen_shot command "${command}".`
|
|
1353
|
+
]);
|
|
1354
|
+
});
|
|
1355
|
+
server.registerTool(
|
|
1356
|
+
// Name of the tool (used to call it)
|
|
1357
|
+
"screen_dump", {
|
|
1358
|
+
title: "Screen dump tools",
|
|
1359
|
+
// Description of the tool (what it does)
|
|
1360
|
+
description: `Take a screendump of the openMSX emulator screen as SC?.
|
|
1361
|
+
The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'.
|
|
1362
|
+
`,
|
|
1363
|
+
// Schema for the tool (input validation)
|
|
1364
|
+
inputSchema: {
|
|
1365
|
+
scrbasename: z.string()
|
|
1366
|
+
.min(1, 'Screendump basename is too short')
|
|
1367
|
+
.max(100, 'Screendump basename is too long')
|
|
1368
|
+
.default("screendump")
|
|
1369
|
+
.describe("Screendump filename (without path nor extension) to save the screendump; default is 'screendump'"),
|
|
1370
|
+
},
|
|
1371
|
+
annotations: {
|
|
1372
|
+
"readOnlyHint": false,
|
|
1373
|
+
"destructiveHint": true,
|
|
1374
|
+
"idempotentHint": false,
|
|
1375
|
+
"openWorldHint": false,
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1378
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1379
|
+
async ({ scrbasename }) => {
|
|
1380
|
+
const openmsxCommand = `save_msx_screen "${path.join(emuDirectories.OPENMSX_SCREENDUMP_DIR, scrbasename)}"`;
|
|
1381
|
+
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
1382
|
+
return getResponseContent([
|
|
1383
|
+
isErrorResponse(response) ? 'Fail:' : 'Screendump file saved as:',
|
|
1384
|
+
response
|
|
1385
|
+
]);
|
|
1386
|
+
});
|
|
1387
|
+
server.registerTool(
|
|
1388
|
+
// Name of the tool (used to call it)
|
|
1389
|
+
"basic_programming", {
|
|
1390
|
+
title: "BASIC programming tools",
|
|
1391
|
+
// Description of the tool (what it does)
|
|
1392
|
+
description: "Helper tool for developing BASIC programs.",
|
|
1393
|
+
// Schema for the tool (input validation)
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
command: z.enum(["isBasicAvailable", "newProgram", "runProgram", "setProgram", "getFullProgram",
|
|
1396
|
+
"getFullProgramAdvanced", "listProgramLines", "deleteProgramLines"])
|
|
1397
|
+
.describe(`Available commands:
|
|
1398
|
+
'isBasicAvailable': checks if the current machine is ready to manage BASIC programs (true), or not (false).
|
|
1399
|
+
'newProgram': clears the current BASIC program.
|
|
1400
|
+
'setProgram <program>': sets a full BASIC program or updates part of the current BASIC program with the specified string.
|
|
1401
|
+
'runProgram': runs the current BASIC program.
|
|
1402
|
+
'getFullProgram': retrieves the current BASIC program as plain text; very useful in text screen modes.
|
|
1403
|
+
'getFullProgramAdvanced': retrieves the current BASIC program along with the RAM address where each line is stored.
|
|
1404
|
+
'listProgramLines <startLine> [endLine]': lists the selected range of lines from the current BASIC program on the emulator screen.
|
|
1405
|
+
'deleteProgramLines <startLine> [endLine]': deletes a specific range of lines from the current BASIC program; if endLine is not specified, only the startLine is deleted.
|
|
1406
|
+
**Important Note**: if error 'not in BASIC mode' then use the command 'isBasicAvailable' to wait for a ready state.
|
|
1407
|
+
**Important Note**: prioritize these tools for developing BASIC programs, as they are more efficient than using the 'sendText' tool.
|
|
1408
|
+
**Important Note**: if you have questions about MSX BASIC, use the resources provided by this MCP server.
|
|
1409
|
+
`),
|
|
1410
|
+
program: z.string()
|
|
1411
|
+
.max(10000, 'BASIC program too long')
|
|
1412
|
+
.optional()
|
|
1413
|
+
.describe("Basic program to set. Used by [setProgram]"),
|
|
1414
|
+
startLine: z.number()
|
|
1415
|
+
.min(0, 'Minimum start line number too low')
|
|
1416
|
+
.max(9999, 'Maximum start line number too high')
|
|
1417
|
+
.optional()
|
|
1418
|
+
.describe("Start line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
|
|
1419
|
+
endLine: z.number()
|
|
1420
|
+
.min(0, 'Minimum end line number too low')
|
|
1421
|
+
.max(9999, 'Maximum end line number too high')
|
|
1422
|
+
.optional()
|
|
1423
|
+
.describe("End line number to list/delete BASIC program lines. Used by [listProgramLines, deleteProgramLines]"),
|
|
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
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1442
|
+
async ({ command, program, startLine, endLine }) => {
|
|
1443
|
+
const CTRL_L_TEMPLATE = 'keymatrixdown 6 2 ; keymatrixdown 4 2 ; after time 0.1 { keymatrixup 6 2 ; keymatrixup 4 2 ; type_via_keybuf "%s" }';
|
|
1444
|
+
let tclCommand = undefined;
|
|
1445
|
+
let response = undefined;
|
|
1446
|
+
const inBasic = await openMSXInstance.emu_isInBasic();
|
|
1447
|
+
if (command !== "isBasicAvailable" && !inBasic) {
|
|
1448
|
+
response = 'Error: The current MSX machine is not in BASIC mode.';
|
|
1449
|
+
}
|
|
1450
|
+
else
|
|
1451
|
+
switch (command) {
|
|
1452
|
+
case "isBasicAvailable":
|
|
1453
|
+
response = inBasic.toString();
|
|
1454
|
+
break;
|
|
1455
|
+
case "newProgram":
|
|
1456
|
+
response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('new\r')));
|
|
1457
|
+
if (response.startsWith('after#'))
|
|
1458
|
+
response = '';
|
|
1459
|
+
break;
|
|
1460
|
+
case "runProgram":
|
|
1461
|
+
response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText('run\r')));
|
|
1462
|
+
if (response.startsWith('after#'))
|
|
1463
|
+
response = '';
|
|
1464
|
+
break;
|
|
1465
|
+
case "deleteProgramLines":
|
|
1466
|
+
if (startLine === undefined) {
|
|
1467
|
+
response = 'Error: No startLine number provided to delete BASIC program lines.';
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
response = await openMSXInstance.sendCommand(CTRL_L_TEMPLATE.replace('%s', encodeTypeText(`delete ${startLine}-${endLine || startLine}\r`)));
|
|
1471
|
+
break;
|
|
1472
|
+
case "setProgram":
|
|
1473
|
+
if (!program) {
|
|
1474
|
+
response = 'Error: no BASIC program provided to set.';
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
// Transform the program \n into \r, and (\r)+ into \r, as openMSX does not support \n in BASIC programs
|
|
1478
|
+
// and if last character is not \r, add it
|
|
1479
|
+
program = program
|
|
1480
|
+
.replace(/\n/g, '\r')
|
|
1481
|
+
.replace(/(\r)+/g, '\r');
|
|
1482
|
+
if (!program.endsWith('\r'))
|
|
1483
|
+
program += '\r';
|
|
1484
|
+
// Escape '$' characters if '(' is the next character and is not escaped yet (openMSX variable substitutions)
|
|
1485
|
+
// and escape '[' and ']' characters if not escaped yet
|
|
1486
|
+
program = program
|
|
1487
|
+
.replace(/([^\\])(\$\()/g, '$1\\$2')
|
|
1488
|
+
.replace(/([^\\])([\]\[])/g, '$1\\$2');
|
|
1489
|
+
// Get current speed to restore it later
|
|
1490
|
+
let speed = '100';
|
|
1491
|
+
if (isErrorResponse(speed = await openMSXInstance.sendCommand('set speed'))) {
|
|
1492
|
+
response = speed;
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
// Set speed to fast, type de program, wait to end, and restore speed
|
|
1496
|
+
if (isErrorResponse(response = await openMSXInstance.sendCommand(`set speed 10000 ; type_via_keybuf "${encodeTypeText(program)}" ; after idle 20 { set speed ${speed} }`))) {
|
|
1497
|
+
// Restore speed in case of error
|
|
1498
|
+
await openMSXInstance.sendCommand(`set speed ${speed}`);
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
// Success response
|
|
1502
|
+
response = '';
|
|
1503
|
+
break;
|
|
1504
|
+
case "getFullProgram":
|
|
1505
|
+
// Source: https://www.msx.org/forum/msx-talk/openmsx/export-basic-listing#comment-407392
|
|
1506
|
+
tclCommand = 'regsub -all -line {^[0-9a-f]x[0-9a-f]{4} > } [ listing ] ""';
|
|
1507
|
+
break;
|
|
1508
|
+
case "getFullProgramAdvanced":
|
|
1509
|
+
tclCommand = "listing";
|
|
1510
|
+
break;
|
|
1511
|
+
case "listProgramLines":
|
|
1512
|
+
if (startLine === undefined) {
|
|
1513
|
+
response = 'Error: No start line provided to list BASIC program lines.';
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
tclCommand = `type_via_keybuf \"${encodeTypeText(`list ${startLine}-${endLine || startLine}\r`)}\"`;
|
|
1517
|
+
break;
|
|
1518
|
+
default:
|
|
1519
|
+
response = `Error: Unknown command "${command}".`;
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
if (response === undefined && tclCommand) {
|
|
1523
|
+
response = await openMSXInstance.sendCommand(tclCommand);
|
|
1524
|
+
}
|
|
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
|
+
};
|
|
1551
|
+
});
|
|
1552
|
+
server.registerTool(
|
|
1553
|
+
// Name of the tool (used to call it)
|
|
1554
|
+
"vector_db_query", {
|
|
1555
|
+
title: "Vector DB query from resources",
|
|
1556
|
+
// Description of the tool (what it does)
|
|
1557
|
+
description: `Query the Vector DB resources to obtain information about MSX system, cartridges, programming, and other development resources.
|
|
1558
|
+
The query is a string used to search within the Vector DB resources; it is case-insensitive and may contain spaces.
|
|
1559
|
+
The response is the list of the top 10 result resources that match the query, including their score, title, and resource URI, and are sorted in descending order by proximity score to the query.
|
|
1560
|
+
**Important Note**: The Vector DB resources are in english, japanese, or dutch.
|
|
1561
|
+
`,
|
|
1562
|
+
// Schema for the tool (input validation)
|
|
1563
|
+
inputSchema: {
|
|
1564
|
+
query: z.string()
|
|
1565
|
+
.min(2, 'Query string too short')
|
|
1566
|
+
.max(100, 'Query string too long')
|
|
1567
|
+
.describe("Query string to search in the Vector DB resources, case-insensitive and may contain spaces."),
|
|
1568
|
+
},
|
|
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
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1586
|
+
async ({ query }) => {
|
|
1587
|
+
const results = await VectorDB.getInstance().query(query);
|
|
1588
|
+
return {
|
|
1589
|
+
content: [{
|
|
1590
|
+
type: "text",
|
|
1591
|
+
text: JSON.stringify(results),
|
|
1592
|
+
}],
|
|
1593
|
+
structuredContent: { results },
|
|
1594
|
+
isError: false,
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
// ============================================================================
|
|
1598
|
+
// Register a tool to get a specific MSX documentation resource
|
|
1599
|
+
// Retrieve MCP resources for MCP clients that don't support MCP resources.
|
|
1600
|
+
server.registerTool(
|
|
1601
|
+
// Name of the tool (used to call it)
|
|
1602
|
+
"msxdocs_resource_get", {
|
|
1603
|
+
title: "Tool to get a resource",
|
|
1604
|
+
// Description of the tool (what it does)
|
|
1605
|
+
description: "Get a specific available MSX documentation resource from this MCP server resources.",
|
|
1606
|
+
// Schema for the tool (input validation)
|
|
1607
|
+
inputSchema: {
|
|
1608
|
+
resourceName: z.enum(getRegisteredResourcesList().map(res => res.resource.name))
|
|
1609
|
+
.describe("Name of the resource to obtain, e.g. 'msxdocs_programming_interrupts'"),
|
|
1610
|
+
},
|
|
1611
|
+
annotations: {
|
|
1612
|
+
"readOnlyHint": true,
|
|
1613
|
+
"destructiveHint": false,
|
|
1614
|
+
"idempotentHint": true,
|
|
1615
|
+
"openWorldHint": true,
|
|
1616
|
+
},
|
|
1617
|
+
},
|
|
1618
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
1619
|
+
async ({ resourceName }, extra) => {
|
|
1620
|
+
const regResources = getRegisteredResourcesList();
|
|
1621
|
+
const index = regResources.findIndex((res) => res.resource.name === resourceName);
|
|
1622
|
+
const uriString = index !== -1 ? regResources[index].uri : undefined;
|
|
1623
|
+
const resource = index !== -1 ? regResources[index].resource : undefined;
|
|
1624
|
+
if (!resource || !uriString) {
|
|
1625
|
+
return getResponseContent([
|
|
1626
|
+
`Error: Resource '${resourceName}' not found.`
|
|
1627
|
+
]);
|
|
1628
|
+
}
|
|
1629
|
+
let documentationText = '';
|
|
1630
|
+
try {
|
|
1631
|
+
// If the resource is found, return its content
|
|
1632
|
+
let resourceContent = await resource.readCallback(new URL(uriString), extra);
|
|
1633
|
+
if (!resourceContent.contents?.length) {
|
|
1634
|
+
return getResponseContent([
|
|
1635
|
+
`Error: Resource '${resourceName}' has no content available.`
|
|
1636
|
+
]);
|
|
1637
|
+
}
|
|
1638
|
+
// Return the first content item (assuming it's the main content)
|
|
1639
|
+
const content = resourceContent.contents[0];
|
|
1640
|
+
if ('text' in content) {
|
|
1641
|
+
documentationText = content.text;
|
|
1642
|
+
}
|
|
1643
|
+
else {
|
|
1644
|
+
return getResponseContent([
|
|
1645
|
+
`Error: Resource '${resourceName}' has no content available.`
|
|
1646
|
+
]);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
catch (error) {
|
|
1650
|
+
return getResponseContent([
|
|
1651
|
+
`Error: error reading resource '${resourceName}': ${error instanceof Error ? error.message : String(error)}`
|
|
1652
|
+
]);
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
content: [{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: `Content from resource: '${resourceName}'`,
|
|
1658
|
+
}, {
|
|
1659
|
+
type: "text",
|
|
1660
|
+
text: documentationText || 'No content available for this resource.',
|
|
1661
|
+
mimeType: resource.metadata?.mimeType || 'text/plain',
|
|
1662
|
+
} /*, {
|
|
1663
|
+
type: "resource",
|
|
1664
|
+
resource: {
|
|
1665
|
+
uri: resource.metadata?.uri || resourceName,
|
|
1666
|
+
title: resource.metadata?.title || `Resource: ${resourceName}`,
|
|
1667
|
+
mimeType: resource.metadata?.mimeType || 'text/plain',
|
|
1668
|
+
text: documentationText || 'No content available for this resource.',
|
|
1669
|
+
}
|
|
1670
|
+
}*/
|
|
1671
|
+
],
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1674
|
+
}
|