From 3ded1f161d59ceee4c8e98cbc4dfe54b518f3533 Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 22:32:38 -0500 Subject: [PATCH 1/4] Updated Filesystem --- .gitignore | 4 +- package-lock.json | 365 +++++++++++++++++++++++- package.json | 3 +- src/filesystem/README.md | 76 +++++ src/filesystem/index.ts | 537 +++++++++++++++++++++++++++++++++++ src/filesystem/package.json | 30 ++ src/filesystem/tsconfig.json | 12 + 7 files changed, 1022 insertions(+), 5 deletions(-) create mode 100644 src/filesystem/README.md create mode 100644 src/filesystem/index.ts create mode 100644 src/filesystem/package.json create mode 100644 src/filesystem/tsconfig.json diff --git a/.gitignore b/.gitignore index 38ff4cd0..7ecb7109 100644 --- a/.gitignore +++ b/.gitignore @@ -290,9 +290,11 @@ dmypy.json # Cython debug symbols cython_debug/ +.DS_Store + # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/package-lock.json b/package-lock.json index 55939430..082300d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,102 @@ "node": ">=14.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", @@ -74,6 +170,10 @@ "resolved": "src/everything", "link": true }, + "node_modules/@modelcontextprotocol/server-filesystem": { + "resolved": "src/filesystem", + "link": true + }, "node_modules/@modelcontextprotocol/server-gdrive": { "resolved": "src/gdrive", "link": true @@ -98,6 +198,16 @@ "resolved": "src/slack", "link": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.1.tgz", @@ -380,8 +490,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bare-events": { "version": "2.5.0", @@ -693,6 +802,20 @@ } } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -769,6 +892,12 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1046,6 +1175,22 @@ "node": ">= 0.8" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", @@ -1589,6 +1734,27 @@ "node": ">=8" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1741,6 +1907,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -1907,6 +2082,12 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -1952,12 +2133,43 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", @@ -2499,6 +2711,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", @@ -2549,6 +2782,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2661,6 +2906,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2672,6 +2932,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2856,6 +3129,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2872,6 +3160,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3059,7 +3365,6 @@ "src/filesystem": { "name": "@modelcontextprotocol/server-filesystem", "version": "0.1.0", - "extraneous": true, "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "0.5.0", @@ -3074,6 +3379,60 @@ "typescript": "^5.3.3" } }, + "src/filesystem/node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "src/filesystem/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "src/filesystem/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/filesystem/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "src/gdrive": { "name": "@modelcontextprotocol/server-gdrive", "version": "0.1.0", diff --git a/package.json b/package.json index 57e35a1d..4da25f78 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@modelcontextprotocol/server-puppeteer": "*", "@modelcontextprotocol/server-slack": "*", "@modelcontextprotocol/server-brave-search": "*", - "@modelcontextprotocol/server-memory": "*" + "@modelcontextprotocol/server-memory": "*", + "@modelcontextprotocol/server-filesystem": "*" } } diff --git a/src/filesystem/README.md b/src/filesystem/README.md new file mode 100644 index 00000000..302fc4c4 --- /dev/null +++ b/src/filesystem/README.md @@ -0,0 +1,76 @@ +# Filesystem MCP Server + +Node.js server implementing Model Context Protocol (MCP) for filesystem operations. + +## Features + +- Read/write files +- Create/list/delete directories +- Move files/directories +- Search files +- Get file metadata + +## API + +### Resources + +- `file://system`: File system operations interface + +### Tools + +- **read_file** + - Read complete contents of a file + - Input: `path` (string) + - Reads complete file contents with UTF-8 encoding + +- **read_multiple_files** + - Read multiple files simultaneously + - Input: `paths` (string[]) + - Failed reads won't stop the entire operation + +- **write_file** + - Create new file or overwrite existing + - Inputs: + - `path` (string): File location + - `content` (string): File content + +- **create_directory** + - Create new directory or ensure it exists + - Input: `path` (string) + - Creates parent directories if needed + - Succeeds silently if directory exists + +- **list_directory** + - List directory contents with [FILE] or [DIR] prefixes + - Input: `path` (string) + +- **move_file** + - Move or rename files and directories + - Inputs: + - `source` (string) + - `destination` (string) + - Fails if destination exists + +- **search_files** + - Recursively search for files/directories + - Inputs: + - `path` (string): Starting directory + - `pattern` (string): Search pattern + - Case-insensitive matching + - Returns full paths to matches + +- **get_file_info** + - Get detailed file/directory metadata + - Input: `path` (string) + - Returns: + - Size + - Creation time + - Modified time + - Access time + - Type (file/directory) + - Permissions + +## Notes + +- Exercise caution with `write_file`, since it can overwrite an existing file +- File paths can be absolute or relative diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts new file mode 100644 index 00000000..25e22bea --- /dev/null +++ b/src/filesystem/index.ts @@ -0,0 +1,537 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import fs from "fs/promises"; +import path from "path"; +import { promisify } from "util"; +import { exec as execCallback } from "child_process"; + +// Define interfaces for the tool arguments +interface ReadFileArgs { + path: string; +} + +interface ReadMultipleFilesArgs { + paths: string[]; +} + +interface WriteFileArgs { + path: string; + content: string; +} + +interface CreateDirectoryArgs { + path: string; +} + +interface ListDirectoryArgs { + path: string; +} + +interface MoveFileArgs { + source: string; + destination: string; +} + +interface SearchFilesArgs { + path: string; + pattern: string; +} + +interface FileInfo { + size: number; + created: Date; + modified: Date; + accessed: Date; + isDirectory: boolean; + isFile: boolean; + permissions: string; +} + +const exec = promisify(execCallback); + +const server = new Server( + { + name: "example-servers/filesystem", + version: "0.1.0", + }, + { + capabilities: { + tools: {}, + resources: {}, // Need this since we're using resources + }, + }, +); + +// Add Resources List Handler +server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [ + { + uri: "file://system", + mimeType: "text/plain", + name: "File System Operations", + }, + ], + }; +}); + +// Add Read Resource Handler +server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if (request.params.uri.toString() === "file://system") { + return { + contents: [ + { + uri: "file://system", + mimeType: "text/plain", + text: "File system operations interface", + }, + ], + }; + } + throw new Error("Resource not found"); +}); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "read_file", + description: + "Read the complete contents of a file from the file system. " + + "Handles various text encodings and provides detailed error messages " + + "if the file cannot be read. Use this tool when you need to examine " + + "the contents of a single file.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: + "Absolute or relative path to the file you want to read", + }, + }, + required: ["path"], + }, + }, + { + name: "read_multiple_files", + description: + "Read the contents of multiple files simultaneously. This is more " + + "efficient than reading files one by one when you need to analyze " + + "or compare multiple files. Each file's content is returned with its " + + "path as a reference. Failed reads for individual files won't stop " + + "the entire operation.", + inputSchema: { + type: "object", + properties: { + paths: { + type: "array", + items: { + type: "string", + }, + description: + "List of file paths to read. Can be absolute or relative paths.", + }, + }, + required: ["paths"], + }, + }, + { + name: "write_file", + description: + "Create a new file or completely overwrite an existing file with new content. " + + "Use with caution as it will overwrite existing files without warning. " + + "Handles text content with proper encoding.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: + "Path where the file should be written. Parent directories will be created if needed.", + }, + content: { + type: "string", + description: + "Content to write to the file. Can include newlines and special characters.", + }, + }, + required: ["path", "content"], + }, + }, + { + name: "create_directory", + description: + "Create a new directory or ensure a directory exists. Can create multiple " + + "nested directories in one operation. If the directory already exists, " + + "this operation will succeed silently. Perfect for setting up directory " + + "structures for projects or ensuring required paths exist.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: + "Path of the directory to create. Will create parent directories if they don't exist.", + }, + }, + required: ["path"], + }, + }, + { + name: "list_directory", + description: + "Get a detailed listing of all files and directories in a specified path. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. This tool is essential for understanding directory structure and " + + "finding specific files within a directory.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: + "Path of the directory to list. Must be an existing directory.", + }, + }, + required: ["path"], + }, + }, + { + name: "move_file", + description: + "Move or rename files and directories. Can move files between directories " + + "and rename them in a single operation. If the destination exists, the " + + "operation will fail. Works across different directories and can be used " + + "for simple renaming within the same directory.", + inputSchema: { + type: "object", + properties: { + source: { + type: "string", + description: "Current path of the file or directory", + }, + destination: { + type: "string", + description: + "New path where the file or directory should be moved to", + }, + }, + required: ["source", "destination"], + }, + }, + { + name: "search_files", + description: + "Recursively search for files and directories matching a pattern. " + + "Searches through all subdirectories from the starting path. The search " + + "is case-insensitive and matches partial names. Returns full paths to all " + + "matching items. Great for finding files when you don't know their exact location.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Starting directory for the search", + }, + pattern: { + type: "string", + description: + "Text pattern to search for in file and directory names", + }, + }, + required: ["path", "pattern"], + }, + }, + { + name: "get_file_info", + description: + "Retrieve detailed metadata about a file or directory. Returns comprehensive " + + "information including size, creation time, last modified time, permissions, " + + "and type. This tool is perfect for understanding file characteristics " + + "without reading the actual content.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: + "Path to the file or directory to get information about", + }, + }, + required: ["path"], + }, + }, + ], + }; +}); + +async function getFileStats(filePath: string): Promise { + const stats = await fs.stat(filePath); + return { + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + accessed: stats.atime, + isDirectory: stats.isDirectory(), + isFile: stats.isFile(), + permissions: stats.mode.toString(8).slice(-3), + }; +} + +async function searchFiles( + rootPath: string, + pattern: string, +): Promise { + const results: string[] = []; + + async function search(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { + results.push(fullPath); + } + + if (entry.isDirectory()) { + await search(fullPath); + } + } + } + + await search(rootPath); + return results; +} + +// Add type guard functions for each argument type +function isReadFileArgs(args: unknown): args is ReadFileArgs { + return ( + typeof args === "object" && + args !== null && + "path" in args && + typeof (args as ReadFileArgs).path === "string" + ); +} + +function isReadMultipleFilesArgs(args: unknown): args is ReadMultipleFilesArgs { + return ( + typeof args === "object" && + args !== null && + "paths" in args && + Array.isArray((args as ReadMultipleFilesArgs).paths) && + (args as ReadMultipleFilesArgs).paths.every( + (path) => typeof path === "string", + ) + ); +} + +function isWriteFileArgs(args: unknown): args is WriteFileArgs { + return ( + typeof args === "object" && + args !== null && + "path" in args && + "content" in args && + typeof (args as WriteFileArgs).path === "string" && + typeof (args as WriteFileArgs).content === "string" + ); +} + +function isCreateDirectoryArgs(args: unknown): args is CreateDirectoryArgs { + return ( + typeof args === "object" && + args !== null && + "path" in args && + typeof (args as CreateDirectoryArgs).path === "string" + ); +} + +function isListDirectoryArgs(args: unknown): args is ListDirectoryArgs { + return ( + typeof args === "object" && + args !== null && + "path" in args && + typeof (args as ListDirectoryArgs).path === "string" + ); +} + +function isMoveFileArgs(args: unknown): args is MoveFileArgs { + return ( + typeof args === "object" && + args !== null && + "source" in args && + "destination" in args && + typeof (args as MoveFileArgs).source === "string" && + typeof (args as MoveFileArgs).destination === "string" + ); +} + +function isSearchFilesArgs(args: unknown): args is SearchFilesArgs { + return ( + typeof args === "object" && + args !== null && + "path" in args && + "pattern" in args && + typeof (args as SearchFilesArgs).path === "string" && + typeof (args as SearchFilesArgs).pattern === "string" + ); +} + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + const { name, arguments: args } = request.params; + + switch (name) { + case "read_file": { + if (!isReadFileArgs(args)) { + throw new Error("Invalid arguments for read_file"); + } + const content = await fs.readFile(args.path, "utf-8"); + return { + content: [{ type: "text", text: content }], + }; + } + + case "read_multiple_files": { + if (!isReadMultipleFilesArgs(args)) { + throw new Error("Invalid arguments for read_multiple_files"); + } + const results = await Promise.all( + args.paths.map(async (filePath: string) => { + try { + const content = await fs.readFile(filePath, "utf-8"); + return `${filePath}:\n${content}\n`; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return `${filePath}: Error - ${errorMessage}`; + } + }), + ); + return { + content: [{ type: "text", text: results.join("\n---\n") }], + }; + } + + case "write_file": { + if (!isWriteFileArgs(args)) { + throw new Error("Invalid arguments for write_file"); + } + await fs.writeFile(args.path, args.content, "utf-8"); + return { + content: [ + { type: "text", text: `Successfully wrote to ${args.path}` }, + ], + }; + } + + case "create_directory": { + if (!isCreateDirectoryArgs(args)) { + throw new Error("Invalid arguments for create_directory"); + } + await fs.mkdir(args.path, { recursive: true }); + return { + content: [ + { + type: "text", + text: `Successfully created directory ${args.path}`, + }, + ], + }; + } + + case "list_directory": { + if (!isListDirectoryArgs(args)) { + throw new Error("Invalid arguments for list_directory"); + } + const entries = await fs.readdir(args.path, { withFileTypes: true }); + const formatted = entries + .map( + (entry) => + `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`, + ) + .join("\n"); + return { + content: [{ type: "text", text: formatted }], + }; + } + + case "move_file": { + if (!isMoveFileArgs(args)) { + throw new Error("Invalid arguments for move_file"); + } + await fs.rename(args.source, args.destination); + return { + content: [ + { + type: "text", + text: `Successfully moved ${args.source} to ${args.destination}`, + }, + ], + }; + } + + case "search_files": { + if (!isSearchFilesArgs(args)) { + throw new Error("Invalid arguments for search_files"); + } + const results = await searchFiles(args.path, args.pattern); + return { + content: [ + { + type: "text", + text: + results.length > 0 ? results.join("\n") : "No matches found", + }, + ], + }; + } + + case "get_file_info": { + if (!isCreateDirectoryArgs(args)) { + throw new Error("Invalid arguments for get_file_info"); + } + const info = await getFileStats(args.path); + return { + content: [ + { + type: "text", + text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"), + }, + ], + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + content: [{ type: "text", text: `Error: ${errorMessage}` }], + isError: true, + }; + } +}); + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("MCP Server running on stdio"); +} + +runServer().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); diff --git a/src/filesystem/package.json b/src/filesystem/package.json new file mode 100644 index 00000000..8bc2b770 --- /dev/null +++ b/src/filesystem/package.json @@ -0,0 +1,30 @@ +{ + "name": "@modelcontextprotocol/server-filesystem", + "version": "0.1.0", + "description": "MCP server for filesystem access", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "type": "module", + "bin": { + "mcp-server-filesystem": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.5.0", + "glob": "^10.3.10" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "shx": "^0.3.4", + "typescript": "^5.3.3" + } +} diff --git a/src/filesystem/tsconfig.json b/src/filesystem/tsconfig.json new file mode 100644 index 00000000..c0c20f35 --- /dev/null +++ b/src/filesystem/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": [ + "./**/*.ts" + ] +} From f9041bbced7bcafd06b8c9a21544ae3092f1d58f Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 22:45:22 -0500 Subject: [PATCH 2/4] Added Zod --- src/filesystem/index.ts | 430 +++++++++------------------------------- 1 file changed, 91 insertions(+), 339 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 25e22bea..82a43278 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -4,46 +4,51 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, - ListResourcesRequestSchema, ListToolsRequestSchema, - ReadResourceRequestSchema, + ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; -import { promisify } from "util"; -import { exec as execCallback } from "child_process"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; -// Define interfaces for the tool arguments -interface ReadFileArgs { - path: string; -} +const ReadFileArgsSchema = z.object({ + path: z.string(), +}); -interface ReadMultipleFilesArgs { - paths: string[]; -} +const ReadMultipleFilesArgsSchema = z.object({ + paths: z.array(z.string()), +}); -interface WriteFileArgs { - path: string; - content: string; -} +const WriteFileArgsSchema = z.object({ + path: z.string(), + content: z.string(), +}); -interface CreateDirectoryArgs { - path: string; -} +const CreateDirectoryArgsSchema = z.object({ + path: z.string(), +}); -interface ListDirectoryArgs { - path: string; -} +const ListDirectoryArgsSchema = z.object({ + path: z.string(), +}); -interface MoveFileArgs { - source: string; - destination: string; -} +const MoveFileArgsSchema = z.object({ + source: z.string(), + destination: z.string(), +}); -interface SearchFilesArgs { - path: string; - pattern: string; -} +const SearchFilesArgsSchema = z.object({ + path: z.string(), + pattern: z.string(), +}); + +const GetFileInfoArgsSchema = z.object({ + path: z.string(), +}); + +const ToolInputSchema = ToolSchema.shape.inputSchema; +type ToolInput = z.infer; interface FileInfo { size: number; @@ -55,8 +60,6 @@ interface FileInfo { permissions: string; } -const exec = promisify(execCallback); - const server = new Server( { name: "example-servers/filesystem", @@ -65,215 +68,56 @@ const server = new Server( { capabilities: { tools: {}, - resources: {}, // Need this since we're using resources }, }, ); -// Add Resources List Handler -server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { - resources: [ - { - uri: "file://system", - mimeType: "text/plain", - name: "File System Operations", - }, - ], - }; -}); - -// Add Read Resource Handler -server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - if (request.params.uri.toString() === "file://system") { - return { - contents: [ - { - uri: "file://system", - mimeType: "text/plain", - text: "File system operations interface", - }, - ], - }; - } - throw new Error("Resource not found"); -}); - server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "read_file", - description: - "Read the complete contents of a file from the file system. " + - "Handles various text encodings and provides detailed error messages " + - "if the file cannot be read. Use this tool when you need to examine " + - "the contents of a single file.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: - "Absolute or relative path to the file you want to read", - }, - }, - required: ["path"], - }, + description: "Read the complete contents of a file from the file system.", + inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, }, { name: "read_multiple_files", - description: - "Read the contents of multiple files simultaneously. This is more " + - "efficient than reading files one by one when you need to analyze " + - "or compare multiple files. Each file's content is returned with its " + - "path as a reference. Failed reads for individual files won't stop " + - "the entire operation.", - inputSchema: { - type: "object", - properties: { - paths: { - type: "array", - items: { - type: "string", - }, - description: - "List of file paths to read. Can be absolute or relative paths.", - }, - }, - required: ["paths"], - }, + description: "Read the contents of multiple files simultaneously.", + inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput, }, { name: "write_file", - description: - "Create a new file or completely overwrite an existing file with new content. " + - "Use with caution as it will overwrite existing files without warning. " + - "Handles text content with proper encoding.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: - "Path where the file should be written. Parent directories will be created if needed.", - }, - content: { - type: "string", - description: - "Content to write to the file. Can include newlines and special characters.", - }, - }, - required: ["path", "content"], - }, + description: "Create a new file or completely overwrite an existing file with new content.", + inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput, }, { name: "create_directory", - description: - "Create a new directory or ensure a directory exists. Can create multiple " + - "nested directories in one operation. If the directory already exists, " + - "this operation will succeed silently. Perfect for setting up directory " + - "structures for projects or ensuring required paths exist.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: - "Path of the directory to create. Will create parent directories if they don't exist.", - }, - }, - required: ["path"], - }, + description: "Create a new directory or ensure a directory exists.", + inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput, }, { name: "list_directory", - description: - "Get a detailed listing of all files and directories in a specified path. " + - "Results clearly distinguish between files and directories with [FILE] and [DIR] " + - "prefixes. This tool is essential for understanding directory structure and " + - "finding specific files within a directory.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: - "Path of the directory to list. Must be an existing directory.", - }, - }, - required: ["path"], - }, + description: "Get a detailed listing of all files and directories in a specified path.", + inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, }, { name: "move_file", - description: - "Move or rename files and directories. Can move files between directories " + - "and rename them in a single operation. If the destination exists, the " + - "operation will fail. Works across different directories and can be used " + - "for simple renaming within the same directory.", - inputSchema: { - type: "object", - properties: { - source: { - type: "string", - description: "Current path of the file or directory", - }, - destination: { - type: "string", - description: - "New path where the file or directory should be moved to", - }, - }, - required: ["source", "destination"], - }, + description: "Move or rename files and directories.", + inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput, }, { name: "search_files", - description: - "Recursively search for files and directories matching a pattern. " + - "Searches through all subdirectories from the starting path. The search " + - "is case-insensitive and matches partial names. Returns full paths to all " + - "matching items. Great for finding files when you don't know their exact location.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: "Starting directory for the search", - }, - pattern: { - type: "string", - description: - "Text pattern to search for in file and directory names", - }, - }, - required: ["path", "pattern"], - }, + description: "Recursively search for files and directories matching a pattern.", + inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, }, { name: "get_file_info", - description: - "Retrieve detailed metadata about a file or directory. Returns comprehensive " + - "information including size, creation time, last modified time, permissions, " + - "and type. This tool is perfect for understanding file characteristics " + - "without reading the actual content.", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: - "Path to the file or directory to get information about", - }, - }, - required: ["path"], - }, + description: "Retrieve detailed metadata about a file or directory.", + inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput, }, ], }; }); - async function getFileStats(filePath: string): Promise { const stats = await fs.stat(filePath); return { @@ -313,106 +157,34 @@ async function searchFiles( return results; } -// Add type guard functions for each argument type -function isReadFileArgs(args: unknown): args is ReadFileArgs { - return ( - typeof args === "object" && - args !== null && - "path" in args && - typeof (args as ReadFileArgs).path === "string" - ); -} - -function isReadMultipleFilesArgs(args: unknown): args is ReadMultipleFilesArgs { - return ( - typeof args === "object" && - args !== null && - "paths" in args && - Array.isArray((args as ReadMultipleFilesArgs).paths) && - (args as ReadMultipleFilesArgs).paths.every( - (path) => typeof path === "string", - ) - ); -} - -function isWriteFileArgs(args: unknown): args is WriteFileArgs { - return ( - typeof args === "object" && - args !== null && - "path" in args && - "content" in args && - typeof (args as WriteFileArgs).path === "string" && - typeof (args as WriteFileArgs).content === "string" - ); -} - -function isCreateDirectoryArgs(args: unknown): args is CreateDirectoryArgs { - return ( - typeof args === "object" && - args !== null && - "path" in args && - typeof (args as CreateDirectoryArgs).path === "string" - ); -} - -function isListDirectoryArgs(args: unknown): args is ListDirectoryArgs { - return ( - typeof args === "object" && - args !== null && - "path" in args && - typeof (args as ListDirectoryArgs).path === "string" - ); -} - -function isMoveFileArgs(args: unknown): args is MoveFileArgs { - return ( - typeof args === "object" && - args !== null && - "source" in args && - "destination" in args && - typeof (args as MoveFileArgs).source === "string" && - typeof (args as MoveFileArgs).destination === "string" - ); -} - -function isSearchFilesArgs(args: unknown): args is SearchFilesArgs { - return ( - typeof args === "object" && - args !== null && - "path" in args && - "pattern" in args && - typeof (args as SearchFilesArgs).path === "string" && - typeof (args as SearchFilesArgs).pattern === "string" - ); -} - server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case "read_file": { - if (!isReadFileArgs(args)) { - throw new Error("Invalid arguments for read_file"); + const parsed = ReadFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_file: ${parsed.error}`); } - const content = await fs.readFile(args.path, "utf-8"); + const content = await fs.readFile(parsed.data.path, "utf-8"); return { content: [{ type: "text", text: content }], }; } case "read_multiple_files": { - if (!isReadMultipleFilesArgs(args)) { - throw new Error("Invalid arguments for read_multiple_files"); + const parsed = ReadMultipleFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`); } const results = await Promise.all( - args.paths.map(async (filePath: string) => { + parsed.data.paths.map(async (filePath: string) => { try { const content = await fs.readFile(filePath, "utf-8"); return `${filePath}:\n${content}\n`; } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); + const errorMessage = error instanceof Error ? error.message : String(error); return `${filePath}: Error - ${errorMessage}`; } }), @@ -423,42 +195,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "write_file": { - if (!isWriteFileArgs(args)) { - throw new Error("Invalid arguments for write_file"); + const parsed = WriteFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for write_file: ${parsed.error}`); } - await fs.writeFile(args.path, args.content, "utf-8"); + await fs.writeFile(parsed.data.path, parsed.data.content, "utf-8"); return { - content: [ - { type: "text", text: `Successfully wrote to ${args.path}` }, - ], + content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }], }; } case "create_directory": { - if (!isCreateDirectoryArgs(args)) { - throw new Error("Invalid arguments for create_directory"); + const parsed = CreateDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); } - await fs.mkdir(args.path, { recursive: true }); + await fs.mkdir(parsed.data.path, { recursive: true }); return { - content: [ - { - type: "text", - text: `Successfully created directory ${args.path}`, - }, - ], + content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }], }; } case "list_directory": { - if (!isListDirectoryArgs(args)) { - throw new Error("Invalid arguments for list_directory"); + const parsed = ListDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); } - const entries = await fs.readdir(args.path, { withFileTypes: true }); + const entries = await fs.readdir(parsed.data.path, { withFileTypes: true }); const formatted = entries - .map( - (entry) => - `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`, - ) + .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) .join("\n"); return { content: [{ type: "text", text: formatted }], @@ -466,50 +231,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "move_file": { - if (!isMoveFileArgs(args)) { - throw new Error("Invalid arguments for move_file"); + const parsed = MoveFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for move_file: ${parsed.error}`); } - await fs.rename(args.source, args.destination); + await fs.rename(parsed.data.source, parsed.data.destination); return { - content: [ - { - type: "text", - text: `Successfully moved ${args.source} to ${args.destination}`, - }, - ], + content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }], }; } case "search_files": { - if (!isSearchFilesArgs(args)) { - throw new Error("Invalid arguments for search_files"); + const parsed = SearchFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for search_files: ${parsed.error}`); } - const results = await searchFiles(args.path, args.pattern); + const results = await searchFiles(parsed.data.path, parsed.data.pattern); return { - content: [ - { - type: "text", - text: - results.length > 0 ? results.join("\n") : "No matches found", - }, - ], + content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], }; } case "get_file_info": { - if (!isCreateDirectoryArgs(args)) { - throw new Error("Invalid arguments for get_file_info"); + const parsed = GetFileInfoArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); } - const info = await getFileStats(args.path); + const info = await getFileStats(parsed.data.path); return { - content: [ - { - type: "text", - text: Object.entries(info) - .map(([key, value]) => `${key}: ${value}`) - .join("\n"), - }, - ], + content: [{ type: "text", text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n") }], }; } From e935bfee8033262556afcfb014762398cf15cf8c Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 23:28:00 -0500 Subject: [PATCH 3/4] Updated Filesystem --- package-lock.json | 1 + src/filesystem/index.ts | 272 +++++++++++++++++++++++++++++++--------- 2 files changed, 216 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 082300d3..c50a65d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dependencies": { "@modelcontextprotocol/server-brave-search": "*", "@modelcontextprotocol/server-everything": "*", + "@modelcontextprotocol/server-filesystem": "*", "@modelcontextprotocol/server-gdrive": "*", "@modelcontextprotocol/server-memory": "*", "@modelcontextprotocol/server-postgres": "*", diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 82a43278..b4c4e92d 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -9,9 +9,90 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; +import os from 'os'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; +// Command line argument parsing +const args = process.argv.slice(2); +if (args.length === 0) { + console.error("Usage: mcp-server-filesystem [additional-directories...]"); + process.exit(1); +} + +// Normalize all paths consistently +function normalizePath(p: string): string { + return path.normalize(p).toLowerCase(); +} + +function expandHome(filepath: string): string { + if (filepath.startsWith('~/') || filepath === '~') { + return path.join(os.homedir(), filepath.slice(1)); + } + return filepath; +} + +// Store allowed directories in normalized form +const allowedDirectories = args.map(dir => + normalizePath(path.resolve(expandHome(dir))) +); + +// Validate that all directories exist and are accessible +await Promise.all(args.map(async (dir) => { + try { + const stats = await fs.stat(dir); + if (!stats.isDirectory()) { + console.error(`Error: ${dir} is not a directory`); + process.exit(1); + } + } catch (error) { + console.error(`Error accessing directory ${dir}:`, error); + process.exit(1); + } +})); + +// Security utilities +async function validatePath(requestedPath: string): Promise { + const expandedPath = expandHome(requestedPath); + const absolute = path.isAbsolute(expandedPath) + ? path.resolve(expandedPath) + : path.resolve(process.cwd(), expandedPath); + + const normalizedRequested = normalizePath(absolute); + + // Check if path is within allowed directories + const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir)); + if (!isAllowed) { + throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); + } + + // Handle symlinks by checking their real path + try { + const realPath = await fs.realpath(absolute); + const normalizedReal = normalizePath(realPath); + const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir)); + if (!isRealPathAllowed) { + throw new Error("Access denied - symlink target outside allowed directories"); + } + return realPath; + } catch (error) { + // For new files that don't exist yet, verify parent directory + const parentDir = path.dirname(absolute); + try { + const realParentPath = await fs.realpath(parentDir); + const normalizedParent = normalizePath(realParentPath); + const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir)); + if (!isParentAllowed) { + throw new Error("Access denied - parent directory outside allowed directories"); + } + return absolute; + } catch { + throw new Error(`Parent directory does not exist: ${parentDir}`); + } + } +} + +// Schema definitions const ReadFileArgsSchema = z.object({ path: z.string(), }); @@ -60,10 +141,11 @@ interface FileInfo { permissions: string; } +// Server setup const server = new Server( { - name: "example-servers/filesystem", - version: "0.1.0", + name: "secure-filesystem-server", + version: "0.2.0", }, { capabilities: { @@ -72,90 +154,146 @@ const server = new Server( }, ); +// Tool implementations +async function getFileStats(filePath: string): Promise { + const stats = await fs.stat(filePath); + return { + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + accessed: stats.atime, + isDirectory: stats.isDirectory(), + isFile: stats.isFile(), + permissions: stats.mode.toString(8).slice(-3), + }; +} + +async function searchFiles( + rootPath: string, + pattern: string, +): Promise { + const results: string[] = []; + + async function search(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + try { + // Validate each path before processing + await validatePath(fullPath); + + if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { + results.push(fullPath); + } + + if (entry.isDirectory()) { + await search(fullPath); + } + } catch (error) { + // Skip invalid paths during search + continue; + } + } + } + + await search(rootPath); + return results; +} + +// Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "read_file", - description: "Read the complete contents of a file from the file system.", + description: + "Read the complete contents of a file from the file system. " + + "Handles various text encodings and provides detailed error messages " + + "if the file cannot be read. Use this tool when you need to examine " + + "the contents of a single file. Only works within allowed directories.", inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, }, { name: "read_multiple_files", - description: "Read the contents of multiple files simultaneously.", + description: + "Read the contents of multiple files simultaneously. This is more " + + "efficient than reading files one by one when you need to analyze " + + "or compare multiple files. Each file's content is returned with its " + + "path as a reference. Failed reads for individual files won't stop " + + "the entire operation. Only works within allowed directories.", inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput, }, { name: "write_file", - description: "Create a new file or completely overwrite an existing file with new content.", + description: + "Create a new file or completely overwrite an existing file with new content. " + + "Use with caution as it will overwrite existing files without warning. " + + "Handles text content with proper encoding. Only works within allowed directories.", inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput, }, { name: "create_directory", - description: "Create a new directory or ensure a directory exists.", + description: + "Create a new directory or ensure a directory exists. Can create multiple " + + "nested directories in one operation. If the directory already exists, " + + "this operation will succeed silently. Perfect for setting up directory " + + "structures for projects or ensuring required paths exist. Only works within allowed directories.", inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput, }, { name: "list_directory", - description: "Get a detailed listing of all files and directories in a specified path.", + description: + "Get a detailed listing of all files and directories in a specified path. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. This tool is essential for understanding directory structure and " + + "finding specific files within a directory. Only works within allowed directories.", inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, }, { name: "move_file", - description: "Move or rename files and directories.", + description: + "Move or rename files and directories. Can move files between directories " + + "and rename them in a single operation. If the destination exists, the " + + "operation will fail. Works across different directories and can be used " + + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput, }, { name: "search_files", - description: "Recursively search for files and directories matching a pattern.", + description: + "Recursively search for files and directories matching a pattern. " + + "Searches through all subdirectories from the starting path. The search " + + "is case-insensitive and matches partial names. Returns full paths to all " + + "matching items. Great for finding files when you don't know their exact location. " + + "Only searches within allowed directories.", inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, }, { name: "get_file_info", - description: "Retrieve detailed metadata about a file or directory.", + description: + "Retrieve detailed metadata about a file or directory. Returns comprehensive " + + "information including size, creation time, last modified time, permissions, " + + "and type. This tool is perfect for understanding file characteristics " + + "without reading the actual content. Only works within allowed directories.", inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput, }, + { + name: "list_allowed_directories", + description: + "Returns the list of directories that this server is allowed to access. " + + "Use this to understand which directories are available before trying to access files.", + inputSchema: { + type: "object", + properties: {}, + required: [], + }, + }, ], }; }); -async function getFileStats(filePath: string): Promise { - const stats = await fs.stat(filePath); - return { - size: stats.size, - created: stats.birthtime, - modified: stats.mtime, - accessed: stats.atime, - isDirectory: stats.isDirectory(), - isFile: stats.isFile(), - permissions: stats.mode.toString(8).slice(-3), - }; -} -async function searchFiles( - rootPath: string, - pattern: string, -): Promise { - const results: string[] = []; - - async function search(currentPath: string) { - const entries = await fs.readdir(currentPath, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(currentPath, entry.name); - - if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { - results.push(fullPath); - } - - if (entry.isDirectory()) { - await search(fullPath); - } - } - } - - await search(rootPath); - return results; -} server.setRequestHandler(CallToolRequestSchema, async (request) => { try { @@ -167,7 +305,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for read_file: ${parsed.error}`); } - const content = await fs.readFile(parsed.data.path, "utf-8"); + const validPath = await validatePath(parsed.data.path); + const content = await fs.readFile(validPath, "utf-8"); return { content: [{ type: "text", text: content }], }; @@ -181,7 +320,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const results = await Promise.all( parsed.data.paths.map(async (filePath: string) => { try { - const content = await fs.readFile(filePath, "utf-8"); + const validPath = await validatePath(filePath); + const content = await fs.readFile(validPath, "utf-8"); return `${filePath}:\n${content}\n`; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -199,7 +339,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for write_file: ${parsed.error}`); } - await fs.writeFile(parsed.data.path, parsed.data.content, "utf-8"); + const validPath = await validatePath(parsed.data.path); + await fs.writeFile(validPath, parsed.data.content, "utf-8"); return { content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }], }; @@ -210,7 +351,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); } - await fs.mkdir(parsed.data.path, { recursive: true }); + const validPath = await validatePath(parsed.data.path); + await fs.mkdir(validPath, { recursive: true }); return { content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }], }; @@ -221,7 +363,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); } - const entries = await fs.readdir(parsed.data.path, { withFileTypes: true }); + const validPath = await validatePath(parsed.data.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); const formatted = entries .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) .join("\n"); @@ -235,7 +378,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for move_file: ${parsed.error}`); } - await fs.rename(parsed.data.source, parsed.data.destination); + const validSourcePath = await validatePath(parsed.data.source); + const validDestPath = await validatePath(parsed.data.destination); + await fs.rename(validSourcePath, validDestPath); return { content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }], }; @@ -246,7 +391,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for search_files: ${parsed.error}`); } - const results = await searchFiles(parsed.data.path, parsed.data.pattern); + const validPath = await validatePath(parsed.data.path); + const results = await searchFiles(validPath, parsed.data.pattern); return { content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], }; @@ -257,7 +403,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!parsed.success) { throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); } - const info = await getFileStats(parsed.data.path); + const validPath = await validatePath(parsed.data.path); + const info = await getFileStats(validPath); return { content: [{ type: "text", text: Object.entries(info) .map(([key, value]) => `${key}: ${value}`) @@ -265,6 +412,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_allowed_directories": { + return { + content: [{ + type: "text", + text: `Allowed directories:\n${allowedDirectories.join('\n')}` + }], + }; + } + default: throw new Error(`Unknown tool: ${name}`); } @@ -277,13 +433,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } }); +// Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); - console.error("MCP Server running on stdio"); + console.error("Secure MCP Filesystem Server running on stdio"); + console.error("Allowed directories:", allowedDirectories); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); -}); +}); \ No newline at end of file From 18edebe3c299a82d5c24dae8ceca63941b07bfe7 Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 23:34:00 -0500 Subject: [PATCH 4/4] Added README --- src/filesystem/README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 302fc4c4..7a628356 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -10,6 +10,8 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - Search files - Get file metadata +**Note**: The server will only allow operations within directories specified via `args`. + ## API ### Resources @@ -29,7 +31,7 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - Failed reads won't stop the entire operation - **write_file** - - Create new file or overwrite existing + - Create new file or overwrite existing (exercise caution with this) - Inputs: - `path` (string): File location - `content` (string): File content @@ -70,7 +72,18 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - Type (file/directory) - Permissions -## Notes - -- Exercise caution with `write_file`, since it can overwrite an existing file -- File paths can be absolute or relative +- **list_allowed_directories** + - List all directories the server is allowed to access + - No input required + - Returns: + - Directories that this server can read/write from + +## Usage with Claude Desktop +Add this to your `claude_desktop_config.json`: +```json +{ + "mcp-server-filesystem": { + "command": "mcp-server-filesystem", + "args": ["Users/username/Desktop", "Users/username/Desktop", "/path/to/other/allowed/dir"] + } +}