@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 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
+ [![Built by NataliaPC](https://img.shields.io/badge/Built%20by-NataliaPC-blue)](https://github.com/nataliapc)
6
+ [![License](https://img.shields.io/badge/License-GPL2-blue.svg)](https://github.com/nataliapc/mcp-openmsx/blob/main/LICENSE)
7
+ [![GitHub Repo stars](https://img.shields.io/github/stars/nataliapc/mcp-openmsx)
8
+ ](https://github.com/nataliapc/mcp-openmsx/stargazers/)
9
+ [![NPM Version](https://img.shields.io/npm/v/%40nataliapc%2Fmcp-openmsx)](https://www.npmjs.com/package/@nataliapc/mcp-openmsx?activeTab=versions)
10
+ [![NPM Downloads](https://img.shields.io/npm/dm/%40nataliapc%2Fmcp-openmsx?color=808000)]()
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 two books:
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
- - [MSX Wiki](https://www.msx.org/wiki/Main_Page)
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
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?logo=visualstudiocode&logoColor=white)](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
+ [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?logo=visualstudiocode&logoColor=white)](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.13
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.13";
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
- server.registerResource(
875
- // Name of the resource (used to call it)
876
- `msxdocs_${sectionName}_${itemName}`,
877
- // Resource URI template
878
- item.uri,
879
- // Metadata for the resource
880
- {
881
- title: item.title || `MSX Documentation '${sectionName}' - ${itemName}`,
882
- description: item.description || `Documentation for MSX resource '${sectionName}' - ${itemName}`,
883
- mimeType: item.mimeType || 'text/markdown',
884
- },
885
- // Handler for the resource (function to be executed when the resource is called)
886
- async (uri) => {
887
- let resourceContent;
888
- let mimeType;
889
- if (uri.href.startsWith('http://') || uri.href.startsWith('https://')) {
890
- // Fetch the resource from the URL
891
- try {
892
- resourceContent = await fetch(uri.href).then(response => {
893
- mimeType = response.headers.get('content-type') || 'text/plain';
894
- return response.text();
895
- }) || 'Error downloading resource content';
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
- catch (error) {
898
- // Throw exception (MCP protocol requirement)
899
- throw new Error(`Error fetching resource from ${uri.href}: ${error instanceof Error ? error.message : String(error)}`);
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
- 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
- }
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
- server.resource("msxdocs_msxorg_wiki", new ResourceTemplate("msxdocs://msxorg_wiki/{section}", {
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
- section: (value) => [
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 MSX Documentation",
945
- description: "Documentation about all the standard MSX-BASIC instructions.",
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 section = variables.section.replace(/ /g, '_').replace(/\?/g, '%3F').replace(/=/g, '%3D');
949
- const url = `https://www.msx.org/wiki/${section}`;
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 fetch(url).then(response => {
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 new Error(`Error fetching resource "${uri}" from "${url}": ${error instanceof Error ? error.message : String(error)}`);
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
- 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
- ];
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
- 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
- }
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
- // Handle GET requests for server-to-client notifications via SSE
1119
- app.get('/mcp', async (req, res) => {
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.13",
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.11.2",
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
  },