@nataliapc/mcp-openmsx 1.1.13 → 1.1.15
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 +16 -2
- package/dist/server.js +151 -115
- package/dist/utils.js +108 -0
- package/package.json +4 -2
- package/resources/audio/MGSC111.txt +669 -0
- package/resources/audio/opl4tech.txt +688 -0
- package/resources/audio/toc.json +10 -3
- package/resources/processors/toc.json +4 -4
- package/resources/processors/z80_detailed_instruction_set.md +80 -68
- package/resources/sdcc/sdccman.lyx +81574 -0
- package/resources/sdcc/toc.json +77 -0
- /package/resources/processors/{Z80_R800_instruction_set.md → z80_r800_instruction_set.md} +0 -0
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
*"Orchestrating a binary opera where AI conducts, MCP interprets, and openMSX acts as the 8-bit diva."*
|
|
4
4
|
|
|
5
|
+
[](https://github.com/nataliapc)
|
|
6
|
+
[](https://github.com/nataliapc/mcp-openmsx/blob/main/LICENSE)
|
|
7
|
+
[
|
|
8
|
+
](https://github.com/nataliapc/mcp-openmsx/stargazers/)
|
|
9
|
+
[](https://www.npmjs.com/package/@nataliapc/mcp-openmsx?activeTab=versions)
|
|
10
|
+
[]()
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
A [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) server for automating [openMSX emulator](https://github.com/openMSX/openMSX) instances.
|
|
6
14
|
|
|
7
15
|
This server provides comprehensive tools for MSX software development, testing, and automation through standardized MCP protocols.
|
|
@@ -64,6 +72,7 @@ The MCP server translates high-level commands from your Copilot AI into `TCL` co
|
|
|
64
72
|
- `emu_savestates`: Save and restore machine states: _`load`, `save`, `list`_.
|
|
65
73
|
- `screen_shot`: Capture emulator screen: _`as_image`, `to_file`_.
|
|
66
74
|
- `screen_dump`: Export screen data as BASIC BSAVE instruction.
|
|
75
|
+
- `msxdocs_resource_get`: Retrieve MCP resources for MCP clients that don't support MCP resources.
|
|
67
76
|
|
|
68
77
|
## 📚 Available MCP Resources
|
|
69
78
|
|
|
@@ -85,10 +94,11 @@ There are more than 60 resources available, some included directly in the MCP an
|
|
|
85
94
|
- `MSX-UNAPI`
|
|
86
95
|
- `MSX BASIC`
|
|
87
96
|
|
|
88
|
-
And
|
|
97
|
+
And books and manuals:
|
|
89
98
|
|
|
90
99
|
- `MSX2 Technical Handbook`
|
|
91
100
|
- `The MSX Red Book`
|
|
101
|
+
- `SDCC Compiler`
|
|
92
102
|
|
|
93
103
|
### Resources from:
|
|
94
104
|
|
|
@@ -98,8 +108,9 @@ And two books:
|
|
|
98
108
|
- [MSX2 Technical Handbook](https://github.com/Konamiman/MSX2-Technical-Handbook)
|
|
99
109
|
- [Konamiman MSX-UNAPI-specification](https://github.com/Konamiman/MSX-UNAPI-specification)
|
|
100
110
|
- [BiFi MSX Net](http://bifi.msxnet.org/msxnet/)
|
|
101
|
-
- [
|
|
111
|
+
- [MRC Wiki](https://www.msx.org/wiki/Main_Page)
|
|
102
112
|
- [MSX Banzai!](http://msxbanzai.tni.nl/)
|
|
113
|
+
- [SDCC](https://sdcc.sourceforge.net/)
|
|
103
114
|
|
|
104
115
|
Thanks to the authors of these resources, who have made them available under various licenses. This MCP server includes some of these resources to enhance the development experience.
|
|
105
116
|
|
|
@@ -116,6 +127,9 @@ You can use this MCP server in this basic way with the [precompiled NPM package]
|
|
|
116
127
|
|
|
117
128
|
### STDIO mode (recommended)
|
|
118
129
|
|
|
130
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22stdio%22%2C%22command%22%3A%20%22npx%22%2C%22args%22%3A%20%5B%22%40nataliapc%2Fmcp-openmsx%22%5D%2C%20%22env%22%3A%20%7B%22OPENMSX_SHARE_DIR%22%3A%20%22%2Fusr%2Fshare%2Fopenmsx%22%7D%7D)
|
|
131
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22stdio%22%2C%22command%22%3A%20%22npx%22%2C%22args%22%3A%20%5B%22%40nataliapc%2Fmcp-openmsx%22%5D%2C%20%22env%22%3A%20%7B%22OPENMSX_SHARE_DIR%22%3A%20%22%2Fusr%2Fshare%2Fopenmsx%22%7D%7D&quality=insiders)
|
|
132
|
+
|
|
119
133
|
```json
|
|
120
134
|
{
|
|
121
135
|
"servers": {
|
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.15
|
|
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
|
+
export const PACKAGE_VERSION = "1.1.15";
|
|
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,102 +863,105 @@ 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();
|
|
867
|
+
const regResources = [];
|
|
868
868
|
for (let index = 0; index < resdocs.length; index++) {
|
|
869
869
|
const sectionName = resdocs[index];
|
|
870
870
|
const tocFile = path.join(resourcesDir, `${sectionName}/toc.json`);
|
|
871
871
|
const tocContent = JSON.parse(await fs.readFile(tocFile, 'utf8'));
|
|
872
872
|
tocContent.toc.forEach((item, itemIndex) => {
|
|
873
|
-
const itemName = item.uri.split('/').pop() || '';
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
873
|
+
const itemName = path.parse(item.uri.split('/').pop()).name || '';
|
|
874
|
+
let resource = {
|
|
875
|
+
uri: item.uri,
|
|
876
|
+
filename: '',
|
|
877
|
+
resource: server.registerResource(
|
|
878
|
+
// Name of the resource (used to call it)
|
|
879
|
+
`msxdocs_${sectionName}_${item.title.replace(/[^a-z0-9]+/gi, '_').toLowerCase()}`,
|
|
880
|
+
// Resource URI template
|
|
881
|
+
item.uri,
|
|
882
|
+
// Metadata for the resource
|
|
883
|
+
{
|
|
884
|
+
title: item.title || `MSX Documentation '${sectionName}': ${itemName}`,
|
|
885
|
+
description: item.description || `Documentation for MSX resource '${sectionName}': ${itemName}`,
|
|
886
|
+
mimeType: item.mimeType || 'text/markdown',
|
|
887
|
+
},
|
|
888
|
+
// Handler for the resource (function to be executed when the resource is called)
|
|
889
|
+
async (uri) => {
|
|
890
|
+
let resourceContent;
|
|
891
|
+
let mimeType;
|
|
892
|
+
if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
|
|
893
|
+
// Fetch the resource from the URL
|
|
894
|
+
try {
|
|
895
|
+
[resourceContent, mimeType] = await fetchCleanWebpage(uri.href);
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
// Throw exception (MCP protocol requirement)
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
896
901
|
}
|
|
897
|
-
|
|
898
|
-
//
|
|
899
|
-
|
|
902
|
+
else {
|
|
903
|
+
// Read the resource from the local MCP server resources directory
|
|
904
|
+
try {
|
|
905
|
+
let resourceFile;
|
|
906
|
+
[mimeType, resourceFile] = await addFileExtension(path.join(resourcesDir, `${sectionName}/${itemName}`));
|
|
907
|
+
resourceContent = await fs.readFile(resourceFile, 'utf8');
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
// Throw exception (MCP protocol requirement)
|
|
911
|
+
throw new Error(`Error reading resource ${sectionName}/${item.uri}: ${error instanceof Error ? error.message : String(error)}`);
|
|
912
|
+
}
|
|
900
913
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
throw new Error(`Error reading resource ${sectionName}/${item.uri}: ${error instanceof Error ? error.message : String(error)}`);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
contents: [{
|
|
916
|
-
uri: uri.href,
|
|
917
|
-
text: resourceContent,
|
|
918
|
-
mimeType: mimeType || 'text/plain',
|
|
919
|
-
}],
|
|
920
|
-
};
|
|
921
|
-
});
|
|
914
|
+
return {
|
|
915
|
+
contents: [{
|
|
916
|
+
uri: uri.href,
|
|
917
|
+
text: resourceContent,
|
|
918
|
+
mimeType: mimeType || 'text/plain',
|
|
919
|
+
}],
|
|
920
|
+
};
|
|
921
|
+
})
|
|
922
|
+
};
|
|
923
|
+
regResources.push(resource);
|
|
922
924
|
});
|
|
923
925
|
}
|
|
924
926
|
;
|
|
925
|
-
|
|
927
|
+
// Source: https://www.msx.org/wiki/Category:MSX-BASIC_Instructions
|
|
928
|
+
const basicInstructions = [
|
|
929
|
+
"ABS()", "AND", "ASC()", "ATN()", "AUTO", "BASE()", "BEEP", "BIN$()", "BLOAD", "BSAVE", "CALL", "CALL ADJUST", "CALL PAUSE", "CALL PCMPLAY", "CALL PCMREC",
|
|
930
|
+
"CDBL()", "CHR$()", "CINT()", "CIRCLE", "CLEAR", "CLOAD", "CLOAD?", "CLOSE", "CLS", "COLOR", "COLOR=", "COLOR SPRITE()", "COLOR SPRITE$()", "CONT", "COPY",
|
|
931
|
+
"COPY SCREEN", "COS()", "CSAVE", "CSNG()", "CSRLIN", "DATA", "DEFDBL", "DEF FN", "DEFINT", "DEFSNG", "DEFSTR", "DEF USR", "DELETE", "DIM", "DRAW", "ELSE",
|
|
932
|
+
"END", "EOF()", "EQV", "ERASE", "ERL", "ERR", "ERROR", "EXP()", "FIX()", "FN", "FOR...NEXT", "FRE()", "GET DATE", "GET TIME", "GOSUB", "GOTO", "HEX$()",
|
|
933
|
+
"IF...GOTO...ELSE", "IF...THEN...ELSE", "IMP", "INKEY$", "INP()", "INPUT", "INPUT$()", "INSTR()", "INT()", "INTERVAL", "KEY", "KEY()", "LEFT$()", "LEN()",
|
|
934
|
+
"LET", "LINE", "LINE INPUT", "LIST", "LLIST", "LOAD", "LOCATE", "LOG()", "LPOS()", "LPRINT", "MAXFILES", "MERGE", "MID$()", "MOD", "MOTOR", "NEW", "NOT",
|
|
935
|
+
"OCT$()", "ON...GOSUB", "ON...GOTO", "ON ERROR GOTO", "ON INTERVAL GOSUB", "ON KEY GOSUB", "ON SPRITE GOSUB", "ON STOP GOSUB", "ON STRIG GOSUB", "OPEN",
|
|
936
|
+
"OR", "OUT", "PAD()", "PAINT", "PDL()", "PEEK()", "PLAY", "PLAY()", "POINT", "POKE", "POS()", "PRESET", "PRINT", "PSET", "PUT KANJI", "PUT SPRITE", "READ",
|
|
937
|
+
"REM", "RENUM", "RESTORE", "RESUME", "RETURN", "RIGHT$()", "RND()", "RUN", "SAVE", "SCREEN", "SET ADJUST", "SET BEEP", "SET DATE", "SET PAGE", "SET PASSWORD",
|
|
938
|
+
"SET PROMPT", "SET SCREEN", "SET SCROLL", "SET TIME", "SET TITLE", "SET VIDEO", "SGN()", "SIN()", "SOUND", "SPACE$()", "SPC()", "SPRITE", "SPRITE$()",
|
|
939
|
+
"SQR()", "STICK()", "STOP", "STR$()", "STRIG()", "STRING$()", "SWAP", "TAB()", "TAN()", "TIME", "TROFF", "TRON", "USR()", "VAL()", "VARPTR()", "VDP()",
|
|
940
|
+
"VPEEK()", "VPOKE", "WAIT", "WIDTH", "XOR"
|
|
941
|
+
];
|
|
942
|
+
server.resource("msxdocs_basic_wiki", new ResourceTemplate("msxdocs://basic_wiki/{instruction}", {
|
|
926
943
|
list: undefined,
|
|
927
944
|
complete: {
|
|
928
|
-
|
|
929
|
-
"ABS()", "AND", "ASC()", "ATN()", "AUTO", "BASE()", "BEEP", "BIN$()", "BLOAD", "BSAVE", "CALL", "CALL ADJUST", "CALL PAUSE", "CALL PCMPLAY", "CALL PCMREC",
|
|
930
|
-
"CDBL()", "CHR$()", "CINT()", "CIRCLE", "CLEAR", "CLOAD", "CLOAD?", "CLOSE", "CLS", "COLOR", "COLOR=", "COLOR", "COLOR", "CONT", "COPY", "COPY", "COS()", "CSAVE",
|
|
931
|
-
"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 MSX1", "FOR...NEXT", "FRE()", "GET DATE", "GET TIME", "GOSUB", "GOTO", "HEX$()", "IF...GOTO...ELSE", "IF...THEN...ELSE",
|
|
933
|
-
"IMP", "INKEY$", "INP()", "INPUT", "INPUT$()", "INSTR()", "INT()", "INTERVAL", "KEY", "KEY()", "LEFT$()", "LEN()", "LET", "LINE", "LINE INPUT", "LIST",
|
|
934
|
-
"LLIST", "LOAD", "LOCATE", "LOG()", "LPOS()", "LPRINT", "MAXFILES", "MERGE", "MID$()", "MOD", "MOTOR", "NEW", "NOT", "OCT$()", "ON...GOSUB", "ON...GOTO",
|
|
935
|
-
"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
|
-
"PEEK()", "PLAY", "PLAY()", "POINT", "POKE", "POS()", "PRESET", "PRINT", "PSET", "PUT KANJI", "PUT SPRITE", "READ", "REM", "RENUM", "RESTORE", "RESUME",
|
|
937
|
-
"RETURN", "RIGHT$()", "RND()", "RUN", "SAVE", "SCREEN", "SET ADJUST", "SET BEEP", "SET DATE", "SET PAGE", "SET PASSWORD", "SET PROMPT", "SET SCREEN",
|
|
938
|
-
"SET SCROLL", "SET TIME", "SET TITLE", "SET VIDEO", "SGN()", "SIN()", "SOUND", "SPACE$()", "SPC()", "SPRITE", "SPRITE$()", "SQR()", "STICK()", "STOP",
|
|
939
|
-
"STR$()", "STRIG()", "STRING$()", "SWAP", "TAB()", "TAN()", "TIME", "TROFF", "TRON", "USR()", "VAL()", "VARPTR()", "VDP()", "VPEEK()", "VPOKE", "WAIT",
|
|
940
|
-
"WIDTH", "XOR"
|
|
941
|
-
],
|
|
945
|
+
instruction: (value) => basicInstructions,
|
|
942
946
|
},
|
|
943
947
|
}), {
|
|
944
|
-
title: "BASIC
|
|
945
|
-
description: "Documentation about all the standard MSX
|
|
948
|
+
title: "MSX BASIC Instructions Documentation",
|
|
949
|
+
description: "Documentation about all the standard MSX BASIC instructions from www.msx.org",
|
|
946
950
|
mimeType: "text/html",
|
|
947
951
|
}, async (uri, variables) => {
|
|
948
|
-
const
|
|
949
|
-
|
|
952
|
+
const instruction = variables.instruction
|
|
953
|
+
.replace(/ /g, '_')
|
|
954
|
+
.replace(/\?/g, '%3F')
|
|
955
|
+
.replace(/=/g, '%3D');
|
|
956
|
+
const url = `https://www.msx.org/wiki/${instruction}`;
|
|
950
957
|
let resourceContent;
|
|
951
958
|
let mimeType;
|
|
952
959
|
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, '');
|
|
960
|
+
[resourceContent, mimeType] = await fetchCleanWebpage(url);
|
|
959
961
|
}
|
|
960
962
|
catch (error) {
|
|
961
963
|
// Throw exception (MCP protocol requirement)
|
|
962
|
-
throw
|
|
964
|
+
throw error;
|
|
963
965
|
}
|
|
964
966
|
return {
|
|
965
967
|
contents: [{
|
|
@@ -969,41 +971,73 @@ The parameter scrbasename is the name of the filename (without path) to save the
|
|
|
969
971
|
}],
|
|
970
972
|
};
|
|
971
973
|
});
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
974
|
+
// Register the tool to get a specific MSX documentation resource
|
|
975
|
+
server.registerTool(
|
|
976
|
+
// Name of the tool (used to call it)
|
|
977
|
+
"msxdocs_resource_get", {
|
|
978
|
+
title: "Tool to get a resource",
|
|
979
|
+
// Description of the tool (what it does)
|
|
980
|
+
description: "Get a specific available MSX documentation resource from this MCP server resources.",
|
|
981
|
+
// Schema for the tool (input validation)
|
|
982
|
+
inputSchema: {
|
|
983
|
+
resourceName: z.enum(regResources.map(res => res.resource.name)).describe("Name of the resource to obtain, e.g. 'msxdocs_programming_interrupts'"),
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
// Handler for the tool (function to be executed when the tool is called)
|
|
987
|
+
async ({ resourceName }, extra) => {
|
|
988
|
+
const index = regResources.findIndex((res) => res.resource.name === resourceName);
|
|
989
|
+
const uriString = index !== -1 ? regResources[index].uri : undefined;
|
|
990
|
+
const resource = index !== -1 ? regResources[index].resource : undefined;
|
|
991
|
+
if (!resource || !uriString) {
|
|
992
|
+
return getResponseContent([
|
|
993
|
+
`Error: Resource '${resourceName}' not found.`
|
|
994
|
+
]);
|
|
987
995
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
996
|
+
let documentationText = '';
|
|
997
|
+
try {
|
|
998
|
+
// If the resource is found, return its content
|
|
999
|
+
let resourceContent = await resource.readCallback(new URL(uriString), extra);
|
|
1000
|
+
if (!resourceContent.contents?.length) {
|
|
1001
|
+
return getResponseContent([
|
|
1002
|
+
`Error: Resource '${resourceName}' has no content available.`
|
|
1003
|
+
]);
|
|
1004
|
+
}
|
|
1005
|
+
// Return the first content item (assuming it's the main content)
|
|
1006
|
+
const content = resourceContent.contents[0];
|
|
1007
|
+
if ('text' in content) {
|
|
1008
|
+
documentationText = content.text;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
return getResponseContent([
|
|
1012
|
+
`Error: Resource '${resourceName}' has no content available.`
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
return getResponseContent([
|
|
1018
|
+
`Error: error reading resource '${resourceName}': ${error instanceof Error ? error.message : String(error)}`
|
|
1019
|
+
]);
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
content: [{
|
|
1023
|
+
type: "text",
|
|
1024
|
+
text: `Content from resource: '${resourceName}'`,
|
|
1025
|
+
}, {
|
|
1026
|
+
type: "text",
|
|
1027
|
+
text: documentationText || 'No content available for this resource.',
|
|
1028
|
+
mimeType: resource.metadata?.mimeType || 'text/plain',
|
|
1029
|
+
} /*, {
|
|
1030
|
+
type: "resource",
|
|
1031
|
+
resource: {
|
|
1032
|
+
uri: resource.metadata?.uri || resourceName,
|
|
1033
|
+
title: resource.metadata?.title || `Resource: ${resourceName}`,
|
|
1034
|
+
mimeType: resource.metadata?.mimeType || 'text/plain',
|
|
1035
|
+
text: documentationText || 'No content available for this resource.',
|
|
1036
|
+
}
|
|
1037
|
+
}*/
|
|
1038
|
+
],
|
|
1039
|
+
};
|
|
1040
|
+
});
|
|
1007
1041
|
}
|
|
1008
1042
|
// ============================================================================
|
|
1009
1043
|
// Cleanup handlers for graceful shutdown of MCP server
|
|
@@ -1115,8 +1149,8 @@ async function startHttpServer() {
|
|
|
1115
1149
|
}
|
|
1116
1150
|
await transport.handleRequest(req, res, req.body);
|
|
1117
1151
|
});
|
|
1118
|
-
//
|
|
1119
|
-
|
|
1152
|
+
// Reusable handle GET / DELETE requests
|
|
1153
|
+
const handleSessionRequest = async (req, res) => {
|
|
1120
1154
|
const sessionId = req.headers['mcp-session-id'];
|
|
1121
1155
|
if (!sessionId || !transports[sessionId]) {
|
|
1122
1156
|
res.status(400).send('Invalid or missing session ID');
|
|
@@ -1124,7 +1158,9 @@ async function startHttpServer() {
|
|
|
1124
1158
|
}
|
|
1125
1159
|
const transport = transports[sessionId];
|
|
1126
1160
|
await transport.handleRequest(req, res);
|
|
1127
|
-
}
|
|
1161
|
+
};
|
|
1162
|
+
app.get('/mcp', handleSessionRequest);
|
|
1163
|
+
app.delete('/mcp', handleSessionRequest);
|
|
1128
1164
|
const port = process.env.MCP_HTTP_PORT || 3000;
|
|
1129
1165
|
app.listen(port, () => {
|
|
1130
1166
|
console.log(`MCP Server listening on port ${port}`);
|
package/dist/utils.js
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* @license GPL2
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import mime from 'mime-types';
|
|
10
|
+
import { gunzipSync } from 'zlib';
|
|
11
|
+
import { PACKAGE_VERSION } from "./server.js";
|
|
12
|
+
import sanitizeHtml from 'sanitize-html';
|
|
8
13
|
/**
|
|
9
14
|
* Extract description from XML file
|
|
10
15
|
* @param filePath - Full path to the XML file
|
|
@@ -21,6 +26,109 @@ export async function extractDescriptionFromXML(filePath) {
|
|
|
21
26
|
return 'Error reading description';
|
|
22
27
|
}
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Add file extension to a file path if it exists in the same directory and determine its MIME type
|
|
31
|
+
* @param filePath - Full path to the file excluding extension
|
|
32
|
+
* @returns Promise<string[]> - An array containing the MIME type and the full file path with extension
|
|
33
|
+
*/
|
|
34
|
+
export async function addFileExtension(filePath) {
|
|
35
|
+
// Get directory and filename
|
|
36
|
+
const directory = path.dirname(filePath);
|
|
37
|
+
const filename = path.basename(filePath);
|
|
38
|
+
try {
|
|
39
|
+
// Get all files in directory that start with our filename
|
|
40
|
+
const files = await fs.readdir(directory);
|
|
41
|
+
const matchingFiles = files.filter(file => file.startsWith(filename));
|
|
42
|
+
if (matchingFiles.length > 0) {
|
|
43
|
+
const fileFound = path.join(directory, matchingFiles[0]);
|
|
44
|
+
return [
|
|
45
|
+
mime.lookup(fileFound) || 'text/plain',
|
|
46
|
+
fileFound
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('Error reading directory:', error);
|
|
52
|
+
}
|
|
53
|
+
// Return original if no matches found
|
|
54
|
+
return ['text/plain', filePath];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* List all folders in the resources directory
|
|
58
|
+
* @param resourcesDir - Path to the resources directory
|
|
59
|
+
* @returns Promise<string[]> - List of folder names in the resources directory
|
|
60
|
+
*/
|
|
61
|
+
export async function listResourcesDirectory(resourcesDir) {
|
|
62
|
+
try {
|
|
63
|
+
const directories = await fs.readdir(resourcesDir, { withFileTypes: true });
|
|
64
|
+
const folderNames = directories
|
|
65
|
+
.filter(dirent => dirent.isDirectory())
|
|
66
|
+
.map(dirent => dirent.name);
|
|
67
|
+
return folderNames;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error("Error reading resources directory:", error);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetch a webpage and return its content (without scripts, styles, or links) and its MIME type
|
|
76
|
+
* @param url - URL of the webpage to fetch
|
|
77
|
+
* @returns Promise<[string, string]> - A tuple containing the webpage content and its MIME type
|
|
78
|
+
*/
|
|
79
|
+
export async function fetchCleanWebpage(url) {
|
|
80
|
+
let resourceContent;
|
|
81
|
+
let mimeType = 'text/plain';
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
headers: {
|
|
85
|
+
// Accept compressed content gzip/deflate
|
|
86
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
87
|
+
// User agent to avoid blocking by some servers
|
|
88
|
+
'User-Agent': `Mozilla/5.0 (compatible; MCP-openMSX/${PACKAGE_VERSION})`
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (response.status === 200) {
|
|
92
|
+
mimeType = response.headers.get('content-type') || 'text/plain';
|
|
93
|
+
const contentType = response.headers.get('content-type') || '';
|
|
94
|
+
if (contentType.includes('x-gzip') || contentType.includes('gzip')) {
|
|
95
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
96
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
97
|
+
if (uint8Array[0] === 0x1f && uint8Array[1] === 0x8b) {
|
|
98
|
+
try {
|
|
99
|
+
const decompressed = gunzipSync(Buffer.from(uint8Array));
|
|
100
|
+
resourceContent = decompressed.toString('utf8');
|
|
101
|
+
mimeType = 'text/html';
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
resourceContent = new TextDecoder().decode(uint8Array);
|
|
105
|
+
mimeType = 'text/html';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
resourceContent = new TextDecoder().decode(uint8Array);
|
|
110
|
+
mimeType = 'text/html';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Normal case, use response.text() which automatically handles decompression
|
|
115
|
+
resourceContent = await response.text();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
120
|
+
}
|
|
121
|
+
// Remove script, style, form, and link tags from the content if it's HTML
|
|
122
|
+
if (mimeType.startsWith('text/html')) {
|
|
123
|
+
resourceContent = sanitizeHtml(resourceContent);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// Throw exception (MCP protocol requirement)
|
|
128
|
+
throw new Error(`Error fetching resource from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
}
|
|
130
|
+
return [resourceContent, mimeType];
|
|
131
|
+
}
|
|
24
132
|
/**
|
|
25
133
|
* Decode HTML entities in a string to plain text
|
|
26
134
|
* @param text - String containing HTML entities
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nataliapc/mcp-openmsx",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Model context protocol server for openMSX automation and control",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
42
42
|
"@types/express": "^5.0.2",
|
|
43
43
|
"express": "^5.1.0",
|
|
44
|
+
"sanitize-html": "^2.17.0",
|
|
44
45
|
"tsx": "^4.7.1",
|
|
45
46
|
"zod": "^3.24.4"
|
|
46
47
|
},
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@modelcontextprotocol/inspector": "^0.15.0",
|
|
49
50
|
"@types/mime-types": "^3.0.1",
|
|
50
51
|
"@types/node": "^22.15.27",
|
|
52
|
+
"@types/sanitize-html": "^2.16.0",
|
|
51
53
|
"shx": "^0.4.0",
|
|
52
54
|
"typescript": "^5.8.3"
|
|
53
55
|
},
|