UNPKG

@cequenceai/mcp-stdio

Version:

Cequence MCP stdio bridge - Bridges MCP protocol from stdio to HTTP for Cequence MCP servers

230 lines (229 loc) 11.4 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); commander_1.program .name('cequence-mcp-stdio') .description('Cequence MCP stdio bridge - Bridges MCP protocol from stdio to HTTP for Cequence MCP servers') .version('1.0.0'); commander_1.program .command('start') .description('Start stdio-to-HTTP bridge for Cequence MCP gateway') .requiredOption('-u, --url <url>', 'Cequence MCP gateway URL to bridge to') .option('-k, --api-key <key>', 'API key for authentication (if required)') .action(async (options) => { // This creates a proper stdio-to-HTTP bridge for MCP protocol const http = require('http'); const https = require('https'); const { URL } = require('url'); try { const serverUrl = new URL(options.url); const client = serverUrl.protocol === 'https:' ? https : http; // Set up stdio for MCP protocol process.stdin.setEncoding('utf8'); process.stdin.resume(); let buffer = ''; let sessionId = null; // Helper to send JSON-RPC response const sendResponse = (response) => { const responseStr = JSON.stringify(response); process.stdout.write(responseStr + '\n'); }; const sendError = (id, code, message) => { const error = { jsonrpc: '2.0', id, error: { code, message } }; sendResponse(error); }; // Make HTTP request to MCP server const makeHttpRequest = async (mcpRequest) => { return new Promise((resolve, reject) => { const postData = JSON.stringify(mcpRequest); const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'User-Agent': 'Cequence-MCP-stdio/1.0.0' }; // Add API key header if provided if (options.apiKey) { headers['Authorization'] = `Bearer ${options.apiKey}`; headers['X-API-Key'] = options.apiKey; } // Add session ID if we have one if (sessionId) { headers['mcp-session-id'] = sessionId; console.error(`[mcp-stdio] Using session ID: ${sessionId}`); } console.error(`[mcp-stdio] Making fetch request to ${options.url}`); fetch(options.url, { method: 'POST', headers, body: postData }) .then(response => { console.error(`[mcp-stdio] Fetch response status: ${response.status}`); // Extract session ID from response headers const responseSessionId = response.headers.get('mcp-session-id'); if (responseSessionId) { sessionId = responseSessionId; console.error(`[mcp-stdio] Updated session ID: ${sessionId}`); } return response.text(); }) .then(responseText => { console.error(`[mcp-stdio] Fetch response data: ${responseText}`); if (responseText.trim()) { let jsonResponse; // Check if this is SSE format (Server-Sent Events) if (responseText.startsWith('event:') || responseText.includes('data:')) { // Parse SSE format const lines = responseText.split('\n'); let dataLine = ''; for (const line of lines) { if (line.startsWith('data:')) { dataLine = line.substring(5).trim(); // Remove 'data:' prefix break; } } if (dataLine) { jsonResponse = JSON.parse(dataLine); } else { reject(new Error('No data field found in SSE response')); return; } } else { // Parse regular JSON response jsonResponse = JSON.parse(responseText); } // Check if this is a valid JSON-RPC response if (jsonResponse.jsonrpc === '2.0' && (jsonResponse.result !== undefined || jsonResponse.error !== undefined)) { resolve(jsonResponse); } else { resolve(jsonResponse); } } else { reject(new Error('Empty response from server')); } }) .catch(error => { console.error(`[mcp-stdio] Fetch error: ${error.message}`); reject(error); }); }); }; // Process incoming stdio messages process.stdin.on('data', async (chunk) => { buffer += chunk; // Process complete JSON-RPC messages (each ends with newline) let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (line) { try { const request = JSON.parse(line); // Debug log the incoming request console.error(`[mcp-stdio] Received request: ${JSON.stringify(request)}`); // Validate JSON-RPC request structure if (!request.jsonrpc || request.jsonrpc !== '2.0') { console.error(`[mcp-stdio] Invalid jsonrpc field: ${request.jsonrpc}`); sendError(request.id, -32600, 'Invalid Request: Missing or invalid jsonrpc field'); continue; } if (!request.method) { console.error(`[mcp-stdio] Missing method field`); sendError(request.id, -32600, 'Invalid Request: Missing method field'); continue; } // Check if this is a notification (no id field) or a request (has id field) const isNotification = request.id === undefined; console.error(`[mcp-stdio] Processing ${isNotification ? 'notification' : 'request'} for method: ${request.method}`); // Forward request to HTTP MCP server and await response try { const httpResponse = await makeHttpRequest(request); console.error(`[mcp-stdio] HTTP response: ${JSON.stringify(httpResponse)}`); // Only send response for requests, not notifications if (!isNotification) { // Handle different response types from HTTP server let mcpResponse; if (httpResponse.error) { // HTTP server returned an error mcpResponse = { jsonrpc: '2.0', id: request.id, error: httpResponse.error }; } else if (httpResponse.result !== undefined) { // HTTP server returned a result mcpResponse = { jsonrpc: '2.0', id: request.id, result: httpResponse.result }; } else { // HTTP server returned raw data - wrap it as result mcpResponse = { jsonrpc: '2.0', id: request.id, result: httpResponse }; } console.error(`[mcp-stdio] Sending response: ${JSON.stringify(mcpResponse)}`); sendResponse(mcpResponse); } } catch (httpError) { console.error(`[mcp-stdio] HTTP error: ${httpError.message}`); // Handle HTTP errors if (!isNotification) { const errorMessage = httpError?.message || 'Unknown HTTP error'; sendError(request.id, -32603, `Server error: ${errorMessage}`); } } } catch (parseError) { console.error(`[mcp-stdio] Parse error: ${parseError.message}`); //Say invalid JSON sendError(null, -32700, `Parse error: ${parseError?.message || 'Invalid JSON'}`); } } } }); process.stdin.on('end', () => { process.exit(0); }); process.stdin.on('error', (error) => { console.error('Stdin error:', error); process.exit(1); }); // Log that the bridge is running for debugging console.error(`[mcp-stdio] Starting stdio-to-HTTP bridge`); console.error(`[mcp-stdio] Bridging to: ${options.url}`); console.error(`[mcp-stdio] API Key: ${options.apiKey ? 'configured' : 'not configured'}`); console.error(`[mcp-stdio] Ready - listening on stdin...`); } catch (error) { console.error('Bridge server error:', error); process.exit(1); } }); // Handle unknown commands commander_1.program.on('command:*', () => { console.error(chalk_1.default.red('Invalid command:'), commander_1.program.args.join(' ')); console.log(chalk_1.default.cyan('Available commands:')); console.log(' start - Start stdio-to-HTTP bridge for Cequence MCP gateway'); process.exit(1); }); // Show help when no command is provided if (!process.argv.slice(2).length) { commander_1.program.outputHelp(); } commander_1.program.parse(process.argv);