Skip to main content
Glama
mcp-bridge.cjs4.16 kB
#!/usr/bin/env node /** * MCP Bridge for Thunderbird * * Converts stdio MCP protocol to HTTP requests for the Thunderbird MCP extension. * The extension exposes an HTTP endpoint on localhost:8765. */ const http = require('http'); const readline = require('readline'); const THUNDERBIRD_PORT = 8765; const REQUEST_TIMEOUT = 30000; // Ensure stdout doesn't buffer - critical for MCP protocol if (process.stdout._handle?.setBlocking) { process.stdout._handle.setBlocking(true); } let pendingRequests = 0; let stdinClosed = false; function checkExit() { if (stdinClosed && pendingRequests === 0) { process.exit(0); } } // Write with backpressure handling function writeOutput(data) { return new Promise((resolve) => { if (process.stdout.write(data)) { resolve(); } else { process.stdout.once('drain', resolve); } }); } /** * Sanitize JSON response that may contain invalid control characters. * Email bodies often contain raw control chars that break JSON parsing. */ function sanitizeJson(data) { // Remove control chars except \n, \r, \t let sanitized = data.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ''); // Escape raw newlines/carriage returns/tabs that aren't already escaped sanitized = sanitized.replace(/(?<!\\)\r/g, '\\r'); sanitized = sanitized.replace(/(?<!\\)\n/g, '\\n'); sanitized = sanitized.replace(/(?<!\\)\t/g, '\\t'); return sanitized; } async function handleMessage(line) { const message = JSON.parse(line); // Handle MCP protocol methods locally switch (message.method) { case 'initialize': return { jsonrpc: '2.0', id: message.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'thunderbird-mcp', version: '0.1.0' } } }; case 'notifications/initialized': return null; // Notifications don't expect responses case 'notifications/cancelled': return null; // No response needed for notifications default: return forwardToThunderbird(message); } } function forwardToThunderbird(message) { return new Promise((resolve, reject) => { const postData = JSON.stringify(message); const req = http.request({ hostname: 'localhost', port: THUNDERBIRD_PORT, path: '/', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const data = Buffer.concat(chunks).toString('utf8'); try { resolve(JSON.parse(data)); } catch { // Thunderbird may return JSON with invalid control chars in email content try { resolve(JSON.parse(sanitizeJson(data))); } catch (e) { reject(new Error(`Invalid JSON from Thunderbird: ${e.message}`)); } } }); }); req.on('error', (e) => { reject(new Error(`Connection failed: ${e.message}. Is Thunderbird running with the MCP extension?`)); }); req.setTimeout(REQUEST_TIMEOUT, () => { req.destroy(); reject(new Error('Request to Thunderbird timed out')); }); req.write(postData); req.end(); }); } // Process stdin as JSON-RPC messages const rl = readline.createInterface({ input: process.stdin, terminal: false }); rl.on('line', (line) => { if (!line.trim()) return; pendingRequests++; handleMessage(line) .then(async (response) => { if (response !== null) { await writeOutput(JSON.stringify(response) + '\n'); } }) .catch(async (err) => { await writeOutput(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: `Bridge error: ${err.message}` } }) + '\n'); }) .finally(() => { pendingRequests--; checkExit(); }); }); rl.on('close', () => { stdinClosed = true; checkExit(); }); process.on('SIGINT', () => process.exit(0)); process.on('SIGTERM', () => process.exit(0));

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TKasperczyk/thunderbird-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server