#!/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));