#
tokens: 6300/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .whitesource
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── postbuild.js
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.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 | }
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/dazeb-markdown-downloader-badge.jpg)](https://mseep.ai/app/dazeb-markdown-downloader)
  2 | [![MseeP Badge](https://mseep.net/pr/dazeb-markdown-downloader-badge.jpg)](https://mseep.ai/app/dazeb-markdown-downloader)
  3 | 
  4 | [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/e85a9805-464e-46bd-a953-ccac0c4a5129)
  5 | 
  6 | # Markdown Downloader MCP Server
  7 | 
  8 | [![smithery badge](https://smithery.ai/badge/@dazeb/markdown-downloader)](https://smithery.ai/server/@dazeb/markdown-downloader)
  9 | 
 10 | ## Overview
 11 | 
 12 | 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.
 13 | 
 14 | <a href="https://glama.ai/mcp/servers/jrki7zltg7">
 15 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/jrki7zltg7/badge" alt="Markdown Downloader MCP server" />
 16 | </a>
 17 | 
 18 | ## Features
 19 | 
 20 | - 🌐 Download webpages as markdown using r.jina.ai
 21 | - 📁 Configurable download directory
 22 | - 📝 Automatically generates date-stamped filenames
 23 | - 🔍 List downloaded markdown files
 24 | - 💾 Persistent configuration
 25 | 
 26 | ## Prerequisites
 27 | 
 28 | - Node.js (version 16 or higher)
 29 | - npm (Node Package Manager)
 30 | 
 31 | ## Installation
 32 | 
 33 | ### Installing via Smithery
 34 | 
 35 | To install Markdown Downloader for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@dazeb/markdown-downloader):
 36 | 
 37 | ```bash
 38 | npx -y @smithery/cli install @dazeb/markdown-downloader --client claude
 39 | ```
 40 | 
 41 | ### Installing manually
 42 | 
 43 | 1. Clone the repository:
 44 |    ```bash
 45 |    git clone https://github.com/your-username/markdown-downloader.git
 46 |    cd markdown-downloader
 47 |    ```
 48 | 
 49 | 2. Install dependencies:
 50 |    ```bash
 51 |    npm install
 52 |    ```
 53 | 
 54 | 3. Build the project:
 55 |    ```bash
 56 |    npm run build
 57 |    ```
 58 | 
 59 | ## Manually Add Server to Cline/Roo-Cline MCP Settings file
 60 | 
 61 | ### Linux/macOS
 62 | ```json
 63 | {
 64 |   "mcpServers": {
 65 |     "markdown-downloader": {
 66 |       "command": "node",
 67 |       "args": [
 68 |         "/home/user/Documents/Cline/MCP/markdown-downloader/build/index.js"
 69 |       ],
 70 |       "disabled": false,
 71 |       "alwaysAllow": [
 72 |         "download_markdown",
 73 |         "set_download_directory"
 74 |       ]
 75 |     }
 76 |   }
 77 | }
 78 | ```
 79 | 
 80 | ### Windows
 81 | ```json
 82 | {
 83 |   "mcpServers": {
 84 |     "markdown-downloader": {
 85 |       "command": "node",
 86 |       "args": [
 87 |         "C:\\Users\\username\\Documents\\Cline\\MCP\\markdown-downloader\\build\\index.js"
 88 |       ],
 89 |       "disabled": false,
 90 |       "alwaysAllow": [
 91 |         "download_markdown",
 92 |         "set_download_directory"
 93 |       ]
 94 |     }
 95 |   }
 96 | }
 97 | ```
 98 | 
 99 | ## Tools and Usage
100 | 
101 | ### 1. Set Download Directory
102 | 
103 | Change the download directory:
104 | 
105 | ```bash
106 | use set_download_directory /path/to/your/local/download/folder
107 | ```
108 | 
109 | - Validates directory exists and is writable
110 | - Persists the configuration for future use
111 | 
112 | ### 2. Download Markdown
113 | 
114 | Download a webpage as a markdown file:
115 | 
116 | ```bash
117 | use tool download_markdown https://example.com/blog-post
118 | ```
119 | 
120 | - The URL will be prepended with `r.jina.ai`
121 | - Filename format: `{sanitized-url}-{date}.md`
122 | - Saved in the configured download directory
123 | 
124 | ### 3. List Downloaded Files
125 | 
126 | List all downloaded markdown files:
127 | 
128 | ```bash
129 | use list_downloaded_files
130 | ```
131 | 
132 | ### 4. Get Download Directory
133 | 
134 | Retrieve the current download directory:
135 | 
136 | ```bash
137 | use get_download_directory
138 | ```
139 | 
140 | ## Configuration
141 | 
142 | ### Linux/macOS
143 | - Configuration is stored in `~/.config/markdown-downloader/config.json`
144 | - Default download directory: `~/.markdown-downloads`
145 | 
146 | ### Windows
147 | - Configuration is stored in `%APPDATA%\markdown-downloader\config.json`
148 | - Default download directory: `%USERPROFILE%\Documents\markdown-downloads`
149 | 
150 | ## Troubleshooting
151 | 
152 | - Ensure you have an active internet connection
153 | - Check that the URL is valid and accessible
154 | - Verify write permissions for the download directory
155 | 
156 | ## Security
157 | 
158 | - The tool uses r.jina.ai to fetch markdown content
159 | - Local files are saved with sanitized filenames
160 | - Configurable download directory allows flexibility
161 | 
162 | ## Contributing
163 | 
164 | Contributions are welcome! Please feel free to submit a Pull Request.
165 | 
166 | ## License
167 | 
168 | This project is licensed under the MIT License. See the LICENSE file for details.
169 | 
170 | ## Disclaimer
171 | 
172 | This tool is provided as-is. Always review downloaded content for accuracy and appropriateness.
173 | 
174 | ## Support
175 | 
176 | For issues or feature requests, please open an issue on the GitHub repository.
177 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```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 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```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 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```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 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```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 | 
```

--------------------------------------------------------------------------------
/scripts/postbuild.js:
--------------------------------------------------------------------------------

```javascript
 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 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 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 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  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<void> {
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));
```