├── .gitignore ├── .whitesource ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts └── postbuild.js ├── smithery.yaml ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff", 8 | "useMendCheckNames": true 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "LOW", 12 | "issueType": "DEPENDENCY" 13 | } 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.0] - 2025-04-XX 9 | 10 | ### Added 11 | - Windows platform support 12 | - Cross-platform configuration paths 13 | - Platform-specific default download directories 14 | - Updated documentation with Windows-specific instructions 15 | 16 | ### Changed 17 | - Replaced Unix-specific build script with cross-platform version 18 | - Improved environment variable handling using Node.js os module 19 | - Updated README with platform-specific configuration information 20 | 21 | ### Security 22 | - Updated axios from 1.7.9 to 1.8.3 to fix CVE-2025-27152 (High severity) 23 | 24 | ## [1.0.0] - 2025-XX-XX 25 | 26 | ### Added 27 | - Initial release 28 | - Download webpages as markdown using r.jina.ai 29 | - Configurable download directory 30 | - List downloaded markdown files 31 | - Create subdirectories for organizing downloads 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install --ignore-scripts 12 | 13 | # Copy source code and other necessary files 14 | COPY . . 15 | 16 | # Build the project 17 | RUN npm run build 18 | 19 | # Expose port if needed (optional) 20 | # EXPOSE 3000 21 | 22 | # Command to run the MCP server 23 | CMD [ "node", "build/index.js" ] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Markdown Downloader Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MseeP Badge](https://mseep.net/pr/dazeb-markdown-downloader-badge.jpg)](https://mseep.ai/app/dazeb-markdown-downloader) 2 | 3 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/e85a9805-464e-46bd-a953-ccac0c4a5129) 4 | 5 | # Markdown Downloader MCP Server 6 | 7 | [![smithery badge](https://smithery.ai/badge/@dazeb/markdown-downloader)](https://smithery.ai/server/@dazeb/markdown-downloader) 8 | 9 | ## Overview 10 | 11 | Markdown Downloader is a powerful MCP (Model Context Protocol) server that allows you to download webpages as markdown files with ease. Leveraging the r.jina.ai service, this tool provides a seamless way to convert web content into markdown format. 12 | 13 | 14 | Markdown Downloader MCP server 15 | 16 | 17 | ## Features 18 | 19 | - 🌐 Download webpages as markdown using r.jina.ai 20 | - 📁 Configurable download directory 21 | - 📝 Automatically generates date-stamped filenames 22 | - 🔍 List downloaded markdown files 23 | - 💾 Persistent configuration 24 | 25 | ## Prerequisites 26 | 27 | - Node.js (version 16 or higher) 28 | - npm (Node Package Manager) 29 | 30 | ## Installation 31 | 32 | ### Installing via Smithery 33 | 34 | To install Markdown Downloader for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@dazeb/markdown-downloader): 35 | 36 | ```bash 37 | npx -y @smithery/cli install @dazeb/markdown-downloader --client claude 38 | ``` 39 | 40 | ### Installing manually 41 | 42 | 1. Clone the repository: 43 | ```bash 44 | git clone https://github.com/your-username/markdown-downloader.git 45 | cd markdown-downloader 46 | ``` 47 | 48 | 2. Install dependencies: 49 | ```bash 50 | npm install 51 | ``` 52 | 53 | 3. Build the project: 54 | ```bash 55 | npm run build 56 | ``` 57 | 58 | ## Manually Add Server to Cline/Roo-Cline MCP Settings file 59 | 60 | ### Linux/macOS 61 | ```json 62 | { 63 | "mcpServers": { 64 | "markdown-downloader": { 65 | "command": "node", 66 | "args": [ 67 | "/home/user/Documents/Cline/MCP/markdown-downloader/build/index.js" 68 | ], 69 | "disabled": false, 70 | "alwaysAllow": [ 71 | "download_markdown", 72 | "set_download_directory" 73 | ] 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | ### Windows 80 | ```json 81 | { 82 | "mcpServers": { 83 | "markdown-downloader": { 84 | "command": "node", 85 | "args": [ 86 | "C:\\Users\\username\\Documents\\Cline\\MCP\\markdown-downloader\\build\\index.js" 87 | ], 88 | "disabled": false, 89 | "alwaysAllow": [ 90 | "download_markdown", 91 | "set_download_directory" 92 | ] 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | ## Tools and Usage 99 | 100 | ### 1. Set Download Directory 101 | 102 | Change the download directory: 103 | 104 | ```bash 105 | use set_download_directory /path/to/your/local/download/folder 106 | ``` 107 | 108 | - Validates directory exists and is writable 109 | - Persists the configuration for future use 110 | 111 | ### 2. Download Markdown 112 | 113 | Download a webpage as a markdown file: 114 | 115 | ```bash 116 | use tool download_markdown https://example.com/blog-post 117 | ``` 118 | 119 | - The URL will be prepended with `r.jina.ai` 120 | - Filename format: `{sanitized-url}-{date}.md` 121 | - Saved in the configured download directory 122 | 123 | ### 3. List Downloaded Files 124 | 125 | List all downloaded markdown files: 126 | 127 | ```bash 128 | use list_downloaded_files 129 | ``` 130 | 131 | ### 4. Get Download Directory 132 | 133 | Retrieve the current download directory: 134 | 135 | ```bash 136 | use get_download_directory 137 | ``` 138 | 139 | ## Configuration 140 | 141 | ### Linux/macOS 142 | - Configuration is stored in `~/.config/markdown-downloader/config.json` 143 | - Default download directory: `~/.markdown-downloads` 144 | 145 | ### Windows 146 | - Configuration is stored in `%APPDATA%\markdown-downloader\config.json` 147 | - Default download directory: `%USERPROFILE%\Documents\markdown-downloads` 148 | 149 | ## Troubleshooting 150 | 151 | - Ensure you have an active internet connection 152 | - Check that the URL is valid and accessible 153 | - Verify write permissions for the download directory 154 | 155 | ## Security 156 | 157 | - The tool uses r.jina.ai to fetch markdown content 158 | - Local files are saved with sanitized filenames 159 | - Configurable download directory allows flexibility 160 | 161 | ## Contributing 162 | 163 | Contributions are welcome! Please feel free to submit a Pull Request. 164 | 165 | ## License 166 | 167 | This project is licensed under the MIT License. See the LICENSE file for details. 168 | 169 | ## Disclaimer 170 | 171 | This tool is provided as-is. Always review downloaded content for accuracy and appropriateness. 172 | 173 | ## Support 174 | 175 | For issues or feature requests, please open an issue on the GitHub repository. 176 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-downloader", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "markdown-downloader", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.0.4", 12 | "axios": "^1.8.3", 13 | "fs-extra": "^11.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/fs-extra": "^11.0.4", 17 | "@types/node": "^20.17.10", 18 | "typescript": "^5.3.2" 19 | } 20 | }, 21 | "node_modules/@modelcontextprotocol/sdk": { 22 | "version": "1.0.4", 23 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.4.tgz", 24 | "integrity": "sha512-C+jw1lF6HSGzs7EZpzHbXfzz9rj9him4BaoumlTciW/IDDgIpweF/qiCWKlP02QKg5PPcgY6xY2WCt5y2tpYow==", 25 | "license": "MIT", 26 | "dependencies": { 27 | "content-type": "^1.0.5", 28 | "raw-body": "^3.0.0", 29 | "zod": "^3.23.8" 30 | } 31 | }, 32 | "node_modules/@types/fs-extra": { 33 | "version": "11.0.4", 34 | "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", 35 | "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", 36 | "dev": true, 37 | "license": "MIT", 38 | "dependencies": { 39 | "@types/jsonfile": "*", 40 | "@types/node": "*" 41 | } 42 | }, 43 | "node_modules/@types/jsonfile": { 44 | "version": "6.1.4", 45 | "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", 46 | "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", 47 | "dev": true, 48 | "license": "MIT", 49 | "dependencies": { 50 | "@types/node": "*" 51 | } 52 | }, 53 | "node_modules/@types/node": { 54 | "version": "20.17.10", 55 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", 56 | "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", 57 | "dev": true, 58 | "license": "MIT", 59 | "dependencies": { 60 | "undici-types": "~6.19.2" 61 | } 62 | }, 63 | "node_modules/asynckit": { 64 | "version": "0.4.0", 65 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 66 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 67 | "license": "MIT" 68 | }, 69 | "node_modules/axios": { 70 | "version": "1.9.0", 71 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", 72 | "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", 73 | "license": "MIT", 74 | "dependencies": { 75 | "follow-redirects": "^1.15.6", 76 | "form-data": "^4.0.0", 77 | "proxy-from-env": "^1.1.0" 78 | } 79 | }, 80 | "node_modules/bytes": { 81 | "version": "3.1.2", 82 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 83 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 84 | "license": "MIT", 85 | "engines": { 86 | "node": ">= 0.8" 87 | } 88 | }, 89 | "node_modules/combined-stream": { 90 | "version": "1.0.8", 91 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 92 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 93 | "license": "MIT", 94 | "dependencies": { 95 | "delayed-stream": "~1.0.0" 96 | }, 97 | "engines": { 98 | "node": ">= 0.8" 99 | } 100 | }, 101 | "node_modules/content-type": { 102 | "version": "1.0.5", 103 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 104 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 105 | "license": "MIT", 106 | "engines": { 107 | "node": ">= 0.6" 108 | } 109 | }, 110 | "node_modules/delayed-stream": { 111 | "version": "1.0.0", 112 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 113 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 114 | "license": "MIT", 115 | "engines": { 116 | "node": ">=0.4.0" 117 | } 118 | }, 119 | "node_modules/depd": { 120 | "version": "2.0.0", 121 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 122 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 123 | "license": "MIT", 124 | "engines": { 125 | "node": ">= 0.8" 126 | } 127 | }, 128 | "node_modules/follow-redirects": { 129 | "version": "1.15.9", 130 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 131 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 132 | "funding": [ 133 | { 134 | "type": "individual", 135 | "url": "https://github.com/sponsors/RubenVerborgh" 136 | } 137 | ], 138 | "license": "MIT", 139 | "engines": { 140 | "node": ">=4.0" 141 | }, 142 | "peerDependenciesMeta": { 143 | "debug": { 144 | "optional": true 145 | } 146 | } 147 | }, 148 | "node_modules/form-data": { 149 | "version": "4.0.1", 150 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", 151 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 152 | "license": "MIT", 153 | "dependencies": { 154 | "asynckit": "^0.4.0", 155 | "combined-stream": "^1.0.8", 156 | "mime-types": "^2.1.12" 157 | }, 158 | "engines": { 159 | "node": ">= 6" 160 | } 161 | }, 162 | "node_modules/fs-extra": { 163 | "version": "11.2.0", 164 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", 165 | "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", 166 | "license": "MIT", 167 | "dependencies": { 168 | "graceful-fs": "^4.2.0", 169 | "jsonfile": "^6.0.1", 170 | "universalify": "^2.0.0" 171 | }, 172 | "engines": { 173 | "node": ">=14.14" 174 | } 175 | }, 176 | "node_modules/graceful-fs": { 177 | "version": "4.2.11", 178 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 179 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 180 | "license": "ISC" 181 | }, 182 | "node_modules/http-errors": { 183 | "version": "2.0.0", 184 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 185 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 186 | "license": "MIT", 187 | "dependencies": { 188 | "depd": "2.0.0", 189 | "inherits": "2.0.4", 190 | "setprototypeof": "1.2.0", 191 | "statuses": "2.0.1", 192 | "toidentifier": "1.0.1" 193 | }, 194 | "engines": { 195 | "node": ">= 0.8" 196 | } 197 | }, 198 | "node_modules/iconv-lite": { 199 | "version": "0.6.3", 200 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 201 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 202 | "license": "MIT", 203 | "dependencies": { 204 | "safer-buffer": ">= 2.1.2 < 3.0.0" 205 | }, 206 | "engines": { 207 | "node": ">=0.10.0" 208 | } 209 | }, 210 | "node_modules/inherits": { 211 | "version": "2.0.4", 212 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 213 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 214 | "license": "ISC" 215 | }, 216 | "node_modules/jsonfile": { 217 | "version": "6.1.0", 218 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 219 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 220 | "license": "MIT", 221 | "dependencies": { 222 | "universalify": "^2.0.0" 223 | }, 224 | "optionalDependencies": { 225 | "graceful-fs": "^4.1.6" 226 | } 227 | }, 228 | "node_modules/mime-db": { 229 | "version": "1.52.0", 230 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 231 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 232 | "license": "MIT", 233 | "engines": { 234 | "node": ">= 0.6" 235 | } 236 | }, 237 | "node_modules/mime-types": { 238 | "version": "2.1.35", 239 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 240 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 241 | "license": "MIT", 242 | "dependencies": { 243 | "mime-db": "1.52.0" 244 | }, 245 | "engines": { 246 | "node": ">= 0.6" 247 | } 248 | }, 249 | "node_modules/proxy-from-env": { 250 | "version": "1.1.0", 251 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 252 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 253 | "license": "MIT" 254 | }, 255 | "node_modules/raw-body": { 256 | "version": "3.0.0", 257 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 258 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 259 | "license": "MIT", 260 | "dependencies": { 261 | "bytes": "3.1.2", 262 | "http-errors": "2.0.0", 263 | "iconv-lite": "0.6.3", 264 | "unpipe": "1.0.0" 265 | }, 266 | "engines": { 267 | "node": ">= 0.8" 268 | } 269 | }, 270 | "node_modules/safer-buffer": { 271 | "version": "2.1.2", 272 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 273 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 274 | "license": "MIT" 275 | }, 276 | "node_modules/setprototypeof": { 277 | "version": "1.2.0", 278 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 279 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 280 | "license": "ISC" 281 | }, 282 | "node_modules/statuses": { 283 | "version": "2.0.1", 284 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 285 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 286 | "license": "MIT", 287 | "engines": { 288 | "node": ">= 0.8" 289 | } 290 | }, 291 | "node_modules/toidentifier": { 292 | "version": "1.0.1", 293 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 294 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 295 | "license": "MIT", 296 | "engines": { 297 | "node": ">=0.6" 298 | } 299 | }, 300 | "node_modules/typescript": { 301 | "version": "5.7.2", 302 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 303 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 304 | "dev": true, 305 | "license": "Apache-2.0", 306 | "bin": { 307 | "tsc": "bin/tsc", 308 | "tsserver": "bin/tsserver" 309 | }, 310 | "engines": { 311 | "node": ">=14.17" 312 | } 313 | }, 314 | "node_modules/undici-types": { 315 | "version": "6.19.8", 316 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 317 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 318 | "dev": true, 319 | "license": "MIT" 320 | }, 321 | "node_modules/universalify": { 322 | "version": "2.0.1", 323 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", 324 | "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", 325 | "license": "MIT", 326 | "engines": { 327 | "node": ">= 10.0.0" 328 | } 329 | }, 330 | "node_modules/unpipe": { 331 | "version": "1.0.0", 332 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 333 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 334 | "license": "MIT", 335 | "engines": { 336 | "node": ">= 0.8" 337 | } 338 | }, 339 | "node_modules/zod": { 340 | "version": "3.24.1", 341 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 342 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 343 | "license": "MIT", 344 | "funding": { 345 | "url": "https://github.com/sponsors/colinhacks" 346 | } 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-downloader", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "build": "tsc && node scripts/postbuild.js", 7 | "start": "node build/index.js" 8 | }, 9 | "dependencies": { 10 | "@modelcontextprotocol/sdk": "^1.0.4", 11 | "axios": "^1.8.3", 12 | "fs-extra": "^11.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/fs-extra": "^11.0.4", 16 | "@types/node": "^20.17.10", 17 | "typescript": "^5.3.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | // Cross-platform post-build script 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const buildIndexPath = path.join(__dirname, '..', 'build', 'index.js'); 9 | 10 | // Only set executable permissions on Unix-like systems 11 | if (process.platform !== 'win32') { 12 | try { 13 | fs.chmodSync(buildIndexPath, '755'); 14 | console.log('Set executable permissions on build/index.js'); 15 | } catch (error) { 16 | console.error('Error setting executable permissions:', error); 17 | } 18 | } else { 19 | console.log('Skipping chmod on Windows platform'); 20 | } 21 | 22 | console.log('Post-build tasks completed'); 23 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | {} 8 | commandFunction: 9 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 10 | |- 11 | (config) => ({ 12 | command: 'node', 13 | args: ['build/index.js'] 14 | }) 15 | exampleConfig: {} 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | import axios from 'axios'; 11 | import fs from 'fs-extra'; 12 | import path from 'path'; 13 | import os from 'os'; 14 | 15 | // Configuration management 16 | // Use platform-specific paths for configuration 17 | const homedir = os.homedir(); 18 | const configBasePath = process.platform === 'win32' 19 | ? path.join(process.env.APPDATA || homedir, 'markdown-downloader') 20 | : path.join(homedir, '.config', 'markdown-downloader'); 21 | const CONFIG_DIR = configBasePath; 22 | const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); 23 | 24 | // Default download directory based on platform 25 | const getDefaultDownloadDir = () => { 26 | return process.platform === 'win32' 27 | ? path.join(homedir, 'Documents', 'markdown-downloads') 28 | : path.join(homedir, '.markdown-downloads'); 29 | }; 30 | 31 | interface MarkdownDownloaderConfig { 32 | downloadDirectory: string; 33 | } 34 | 35 | function getConfig(): MarkdownDownloaderConfig { 36 | try { 37 | fs.ensureDirSync(CONFIG_DIR); 38 | if (!fs.existsSync(CONFIG_FILE)) { 39 | // Default to platform-specific directory if no config exists 40 | const defaultDownloadDir = getDefaultDownloadDir(); 41 | const defaultConfig: MarkdownDownloaderConfig = { 42 | downloadDirectory: defaultDownloadDir 43 | }; 44 | fs.writeJsonSync(CONFIG_FILE, defaultConfig); 45 | fs.ensureDirSync(defaultConfig.downloadDirectory); 46 | return defaultConfig; 47 | } 48 | return fs.readJsonSync(CONFIG_FILE); 49 | } catch (error) { 50 | console.error('Error reading config:', error); 51 | // Fallback to default 52 | const defaultDownloadDir = getDefaultDownloadDir(); 53 | return { 54 | downloadDirectory: defaultDownloadDir 55 | }; 56 | } 57 | } 58 | 59 | function saveConfig(config: MarkdownDownloaderConfig) { 60 | try { 61 | fs.ensureDirSync(CONFIG_DIR); 62 | fs.writeJsonSync(CONFIG_FILE, config); 63 | fs.ensureDirSync(config.downloadDirectory); 64 | } catch (error) { 65 | console.error('Error saving config:', error); 66 | } 67 | } 68 | 69 | function sanitizeFilename(url: string): string { 70 | // Remove protocol, replace non-alphanumeric chars with dash 71 | return url 72 | .replace(/^https?:\/\//, '') 73 | .replace(/[^a-z0-9]/gi, '-') 74 | .toLowerCase(); 75 | } 76 | 77 | function generateFilename(url: string): string { 78 | const sanitizedUrl = sanitizeFilename(url); 79 | const datestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); 80 | return `${sanitizedUrl}-${datestamp}.md`; 81 | } 82 | 83 | class MarkdownDownloaderServer { 84 | private server: Server; 85 | 86 | constructor() { 87 | this.server = new Server( 88 | { 89 | name: 'markdown-downloader', 90 | version: '1.0.0', 91 | }, 92 | { 93 | capabilities: { 94 | resources: {}, 95 | tools: {}, 96 | }, 97 | } 98 | ); 99 | 100 | this.setupToolHandlers(); 101 | 102 | // Error handling 103 | this.server.onerror = (serverError: unknown) => console.error('[MCP Error]', serverError); 104 | process.on('SIGINT', async () => { 105 | await this.server.close(); 106 | process.exit(0); 107 | }); 108 | } 109 | 110 | private setupToolHandlers(): void { 111 | // List available tools 112 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 113 | tools: [ 114 | { 115 | name: 'download_markdown', 116 | description: 'Download a webpage as markdown using r.jina.ai', 117 | inputSchema: { 118 | type: 'object', 119 | properties: { 120 | url: { 121 | type: 'string', 122 | description: 'URL of the webpage to download' 123 | }, 124 | subdirectory: { 125 | type: 'string', 126 | description: 'Optional subdirectory to save the file in' 127 | } 128 | }, 129 | required: ['url'] 130 | } 131 | }, 132 | { 133 | name: 'list_downloaded_files', 134 | description: 'List all downloaded markdown files', 135 | inputSchema: { 136 | type: 'object', 137 | properties: { 138 | subdirectory: { 139 | type: 'string', 140 | description: 'Optional subdirectory to list files from' 141 | } 142 | } 143 | } 144 | }, 145 | { 146 | name: 'set_download_directory', 147 | description: 'Set the main local download folder for markdown files', 148 | inputSchema: { 149 | type: 'object', 150 | properties: { 151 | directory: { 152 | type: 'string', 153 | description: 'Full path to the download directory' 154 | } 155 | }, 156 | required: ['directory'] 157 | } 158 | }, 159 | { 160 | name: 'get_download_directory', 161 | description: 'Get the current download directory', 162 | inputSchema: { 163 | type: 'object', 164 | properties: {} 165 | } 166 | }, 167 | { 168 | name: 'create_subdirectory', 169 | description: 'Create a new subdirectory in the root download folder', 170 | inputSchema: { 171 | type: 'object', 172 | properties: { 173 | name: { 174 | type: 'string', 175 | description: 'Name of the subdirectory to create' 176 | } 177 | }, 178 | required: ['name'] 179 | } 180 | } 181 | ] 182 | })); 183 | 184 | // Tool to download markdown 185 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 186 | // Download markdown 187 | if (request.params.name === 'download_markdown') { 188 | const url = request.params.arguments?.url; 189 | const subdirectory = request.params.arguments?.subdirectory; 190 | 191 | if (!url || typeof url !== 'string') { 192 | throw new McpError( 193 | ErrorCode.InvalidParams, 194 | 'A valid URL must be provided' 195 | ); 196 | } 197 | 198 | try { 199 | // Get current download directory 200 | const config = getConfig(); 201 | 202 | // Prepend r.jina.ai to the URL 203 | const jinaUrl = `https://r.jina.ai/${url}`; 204 | 205 | // Download markdown 206 | const response = await axios.get(jinaUrl, { 207 | headers: { 208 | 'Accept': 'text/markdown' 209 | } 210 | }); 211 | 212 | // Generate filename 213 | const filename = generateFilename(url); 214 | let filepath = path.join(config.downloadDirectory, filename); 215 | 216 | // If subdirectory is specified, use it 217 | if (subdirectory && typeof subdirectory === 'string') { 218 | filepath = path.join(config.downloadDirectory, subdirectory, filename); 219 | fs.ensureDirSync(path.dirname(filepath)); 220 | } 221 | 222 | // Save markdown file 223 | await fs.writeFile(filepath, response.data); 224 | 225 | return { 226 | content: [ 227 | { 228 | type: 'text', 229 | text: `Markdown downloaded and saved as ${filename} in ${path.dirname(filepath)}` 230 | } 231 | ] 232 | }; 233 | } catch (downloadError) { 234 | console.error('Download error:', downloadError); 235 | return { 236 | content: [ 237 | { 238 | type: 'text', 239 | text: `Failed to download markdown: ${downloadError instanceof Error ? downloadError.message : 'Unknown error'}` 240 | } 241 | ], 242 | isError: true 243 | }; 244 | } 245 | } 246 | 247 | // List downloaded files 248 | if (request.params.name === 'list_downloaded_files') { 249 | try { 250 | const config = getConfig(); 251 | const subdirectory = request.params.arguments?.subdirectory; 252 | const listDir = subdirectory && typeof subdirectory === 'string' 253 | ? path.join(config.downloadDirectory, subdirectory) 254 | : config.downloadDirectory; 255 | const files = await fs.readdir(listDir); 256 | return { 257 | content: [ 258 | { 259 | type: 'text', 260 | text: files.join('\n') 261 | } 262 | ] 263 | }; 264 | } catch (listError) { 265 | const errorMessage = listError instanceof Error ? listError.message : 'Unknown error'; 266 | return { 267 | content: [ 268 | { 269 | type: 'text', 270 | text: `Failed to list files: ${errorMessage}` 271 | } 272 | ], 273 | isError: true 274 | }; 275 | } 276 | } 277 | 278 | // Set download directory 279 | if (request.params.name === 'set_download_directory') { 280 | const directory = request.params.arguments?.directory; 281 | 282 | if (!directory || typeof directory !== 'string') { 283 | throw new McpError( 284 | ErrorCode.InvalidParams, 285 | 'A valid directory path must be provided' 286 | ); 287 | } 288 | 289 | try { 290 | // Validate directory exists and is writable 291 | await fs.access(directory, fs.constants.W_OK); 292 | 293 | // Update and save config 294 | const config = getConfig(); 295 | config.downloadDirectory = directory; 296 | saveConfig(config); 297 | 298 | return { 299 | content: [ 300 | { 301 | type: 'text', 302 | text: `Download directory set to: ${directory}` 303 | } 304 | ] 305 | }; 306 | } catch (error) { 307 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 308 | return { 309 | content: [ 310 | { 311 | type: 'text', 312 | text: `Failed to set download directory: ${errorMessage}` 313 | } 314 | ], 315 | isError: true 316 | }; 317 | } 318 | } 319 | 320 | // Get download directory 321 | if (request.params.name === 'get_download_directory') { 322 | const config = getConfig(); 323 | return { 324 | content: [ 325 | { 326 | type: 'text', 327 | text: config.downloadDirectory 328 | } 329 | ] 330 | }; 331 | } 332 | 333 | // Create subdirectory 334 | if (request.params.name === 'create_subdirectory') { 335 | const subdirectoryName = request.params.arguments?.name; 336 | 337 | if (!subdirectoryName || typeof subdirectoryName !== 'string') { 338 | throw new McpError( 339 | ErrorCode.InvalidParams, 340 | 'A valid subdirectory name must be provided' 341 | ); 342 | } 343 | 344 | try { 345 | const config = getConfig(); 346 | const newSubdirectoryPath = path.join(config.downloadDirectory, subdirectoryName); 347 | 348 | // Create the subdirectory 349 | await fs.ensureDir(newSubdirectoryPath); 350 | 351 | return { 352 | content: [ 353 | { 354 | type: 'text', 355 | text: `Subdirectory created: ${newSubdirectoryPath}` 356 | } 357 | ] 358 | }; 359 | } catch (error) { 360 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 361 | return { 362 | content: [ 363 | { 364 | type: 'text', 365 | text: `Failed to create subdirectory: ${errorMessage}` 366 | } 367 | ], 368 | isError: true 369 | }; 370 | } 371 | } 372 | 373 | throw new McpError( 374 | ErrorCode.MethodNotFound, 375 | `Unknown tool: ${request.params.name}` 376 | ); 377 | }); 378 | } 379 | 380 | async run(): Promise { 381 | const transport = new StdioServerTransport(); 382 | await this.server.connect(transport); 383 | console.error('Markdown Downloader MCP server running on stdio'); 384 | } 385 | } 386 | 387 | const server = new MarkdownDownloaderServer(); 388 | server.run().catch((error: Error) => console.error('Server error:', error)); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "./build" 12 | }, 13 | "include": ["src/**/*"] 14 | } --------------------------------------------------------------------------------