@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,178 @@
|
|
|
1
|
+
import { openMSXInstance } from "./openmsx.js";
|
|
2
|
+
import { isErrorResponse } from "./utils.js";
|
|
3
|
+
import { samplingFindMatches } from "./server_sampling.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Default machines offered when no machine is specified in launch command
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const DEFAULT_MACHINES = [
|
|
8
|
+
{ const: 'C-BIOS_MSX1', title: 'MSX1 (C-BIOS, no system ROMs needed)' },
|
|
9
|
+
{ const: 'Philips_VG_8020', title: 'MSX1 European (Philips VG-8020)' },
|
|
10
|
+
{ const: 'C-BIOS_MSX2', title: 'MSX2 (C-BIOS, no system ROMs needed)' },
|
|
11
|
+
{ const: 'Philips_NMS_8250', title: 'MSX2 European (Philips NMS-8250)' },
|
|
12
|
+
{ const: 'C-BIOS_MSX2+', title: 'MSX2+ (C-BIOS, openMSX default)' },
|
|
13
|
+
{ const: 'Panasonic_FS-A1GT', title: 'MSX turboR (Panasonic FS-A1GT)' },
|
|
14
|
+
];
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helper: Resolve machine and extensions for launch using elicitation + sampling
|
|
17
|
+
// ============================================================================
|
|
18
|
+
export async function resolveLaunchParams(server, emuDirectories, machine, extensions) {
|
|
19
|
+
// Load lists lazily only when needed
|
|
20
|
+
let machinesList;
|
|
21
|
+
let extensionsList;
|
|
22
|
+
const loadMachinesList = async () => {
|
|
23
|
+
if (!machinesList) {
|
|
24
|
+
const raw = await openMSXInstance.getMachineList(emuDirectories.MACHINES_DIR);
|
|
25
|
+
if (isErrorResponse(raw))
|
|
26
|
+
throw new Error(raw);
|
|
27
|
+
machinesList = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
return machinesList;
|
|
30
|
+
};
|
|
31
|
+
const loadExtensionsList = async () => {
|
|
32
|
+
if (!extensionsList) {
|
|
33
|
+
const raw = await openMSXInstance.getExtensionList(emuDirectories.EXTENSIONS_DIR);
|
|
34
|
+
if (isErrorResponse(raw))
|
|
35
|
+
throw new Error(raw);
|
|
36
|
+
extensionsList = JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
return extensionsList;
|
|
39
|
+
};
|
|
40
|
+
// ── Resolve machine ──────────────────────────────────────────────────
|
|
41
|
+
let resolvedMachine = machine || '';
|
|
42
|
+
if (machine) {
|
|
43
|
+
// Check for exact match
|
|
44
|
+
const machines = await loadMachinesList();
|
|
45
|
+
const exactMatch = machines.find(m => m.name.toLowerCase() === machine.toLowerCase());
|
|
46
|
+
if (exactMatch) {
|
|
47
|
+
resolvedMachine = exactMatch.name;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Ambiguous machine name → use sampling to find suggestions, then elicit
|
|
51
|
+
try {
|
|
52
|
+
const suggestions = await samplingFindMatches(server, machine, machines, 'machine');
|
|
53
|
+
if (suggestions.length > 0) {
|
|
54
|
+
const elicitResult = await server.server.elicitInput({
|
|
55
|
+
mode: 'form',
|
|
56
|
+
message: `No exact match found for machine "${machine}". Please select the MSX machine to emulate:`,
|
|
57
|
+
requestedSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
machine: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
title: 'MSX Machine',
|
|
63
|
+
description: `Best matches for "${machine}"`,
|
|
64
|
+
oneOf: suggestions,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['machine'],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (elicitResult.action !== 'accept' || !elicitResult.content) {
|
|
71
|
+
return { machine: '', extensions: [], cancelled: true };
|
|
72
|
+
}
|
|
73
|
+
resolvedMachine = elicitResult.content.machine;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Sampling found nothing → use the original value (let openMSX handle the error)
|
|
77
|
+
resolvedMachine = machine;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Client doesn't support sampling/elicitation → fallback
|
|
82
|
+
resolvedMachine = machine;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// No machine provided → elicit with common defaults
|
|
88
|
+
try {
|
|
89
|
+
const elicitResult = await server.server.elicitInput({
|
|
90
|
+
mode: 'form',
|
|
91
|
+
message: 'No machine specified. Please select the MSX machine to emulate:',
|
|
92
|
+
requestedSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
machine: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
title: 'MSX Machine',
|
|
98
|
+
description: 'Select the type of MSX machine to launch',
|
|
99
|
+
oneOf: DEFAULT_MACHINES,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['machine'],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (elicitResult.action !== 'accept' || !elicitResult.content) {
|
|
106
|
+
return { machine: '', extensions: [], cancelled: true };
|
|
107
|
+
}
|
|
108
|
+
resolvedMachine = elicitResult.content.machine;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Client doesn't support elicitation → launch with openMSX default
|
|
112
|
+
resolvedMachine = '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── Resolve extensions ───────────────────────────────────────────────
|
|
116
|
+
let resolvedExtensions = extensions || [];
|
|
117
|
+
if (extensions && extensions.length > 0) {
|
|
118
|
+
try {
|
|
119
|
+
const allExtensions = await loadExtensionsList();
|
|
120
|
+
const extNameMap = new Map(allExtensions.map(e => [e.name.toLowerCase(), e]));
|
|
121
|
+
const matched = [];
|
|
122
|
+
const ambiguous = [];
|
|
123
|
+
for (const ext of extensions) {
|
|
124
|
+
const exact = extNameMap.get(ext.toLowerCase());
|
|
125
|
+
if (exact) {
|
|
126
|
+
matched.push(exact.name);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
ambiguous.push(ext);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (ambiguous.length > 0) {
|
|
133
|
+
// Use sampling to find best matches for ALL ambiguous extensions at once
|
|
134
|
+
const ambiguousQuery = ambiguous.join(', ');
|
|
135
|
+
const suggestions = await samplingFindMatches(server, ambiguousQuery, allExtensions, 'extension', 6);
|
|
136
|
+
if (suggestions.length > 0) {
|
|
137
|
+
// Pre-select the already matched extensions in the suggestions
|
|
138
|
+
const elicitResult = await server.server.elicitInput({
|
|
139
|
+
mode: 'form',
|
|
140
|
+
message: `No exact match found for extension(s): ${ambiguous.map(e => `"${e}"`).join(', ')}. Please select the extensions to use:`,
|
|
141
|
+
requestedSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
extensions: {
|
|
145
|
+
type: 'array',
|
|
146
|
+
title: 'MSX Extensions',
|
|
147
|
+
description: `Best matches for: ${ambiguousQuery}`,
|
|
148
|
+
items: {
|
|
149
|
+
anyOf: suggestions,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['extensions'],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (elicitResult.action !== 'accept' || !elicitResult.content) {
|
|
157
|
+
return { machine: resolvedMachine, extensions: [], cancelled: true };
|
|
158
|
+
}
|
|
159
|
+
const selectedExtensions = elicitResult.content.extensions;
|
|
160
|
+
resolvedExtensions = [...matched, ...selectedExtensions];
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Sampling found nothing → use original values
|
|
164
|
+
resolvedExtensions = extensions;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// All extensions matched exactly
|
|
169
|
+
resolvedExtensions = matched;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Client doesn't support sampling/elicitation → fallback
|
|
174
|
+
resolvedExtensions = extensions;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { machine: resolvedMachine, extensions: resolvedExtensions };
|
|
178
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { basicInstructions } from "./server_resources.js";
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Prompts available in the MCP server
|
|
5
|
+
// https://modelcontextprotocol.io/docs/concepts/prompts
|
|
6
|
+
export async function registerPrompts(server) {
|
|
7
|
+
server.registerPrompt(
|
|
8
|
+
// Name of the prompt (used to call it)
|
|
9
|
+
"basic", {
|
|
10
|
+
title: "MSX BASIC Instruction Manual",
|
|
11
|
+
// Description of the prompt (what it does)
|
|
12
|
+
description: "MSX BASIC instructions manual",
|
|
13
|
+
// Schema for the prompt (input validation)
|
|
14
|
+
argsSchema: {
|
|
15
|
+
instruction: z.string()
|
|
16
|
+
.min(1, "Instruction name cannot be empty")
|
|
17
|
+
.max(50, "Instruction name too long")
|
|
18
|
+
.describe("Name of the MSX BASIC instruction to get information about (e.g. 'PRINT', 'FOR', 'POKE'). Case insensitive.")
|
|
19
|
+
.transform(val => val.trim().toUpperCase()),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// Handler for the prompt (function to be executed when the prompt is called)
|
|
23
|
+
({ instruction }) => {
|
|
24
|
+
// Normalize instruction name for better matching
|
|
25
|
+
const normalizedInstruction = instruction.replace(/[()]/g, '').trim();
|
|
26
|
+
let prompt = '';
|
|
27
|
+
// Check if instruction is in the known list
|
|
28
|
+
const isKnownInstruction = basicInstructions.some(basic => basic.replace(/[()]/g, '').toUpperCase() === normalizedInstruction);
|
|
29
|
+
if (isKnownInstruction) {
|
|
30
|
+
prompt = `Provide comprehensive information about the MSX BASIC instruction '${instruction}'.
|
|
31
|
+
STRICT REQUIREMENTS:
|
|
32
|
+
- NEVER invent or assume any information not explicitly found in the MCP resources
|
|
33
|
+
- ONLY use information from 'msxdocs:' resources and tools #msxdocs_resource_get and #vector_db_query
|
|
34
|
+
- If specific details (ranges, values, behaviors) are not in the resources, state "Not specified in available documentation"
|
|
35
|
+
- Do NOT use general knowledge about MSX systems - stick ONLY to what the resources contain
|
|
36
|
+
- When citing parameters or ranges, quote them EXACTLY as written in the source documentation
|
|
37
|
+
MANDATORY SECTIONS:
|
|
38
|
+
- Include these sections if information is available in resources: 'Description', 'Syntax', 'Parameters', 'Notes', 'Usage examples', 'Related instructions', and 'Availability'.
|
|
39
|
+
- You MUST respond in the user language, unless otherwise specified.
|
|
40
|
+
SOURCES:
|
|
41
|
+
- Always cite the specific MCP resources used at the end in section 'Sources' with exact resource uri.
|
|
42
|
+
- For each resource, add a markdown link to the resource if external url is available.`;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const suggestions = basicInstructions
|
|
46
|
+
.filter(basic => basic.toUpperCase().includes(normalizedInstruction) ||
|
|
47
|
+
normalizedInstruction.includes(basic.replace(/[()]/g, '').toUpperCase()))
|
|
48
|
+
.slice(0, 5);
|
|
49
|
+
// If the instruction is not known, suggest alternatives
|
|
50
|
+
prompt = `Explain that '${instruction}' does not appear to be a standard MSX BASIC instruction.
|
|
51
|
+
STRICT REQUIREMENTS:
|
|
52
|
+
- ONLY use information found in MCP resources via #vector_db_query tool
|
|
53
|
+
- Do NOT assume or invent any information about MSX BASIC instructions
|
|
54
|
+
- If suggesting alternatives, provide ONLY what is found in the search results
|
|
55
|
+
${suggestions.length > 0 ? `Suggest one of these: ${suggestions.join(', ')} instructions as a correct search string. List them with a brief description using ONLY information found in the resources 'msxdocs://basic_wiki/{instruction}'` : 'Use #vector_db_query to search for similar instructions.'}
|
|
56
|
+
IMPORTANT: Review the available MSX BASIC instructions using the available MCP resources. You may still try to find information using the available resources, as it could be documented under a different name or as part of another instruction. Base your response EXCLUSIVELY on what the resources and tools return.
|
|
57
|
+
You MUST respond in the user language, unless otherwise specified.`;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
messages: [{
|
|
61
|
+
role: "assistant",
|
|
62
|
+
content: {
|
|
63
|
+
type: "text",
|
|
64
|
+
text: prompt,
|
|
65
|
+
},
|
|
66
|
+
}]
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @package @nataliapc/mcp-openmsx
|
|
3
|
+
* @author Natalia Pujol Cremades (@nataliapc)
|
|
4
|
+
* @license GPL2
|
|
5
|
+
*/
|
|
6
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fetchCleanWebpage, addFileExtension, listResourcesDirectory } from "./utils.js";
|
|
10
|
+
// Source: https://www.msx.org/wiki/Category:MSX-BASIC_Instructions
|
|
11
|
+
export const basicInstructions = [
|
|
12
|
+
"ABS()", "AND", "ASC()", "ATN()", "AUTO", "BASE()", "BEEP", "BIN$()", "BLOAD", "BSAVE", "CALL", "CALL ADJUST",
|
|
13
|
+
"CALL IMPOSE", "CALL OPTIONS", "CALL PAUSE", "CALL PCMPLAY", "CALL PCMREC", "CDBL()", "CHR$()", "CINT()", "CIRCLE",
|
|
14
|
+
"CLEAR", "CLOAD", "CLOAD?", "CLOSE", "CLS", "COLOR", "COLOR=", "COLOR SPRITE()", "COLOR SPRITE$()", "CONT", "COPY",
|
|
15
|
+
"COPY SCREEN", "COS()", "CSAVE", "CSNG()", "CSRLIN", "DATA", "DEFDBL", "DEF FN", "DEFINT", "DEFSNG", "DEFSTR",
|
|
16
|
+
"DEF USR", "DELETE", "DIM", "DRAW", "ELSE", "END", "EOF()", "EQV", "ERASE", "ERL", "ERR", "ERROR", "EXP()", "FIELD",
|
|
17
|
+
"FIX()", "FN", "FOR...NEXT", "FRE()", "GET DATE", "GET TIME", "GOSUB", "GOTO", "HEX$()", "IF...GOTO...ELSE",
|
|
18
|
+
"IF...THEN...ELSE", "IMP", "INKEY$", "INP()", "INPUT", "INPUT$()", "INSTR()", "INT()", "INTERVAL", "KEY", "KEY()",
|
|
19
|
+
"LEFT$()", "LEN()", "LET", "LINE", "LINE INPUT", "LIST", "LLIST", "LOAD", "LOCATE", "LOG()", "LPOS()", "LPRINT",
|
|
20
|
+
"MAXFILES", "MERGE", "MID$()", "MOD", "MOTOR", "NEW", "NOT", "OCT$()", "ON...GOSUB", "ON...GOTO", "ON ERROR GOTO",
|
|
21
|
+
"ON INTERVAL GOSUB", "ON KEY GOSUB", "ON SPRITE GOSUB", "ON STOP GOSUB", "ON STRIG GOSUB", "OPEN", "OR", "OUT",
|
|
22
|
+
"PAD()", "PAINT", "PDL()", "PEEK()", "PLAY", "PLAY()", "POINT", "POKE", "POS()", "PRESET", "PRINT", "PSET",
|
|
23
|
+
"PUT KANJI", "PUT SPRITE", "READ", "REM", "RENUM", "RESTORE", "RESUME", "RETURN", "RIGHT$()", "RND()", "RUN",
|
|
24
|
+
"SAVE", "SCREEN", "SET ADJUST", "SET BEEP", "SET DATE", "SET PAGE", "SET PASSWORD", "SET PROMPT", "SET SCREEN",
|
|
25
|
+
"SET SCROLL", "SET TIME", "SET TITLE", "SET VIDEO", "SGN()", "SIN()", "SOUND", "SPACE$()", "SPC()", "SPRITE",
|
|
26
|
+
"SPRITE$()", "SQR()", "STICK()", "STOP", "STR$()", "STRIG()", "STRING$()", "SWAP", "TAB()", "TAN()", "TIME",
|
|
27
|
+
"TROFF", "TRON", "USR()", "VAL()", "VARPTR()", "VDP()", "VPEEK()", "VPOKE", "WAIT", "WIDTH", "XOR"
|
|
28
|
+
];
|
|
29
|
+
const regResources = [];
|
|
30
|
+
export function getRegisteredResourcesList() {
|
|
31
|
+
return regResources;
|
|
32
|
+
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Resources available in the MCP server
|
|
35
|
+
// https://modelcontextprotocol.io/docs/concepts/resources
|
|
36
|
+
export async function registerResources(server, resourcesDir) {
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// MSX Documentation resources
|
|
39
|
+
const resdocs = (await listResourcesDirectory(resourcesDir)).sort();
|
|
40
|
+
for (let index = 0; index < resdocs.length; index++) {
|
|
41
|
+
// Read the toc.json file if exists, otherwise skip this section
|
|
42
|
+
const sectionName = resdocs[index];
|
|
43
|
+
const tocFile = path.join(resourcesDir, `${sectionName}/toc.json`);
|
|
44
|
+
let tocContent = { toc: [] };
|
|
45
|
+
try {
|
|
46
|
+
tocContent = JSON.parse(await fs.readFile(tocFile, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// The toc.json file does not exist or is invalid, skip this section
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Register each item in the toc.json as a resource
|
|
53
|
+
tocContent.toc.forEach((item, itemIndex) => {
|
|
54
|
+
const itemName = path.parse(item.uri.split('/').pop()).base || '';
|
|
55
|
+
let resource = {
|
|
56
|
+
uri: item.uri,
|
|
57
|
+
filename: '',
|
|
58
|
+
resource: server.registerResource(
|
|
59
|
+
// Name of the resource (used to call it)
|
|
60
|
+
`msxdocs_${sectionName}_${item.title.replace(/[^a-z0-9]+/gi, '_').toLowerCase()}`,
|
|
61
|
+
// Resource URI template
|
|
62
|
+
item.uri,
|
|
63
|
+
// Metadata for the resource
|
|
64
|
+
{
|
|
65
|
+
title: item.title || `MSX Documentation '${sectionName}': ${itemName}`,
|
|
66
|
+
description: item.description || `Documentation for MSX resource '${sectionName}': ${itemName}`,
|
|
67
|
+
mimeType: item.mimeType || 'text/markdown',
|
|
68
|
+
annotations: {
|
|
69
|
+
"audience": ["user", "assistant"],
|
|
70
|
+
"priority": 0.8,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
// Handler for the resource (function to be executed when the resource is called)
|
|
74
|
+
async (uri) => {
|
|
75
|
+
let resourceContent;
|
|
76
|
+
let mimeType;
|
|
77
|
+
if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
|
|
78
|
+
// Fetch the resource from the URL
|
|
79
|
+
try {
|
|
80
|
+
[resourceContent, mimeType] = await fetchCleanWebpage(uri.href);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// Throw exception (MCP protocol requirement)
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Read the resource from the local MCP server resources directory
|
|
89
|
+
try {
|
|
90
|
+
let resourceFile;
|
|
91
|
+
[mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, `${sectionName}/${itemName}`));
|
|
92
|
+
resourceContent = await fs.readFile(resourceFile, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Throw exception (MCP protocol requirement)
|
|
96
|
+
throw new Error(`Error reading resource ${sectionName}/${item.uri}: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
contents: [{
|
|
101
|
+
uri: uri.href,
|
|
102
|
+
text: resourceContent,
|
|
103
|
+
mimeType: mimeType || 'text/plain',
|
|
104
|
+
}],
|
|
105
|
+
};
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
regResources.push(resource);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
;
|
|
112
|
+
// Source: https://www.msx.org/wiki/Category:MSX-BASIC_Instructions
|
|
113
|
+
server.registerResource("msxdocs_basic_wiki", new ResourceTemplate("msxdocs://basic_wiki/{instruction}", {
|
|
114
|
+
list: undefined,
|
|
115
|
+
complete: {
|
|
116
|
+
instruction: (value) => basicInstructions,
|
|
117
|
+
},
|
|
118
|
+
}), {
|
|
119
|
+
title: "MSX BASIC Instructions Documentation",
|
|
120
|
+
description: "Documentation about all the standard MSX BASIC instructions from www.msx.org/wiki/Category:MSX-BASIC_Instructions",
|
|
121
|
+
mimeType: "text/html",
|
|
122
|
+
annotations: {
|
|
123
|
+
"audience": ["user", "assistant"],
|
|
124
|
+
"priority": 0.8,
|
|
125
|
+
},
|
|
126
|
+
}, async (uri, variables) => {
|
|
127
|
+
let instruction = variables.instruction;
|
|
128
|
+
let resourceContent;
|
|
129
|
+
let mimeType;
|
|
130
|
+
// urldecode the instruction to avoid issues with special characters
|
|
131
|
+
instruction = decodeURIComponent(instruction).replaceAll(' ', '_');
|
|
132
|
+
try {
|
|
133
|
+
let resourceFile;
|
|
134
|
+
[mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, 'programming', 'basic_wiki', instruction));
|
|
135
|
+
resourceContent = await fs.readFile(resourceFile, 'utf8');
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Throw exception (MCP protocol requirement)
|
|
139
|
+
throw new Error(`Error reading resource programming/basic_wiki/${instruction}: ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
contents: [{
|
|
143
|
+
uri: uri.href,
|
|
144
|
+
text: resourceContent,
|
|
145
|
+
mimeType: mimeType || 'text/plain',
|
|
146
|
+
}],
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Helper: Use sampling (LLM) to pick the best matches from a list
|
|
3
|
+
// ============================================================================
|
|
4
|
+
export async function samplingFindMatches(server, userQuery, items, itemType, maxResults = 6) {
|
|
5
|
+
// Build a compact list for the LLM
|
|
6
|
+
const itemList = items.map(m => `${m.name}: ${m.description}`).join('\n');
|
|
7
|
+
const samplingResult = await server.server.createMessage({
|
|
8
|
+
messages: [{
|
|
9
|
+
role: 'user',
|
|
10
|
+
content: {
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `The user wants to launch an MSX emulator with ${itemType}: "${userQuery}".\n\nHere is the full list of available ${itemType}s:\n${itemList}\n\nReturn ONLY the top ${maxResults} best matching ${itemType} names (exact names from the list), one per line, no numbering, no descriptions, no extra text. Most relevant first.`
|
|
13
|
+
}
|
|
14
|
+
}],
|
|
15
|
+
maxTokens: 300,
|
|
16
|
+
systemPrompt: `You are a helpful assistant that matches user queries to MSX ${itemType} names. Return ONLY exact names from the provided list, one per line. No extra text.`,
|
|
17
|
+
temperature: 0,
|
|
18
|
+
});
|
|
19
|
+
if (samplingResult.content.type !== 'text')
|
|
20
|
+
return [];
|
|
21
|
+
const suggestedNames = samplingResult.content.text
|
|
22
|
+
.split('\n')
|
|
23
|
+
.map(l => l.trim())
|
|
24
|
+
.filter(l => l.length > 0);
|
|
25
|
+
// Map back to items with validation
|
|
26
|
+
const nameMap = new Map(items.map(m => [m.name.toLowerCase(), m]));
|
|
27
|
+
const matches = [];
|
|
28
|
+
for (const name of suggestedNames) {
|
|
29
|
+
const item = nameMap.get(name.toLowerCase());
|
|
30
|
+
if (item && matches.length < maxResults) {
|
|
31
|
+
matches.push({ const: item.name, title: `${item.name} — ${item.description}` });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return matches;
|
|
35
|
+
}
|