#
tokens: 4493/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
/build
/node_modules
```

--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------

```
{
  "scanSettings": {
    "baseBranches": []
  },
  "checkRunSettings": {
    "vulnerableCheckRunConclusionLevel": "failure",
    "displayMode": "diff",
    "useMendCheckNames": true
  },
  "issueSettings": {
    "minSeverityLevel": "LOW",
    "issueType": "DEPENDENCY"
  }
}
```

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

```markdown
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/dazeb-markdown-downloader-badge.jpg)](https://mseep.ai/app/dazeb-markdown-downloader)
[![MseeP Badge](https://mseep.net/pr/dazeb-markdown-downloader-badge.jpg)](https://mseep.ai/app/dazeb-markdown-downloader)

[![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/e85a9805-464e-46bd-a953-ccac0c4a5129)

# Markdown Downloader MCP Server

[![smithery badge](https://smithery.ai/badge/@dazeb/markdown-downloader)](https://smithery.ai/server/@dazeb/markdown-downloader)

## Overview

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.

<a href="https://glama.ai/mcp/servers/jrki7zltg7">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/jrki7zltg7/badge" alt="Markdown Downloader MCP server" />
</a>

## Features

- 🌐 Download webpages as markdown using r.jina.ai
- 📁 Configurable download directory
- 📝 Automatically generates date-stamped filenames
- 🔍 List downloaded markdown files
- 💾 Persistent configuration

## Prerequisites

- Node.js (version 16 or higher)
- npm (Node Package Manager)

## Installation

### Installing via Smithery

To install Markdown Downloader for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@dazeb/markdown-downloader):

```bash
npx -y @smithery/cli install @dazeb/markdown-downloader --client claude
```

### Installing manually

1. Clone the repository:
   ```bash
   git clone https://github.com/your-username/markdown-downloader.git
   cd markdown-downloader
   ```

2. Install dependencies:
   ```bash
   npm install
   ```

3. Build the project:
   ```bash
   npm run build
   ```

## Manually Add Server to Cline/Roo-Cline MCP Settings file

### Linux/macOS
```json
{
  "mcpServers": {
    "markdown-downloader": {
      "command": "node",
      "args": [
        "/home/user/Documents/Cline/MCP/markdown-downloader/build/index.js"
      ],
      "disabled": false,
      "alwaysAllow": [
        "download_markdown",
        "set_download_directory"
      ]
    }
  }
}
```

### Windows
```json
{
  "mcpServers": {
    "markdown-downloader": {
      "command": "node",
      "args": [
        "C:\\Users\\username\\Documents\\Cline\\MCP\\markdown-downloader\\build\\index.js"
      ],
      "disabled": false,
      "alwaysAllow": [
        "download_markdown",
        "set_download_directory"
      ]
    }
  }
}
```

## Tools and Usage

### 1. Set Download Directory

Change the download directory:

```bash
use set_download_directory /path/to/your/local/download/folder
```

- Validates directory exists and is writable
- Persists the configuration for future use

### 2. Download Markdown

Download a webpage as a markdown file:

```bash
use tool download_markdown https://example.com/blog-post
```

- The URL will be prepended with `r.jina.ai`
- Filename format: `{sanitized-url}-{date}.md`
- Saved in the configured download directory

### 3. List Downloaded Files

List all downloaded markdown files:

```bash
use list_downloaded_files
```

### 4. Get Download Directory

Retrieve the current download directory:

```bash
use get_download_directory
```

## Configuration

### Linux/macOS
- Configuration is stored in `~/.config/markdown-downloader/config.json`
- Default download directory: `~/.markdown-downloads`

### Windows
- Configuration is stored in `%APPDATA%\markdown-downloader\config.json`
- Default download directory: `%USERPROFILE%\Documents\markdown-downloads`

## Troubleshooting

- Ensure you have an active internet connection
- Check that the URL is valid and accessible
- Verify write permissions for the download directory

## Security

- The tool uses r.jina.ai to fetch markdown content
- Local files are saved with sanitized filenames
- Configurable download directory allows flexibility

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License. See the LICENSE file for details.

## Disclaimer

This tool is provided as-is. Always review downloaded content for accuracy and appropriateness.

## Support

For issues or feature requests, please open an issue on the GitHub repository.

```

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

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./build"
  },
  "include": ["src/**/*"]
}
```

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

```json
{
  "name": "markdown-downloader",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc && node scripts/postbuild.js",
    "start": "node build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.4",
    "axios": "^1.8.3",
    "fs-extra": "^11.2.0"
  },
  "devDependencies": {
    "@types/fs-extra": "^11.0.4",
    "@types/node": "^20.17.10",
    "typescript": "^5.3.2"
  }
}

```

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

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    {}
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'node',
      args: ['build/index.js']
    })
  exampleConfig: {}

```

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

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

# Create app directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install --ignore-scripts

# Copy source code and other necessary files
COPY . .

# Build the project
RUN npm run build

# Expose port if needed (optional)
# EXPOSE 3000

# Command to run the MCP server
CMD [ "node", "build/index.js" ]

```

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

```javascript
// Cross-platform post-build script
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const buildIndexPath = path.join(__dirname, '..', 'build', 'index.js');

// Only set executable permissions on Unix-like systems
if (process.platform !== 'win32') {
  try {
    fs.chmodSync(buildIndexPath, '755');
    console.log('Set executable permissions on build/index.js');
  } catch (error) {
    console.error('Error setting executable permissions:', error);
  }
} else {
  console.log('Skipping chmod on Windows platform');
}

console.log('Post-build tasks completed');

```

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

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2025-04-XX

### Added
- Windows platform support
- Cross-platform configuration paths
- Platform-specific default download directories
- Updated documentation with Windows-specific instructions

### Changed
- Replaced Unix-specific build script with cross-platform version
- Improved environment variable handling using Node.js os module
- Updated README with platform-specific configuration information

### Security
- Updated axios from 1.7.9 to 1.8.3 to fix CVE-2025-27152 (High severity)

## [1.0.0] - 2025-XX-XX

### Added
- Initial release
- Download webpages as markdown using r.jina.ai
- Configurable download directory
- List downloaded markdown files
- Create subdirectories for organizing downloads

```

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

```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import fs from 'fs-extra';
import path from 'path';
import os from 'os';

// Configuration management
// Use platform-specific paths for configuration
const homedir = os.homedir();
const configBasePath = process.platform === 'win32'
  ? path.join(process.env.APPDATA || homedir, 'markdown-downloader')
  : path.join(homedir, '.config', 'markdown-downloader');
const CONFIG_DIR = configBasePath;
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');

// Default download directory based on platform
const getDefaultDownloadDir = () => {
  return process.platform === 'win32'
    ? path.join(homedir, 'Documents', 'markdown-downloads')
    : path.join(homedir, '.markdown-downloads');
};

interface MarkdownDownloaderConfig {
  downloadDirectory: string;
}

function getConfig(): MarkdownDownloaderConfig {
  try {
    fs.ensureDirSync(CONFIG_DIR);
    if (!fs.existsSync(CONFIG_FILE)) {
      // Default to platform-specific directory if no config exists
      const defaultDownloadDir = getDefaultDownloadDir();
      const defaultConfig: MarkdownDownloaderConfig = {
        downloadDirectory: defaultDownloadDir
      };
      fs.writeJsonSync(CONFIG_FILE, defaultConfig);
      fs.ensureDirSync(defaultConfig.downloadDirectory);
      return defaultConfig;
    }
    return fs.readJsonSync(CONFIG_FILE);
  } catch (error) {
    console.error('Error reading config:', error);
    // Fallback to default
    const defaultDownloadDir = getDefaultDownloadDir();
    return {
      downloadDirectory: defaultDownloadDir
    };
  }
}

function saveConfig(config: MarkdownDownloaderConfig) {
  try {
    fs.ensureDirSync(CONFIG_DIR);
    fs.writeJsonSync(CONFIG_FILE, config);
    fs.ensureDirSync(config.downloadDirectory);
  } catch (error) {
    console.error('Error saving config:', error);
  }
}

function sanitizeFilename(url: string): string {
  // Remove protocol, replace non-alphanumeric chars with dash
  return url
    .replace(/^https?:\/\//, '')
    .replace(/[^a-z0-9]/gi, '-')
    .toLowerCase();
}

function generateFilename(url: string): string {
  const sanitizedUrl = sanitizeFilename(url);
  const datestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
  return `${sanitizedUrl}-${datestamp}.md`;
}

class MarkdownDownloaderServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'markdown-downloader',
        version: '1.0.0',
      },
      {
        capabilities: {
          resources: {},
          tools: {},
        },
      }
    );

    this.setupToolHandlers();

    // Error handling
    this.server.onerror = (serverError: unknown) => console.error('[MCP Error]', serverError);
    process.on('SIGINT', async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private setupToolHandlers(): void {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'download_markdown',
          description: 'Download a webpage as markdown using r.jina.ai',
          inputSchema: {
            type: 'object',
            properties: {
              url: {
                type: 'string',
                description: 'URL of the webpage to download'
              },
              subdirectory: {
                type: 'string',
                description: 'Optional subdirectory to save the file in'
              }
            },
            required: ['url']
          }
        },
        {
          name: 'list_downloaded_files',
          description: 'List all downloaded markdown files',
          inputSchema: {
            type: 'object',
            properties: {
              subdirectory: {
                type: 'string',
                description: 'Optional subdirectory to list files from'
              }
            }
          }
        },
        {
          name: 'set_download_directory',
          description: 'Set the main local download folder for markdown files',
          inputSchema: {
            type: 'object',
            properties: {
              directory: {
                type: 'string',
                description: 'Full path to the download directory'
              }
            },
            required: ['directory']
          }
        },
        {
          name: 'get_download_directory',
          description: 'Get the current download directory',
          inputSchema: {
            type: 'object',
            properties: {}
          }
        },
        {
          name: 'create_subdirectory',
          description: 'Create a new subdirectory in the root download folder',
          inputSchema: {
            type: 'object',
            properties: {
              name: {
                type: 'string',
                description: 'Name of the subdirectory to create'
              }
            },
            required: ['name']
          }
        }
      ]
    }));

    // Tool to download markdown
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      // Download markdown
      if (request.params.name === 'download_markdown') {
        const url = request.params.arguments?.url;
        const subdirectory = request.params.arguments?.subdirectory;

        if (!url || typeof url !== 'string') {
          throw new McpError(
            ErrorCode.InvalidParams,
            'A valid URL must be provided'
          );
        }

        try {
          // Get current download directory
          const config = getConfig();

          // Prepend r.jina.ai to the URL
          const jinaUrl = `https://r.jina.ai/${url}`;

          // Download markdown
          const response = await axios.get(jinaUrl, {
            headers: {
              'Accept': 'text/markdown'
            }
          });

          // Generate filename
          const filename = generateFilename(url);
          let filepath = path.join(config.downloadDirectory, filename);

          // If subdirectory is specified, use it
          if (subdirectory && typeof subdirectory === 'string') {
            filepath = path.join(config.downloadDirectory, subdirectory, filename);
            fs.ensureDirSync(path.dirname(filepath));
          }

          // Save markdown file
          await fs.writeFile(filepath, response.data);

          return {
            content: [
              {
                type: 'text',
                text: `Markdown downloaded and saved as ${filename} in ${path.dirname(filepath)}`
              }
            ]
          };
        } catch (downloadError) {
          console.error('Download error:', downloadError);
          return {
            content: [
              {
                type: 'text',
                text: `Failed to download markdown: ${downloadError instanceof Error ? downloadError.message : 'Unknown error'}`
              }
            ],
            isError: true
          };
        }
      }

      // List downloaded files
      if (request.params.name === 'list_downloaded_files') {
        try {
          const config = getConfig();
          const subdirectory = request.params.arguments?.subdirectory;
          const listDir = subdirectory && typeof subdirectory === 'string'
            ? path.join(config.downloadDirectory, subdirectory)
            : config.downloadDirectory;
          const files = await fs.readdir(listDir);
          return {
            content: [
              {
                type: 'text',
                text: files.join('\n')
              }
            ]
          };
        } catch (listError) {
          const errorMessage = listError instanceof Error ? listError.message : 'Unknown error';
          return {
            content: [
              {
                type: 'text',
                text: `Failed to list files: ${errorMessage}`
              }
            ],
            isError: true
          };
        }
      }

      // Set download directory
      if (request.params.name === 'set_download_directory') {
        const directory = request.params.arguments?.directory;

        if (!directory || typeof directory !== 'string') {
          throw new McpError(
            ErrorCode.InvalidParams,
            'A valid directory path must be provided'
          );
        }

        try {
          // Validate directory exists and is writable
          await fs.access(directory, fs.constants.W_OK);

          // Update and save config
          const config = getConfig();
          config.downloadDirectory = directory;
          saveConfig(config);

          return {
            content: [
              {
                type: 'text',
                text: `Download directory set to: ${directory}`
              }
            ]
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          return {
            content: [
              {
                type: 'text',
                text: `Failed to set download directory: ${errorMessage}`
              }
            ],
            isError: true
          };
        }
      }

      // Get download directory
      if (request.params.name === 'get_download_directory') {
        const config = getConfig();
        return {
          content: [
            {
              type: 'text',
              text: config.downloadDirectory
            }
          ]
        };
      }

      // Create subdirectory
      if (request.params.name === 'create_subdirectory') {
        const subdirectoryName = request.params.arguments?.name;

        if (!subdirectoryName || typeof subdirectoryName !== 'string') {
          throw new McpError(
            ErrorCode.InvalidParams,
            'A valid subdirectory name must be provided'
          );
        }

        try {
          const config = getConfig();
          const newSubdirectoryPath = path.join(config.downloadDirectory, subdirectoryName);

          // Create the subdirectory
          await fs.ensureDir(newSubdirectoryPath);

          return {
            content: [
              {
                type: 'text',
                text: `Subdirectory created: ${newSubdirectoryPath}`
              }
            ]
          };
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          return {
            content: [
              {
                type: 'text',
                text: `Failed to create subdirectory: ${errorMessage}`
              }
            ],
            isError: true
          };
        }
      }

      throw new McpError(
        ErrorCode.MethodNotFound,
        `Unknown tool: ${request.params.name}`
      );
    });
  }

  async run(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Markdown Downloader MCP server running on stdio');
  }
}

const server = new MarkdownDownloaderServer();
server.run().catch((error: Error) => console.error('Server error:', error));
```