diff --git a/README.md b/README.md index db3e8b1..32d3ea7 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ npx @wonderwhy-er/desktop-commander@latest setup --debug ``` Restart Claude if running. -**āœ… Auto-Updates:** Yes - automatically updates when you restart Claude -**šŸ”„ Manual Update:** Run the setup command again +**āœ… Auto-Updates:** Yes - automatically updates when you restart Claude +**šŸ”„ Manual Update:** Run the setup command again **šŸ—‘ļø Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest setup --uninstall` ### Option 2: Using bash script installer (macOS) ⭐ **Auto-Updates** @@ -91,8 +91,8 @@ curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/re ``` This script handles all dependencies and configuration automatically for a seamless setup experience. -**āœ… Auto-Updates:** Yes - requires manual updates -**šŸ”„ Manual Update:** Re-run the bash installer command above +**āœ… Auto-Updates:** Yes - requires manual updates +**šŸ”„ Manual Update:** Re-run the bash installer command above **šŸ—‘ļø Uninstall:** Remove the MCP server entry from your Claude config file and delete the cloned repository if it exists ### Option 3: Installing via Smithery ⭐ **Auto-Updates** @@ -103,8 +103,8 @@ To install Desktop Commander for Claude Desktop automatically via [Smithery](htt npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude ``` -**āœ… Auto-Updates:** Yes - automatically updates when you restart Claude -**šŸ”„ Manual Update:** Re-run the Smithery install command +**āœ… Auto-Updates:** Yes - automatically updates when you restart Claude +**šŸ”„ Manual Update:** Re-run the Smithery install command **šŸ—‘ļø Uninstall:** `npx -y @smithery/cli uninstall @wonderwhy-er/desktop-commander --client claude` ### Option 4: Add to claude_desktop_config manually āŒ **Manual Updates** @@ -129,8 +129,8 @@ Add this entry to your claude_desktop_config.json: ``` Restart Claude if running. -**āŒ Auto-Updates:** No - uses npx but config might not update automatically -**šŸ”„ Manual Update:** Usually automatic via npx, but if issues occur, update your config file or re-add the entry +**āŒ Auto-Updates:** No - uses npx but config might not update automatically +**šŸ”„ Manual Update:** Usually automatic via npx, but if issues occur, update your config file or re-add the entry **šŸ—‘ļø Uninstall:** Remove the "desktop-commander" entry from your claude_desktop_config.json file ### Option 5: Checkout locally āŒ **Manual Updates** @@ -148,10 +148,17 @@ The setup command will: - Configure Claude's desktop app - Add MCP servers to Claude's config if needed -**āŒ Auto-Updates:** No - requires manual git updates -**šŸ”„ Manual Update:** `cd DesktopCommanderMCP && git pull && npm run setup` +**āŒ Auto-Updates:** No - requires manual git updates +**šŸ”„ Manual Update:** `cd DesktopCommanderMCP && git pull && npm run setup` **šŸ—‘ļø Uninstall:** Remove the cloned directory and remove MCP server entry from Claude config +### Option 6: Running as a http server + +1. Clone the repository using `git clone https://github.com/wonderwhy-er/DesktopCommanderMCP.git`. +2. Run the server using `npx . --port 7777` (or whichever port number you prefer). + +### Updating Desktop Commander + ## Updating & Uninstalling Desktop Commander ### Automatic Updates (Options 1 & 3 only) @@ -435,7 +442,7 @@ This project extends the MCP Filesystem Server to enable: Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us ## DONE -- **20-05-2025 v0.1.40 Release** - Added audit logging for all tool calls, improved line-based file operations, enhanced edit_block with better prompting for smaller edits, added explicit telemetry opt-out prompting +- **20-05-2025 v0.1.40 Release** - Added audit logging for all tool calls, improved line-based file operations, enhanced edit_block with better prompting for smaller edits, added explicit telemetry opt-out prompting - **05-05-2025 Fuzzy Search Logging** - Added comprehensive logging system for fuzzy search operations with detailed analysis tools, character-level diffs, and performance metrics to help debug edit_block failures - **29-04-2025 Telemetry Opt Out through configuration** - There is now setting to disable telemetry in config, ask in chat - **23-04-2025 Enhanced edit functionality** - Improved format, added fuzzy search and multi-occurrence replacements, should fail less and use edit block more often @@ -466,7 +473,7 @@ The following features are currently being explored:

šŸ“¢ SUPPORT THIS PROJECT

Desktop Commander MCP is free and open source, but needs your support to thrive!

- +

Our philosophy is simple: we don't want you to pay for it if you're not successful. But if Desktop Commander contributes to your success, please consider contributing to ours.

Ways to support:

diff --git a/package-lock.json b/package-lock.json index ab34f67..1ac30c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", + "@types/express": "^5.0.2", "@vscode/ripgrep": "^1.15.9", "cross-fetch": "^4.1.0", + "express": "^5.1.0", "fastest-levenshtein": "^1.0.16", "glob": "^10.3.10", "zod": "^3.24.1", @@ -132,18 +134,19 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", - "integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==", + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz", + "integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==", "license": "MIT", "dependencies": { + "ajv": "^8.17.1", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" @@ -152,6 +155,28 @@ "node": ">=18" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -226,6 +251,25 @@ "node": ">=14.16" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/emscripten": { "version": "1.40.1", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz", @@ -262,6 +306,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -269,6 +336,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -276,15 +349,53 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.17.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/source-list-map": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", @@ -1002,21 +1113,6 @@ "node": ">=18" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2417,46 +2513,45 @@ } }, "node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -2474,29 +2569,6 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, - "node_modules/express/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -2528,7 +2600,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2559,7 +2630,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -3755,15 +3825,6 @@ "node": ">=10.4.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4414,9 +4475,9 @@ } }, "node_modules/pkce-challenge": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", - "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -4507,12 +4568,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -4671,7 +4732,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5896,7 +5956,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -5979,15 +6038,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 6dbacd7..0d2e601 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,10 @@ ], "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", + "@types/express": "^5.0.2", "@vscode/ripgrep": "^1.15.9", "cross-fetch": "^4.1.0", + "express": "^5.1.0", "fastest-levenshtein": "^1.0.16", "glob": "^10.3.10", "zod": "^3.24.1", diff --git a/src/http-transport.ts b/src/http-transport.ts new file mode 100644 index 0000000..6043eca --- /dev/null +++ b/src/http-transport.ts @@ -0,0 +1,53 @@ +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from "express"; +import { Request, Response } from "express"; +import { server } from './server.js'; + +export async function runHttpServer(port: number): Promise { + // We return a new promise that never resolves to keep the server alive. + return new Promise((resolve, reject) => { + // Start a server based on the official @modelcontextprotocol/sdk documentation. + const app = express(); + app.use(express.json()); + + app.post('/', async (req: Request, res: Response) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + + try { + const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + res.on('close', () => { + transport.close(); + server.close(); + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Start the server + const httpServerInstance = app.listen(port, () => { + console.log(`MCP Stateless Streamable HTTP Server listening on port ${port}`); + }); + + httpServerInstance.on('error', (err) => { + console.error(`HTTP Server failed to start or encountered an error: ${err.message}`); + reject(err); + }); + }); +} diff --git a/src/index.ts b/src/index.ts index fb0db97..ba69473 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import { FilteredStdioServerTransport } from './custom-stdio.js'; +import { runHttpServer } from './http-transport.js'; import { server } from './server.js'; -import { commandManager } from './command-manager.js'; import { configManager } from './config-manager.js'; import { join, dirname } from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; @@ -50,15 +50,6 @@ async function runSetup() { async function runServer() { try { - // Check if first argument is "setup" - if (process.argv[2] === 'setup') { - await runSetup(); - return; - } - - - - const transport = new FilteredStdioServerTransport(); // Handle uncaught exceptions process.on('uncaughtException', async (error) => { const errorMessage = error instanceof Error ? error.message : String(error); @@ -95,8 +86,13 @@ async function runServer() { process.exit(1); }); - capture('run_server_start'); + // Check if first argument is "setup" + if (process.argv[2] === 'setup') { + await runSetup(); + return; + } + // Load the configuration. try { console.error("Loading configuration..."); await configManager.loadConfig(); @@ -108,10 +104,23 @@ async function runServer() { // Continue anyway - we'll use an in-memory config } + // Parse --port flag + let port: number | undefined; + const portFlagIndex = process.argv.findIndex(arg => arg === '--port'); + if (portFlagIndex !== -1 && process.argv[portFlagIndex + 1]) { + port = parseInt(process.argv[portFlagIndex + 1], 10); + if (isNaN(port)) { + console.error('Invalid port specified after --port'); + process.exit(1); + } + } - console.error("Connecting server..."); - await server.connect(transport); - console.error("Server connected successfully"); + capture('run_server_start'); + if (port !== undefined) { + await runHttpServer(port); + } else { + await runStdioServer(); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`FATAL ERROR: ${errorMessage}`); @@ -129,6 +138,13 @@ async function runServer() { } } +async function runStdioServer() { + console.error("Connecting server through stdio transport ..."); + const transport = new FilteredStdioServerTransport(); + await server.connect(transport); + console.error("Server connected successfully"); +} + runServer().catch(async (error) => { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`RUNTIME ERROR: ${errorMessage}`); @@ -144,4 +160,4 @@ runServer().catch(async (error) => { error: errorMessage }); process.exit(1); -}); \ No newline at end of file +}); diff --git a/test/test-http-transport.js b/test/test-http-transport.js new file mode 100644 index 0000000..2a2d883 --- /dev/null +++ b/test/test-http-transport.js @@ -0,0 +1,247 @@ +/** + * Test script for HTTP transport functionality + * + * This script tests how the HTTP transport works: + * 1. Testing HTTP server startup and connectivity + * 2. Testing MCP protocol implementation over HTTP + * 3. Testing error handling for invalid requests + */ + +import { runHttpServer } from '../dist/http-transport.js'; +import { configManager } from '../dist/config-manager.js'; +import assert from 'assert'; +import net from 'net'; + +/** + * Helper function to find an available port + */ +async function findAvailablePort() { + // Use Node.js net module to let the OS assign an available port + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + // Listen on port 0 to let the OS assign an available port + server.listen(0, () => { + const { port } = server.address(); + server.close(() => resolve(port)); + }); + + server.on('error', reject); + }); +} + +/** + * Helper function to parse Server-Sent Events format + */ +function parseSSE(sseText) { + const lines = sseText.trim().split('\n'); + let data = ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + data = line.substring(6); + } + } + + if (data) { + return JSON.parse(data); + } else { + throw new Error(`No data found in SSE response: ${sseText}`); + } +} + +/** + * Helper function to make HTTP request + */ +async function makeHttpRequest(port, data) { + const response = await fetch(`http://localhost:${port}/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/event-stream', + }, + body: JSON.stringify(data), + }); + + // Verify expected response content type and parse result. + assert.strictEqual( + response.headers.get('content-type'), + 'text/event-stream', + 'Expected text/event-stream content type.' + ); + const responseData = parseSSE(await response.text()); + + return { + status: response.status, + data: responseData + }; +} + +/** + * Teardown function to clean up after tests + */ +async function teardown(originalConfig, httpServer) { + // Close HTTP server if it exists + if (httpServer) { + try { + httpServer.close(); + } catch (error) { + // Ignore errors when closing server + } + } + + // Reset configuration to original + if (originalConfig) { + await configManager.updateConfig(originalConfig); + } +} + +/** + * Test HTTP server startup and basic MCP communication + */ +async function testHttpServerStartup() { + console.log('\nTest 1: HTTP server startup and MCP initialize'); + + // Find an available port + const port = await findAvailablePort(); + console.log(`Using port ${port} for HTTP transport test`); + + // Start HTTP server in background + const serverPromise = runHttpServer(port); + + // Give server time to start + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Test MCP initialize request + console.log('Testing MCP initialize request...'); + const initializeRequest = { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0' + } + } + }; + + const initResponse = await makeHttpRequest(port, initializeRequest); + + // Verify response + assert.strictEqual(initResponse.status, 200, 'Initialize request should return 200 status'); + assert.ok(initResponse.data.result, 'Initialize response should have result'); + assert.ok(initResponse.data.result.capabilities, 'Initialize response should have capabilities'); + + console.log('āœ“ HTTP server startup and MCP initialize test passed'); + return { port, serverPromise }; +} + +/** + * Test MCP tools listing functionality + */ +async function testToolsListing(port) { + console.log('\nTest 2: MCP tools listing'); + + const listToolsRequest = { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: {} + }; + + const toolsResponse = await makeHttpRequest(port, listToolsRequest); + + // Verify response + assert.strictEqual(toolsResponse.status, 200, 'Tools list request should return 200 status'); + assert.ok(toolsResponse.data.result, 'Tools list response should have result'); + assert.ok(Array.isArray(toolsResponse.data.result.tools), 'Tools list should be an array'); + assert.ok(toolsResponse.data.result.tools.length > 0, 'Should have at least one tool available'); + + console.log(`āœ“ Tools list test passed (found ${toolsResponse.data.result.tools.length} tools)`); +} + +/** + * Test error handling for invalid requests + */ +async function testInvalidRequestHandling(port) { + console.log('\nTest 3: Invalid request handling'); + + const invalidRequest = { + jsonrpc: '2.0', + id: 3, + method: 'nonexistent/method', + params: {} + }; + + const invalidResponse = await makeHttpRequest(port, invalidRequest); + + // Verify error response + assert.strictEqual(invalidResponse.status, 200, 'Invalid request should still return 200 (error in body)'); + assert.ok(invalidResponse.data.error, 'Invalid request should return error in response body'); + + console.log('āœ“ Invalid request handling test passed'); +} + +/** + * Main test function for HTTP transport + */ +async function runHttpTransportTests() { + console.log('=== HTTP Transport Tests ==='); + + let serverInfo; + + try { + // Test 1: HTTP server startup and MCP initialize + serverInfo = await testHttpServerStartup(); + + // Test 2: MCP tools listing + await testToolsListing(serverInfo.port); + + // Test 3: Invalid request handling + await testInvalidRequestHandling(serverInfo.port); + + console.log('\nāœ… All HTTP transport tests passed!'); + return { success: true, serverInfo }; + + } catch (error) { + console.error('āŒ HTTP transport test failed:', error.message); + return { success: false, serverInfo }; + } +} + +/** + * Export the main test function + */ +export default async function runTests() { + let originalConfig; + let testResult; + + try { + originalConfig = await configManager.getConfig(); + testResult = await runHttpTransportTests(); + } catch (error) { + console.error('āŒ Test failed:', error.message); + return false; + } finally { + // Clean up server and config + if (testResult && testResult.serverInfo) { + await teardown(originalConfig, testResult.serverInfo.serverPromise); + } else if (originalConfig) { + await teardown(originalConfig); + } + } + + return testResult ? testResult.success : false; +} + +// If this file is run directly (not imported), execute the test +if (import.meta.url === `file://${process.argv[1]}`) { + runTests().catch(error => { + console.error('āŒ Unhandled error:', error); + process.exit(1); + }); +} diff --git a/tsconfig.json b/tsconfig.json index d00ea16..4598115 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "diagnostics": true, "extendedDiagnostics": true, "listEmittedFiles": true, - "types": ["node"] + "types": ["node"], + "sourceMap": true }, "include": [ "src/**/*.ts" @@ -21,4 +22,4 @@ "node_modules", "dist" ] -} \ No newline at end of file +}