@cequenceai/mcp-stdio
Version:
Cequence MCP stdio bridge - Bridges MCP protocol from stdio to HTTP for Cequence MCP servers
254 lines (215 loc) • 9.73 kB
text/typescript
import { program } from 'commander';
import chalk from 'chalk';
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');
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: string | null = null;
// Helper to send JSON-RPC response
const sendResponse = (response: any) => {
const responseStr = JSON.stringify(response);
process.stdout.write(responseStr + '\n');
};
const sendError = (id: any, code: number, message: string) => {
const error = { jsonrpc: '2.0', id, error: { code, message } };
sendResponse(error);
};
// Make HTTP request to MCP server
const makeHttpRequest = async (mcpRequest: any): Promise<any> => {
return new Promise((resolve, reject) => {
const postData = JSON.stringify(mcpRequest);
const headers: Record<string, string> = {
'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: any) {
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: any) {
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
program.on('command:*', () => {
console.error(chalk.red('Invalid command:'), program.args.join(' '));
console.log(chalk.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) {
program.outputHelp();
}
program.parse(process.argv);