@nataliapc/mcp-openmsx 1.0.0
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/LICENSE +339 -0
- package/README.md +193 -0
- package/dist/openmsx.js +331 -0
- package/dist/server.js +912 -0
- package/dist/utils.js +23 -0
- package/package.json +60 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP openMSX Server
|
|
4
|
+
*
|
|
5
|
+
* Model Context Protocol server that manages openMSX emulator instances
|
|
6
|
+
* through TCL commands via stdio.
|
|
7
|
+
*
|
|
8
|
+
* @package @nataliapc/mcp-openmsx
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
* @author Natalia Pujol Cremades (@nataliapc)
|
|
11
|
+
* @license GPL2
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import express from "express";
|
|
20
|
+
import fs from "fs/promises";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { openMSXInstance } from "./openmsx.js";
|
|
23
|
+
// Version info for CLI
|
|
24
|
+
const PACKAGE_VERSION = "1.0.0";
|
|
25
|
+
// Defaults for openMSX paths
|
|
26
|
+
var OPENMSX_EXECUTABLE = 'openmsx';
|
|
27
|
+
var OPENMSX_SHARE_DIR = '/usr/share/openmsx';
|
|
28
|
+
var OPENMSX_SCREENSHOT_DIR = '';
|
|
29
|
+
var OPENMSX_SCREENDUMP_DIR = '';
|
|
30
|
+
var MACHINES_DIR = `${OPENMSX_SHARE_DIR}/machines`;
|
|
31
|
+
var EXTENSIONS_DIR = `${OPENMSX_SHARE_DIR}/extensions`;
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Tools available in the MCP server
|
|
34
|
+
// https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
|
|
35
|
+
//
|
|
36
|
+
function registerAllTools(server) {
|
|
37
|
+
server.tool(
|
|
38
|
+
// Name of the tool (used to call it)
|
|
39
|
+
"emu_control",
|
|
40
|
+
// Description of the tool (what it does)
|
|
41
|
+
"Controls an openMSX emulator. Commands: " +
|
|
42
|
+
"'launch [machine] [extensions]': opens a powered on openMSX emulator, machine and extensions parameters could be specified, always use 'machine_list' and 'extension_list' resources to obtain valid values. " +
|
|
43
|
+
"'close': closes the openMSX emulator. " +
|
|
44
|
+
"'powerOn': powers on the openMSX emulator. " +
|
|
45
|
+
"'powerOff': powers off the openMSX emulator. " +
|
|
46
|
+
"'reset': resets the current machine. " +
|
|
47
|
+
"'getEmulatorSpeed': get current emulator speed in percentage, default is 100. " +
|
|
48
|
+
"'setEmulatorSpeed <emuspeed>': set the emulator speed in percentage, valid values are 1-10000, default is 100. " +
|
|
49
|
+
"'machineList': get a list of all available MSX machines that can be emulated with openMSX. " +
|
|
50
|
+
"'extensionList': get a list of all available MSX extensions that can be used with openMSX. ",
|
|
51
|
+
// Schema for the tool (input validation)
|
|
52
|
+
{
|
|
53
|
+
command: z.enum(["launch", "close", "powerOn", "powerOff", "reset", "getEmulatorSpeed", "setEmulatorSpeed", "machineList", "extensionList"]),
|
|
54
|
+
machine: z.string().min(1).max(100).optional(),
|
|
55
|
+
extensions: z.array(z.string().min(1).max(100)).optional(),
|
|
56
|
+
emuspeed: z.number().min(1).max(10000).optional().default(100),
|
|
57
|
+
},
|
|
58
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
59
|
+
async ({ command, machine, extensions, emuspeed }) => {
|
|
60
|
+
let result = "Error";
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "launch":
|
|
63
|
+
result = await openMSXInstance.emu_launch(OPENMSX_EXECUTABLE, machine || "", extensions || []);
|
|
64
|
+
// Check if launch was successful
|
|
65
|
+
if (result === "Ok") {
|
|
66
|
+
result = "openMSX emulator launched";
|
|
67
|
+
if (machine) {
|
|
68
|
+
result += ` with machine "${machine}"`;
|
|
69
|
+
}
|
|
70
|
+
if (extensions && extensions.length > 0) {
|
|
71
|
+
if (machine) {
|
|
72
|
+
result += ' and ';
|
|
73
|
+
}
|
|
74
|
+
result += ` with extensions: ${extensions.join(', ')}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "close":
|
|
79
|
+
result = await openMSXInstance.emu_close();
|
|
80
|
+
break;
|
|
81
|
+
case "powerOn":
|
|
82
|
+
result = await openMSXInstance.sendCommand('set power on');
|
|
83
|
+
result += result === "Ok" ? ": openMSX emulator powered on" : "";
|
|
84
|
+
break;
|
|
85
|
+
case "powerOff":
|
|
86
|
+
result = await openMSXInstance.sendCommand('set power off');
|
|
87
|
+
result += result === "Ok" ? ": openMSX emulator powered off" : "";
|
|
88
|
+
break;
|
|
89
|
+
case "reset":
|
|
90
|
+
result = await openMSXInstance.sendCommand('reset');
|
|
91
|
+
result += result === "Ok" ? ": openMSX emulator reset" : "";
|
|
92
|
+
break;
|
|
93
|
+
case 'getEmulatorSpeed':
|
|
94
|
+
result = await openMSXInstance.sendCommand('set speed');
|
|
95
|
+
result = !isNaN(Number(result)) ? `Current emulator speed is ${result}%` : result;
|
|
96
|
+
break;
|
|
97
|
+
case 'setEmulatorSpeed':
|
|
98
|
+
result = await openMSXInstance.sendCommand(`set speed ${emuspeed}`);
|
|
99
|
+
result = !isNaN(Number(result)) ? `Emulator speed set to ${emuspeed}%` : result;
|
|
100
|
+
break;
|
|
101
|
+
case "machineList":
|
|
102
|
+
result = await openMSXInstance.getMachineList(MACHINES_DIR);
|
|
103
|
+
break;
|
|
104
|
+
case "extensionList":
|
|
105
|
+
result = await openMSXInstance.getExtensionList(EXTENSIONS_DIR);
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
result = `Error: Unknown command "${command}".`;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
// Return result with proper format for MCP
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: result,
|
|
116
|
+
}],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
server.tool(
|
|
120
|
+
// Name of the tool (used to call it)
|
|
121
|
+
"emu_media",
|
|
122
|
+
// Description of the tool (what it does)
|
|
123
|
+
"Manage tapes, rom cartridges, and floppy disks. Commands: " +
|
|
124
|
+
"'tapeInsert <tapefile>': insert a valid tape file (*.cas, *.wav, *.tsx). " +
|
|
125
|
+
"'tapeRewind': rewind the current tape. " +
|
|
126
|
+
"'tapeEject': remove tape from virtual cassette player. " +
|
|
127
|
+
"'romInsert <romfile>': insert a valid ROM cartridge file (*.rom) at cartridge slot A. " +
|
|
128
|
+
"'romEject': remove the current ROM cartridge from cartridge slot A. " +
|
|
129
|
+
"'diskInsert <diskfile>': insert a valid disk file (*.dsk) in floppy disk A. " +
|
|
130
|
+
"'diskInsertFolder <diskfolder>': use a host folder as a floppy disk A root directory. " +
|
|
131
|
+
"'diskEject': remove the current disk from floppy disk A. ",
|
|
132
|
+
// Schema for the tool (input validation)
|
|
133
|
+
{
|
|
134
|
+
command: z.enum(["tapeInsert", "tapeRewind", "tapeEject", "romInsert", "romEject", "diskInsert", "diskInsertFolder", "diskEject"]),
|
|
135
|
+
tapefile: z.string().min(1).max(200).optional(), // Tape file to insert
|
|
136
|
+
romfile: z.string().min(1).max(200).optional(), // ROM file to insert
|
|
137
|
+
diskfile: z.string().min(1).max(200).optional(), // Disk file to insert
|
|
138
|
+
diskfolder: z.string().min(1).max(200).optional(), // Disk folder to insert
|
|
139
|
+
},
|
|
140
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
141
|
+
async ({ command, tapefile, romfile, diskfile, diskfolder }) => {
|
|
142
|
+
let tclCommand;
|
|
143
|
+
switch (command) {
|
|
144
|
+
case "tapeInsert":
|
|
145
|
+
tclCommand = `cassetteplayer insert "${tapefile}"`;
|
|
146
|
+
break;
|
|
147
|
+
case "tapeRewind":
|
|
148
|
+
tclCommand = "cassetteplayer rewind";
|
|
149
|
+
break;
|
|
150
|
+
case "tapeEject":
|
|
151
|
+
tclCommand = "cassetteplayer eject";
|
|
152
|
+
break;
|
|
153
|
+
case "romInsert":
|
|
154
|
+
tclCommand = `carta insert "${romfile}"`;
|
|
155
|
+
break;
|
|
156
|
+
case "romEject":
|
|
157
|
+
tclCommand = "carta eject";
|
|
158
|
+
break;
|
|
159
|
+
case "diskInsert":
|
|
160
|
+
tclCommand = `diska insert "${diskfile}"`;
|
|
161
|
+
break;
|
|
162
|
+
case "diskInsertFolder":
|
|
163
|
+
tclCommand = `diska insert "${diskfolder}"`;
|
|
164
|
+
break;
|
|
165
|
+
case "diskEject":
|
|
166
|
+
tclCommand = "diska eject";
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
return {
|
|
170
|
+
content: [{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: `Error: Unknown emulator media command "${command}".`,
|
|
173
|
+
}],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
177
|
+
// Return the response from openMSX
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: "text",
|
|
181
|
+
text: response === "" ? "Ok" : response,
|
|
182
|
+
}],
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
server.tool(
|
|
186
|
+
// Name of the tool (used to call it)
|
|
187
|
+
"emu_info",
|
|
188
|
+
// Description of the tool (what it does)
|
|
189
|
+
"Obtain informacion about the current emulated machine. Commands: " +
|
|
190
|
+
"'getStatus': returns the status of the openMSX emulator. " +
|
|
191
|
+
"'getSlotsMap': shows what devices/ROM/RAM are inserted into which slots. " +
|
|
192
|
+
"'getIOPortsMap': shows an overview about the I/O mapped devices. ",
|
|
193
|
+
// Schema for the tool (input validation)
|
|
194
|
+
{
|
|
195
|
+
command: z.enum(["getStatus", "getSlotsMap", "getIOPortsMap"]),
|
|
196
|
+
},
|
|
197
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
198
|
+
async ({ command }) => {
|
|
199
|
+
let tclCommand;
|
|
200
|
+
switch (command) {
|
|
201
|
+
case "getStatus":
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: await openMSXInstance.emu_status(),
|
|
206
|
+
}],
|
|
207
|
+
};
|
|
208
|
+
case "getSlotsMap":
|
|
209
|
+
tclCommand = "slotmap";
|
|
210
|
+
break;
|
|
211
|
+
case "getIOPortsMap":
|
|
212
|
+
tclCommand = "iomap";
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
return {
|
|
216
|
+
content: [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: `Error: Unknown emulator info command "${command}".`,
|
|
219
|
+
}],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
223
|
+
return {
|
|
224
|
+
content: [{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: response,
|
|
227
|
+
}],
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
server.tool(
|
|
231
|
+
// Name of the tool (used to call it)
|
|
232
|
+
"emu_vdp",
|
|
233
|
+
// Description of the tool (what it does)
|
|
234
|
+
"Manage VDP (Video Display Processor). Commands: " +
|
|
235
|
+
"'getPalette': return the current V9938/V9958 colors palette in format RGB333. " +
|
|
236
|
+
"'getRegisters': return all the VDP register values. " +
|
|
237
|
+
"'getRegisterValue <register>': return the value of a specific VDP register (0-31) in decimal format. " +
|
|
238
|
+
"'setRegisterValue <register> <value>': set a hexadecimal value to a specific VDP register (0-31). " +
|
|
239
|
+
"'screenGetMode': returns the current screen mode (0-12) as a number which would also be used for the basic command SCREEN. " +
|
|
240
|
+
"'screenGetFullText': return the full content of an MSX text screen (screen 0 or 1) as a string, useful for debugging. ",
|
|
241
|
+
// Schema for the tool (input validation)
|
|
242
|
+
{
|
|
243
|
+
command: z.enum(["getPalette", "getRegisters", "getRegisterValue", "setRegisterValue", "screenGetMode", "screenGetFullText"]),
|
|
244
|
+
register: z.number().min(0).max(31).optional(), // Register to read/write
|
|
245
|
+
value: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(),
|
|
246
|
+
},
|
|
247
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
248
|
+
async ({ command, register, value }) => {
|
|
249
|
+
let tclCommand;
|
|
250
|
+
switch (command) {
|
|
251
|
+
case "getPalette":
|
|
252
|
+
tclCommand = "palette";
|
|
253
|
+
break;
|
|
254
|
+
case "getRegisters":
|
|
255
|
+
tclCommand = "vdpregs";
|
|
256
|
+
break;
|
|
257
|
+
case "getRegisterValue":
|
|
258
|
+
tclCommand = `vdpreg ${register}`;
|
|
259
|
+
break;
|
|
260
|
+
case "setRegisterValue":
|
|
261
|
+
tclCommand = `vdpreg ${register} ${value}`;
|
|
262
|
+
break;
|
|
263
|
+
case "screenGetMode":
|
|
264
|
+
tclCommand = "get_screen_mode";
|
|
265
|
+
break;
|
|
266
|
+
case "screenGetFullText":
|
|
267
|
+
const response = await openMSXInstance.sendCommand('get_screen');
|
|
268
|
+
return response.startsWith('Error:') ? {
|
|
269
|
+
content: [{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: response,
|
|
272
|
+
}]
|
|
273
|
+
} : {
|
|
274
|
+
content: [{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: "The screen text is:",
|
|
277
|
+
}, {
|
|
278
|
+
type: "text",
|
|
279
|
+
text: response,
|
|
280
|
+
}],
|
|
281
|
+
};
|
|
282
|
+
default:
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: `Error: Unknown emulator vdp command "${command}".`,
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
291
|
+
return {
|
|
292
|
+
content: [{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: response === "" ? "Ok" : response,
|
|
295
|
+
}],
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
server.tool(
|
|
299
|
+
// Name of the tool (used to call it)
|
|
300
|
+
"debug_run",
|
|
301
|
+
// Description of the tool (what it does)
|
|
302
|
+
"Control execution (break, continue, step). Commands: " +
|
|
303
|
+
"'break': to break CPU at current execution position. " +
|
|
304
|
+
"'isBreaked': to check if the CPU is currently in break state (1) or not (0). " +
|
|
305
|
+
"'continue': to continue execution after break. " +
|
|
306
|
+
"'stepIn': to execute one CPU instruction, go into subroutines. " +
|
|
307
|
+
"'stepOver': to execute one CPU instruction, but don't go into subroutines. " +
|
|
308
|
+
"'stepOut': to step out of the current subroutine. " +
|
|
309
|
+
"'stepBack': to step one instruction back in time. " +
|
|
310
|
+
"'runTo <address>': to run the CPU until it reaches the specified address. " +
|
|
311
|
+
"Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
|
|
312
|
+
// Schema for the tool (input validation)
|
|
313
|
+
{
|
|
314
|
+
command: z.enum(["break", "isBreaked", "continue", "stepIn", "stepOut", "stepOver", "stepBack", "runTo"]),
|
|
315
|
+
address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional()
|
|
316
|
+
},
|
|
317
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
318
|
+
async ({ command, address }) => {
|
|
319
|
+
let tclCommand;
|
|
320
|
+
switch (command) {
|
|
321
|
+
case "break":
|
|
322
|
+
tclCommand = "debug break";
|
|
323
|
+
break;
|
|
324
|
+
case "isBreaked":
|
|
325
|
+
tclCommand = "debug breaked";
|
|
326
|
+
break;
|
|
327
|
+
case "continue":
|
|
328
|
+
tclCommand = "debug cont";
|
|
329
|
+
break;
|
|
330
|
+
case "stepIn":
|
|
331
|
+
tclCommand = "step_in";
|
|
332
|
+
break;
|
|
333
|
+
case "stepOver":
|
|
334
|
+
tclCommand = "step_over";
|
|
335
|
+
break;
|
|
336
|
+
case "stepOut":
|
|
337
|
+
tclCommand = "step_out";
|
|
338
|
+
break;
|
|
339
|
+
case "stepBack":
|
|
340
|
+
tclCommand = "step_back";
|
|
341
|
+
break;
|
|
342
|
+
case "runTo":
|
|
343
|
+
tclCommand = `run_to ${address}`;
|
|
344
|
+
break;
|
|
345
|
+
default:
|
|
346
|
+
return {
|
|
347
|
+
content: [{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: `Error: Unknown debug command "${command}".`,
|
|
350
|
+
}],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
354
|
+
return {
|
|
355
|
+
content: [{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: response,
|
|
358
|
+
}],
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
server.tool(
|
|
362
|
+
// Name of the tool (used to call it)
|
|
363
|
+
"debug_cpu",
|
|
364
|
+
// Description of the tool (what it does)
|
|
365
|
+
"Read/write CPU registers, CPU info, Stack pile, and Disassemble code from memory. Commands: " +
|
|
366
|
+
"'getCpuRegisters': to get an overview of all the CPU registers. " +
|
|
367
|
+
"'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). " +
|
|
368
|
+
"'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). " +
|
|
369
|
+
"'getStackPile': to get an overview of the CPU stack. " +
|
|
370
|
+
"'disassemble [address] [size]': to print disassembled instructions at the address parameter location or PC register if empty. " +
|
|
371
|
+
"'getActiveCpu': to return the active cpu: z80 or r800." +
|
|
372
|
+
"Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
|
|
373
|
+
// Schema for the tool (input validation)
|
|
374
|
+
{
|
|
375
|
+
command: z.enum(["getCpuRegisters", "getRegister", "setRegister", "getStackPile", "disassemble", "getActiveCpu"]),
|
|
376
|
+
register: z.enum(["pc", "sp", "ix", "iy", "af", "bc", "de", "hl", "ixh", "ixl", "iyh", "iyl", "a", "f", "b", "c", "d", "e", "h", "l", "i", "r", "im", "iff"]).optional(),
|
|
377
|
+
address: z.string().regex(/^0x[0-9a-fA-F]{4}$/), // 4 hex digits for MSX memory address
|
|
378
|
+
value: z.string().regex(/^0x[0-9a-fA-F]{2-4}$/).optional(), // 2-4 hex digits for byte value for writeByte command
|
|
379
|
+
size: z.number().min(1).max(50).optional().default(8), // Number of bytes for disassemble command
|
|
380
|
+
},
|
|
381
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
382
|
+
async ({ command, address, register, value, size }) => {
|
|
383
|
+
let tclCommand;
|
|
384
|
+
switch (command) {
|
|
385
|
+
case "getCpuRegisters":
|
|
386
|
+
tclCommand = "cpuregs";
|
|
387
|
+
break;
|
|
388
|
+
case "getRegister":
|
|
389
|
+
tclCommand = "reg ${register}";
|
|
390
|
+
break;
|
|
391
|
+
case "setRegister":
|
|
392
|
+
tclCommand = "reg ${register} ${value}";
|
|
393
|
+
break;
|
|
394
|
+
case "getStackPile":
|
|
395
|
+
tclCommand = "stack";
|
|
396
|
+
break;
|
|
397
|
+
case "disassemble":
|
|
398
|
+
tclCommand = `disasm ${address || ""} ${size || ""}`;
|
|
399
|
+
break;
|
|
400
|
+
case "getActiveCpu":
|
|
401
|
+
tclCommand = "get_active_cpu";
|
|
402
|
+
break;
|
|
403
|
+
default:
|
|
404
|
+
return {
|
|
405
|
+
content: [{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: `Error: Unknown memory command "${command}".`,
|
|
408
|
+
}],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
412
|
+
return {
|
|
413
|
+
content: [{
|
|
414
|
+
type: "text",
|
|
415
|
+
text: response,
|
|
416
|
+
}],
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
server.tool(
|
|
420
|
+
// Name of the tool (used to call it)
|
|
421
|
+
"debug_memory",
|
|
422
|
+
// Description of the tool (what it does)
|
|
423
|
+
"Slots info, and Read/write from/to memory in the openMSX emulator. Commands: " +
|
|
424
|
+
"'selectedSlots': to get a list of the currently selected memory slots. " +
|
|
425
|
+
"'getBlock <address> [lines]': to read a block of memory from the specified address. " +
|
|
426
|
+
"'readByte <address>': to read a BYTE from the specified address. " +
|
|
427
|
+
"'readWord <address>': to read a WORD from the specified address. " +
|
|
428
|
+
"'writeByte <address> <value8>': to write a BYTE to the specified address. " +
|
|
429
|
+
"'writeWord <address> <value16>': to write a WORD to the specified address. " +
|
|
430
|
+
"'advanced_basic_listing': to list the current BASIC program, with the ram address of each line listed. " +
|
|
431
|
+
"Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
|
|
432
|
+
// Schema for the tool (input validation)
|
|
433
|
+
{
|
|
434
|
+
command: z.enum(["selectedSlots", "getBlock", "readByte", "readWord", "writeByte", "writeWord", "advanced_basic_listing"]),
|
|
435
|
+
address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
|
|
436
|
+
lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
|
|
437
|
+
value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
|
|
438
|
+
value16: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for byte value for writeByte command
|
|
439
|
+
},
|
|
440
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
441
|
+
async ({ command, address, lines, value8, value16 }) => {
|
|
442
|
+
let tclCommand;
|
|
443
|
+
switch (command) {
|
|
444
|
+
case "selectedSlots":
|
|
445
|
+
tclCommand = "slotselect";
|
|
446
|
+
break;
|
|
447
|
+
case "getBlock":
|
|
448
|
+
tclCommand = `showmem ${address} ${lines}`;
|
|
449
|
+
break;
|
|
450
|
+
case "readByte":
|
|
451
|
+
tclCommand = `peek ${address}`;
|
|
452
|
+
break;
|
|
453
|
+
case "readWord":
|
|
454
|
+
tclCommand = `peek16 ${address}`;
|
|
455
|
+
break;
|
|
456
|
+
case "writeByte":
|
|
457
|
+
tclCommand = `poke ${address} ${value8}`;
|
|
458
|
+
break;
|
|
459
|
+
case "writeWord":
|
|
460
|
+
tclCommand = `poke16 ${address} ${value16}`;
|
|
461
|
+
break;
|
|
462
|
+
case "advanced_basic_listing":
|
|
463
|
+
tclCommand = "listing";
|
|
464
|
+
break;
|
|
465
|
+
default:
|
|
466
|
+
return {
|
|
467
|
+
content: [{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: `Error: Unknown memory command "${command}".`,
|
|
470
|
+
}],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: response,
|
|
478
|
+
}],
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
server.tool(
|
|
482
|
+
// Name of the tool (used to call it)
|
|
483
|
+
"debug_vram",
|
|
484
|
+
// Description of the tool (what it does)
|
|
485
|
+
"Read or write from/to VRAM video memory in the openMSX emulator. Commands: " +
|
|
486
|
+
"'getBlock <address> [lines]': to read a block of VRAM memory from the specified address. " +
|
|
487
|
+
"'readByte <address>': to read a BYTE from the specified VRAM address. " +
|
|
488
|
+
"'writeByte <address> <value8>': to write a BYTE to the specified VRAM address. " +
|
|
489
|
+
"Note: Addresses and values are in hexadecimal format (e.g. 0x0000).",
|
|
490
|
+
// Schema for the tool (input validation)
|
|
491
|
+
{
|
|
492
|
+
command: z.enum(["getBlock", "readByte", "writeByte"]),
|
|
493
|
+
address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
|
|
494
|
+
lines: z.number().min(1).max(50).optional().default(8), // Number of lines for getBlock command
|
|
495
|
+
value8: z.string().regex(/^0x[0-9a-fA-F]{2}$/).optional(), // 2 hex digits for byte value for writeByte command
|
|
496
|
+
},
|
|
497
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
498
|
+
async ({ command, address, lines, value8 }) => {
|
|
499
|
+
let tclCommand;
|
|
500
|
+
switch (command) {
|
|
501
|
+
case "getBlock":
|
|
502
|
+
tclCommand = `showdebuggable VRAM ${address} ${lines}`;
|
|
503
|
+
break;
|
|
504
|
+
case "readByte":
|
|
505
|
+
tclCommand = `vpeek ${address}`;
|
|
506
|
+
break;
|
|
507
|
+
case "writeByte":
|
|
508
|
+
tclCommand = `vpoke ${address} ${value8}`;
|
|
509
|
+
break;
|
|
510
|
+
default:
|
|
511
|
+
return {
|
|
512
|
+
content: [{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: `Error: Unknown video memory command "${command}".`,
|
|
515
|
+
}],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
519
|
+
return {
|
|
520
|
+
content: [{
|
|
521
|
+
type: "text",
|
|
522
|
+
text: response,
|
|
523
|
+
}],
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
server.tool(
|
|
527
|
+
// Name of the tool (used to call it)
|
|
528
|
+
"debug_breakpoints",
|
|
529
|
+
// Description of the tool (what it does)
|
|
530
|
+
"Create, remove, and list breakpoints. Commands: " +
|
|
531
|
+
"'create <address>': create a breakpoint at a specified address, and returns its name. " +
|
|
532
|
+
"'remove <bpname>': remove a breakpoint by name (e.g. bp#1). " +
|
|
533
|
+
"'list': enumerate the active breakpoints. " +
|
|
534
|
+
"Note: Addresses and values are in hexadecimal format (e.g. 0x0000). " +
|
|
535
|
+
"Note: The memory addresses of functions and variables can be previously obtained from *.sym or *.map files.",
|
|
536
|
+
// Schema for the tool (input validation)
|
|
537
|
+
{
|
|
538
|
+
command: z.enum(["create", "remove", "list"]),
|
|
539
|
+
address: z.string().regex(/^0x[0-9a-fA-F]{4}$/).optional(), // 4 hex digits for MSX memory address
|
|
540
|
+
bpname: z.string().min(3).max(10).optional(), // breakpoint name (e.g. bp#1)
|
|
541
|
+
},
|
|
542
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
543
|
+
async ({ command, address, bpname }) => {
|
|
544
|
+
let tclCommand;
|
|
545
|
+
switch (command) {
|
|
546
|
+
case "create":
|
|
547
|
+
tclCommand = `debug set_bp ${address}`;
|
|
548
|
+
break;
|
|
549
|
+
case "remove":
|
|
550
|
+
tclCommand = `debug remove_bp ${bpname}`;
|
|
551
|
+
break;
|
|
552
|
+
case "list":
|
|
553
|
+
tclCommand = 'debug list_bp';
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
return {
|
|
557
|
+
content: [{
|
|
558
|
+
type: "text",
|
|
559
|
+
text: `Error: Unknown breakpoint command "${command}".`,
|
|
560
|
+
}],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
564
|
+
return {
|
|
565
|
+
content: [{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: response,
|
|
568
|
+
}],
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
server.tool(
|
|
572
|
+
// Name of the tool (used to call it)
|
|
573
|
+
"emu_savestates",
|
|
574
|
+
// Description of the tool (what it does)
|
|
575
|
+
"Load, save, and list savestates. Commands: " +
|
|
576
|
+
"'load <name>': restores a previously created savestate. " +
|
|
577
|
+
"'save <name>': creates a snapshot of the currently emulated MSX machine specifying a name for the savestate. " +
|
|
578
|
+
"'list': returns the names of all previously created savestates, separated by spaces. " +
|
|
579
|
+
"Note: names with spaces are enclosed in {}.",
|
|
580
|
+
// Schema for the tool (input validation)
|
|
581
|
+
{
|
|
582
|
+
command: z.enum(["load", "save", "list"]),
|
|
583
|
+
name: z.string().min(1).max(20).optional(), // breakpoint name (e.g. bp#1)
|
|
584
|
+
},
|
|
585
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
586
|
+
async ({ command, name }) => {
|
|
587
|
+
let tclCommand;
|
|
588
|
+
let textResponse = "Error:";
|
|
589
|
+
switch (command) {
|
|
590
|
+
case "load":
|
|
591
|
+
textResponse = "Loaded savestate: ";
|
|
592
|
+
tclCommand = `loadstate ${name}`;
|
|
593
|
+
break;
|
|
594
|
+
case "save":
|
|
595
|
+
textResponse = "Saved savestate: ";
|
|
596
|
+
tclCommand = `savestate ${name}`;
|
|
597
|
+
break;
|
|
598
|
+
case "list":
|
|
599
|
+
textResponse = "Savestate names: ";
|
|
600
|
+
tclCommand = 'list_savestates';
|
|
601
|
+
break;
|
|
602
|
+
default:
|
|
603
|
+
return {
|
|
604
|
+
content: [{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: `Error: Unknown savestate command "${command}".`,
|
|
607
|
+
}],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
611
|
+
return {
|
|
612
|
+
content: [{
|
|
613
|
+
type: "text",
|
|
614
|
+
text: textResponse,
|
|
615
|
+
}, {
|
|
616
|
+
type: "text",
|
|
617
|
+
text: response,
|
|
618
|
+
}],
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
server.tool(
|
|
622
|
+
// Name of the tool (used to call it)
|
|
623
|
+
"emu_keyboard",
|
|
624
|
+
// Description of the tool (what it does)
|
|
625
|
+
"Send a text to the openMSX emulator. Commands: " +
|
|
626
|
+
"'sendText <text>': type a string in the emulated MSX, this command automatically press and release keys in the MSX keyboard matrix, is useful for automating tasks in BASIC, use \\r for Return key. ",
|
|
627
|
+
// Schema for the tool (input validation)
|
|
628
|
+
{
|
|
629
|
+
command: z.enum(["sendText"]),
|
|
630
|
+
text: z.string().min(1).max(100).optional().default(''), // Key to send to the emulator
|
|
631
|
+
},
|
|
632
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
633
|
+
async ({ command, text }) => {
|
|
634
|
+
let tclCommand;
|
|
635
|
+
switch (command) {
|
|
636
|
+
case "sendText":
|
|
637
|
+
tclCommand = `type "${text}"`;
|
|
638
|
+
break;
|
|
639
|
+
default:
|
|
640
|
+
return {
|
|
641
|
+
content: [{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: `Error: Unknown keyboard command "${command}".`,
|
|
644
|
+
}],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const response = await openMSXInstance.sendCommand(tclCommand);
|
|
648
|
+
return {
|
|
649
|
+
content: [{
|
|
650
|
+
type: "text",
|
|
651
|
+
text: response,
|
|
652
|
+
}],
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
server.tool(
|
|
656
|
+
// Name of the tool (used to call it)
|
|
657
|
+
"screen_shot",
|
|
658
|
+
// Description of the tool (what it does)
|
|
659
|
+
"Take a screenshot of the openMSX emulator screen. Commands: " +
|
|
660
|
+
"'as_image': take a screenshot and the image is returned in the response. " +
|
|
661
|
+
"'to_file': take a screenshot and save it to a file, the file name is returned in the response.",
|
|
662
|
+
// Schema for the tool (input validation)
|
|
663
|
+
{
|
|
664
|
+
command: z.enum(["as_image", "to_file"]),
|
|
665
|
+
},
|
|
666
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
667
|
+
async ({ command }) => {
|
|
668
|
+
const openmsxCommand = `screenshot -raw -prefix "${OPENMSX_SCREENSHOT_DIR}mcp_"`;
|
|
669
|
+
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
670
|
+
switch (command) {
|
|
671
|
+
case "as_image":
|
|
672
|
+
try {
|
|
673
|
+
const imageBuffer = await fs.readFile(response);
|
|
674
|
+
const base64image = imageBuffer.toString('base64');
|
|
675
|
+
return {
|
|
676
|
+
content: [{
|
|
677
|
+
type: "text",
|
|
678
|
+
text: "Screenshot taken successfully:",
|
|
679
|
+
}, {
|
|
680
|
+
type: "image",
|
|
681
|
+
data: base64image,
|
|
682
|
+
mimeType: "image/png",
|
|
683
|
+
}],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
return {
|
|
688
|
+
content: [{
|
|
689
|
+
type: "text",
|
|
690
|
+
text: 'Error creating screenshot: ' + response,
|
|
691
|
+
}, {
|
|
692
|
+
type: "text",
|
|
693
|
+
text: error instanceof Error ? error.message : String(error),
|
|
694
|
+
}],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
case "to_file":
|
|
698
|
+
return {
|
|
699
|
+
content: [{
|
|
700
|
+
type: "text",
|
|
701
|
+
text: response.startsWith('Error:') ? response : 'Screenshot taken in file: ' + response,
|
|
702
|
+
}],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
content: [{
|
|
707
|
+
type: "text",
|
|
708
|
+
text: `Error: Unknown screen_shot command "${command}".`,
|
|
709
|
+
}],
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
server.tool(
|
|
713
|
+
// Name of the tool (used to call it)
|
|
714
|
+
"screen_dump",
|
|
715
|
+
// Description of the tool (what it does)
|
|
716
|
+
"Take a screendump of the openMSX emulator screen as SC?. The parameter scrbasename is the name of the filename (without path) to save the screendump, default is 'screendump'. ",
|
|
717
|
+
// Schema for the tool (input validation)
|
|
718
|
+
{
|
|
719
|
+
scrbasename: z.string().min(1).max(100).default("screendump"),
|
|
720
|
+
},
|
|
721
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
722
|
+
async ({ scrbasename }) => {
|
|
723
|
+
const openmsxCommand = `save_msx_screen "${OPENMSX_SCREENDUMP_DIR + scrbasename}"`;
|
|
724
|
+
const response = await openMSXInstance.sendCommand(openmsxCommand);
|
|
725
|
+
return {
|
|
726
|
+
content: [{
|
|
727
|
+
type: "text",
|
|
728
|
+
text: response,
|
|
729
|
+
}],
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// ============================================================================
|
|
734
|
+
// Cleanup handlers for graceful shutdown of MCP server
|
|
735
|
+
// Ensure openMSX emulator is closed when MCP server stops
|
|
736
|
+
let isShuttingDown = false;
|
|
737
|
+
async function gracefulShutdown(exitCode = 0) {
|
|
738
|
+
if (isShuttingDown)
|
|
739
|
+
return;
|
|
740
|
+
isShuttingDown = true;
|
|
741
|
+
try {
|
|
742
|
+
// Try async close first
|
|
743
|
+
await Promise.race([
|
|
744
|
+
openMSXInstance.emu_close(),
|
|
745
|
+
new Promise(resolve => setTimeout(resolve, 2000)) // 2 second timeout
|
|
746
|
+
]);
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
// If async close fails or times out, force close
|
|
750
|
+
openMSXInstance.forceClose();
|
|
751
|
+
}
|
|
752
|
+
// Give a moment for cleanup to complete
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
process.exit(exitCode);
|
|
755
|
+
}, 100);
|
|
756
|
+
}
|
|
757
|
+
// Handle process termination signals
|
|
758
|
+
process.on('SIGINT', () => gracefulShutdown(0));
|
|
759
|
+
process.on('SIGTERM', () => gracefulShutdown(0));
|
|
760
|
+
// Handle when the transport connection is closed (more reliable for MCP)
|
|
761
|
+
process.on('disconnect', () => gracefulShutdown(0));
|
|
762
|
+
// Handle uncaught exceptions and unhandled rejections
|
|
763
|
+
process.on('uncaughtException', async (error) => {
|
|
764
|
+
await gracefulShutdown(1);
|
|
765
|
+
});
|
|
766
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
767
|
+
await gracefulShutdown(1);
|
|
768
|
+
});
|
|
769
|
+
// Additional cleanup when the process is about to exit
|
|
770
|
+
process.on('exit', () => {
|
|
771
|
+
// This is synchronous only - can't use async here
|
|
772
|
+
// Force close as last resort
|
|
773
|
+
openMSXInstance.forceClose();
|
|
774
|
+
});
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// Help function to display usage information
|
|
777
|
+
//
|
|
778
|
+
function showHelp() {
|
|
779
|
+
console.log(`
|
|
780
|
+
MCP-openMSX Server v${PACKAGE_VERSION}
|
|
781
|
+
Model Context Protocol server for openMSX emulator automation
|
|
782
|
+
|
|
783
|
+
Usage:
|
|
784
|
+
mcp-openmsx [transport]
|
|
785
|
+
|
|
786
|
+
Transport options:
|
|
787
|
+
stdio Use stdio transport (default)
|
|
788
|
+
http Use HTTP transport
|
|
789
|
+
|
|
790
|
+
Environment variables:
|
|
791
|
+
OPENMSX_EXECUTABLE Path to openMSX executable
|
|
792
|
+
OPENMSX_SHARE_DIR openMSX share directory
|
|
793
|
+
OPENMSX_SCREENSHOT_DIR Screenshot output directory
|
|
794
|
+
OPENMSX_SCREENDUMP_DIR Screen dump output directory
|
|
795
|
+
MCP_HTTP_PORT HTTP server port (default: 3000)
|
|
796
|
+
|
|
797
|
+
Examples:
|
|
798
|
+
mcp-openmsx # stdio transport
|
|
799
|
+
mcp-openmsx http # HTTP transport
|
|
800
|
+
MCP_TRANSPORT=http mcp-openmsx # HTTP via environment
|
|
801
|
+
`);
|
|
802
|
+
}
|
|
803
|
+
// ============================================================================
|
|
804
|
+
// Start the server
|
|
805
|
+
//
|
|
806
|
+
async function startHttpServer() {
|
|
807
|
+
const app = express();
|
|
808
|
+
app.use(express.json());
|
|
809
|
+
const transports = {};
|
|
810
|
+
// Handle POST requests for client-to-server communication
|
|
811
|
+
app.post('/mcp', async (req, res) => {
|
|
812
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
813
|
+
let transport;
|
|
814
|
+
if (sessionId && transports[sessionId]) {
|
|
815
|
+
transport = transports[sessionId];
|
|
816
|
+
}
|
|
817
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
818
|
+
transport = new StreamableHTTPServerTransport({
|
|
819
|
+
sessionIdGenerator: () => randomUUID(),
|
|
820
|
+
onsessioninitialized: (sessionId) => {
|
|
821
|
+
transports[sessionId] = transport;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
transport.onclose = () => {
|
|
825
|
+
if (transport.sessionId) {
|
|
826
|
+
delete transports[transport.sessionId];
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
// Create a new server instance for this session
|
|
830
|
+
const httpServer = createServerInstance();
|
|
831
|
+
await httpServer.connect(transport);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
res.status(400).json({
|
|
835
|
+
jsonrpc: '2.0',
|
|
836
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
837
|
+
id: null,
|
|
838
|
+
});
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
await transport.handleRequest(req, res, req.body);
|
|
842
|
+
});
|
|
843
|
+
// Handle GET requests for server-to-client notifications via SSE
|
|
844
|
+
app.get('/mcp', async (req, res) => {
|
|
845
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
846
|
+
if (!sessionId || !transports[sessionId]) {
|
|
847
|
+
res.status(400).send('Invalid or missing session ID');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const transport = transports[sessionId];
|
|
851
|
+
await transport.handleRequest(req, res);
|
|
852
|
+
});
|
|
853
|
+
const port = process.env.MCP_HTTP_PORT || 3000;
|
|
854
|
+
app.listen(port, () => {
|
|
855
|
+
console.log(`MCP Server listening on port ${port}`);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function createServerInstance() {
|
|
859
|
+
// Create a new server instance (you might want to extract server creation logic)
|
|
860
|
+
const newServer = new McpServer({
|
|
861
|
+
name: "mcp-stdio-server",
|
|
862
|
+
version: "1.0.0"
|
|
863
|
+
});
|
|
864
|
+
// Re-register all tools (you might want to extract this to a separate function)
|
|
865
|
+
registerAllTools(newServer);
|
|
866
|
+
return newServer;
|
|
867
|
+
}
|
|
868
|
+
// ============================================================================
|
|
869
|
+
// Main function to start the MCP server
|
|
870
|
+
//
|
|
871
|
+
async function main() {
|
|
872
|
+
// Handle CLI arguments
|
|
873
|
+
const args = process.argv.slice(2);
|
|
874
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
875
|
+
showHelp();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
879
|
+
console.log(PACKAGE_VERSION);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
// Environment variables setup
|
|
883
|
+
if (process.env.OPENMSX_EXECUTABLE) {
|
|
884
|
+
OPENMSX_EXECUTABLE = process.env.OPENMSX_EXECUTABLE;
|
|
885
|
+
}
|
|
886
|
+
if (process.env.OPENMSX_SCREENSHOT_DIR && process.env.OPENMSX_SCREENSHOT_DIR !== '') {
|
|
887
|
+
OPENMSX_SCREENSHOT_DIR = process.env.OPENMSX_SCREENSHOT_DIR + path.sep;
|
|
888
|
+
}
|
|
889
|
+
if (process.env.OPENMSX_SCREENDUMP_DIR && process.env.OPENMSX_SCREENDUMP_DIR !== '') {
|
|
890
|
+
OPENMSX_SCREENDUMP_DIR = process.env.OPENMSX_SCREENDUMP_DIR + path.sep;
|
|
891
|
+
}
|
|
892
|
+
if (process.env.OPENMSX_SHARE_DIR) {
|
|
893
|
+
OPENMSX_SHARE_DIR = process.env.OPENMSX_SHARE_DIR + path.sep;
|
|
894
|
+
MACHINES_DIR = `${OPENMSX_SHARE_DIR}machines`;
|
|
895
|
+
EXTENSIONS_DIR = `${OPENMSX_SHARE_DIR}extensions`;
|
|
896
|
+
}
|
|
897
|
+
// Detect transport type from environment or command line
|
|
898
|
+
const transportType = process.env.MCP_TRANSPORT || process.argv[2] || 'stdio';
|
|
899
|
+
if (transportType === 'http') {
|
|
900
|
+
// Start Streamable HTTP server
|
|
901
|
+
await startHttpServer();
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
// Default to stdio
|
|
905
|
+
const transport = new StdioServerTransport();
|
|
906
|
+
await createServerInstance().connect(transport);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
main().catch((error) => {
|
|
910
|
+
gracefulShutdown(1);
|
|
911
|
+
process.exit(1);
|
|
912
|
+
});
|