# 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
[](https://mseep.ai/app/dazeb-markdown-downloader)
[](https://mseep.ai/app/dazeb-markdown-downloader)
[](https://mseep.ai/app/e85a9805-464e-46bd-a953-ccac0c4a5129)
# Markdown Downloader MCP Server
[](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));
```