@nataliapc/mcp-openmsx 1.1.13 → 1.1.14
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/dist/server.js +21 -65
- package/dist/utils.js +69 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* through TCL commands via stdio.
|
|
7
7
|
*
|
|
8
8
|
* @package @nataliapc/mcp-openmsx
|
|
9
|
-
* @version 1.1.
|
|
9
|
+
* @version 1.1.14
|
|
10
10
|
* @author Natalia Pujol Cremades (@nataliapc)
|
|
11
11
|
* @license GPL2
|
|
12
12
|
*/
|
|
@@ -18,12 +18,11 @@ import { randomUUID } from "node:crypto";
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import express from "express";
|
|
20
20
|
import fs from "fs/promises";
|
|
21
|
-
import mime from "mime-types";
|
|
22
21
|
import path from "path";
|
|
23
22
|
import { openMSXInstance } from "./openmsx.js";
|
|
24
|
-
import { encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
|
|
23
|
+
import { fetchCleanWebpage, addFileExtension, listResourcesDirectory, encodeTypeText, isErrorResponse, getResponseContent } from "./utils.js";
|
|
25
24
|
// Version info for CLI
|
|
26
|
-
const PACKAGE_VERSION = "1.1.
|
|
25
|
+
const PACKAGE_VERSION = "1.1.14";
|
|
27
26
|
const resourcesDir = path.join(path.dirname(new URL(import.meta.url).pathname), "../resources");
|
|
28
27
|
// Defaults for openMSX paths
|
|
29
28
|
var OPENMSX_EXECUTABLE = 'openmsx';
|
|
@@ -864,22 +863,22 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
864
863
|
});
|
|
865
864
|
// ============================================================================
|
|
866
865
|
// MSX Documentation resources
|
|
867
|
-
const resdocs = (await listResourcesDirectory()).sort();
|
|
866
|
+
const resdocs = (await listResourcesDirectory(resourcesDir)).sort();
|
|
868
867
|
for (let index = 0; index < resdocs.length; index++) {
|
|
869
868
|
const sectionName = resdocs[index];
|
|
870
869
|
const tocFile = path.join(resourcesDir, `${sectionName}/toc.json`);
|
|
871
870
|
const tocContent = JSON.parse(await fs.readFile(tocFile, 'utf8'));
|
|
872
871
|
tocContent.toc.forEach((item, itemIndex) => {
|
|
873
|
-
const itemName = item.uri.split('/').pop() || '';
|
|
872
|
+
const itemName = path.parse(item.uri.split('/').pop()).name || '';
|
|
874
873
|
server.registerResource(
|
|
875
874
|
// Name of the resource (used to call it)
|
|
876
|
-
`msxdocs_${sectionName}_${
|
|
875
|
+
`msxdocs_${sectionName}_${item.title.replace(/[^a-z0-9]+/gi, '_').toLowerCase()}`,
|
|
877
876
|
// Resource URI template
|
|
878
877
|
item.uri,
|
|
879
878
|
// Metadata for the resource
|
|
880
879
|
{
|
|
881
|
-
title: item.title || `MSX Documentation '${sectionName}'
|
|
882
|
-
description: item.description || `Documentation for MSX resource '${sectionName}'
|
|
880
|
+
title: item.title || `MSX Documentation '${sectionName}': ${itemName}`,
|
|
881
|
+
description: item.description || `Documentation for MSX resource '${sectionName}': ${itemName}`,
|
|
883
882
|
mimeType: item.mimeType || 'text/markdown',
|
|
884
883
|
},
|
|
885
884
|
// Handler for the resource (function to be executed when the resource is called)
|
|
@@ -889,14 +888,11 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
889
888
|
if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
|
|
890
889
|
// Fetch the resource from the URL
|
|
891
890
|
try {
|
|
892
|
-
resourceContent = await
|
|
893
|
-
mimeType = response.headers.get('content-type') || 'text/plain';
|
|
894
|
-
return response.text();
|
|
895
|
-
}) || 'Error downloading resource content';
|
|
891
|
+
[resourceContent, mimeType] = await fetchCleanWebpage(uri.href);
|
|
896
892
|
}
|
|
897
893
|
catch (error) {
|
|
898
894
|
// Throw exception (MCP protocol requirement)
|
|
899
|
-
throw
|
|
895
|
+
throw error;
|
|
900
896
|
}
|
|
901
897
|
}
|
|
902
898
|
else {
|
|
@@ -922,16 +918,16 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
922
918
|
});
|
|
923
919
|
}
|
|
924
920
|
;
|
|
925
|
-
server.resource("
|
|
921
|
+
server.resource("msxdocs_basic_wiki", new ResourceTemplate("msxdocs://basic_wiki/{instruction}", {
|
|
926
922
|
list: undefined,
|
|
927
923
|
complete: {
|
|
928
|
-
|
|
924
|
+
instruction: (value) => [
|
|
929
925
|
"ABS()", "AND", "ASC()", "ATN()", "AUTO", "BASE()", "BEEP", "BIN$()", "BLOAD", "BSAVE", "CALL", "CALL ADJUST", "CALL PAUSE", "CALL PCMPLAY", "CALL PCMREC",
|
|
930
926
|
"CDBL()", "CHR$()", "CINT()", "CIRCLE", "CLEAR", "CLOAD", "CLOAD?", "CLOSE", "CLS", "COLOR", "COLOR=", "COLOR", "COLOR", "CONT", "COPY", "COPY", "COS()", "CSAVE",
|
|
931
927
|
"CSNG()", "CSRLIN", "DATA", "DEFDBL", "DEF FN", "DEFINT", "DEFSNG", "DEFSTR", "DEF USR", "DELETE", "DIM", "DRAW", "ELSE", "END", "EOF()", "EQV", "ERASE", "ERL", "ERR",
|
|
932
|
-
"ERROR", "EXP()", "FIX()", "FN
|
|
933
|
-
"
|
|
934
|
-
"
|
|
928
|
+
"ERROR", "EXP()", "FIX()", "FN", "FOR...NEXT", "FRE()", "GET DATE", "GET TIME", "GOSUB", "GOTO", "HEX$()", "IF...GOTO...ELSE", "IF...THEN...ELSE", "IMP",
|
|
929
|
+
"INKEY$", "INP()", "INPUT", "INPUT$()", "INSTR()", "INT()", "INTERVAL", "KEY", "KEY()", "LEFT$()", "LEN()", "LET", "LINE", "LINE INPUT", "LIST", "LLIST",
|
|
930
|
+
"LOAD", "LOCATE", "LOG()", "LPOS()", "LPRINT", "MAXFILES", "MERGE", "MID$()", "MOD", "MOTOR", "NEW", "NOT", "OCT$()", "ON...GOSUB", "ON...GOTO",
|
|
935
931
|
"ON ERROR GOTO", "ON INTERVAL GOSUB", "ON KEY GOSUB", "ON SPRITE GOSUB", "ON STOP GOSUB", "ON STRIG GOSUB", "OPEN", "OR", "OUT", "PAD()", "PAINT", "PDL()",
|
|
936
932
|
"PEEK()", "PLAY", "PLAY()", "POINT", "POKE", "POS()", "PRESET", "PRINT", "PSET", "PUT KANJI", "PUT SPRITE", "READ", "REM", "RENUM", "RESTORE", "RESUME",
|
|
937
933
|
"RETURN", "RIGHT$()", "RND()", "RUN", "SAVE", "SCREEN", "SET ADJUST", "SET BEEP", "SET DATE", "SET PAGE", "SET PASSWORD", "SET PROMPT", "SET SCREEN",
|
|
@@ -941,25 +937,20 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
941
937
|
],
|
|
942
938
|
},
|
|
943
939
|
}), {
|
|
944
|
-
title: "BASIC
|
|
945
|
-
description: "Documentation about all the standard MSX
|
|
940
|
+
title: "MSX BASIC Instructions Documentation",
|
|
941
|
+
description: "Documentation about all the standard MSX BASIC instructions from www.msx.org",
|
|
946
942
|
mimeType: "text/html",
|
|
947
943
|
}, async (uri, variables) => {
|
|
948
|
-
const
|
|
949
|
-
const url = `https://www.msx.org/wiki/${
|
|
944
|
+
const instruction = variables.instruction.replace(/ /g, '_').replace(/\?/g, '%3F').replace(/=/g, '%3D');
|
|
945
|
+
const url = `https://www.msx.org/wiki/${instruction}`;
|
|
950
946
|
let resourceContent;
|
|
951
947
|
let mimeType;
|
|
952
948
|
try {
|
|
953
|
-
resourceContent = await
|
|
954
|
-
mimeType = response.headers.get('content-type') || 'text/plain';
|
|
955
|
-
return response.text();
|
|
956
|
-
}) || 'Error downloading resource content';
|
|
957
|
-
// Remove script, style, and link tags from the content
|
|
958
|
-
resourceContent = resourceContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>|<link\b[^>]*\/?>/gi, '');
|
|
949
|
+
[resourceContent, mimeType] = await fetchCleanWebpage(url);
|
|
959
950
|
}
|
|
960
951
|
catch (error) {
|
|
961
952
|
// Throw exception (MCP protocol requirement)
|
|
962
|
-
throw
|
|
953
|
+
throw error;
|
|
963
954
|
}
|
|
964
955
|
return {
|
|
965
956
|
contents: [{
|
|
@@ -970,41 +961,6 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
970
961
|
};
|
|
971
962
|
});
|
|
972
963
|
}
|
|
973
|
-
async function addFileExtension(filePath) {
|
|
974
|
-
// Get directory and filename
|
|
975
|
-
const directory = path.dirname(filePath);
|
|
976
|
-
const filename = path.basename(filePath);
|
|
977
|
-
try {
|
|
978
|
-
// Get all files in directory that start with our filename
|
|
979
|
-
const files = await fs.readdir(directory);
|
|
980
|
-
const matchingFiles = files.filter(file => file.startsWith(filename));
|
|
981
|
-
if (matchingFiles.length > 0) {
|
|
982
|
-
const fileFound = path.join(directory, matchingFiles[0]);
|
|
983
|
-
return [
|
|
984
|
-
mime.lookup(fileFound) || 'text/plain',
|
|
985
|
-
fileFound
|
|
986
|
-
];
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
catch (error) {
|
|
990
|
-
console.error('Error reading directory:', error);
|
|
991
|
-
}
|
|
992
|
-
// Return original if no matches found
|
|
993
|
-
return ['text/plain', filePath];
|
|
994
|
-
}
|
|
995
|
-
async function listResourcesDirectory() {
|
|
996
|
-
try {
|
|
997
|
-
const directories = await fs.readdir(resourcesDir, { withFileTypes: true });
|
|
998
|
-
const folderNames = directories
|
|
999
|
-
.filter(dirent => dirent.isDirectory())
|
|
1000
|
-
.map(dirent => dirent.name);
|
|
1001
|
-
return folderNames;
|
|
1002
|
-
}
|
|
1003
|
-
catch (error) {
|
|
1004
|
-
console.error("Error reading resources directory:", error);
|
|
1005
|
-
return [];
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
964
|
// ============================================================================
|
|
1009
965
|
// Cleanup handlers for graceful shutdown of MCP server
|
|
1010
966
|
// Ensure openMSX emulator is closed when MCP server stops
|
package/dist/utils.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @license GPL2
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import mime from 'mime-types';
|
|
8
10
|
/**
|
|
9
11
|
* Extract description from XML file
|
|
10
12
|
* @param filePath - Full path to the XML file
|
|
@@ -21,6 +23,73 @@ export async function extractDescriptionFromXML(filePath) {
|
|
|
21
23
|
return 'Error reading description';
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Add file extension to a file path if it exists in the same directory and determine its MIME type
|
|
28
|
+
* @param filePath - Full path to the file excluding extension
|
|
29
|
+
* @returns Promise<string[]> - An array containing the MIME type and the full file path with extension
|
|
30
|
+
*/
|
|
31
|
+
export async function addFileExtension(filePath) {
|
|
32
|
+
// Get directory and filename
|
|
33
|
+
const directory = path.dirname(filePath);
|
|
34
|
+
const filename = path.basename(filePath);
|
|
35
|
+
try {
|
|
36
|
+
// Get all files in directory that start with our filename
|
|
37
|
+
const files = await fs.readdir(directory);
|
|
38
|
+
const matchingFiles = files.filter(file => file.startsWith(filename));
|
|
39
|
+
if (matchingFiles.length > 0) {
|
|
40
|
+
const fileFound = path.join(directory, matchingFiles[0]);
|
|
41
|
+
return [
|
|
42
|
+
mime.lookup(fileFound) || 'text/plain',
|
|
43
|
+
fileFound
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('Error reading directory:', error);
|
|
49
|
+
}
|
|
50
|
+
// Return original if no matches found
|
|
51
|
+
return ['text/plain', filePath];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* List all folders in the resources directory
|
|
55
|
+
* @param resourcesDir - Path to the resources directory
|
|
56
|
+
* @returns Promise<string[]> - List of folder names in the resources directory
|
|
57
|
+
*/
|
|
58
|
+
export async function listResourcesDirectory(resourcesDir) {
|
|
59
|
+
try {
|
|
60
|
+
const directories = await fs.readdir(resourcesDir, { withFileTypes: true });
|
|
61
|
+
const folderNames = directories
|
|
62
|
+
.filter(dirent => dirent.isDirectory())
|
|
63
|
+
.map(dirent => dirent.name);
|
|
64
|
+
return folderNames;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error("Error reading resources directory:", error);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch a webpage and return its content (without scripts, styles, or links) and its MIME type
|
|
73
|
+
* @param url - URL of the webpage to fetch
|
|
74
|
+
* @returns Promise<[string, string]> - A tuple containing the webpage content and its MIME type
|
|
75
|
+
*/
|
|
76
|
+
export async function fetchCleanWebpage(url) {
|
|
77
|
+
let resourceContent;
|
|
78
|
+
let mimeType = 'text/plain';
|
|
79
|
+
try {
|
|
80
|
+
resourceContent = await fetch(url).then(response => {
|
|
81
|
+
mimeType = response.headers.get('content-type') || 'text/plain';
|
|
82
|
+
return response.text();
|
|
83
|
+
}) || 'Error downloading content';
|
|
84
|
+
// Remove script, style, and link tags from the content
|
|
85
|
+
resourceContent = resourceContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>|<link\b[^>]*\/?>/gi, '');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Throw exception (MCP protocol requirement)
|
|
89
|
+
throw new Error(`Error fetching resource from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
}
|
|
91
|
+
return [resourceContent, mimeType];
|
|
92
|
+
}
|
|
24
93
|
/**
|
|
25
94
|
* Decode HTML entities in a string to plain text
|
|
26
95
|
* @param text - String containing HTML entities
|