├── .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 | [](https://mseep.ai/app/dazeb-markdown-downloader)
2 |
3 | [](https://mseep.ai/app/e85a9805-464e-46bd-a953-ccac0c4a5129)
4 |
5 | # Markdown Downloader MCP Server
6 |
7 | [](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 |
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 | }
--------------------------------------------------------------------------------