mcp-http-bridge
Version:
Generic bridge client for connecting MCP clients to remote MCP servers via HTTP supporting session based environment variables
501 lines (423 loc) โข 16.4 kB
JavaScript
// Check if debug logging is enabled
const DEBUG_ENABLED = process.env.MCP_BRIDGE_DEBUG === '1' || process.env.MCP_BRIDGE_DEBUG === 'true';
// IMMEDIATE DEBUG - Log to stderr before any imports (only if debug enabled)
if (DEBUG_ENABLED) {
console.error('๐ฅ BRIDGE DEBUG: Bridge starting up...');
console.error('๐ฅ BRIDGE DEBUG: Working directory:', process.cwd());
console.error('๐ฅ BRIDGE DEBUG: MCP_SERVER_URL:', process.env.MCP_SERVER_URL || 'NOT SET');
console.error('๐ฅ BRIDGE DEBUG: MCP_PASS_VARS:', process.env.MCP_PASS_VARS || 'NOT SET');
}
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs';
// Setup file logging (only if debug enabled)
const logFile = DEBUG_ENABLED ? 'C:\\mcp-http-bridge\\bridge-debug.log' : null;
const logStream = DEBUG_ENABLED ? fs.createWriteStream(logFile, { flags: 'a' }) : null;
function log(message) {
if (!DEBUG_ENABLED) return; // Skip logging if debug is disabled
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
// Log to stderr (for console)
console.error(message);
// Log to file
if (logStream) {
try {
logStream.write(logMessage);
} catch (error) {
console.error('๐ฅ BRIDGE DEBUG: Failed to write to log file:', error.message);
}
}
}
// Log all environment variables that might be relevant
const relevantEnvVars = [
'MCP_SERVER_URL', 'MCP_PASS_VARS',
'SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'
];
log('๐ Environment variables check:');
for (const envVar of relevantEnvVars) {
const value = process.env[envVar];
if (value) {
const logValue = envVar.includes('PASSWORD') ? '***' : value;
log(` ${envVar}: ${logValue}`);
} else {
log(` ${envVar}: NOT SET`);
}
}
// Log total number of environment variables
log(`๐ Total environment variables: ${Object.keys(process.env).length}`);
// Log first few environment variables to see what's available
const envKeys = Object.keys(process.env).slice(0, 10);
log(`๐ค First 10 env var names: ${envKeys.join(', ')}`);
log('๐ฌ Starting main bridge logic...');
/**
* Generic Cursor-to-HTTP bridge for MCP servers
* Converts stdio interface (for Cursor) to HTTP requests (for remote MCP servers)
* Automatically passes environment variables as X-MCP-* headers
*/
class CursorHTTPBridge {
constructor() {
this.httpClient = null;
this.httpTransport = null;
this.stdioServer = null;
this.stdioTransport = null;
this.sessionId = null; // Let server assign session ID
}
/**
* Convert environment variables to HTTP headers
* Uses MCP_PASS_VARS for explicit control, falls back to auto-detection
*/
envToHeaders() {
const headers = {};
// Get variables to pass (explicit list or auto-detected)
const varsToPass = this.getVariablesToPass();
log(`๐ง Variables to pass: ${varsToPass.join(', ')}`);
// Convert selected variables to headers
for (const key of varsToPass) {
const value = process.env[key];
if (value !== undefined) {
const headerName = `X-MCP-${key.replace(/_/g, '-')}`;
headers[headerName] = value;
// Log without sensitive values
const logValue = key.includes('PASSWORD') || key.includes('SECRET') || key.includes('KEY') ? '***' : value;
log(` ${key} -> ${headerName}: ${logValue}`);
} else {
log(` ${key}: NOT FOUND in environment`);
}
}
log(`๐ Final headers to send: ${Object.keys(headers).join(', ')}`);
return headers;
}
/**
* Get list of environment variables to pass as headers
* Priority: MCP_PASS_VARS (explicit) > auto-detection (fallback)
*/
getVariablesToPass() {
const passVars = process.env.MCP_PASS_VARS;
if (passVars) {
// Explicit list provided - use only these variables
log(`๐ฏ Using explicit variable list: ${passVars}`);
return passVars.split(',').map(v => v.trim()).filter(v => v.length > 0);
}
// Fallback: auto-detect non-system variables
log('๐ Auto-detecting variables (consider using MCP_PASS_VARS for explicit control)');
return this.autoDetectConfiguredVariables();
}
/**
* Auto-detect which variables were likely configured for the MCP server
* Excludes system variables and bridge-specific variables
*/
autoDetectConfiguredVariables() {
const configuredVars = [];
// System variables that should never be passed
const systemVars = [
'PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'PWD', 'OLDPWD',
'NODE_ENV', 'NODE_PATH', 'NODE_OPTIONS', 'INIT_CWD',
'LANG', 'LC_', 'TZ', 'TMPDIR', 'TEMP', 'TMP',
'PROCESSOR_', 'NUMBER_OF_PROCESSORS', 'OS', 'COMPUTERNAME',
'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMFILES',
'SYSTEMROOT', 'WINDIR', 'COMSPEC'
];
// Bridge-specific variables
const bridgeVars = ['MCP_SERVER_URL', 'MCP_PASS_VARS'];
// Check all environment variables
for (const [key, value] of Object.entries(process.env)) {
// Skip system variables
if (systemVars.some(sysVar => key.startsWith(sysVar))) {
continue;
}
// Skip npm variables
if (key.startsWith('npm_')) {
continue;
}
// Skip bridge-specific variables
if (bridgeVars.includes(key)) {
continue;
}
// Everything else gets passed (potentially risky with many variables)
configuredVars.push(key);
}
return configuredVars;
}
/**
* Start the bridge
*/
async start() {
try {
// Get configuration
const serverUrl = process.env.MCP_SERVER_URL;
if (!serverUrl) {
throw new Error('MCP_SERVER_URL environment variable is required');
}
log(`๐ Starting Cursor-HTTP bridge to: ${serverUrl}`);
// Convert environment variables to headers
const headers = this.envToHeaders();
log(`๐ง Passing ${Object.keys(headers).length} environment variables as headers`);
// Store headers for use in requests
this.customHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
'Accept-Language': '*',
'Sec-Fetch-Mode': 'cors',
'User-Agent': 'node',
...headers
};
this.serverUrl = serverUrl;
log(`โ
Custom HTTP bridge configured`);
log(`๐ง Will send headers with every request`);
// Create stdio server for Cursor (skip MCP SDK HTTP client)
await this.createStdioServer();
} catch (error) {
log('โ Failed to start bridge: ' + error.message);
process.exit(1);
}
}
/**
* Create stdio server that Cursor can connect to
*/
async createStdioServer() {
// Create a server that bridges requests to the HTTP client
this.stdioServer = new Server(
{
name: 'cursor-http-bridge',
version: '1.0.0'
},
{
capabilities: {
tools: {},
resources: {},
prompts: {}
}
}
);
// Initialize connection to remote server first
await this.initializeRemoteConnection();
// Bridge all MCP requests to the HTTP client
this.setupRequestHandlers();
// Create stdio transport
this.stdioTransport = new StdioServerTransport();
await this.stdioServer.connect(this.stdioTransport);
log('๐ Bridge ready - Cursor can now connect via stdio');
}
/**
* Initialize connection to remote MCP server
*/
async initializeRemoteConnection() {
try {
log('๐ค Initializing connection to remote MCP server...');
const initResponse = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'cursor-http-bridge',
version: '1.0.0'
}
},
id: 1
}, true); // Pass true to indicate this is initialization
log('โ
Remote MCP server initialized successfully');
log(`๐ Server info: ${initResponse.result?.serverInfo?.name || 'Unknown'}`);
// Store server capabilities
this.serverCapabilities = initResponse.result?.capabilities || {};
// Send initialized notification to complete handshake
await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {}
}, false); // Not initialization, use session ID
log('โ
Initialized notification sent to server');
} catch (error) {
log(`โ Failed to initialize remote MCP server: ${error.message}`);
throw error;
}
}
/**
* Setup request handlers that forward to HTTP client
*/
setupRequestHandlers() {
// List tools
this.stdioServer.setRequestHandler(ListToolsRequestSchema, async () => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'tools/list',
params: {},
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
// Call tool
this.stdioServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'tools/call',
params: request.params,
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
// List resources
this.stdioServer.setRequestHandler(ListResourcesRequestSchema, async () => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'resources/list',
params: {},
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
// Read resource
this.stdioServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'resources/read',
params: request.params,
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
// List prompts
this.stdioServer.setRequestHandler(ListPromptsRequestSchema, async () => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'prompts/list',
params: {},
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
// Get prompt
this.stdioServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
const response = await this.makeHttpRequest({
jsonrpc: '2.0',
method: 'prompts/get',
params: request.params,
id: Math.floor(Math.random() * 1000000)
});
return response.result || response;
});
}
/**
* Make HTTP request to MCP server with custom headers
*/
async makeHttpRequest(jsonrpcRequest, isInitialization = false) {
try {
log(`๐ Making HTTP request: ${jsonrpcRequest.method}`);
// Prepare headers
const requestHeaders = { ...this.customHeaders };
// Only add session ID for non-initialization requests
if (!isInitialization && this.sessionId) {
requestHeaders['mcp-session-id'] = this.sessionId;
log(`๐ Using session ID: ${this.sessionId}`);
} else if (isInitialization) {
log(`๐ Initialization request - no session ID, server will assign one`);
} else {
log(`๐ No session ID available yet`);
}
const response = await fetch(this.serverUrl, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(jsonrpcRequest)
});
// Extract session ID from response headers if this is initialization
if (isInitialization) {
log(`๐ Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
const sessionId = response.headers.get('mcp-session-id');
if (sessionId) {
this.sessionId = sessionId;
log(`๐ Server assigned session ID: ${this.sessionId}`);
} else {
log(`โ ๏ธ No session ID found in response headers - server may not use sessions`);
}
}
if (!response.ok) {
const errorText = await response.text();
log(`โ HTTP ${response.status}: ${errorText.substring(0, 200)}`);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/event-stream')) {
// Handle Server-Sent Events response
const text = await response.text();
log(`๐ก Received SSE response: ${text.substring(0, 100)}...`);
// Parse SSE format: "event: message\ndata: {...}\n\n"
const lines = text.split('\n');
let jsonData = null;
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.substring(6); // Remove "data: " prefix
try {
jsonData = JSON.parse(dataStr);
break;
} catch (e) {
// Continue looking for valid JSON
}
}
}
if (jsonData) {
log(`โ
HTTP request successful: ${jsonrpcRequest.method}`);
return jsonData;
} else {
throw new Error('Could not parse SSE response');
}
} else {
// Handle regular JSON response
const responseText = await response.text();
// Handle empty responses (common for notifications)
if (!responseText.trim()) {
log(`โ
HTTP request successful (empty response): ${jsonrpcRequest.method}`);
return { jsonrpc: '2.0', result: null };
}
try {
const result = JSON.parse(responseText);
log(`โ
HTTP request successful: ${jsonrpcRequest.method}`);
return result;
} catch (parseError) {
log(`โ ๏ธ Failed to parse JSON response: ${responseText.substring(0, 100)}`);
// For notifications, empty or non-JSON responses are often acceptable
if (jsonrpcRequest.method && jsonrpcRequest.method.startsWith('notifications/')) {
log(`โ
Notification sent successfully (non-JSON response): ${jsonrpcRequest.method}`);
return { jsonrpc: '2.0', result: null };
}
throw new Error(`Invalid JSON response: ${parseError.message}`);
}
}
} catch (error) {
log(`โ HTTP request failed: ${error.message}`);
throw error;
}
}
/**
* Cleanup on exit
*/
async cleanup() {
log('๐งน Cleaning up bridge...');
if (this.stdioServer) {
await this.stdioServer.close();
}
}
}
// Handle graceful shutdown
const bridge = new CursorHTTPBridge();
process.on('SIGINT', async () => {
await bridge.cleanup();
process.exit(0);
});
process.on('SIGTERM', async () => {
await bridge.cleanup();
process.exit(0);
});
// Start the bridge
bridge.start().catch(console.error);