Markdown Downloader

by dazeb
Verified
  • src
#!/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'; // Configuration management const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.config', 'markdown-downloader'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); interface MarkdownDownloaderConfig { downloadDirectory: string; } function getConfig(): MarkdownDownloaderConfig { try { fs.ensureDirSync(CONFIG_DIR); if (!fs.existsSync(CONFIG_FILE)) { // Default to home directory if no config exists const defaultConfig: MarkdownDownloaderConfig = { downloadDirectory: path.join(process.env.HOME || '/tmp', '.markdown-downloads') }; 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 return { downloadDirectory: path.join(process.env.HOME || '/tmp', '.markdown-downloads') }; } } 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));