@nataliapc/mcp-openmsx
Version:
Model context protocol server for openMSX automation and control
179 lines (178 loc) • 8.49 kB
JavaScript
import { openMSXInstance } from "./openmsx.js";
import { isErrorResponse } from "./utils.js";
import { samplingFindMatches } from "./server_sampling.js";
// ============================================================================
// Default machines offered when no machine is specified in launch command
// ============================================================================
const DEFAULT_MACHINES = [
{ const: 'C-BIOS_MSX1', title: 'MSX1 (C-BIOS, no system ROMs needed)' },
{ const: 'Philips_VG_8020', title: 'MSX1 European (Philips VG-8020)' },
{ const: 'C-BIOS_MSX2', title: 'MSX2 (C-BIOS, no system ROMs needed)' },
{ const: 'Philips_NMS_8250', title: 'MSX2 European (Philips NMS-8250)' },
{ const: 'C-BIOS_MSX2+', title: 'MSX2+ (C-BIOS, openMSX default)' },
{ const: 'Panasonic_FS-A1GT', title: 'MSX turboR (Panasonic FS-A1GT)' },
];
// ============================================================================
// Helper: Resolve machine and extensions for launch using elicitation + sampling
// ============================================================================
export async function resolveLaunchParams(server, emuDirectories, machine, extensions) {
// Load lists lazily only when needed
let machinesList;
let extensionsList;
const loadMachinesList = async () => {
if (!machinesList) {
const raw = await openMSXInstance.getMachineList(emuDirectories.MACHINES_DIR);
if (isErrorResponse(raw))
throw new Error(raw);
machinesList = JSON.parse(raw);
}
return machinesList;
};
const loadExtensionsList = async () => {
if (!extensionsList) {
const raw = await openMSXInstance.getExtensionList(emuDirectories.EXTENSIONS_DIR);
if (isErrorResponse(raw))
throw new Error(raw);
extensionsList = JSON.parse(raw);
}
return extensionsList;
};
// ── Resolve machine ──────────────────────────────────────────────────
let resolvedMachine = machine || '';
if (machine) {
// Check for exact match
const machines = await loadMachinesList();
const exactMatch = machines.find(m => m.name.toLowerCase() === machine.toLowerCase());
if (exactMatch) {
resolvedMachine = exactMatch.name;
}
else {
// Ambiguous machine name → use sampling to find suggestions, then elicit
try {
const suggestions = await samplingFindMatches(server, machine, machines, 'machine');
if (suggestions.length > 0) {
const elicitResult = await server.server.elicitInput({
mode: 'form',
message: `No exact match found for machine "${machine}". Please select the MSX machine to emulate:`,
requestedSchema: {
type: 'object',
properties: {
machine: {
type: 'string',
title: 'MSX Machine',
description: `Best matches for "${machine}"`,
oneOf: suggestions,
},
},
required: ['machine'],
},
});
if (elicitResult.action !== 'accept' || !elicitResult.content) {
return { machine: '', extensions: [], cancelled: true };
}
resolvedMachine = elicitResult.content.machine;
}
else {
// Sampling found nothing → use the original value (let openMSX handle the error)
resolvedMachine = machine;
}
}
catch {
// Client doesn't support sampling/elicitation → fallback
resolvedMachine = machine;
}
}
}
else {
// No machine provided → elicit with common defaults
try {
const elicitResult = await server.server.elicitInput({
mode: 'form',
message: 'No machine specified. Please select the MSX machine to emulate:',
requestedSchema: {
type: 'object',
properties: {
machine: {
type: 'string',
title: 'MSX Machine',
description: 'Select the type of MSX machine to launch',
oneOf: DEFAULT_MACHINES,
},
},
required: ['machine'],
},
});
if (elicitResult.action !== 'accept' || !elicitResult.content) {
return { machine: '', extensions: [], cancelled: true };
}
resolvedMachine = elicitResult.content.machine;
}
catch {
// Client doesn't support elicitation → launch with openMSX default
resolvedMachine = '';
}
}
// ── Resolve extensions ───────────────────────────────────────────────
let resolvedExtensions = extensions || [];
if (extensions && extensions.length > 0) {
try {
const allExtensions = await loadExtensionsList();
const extNameMap = new Map(allExtensions.map(e => [e.name.toLowerCase(), e]));
const matched = [];
const ambiguous = [];
for (const ext of extensions) {
const exact = extNameMap.get(ext.toLowerCase());
if (exact) {
matched.push(exact.name);
}
else {
ambiguous.push(ext);
}
}
if (ambiguous.length > 0) {
// Use sampling to find best matches for ALL ambiguous extensions at once
const ambiguousQuery = ambiguous.join(', ');
const suggestions = await samplingFindMatches(server, ambiguousQuery, allExtensions, 'extension', 6);
if (suggestions.length > 0) {
// Pre-select the already matched extensions in the suggestions
const elicitResult = await server.server.elicitInput({
mode: 'form',
message: `No exact match found for extension(s): ${ambiguous.map(e => `"${e}"`).join(', ')}. Please select the extensions to use:`,
requestedSchema: {
type: 'object',
properties: {
extensions: {
type: 'array',
title: 'MSX Extensions',
description: `Best matches for: ${ambiguousQuery}`,
items: {
anyOf: suggestions,
},
},
},
required: ['extensions'],
},
});
if (elicitResult.action !== 'accept' || !elicitResult.content) {
return { machine: resolvedMachine, extensions: [], cancelled: true };
}
const selectedExtensions = elicitResult.content.extensions;
resolvedExtensions = [...matched, ...selectedExtensions];
}
else {
// Sampling found nothing → use original values
resolvedExtensions = extensions;
}
}
else {
// All extensions matched exactly
resolvedExtensions = matched;
}
}
catch {
// Client doesn't support sampling/elicitation → fallback
resolvedExtensions = extensions;
}
}
return { machine: resolvedMachine, extensions: resolvedExtensions };
}