mcp-http-bridge
Version:
Generic bridge client for connecting MCP clients to remote MCP servers via HTTP supporting session based environment variables
1,003 lines (845 loc) โข 33.6 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 { 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';
import { randomUUID } from 'node:crypto';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
// Setup file logging (only if debug enabled)
const logFile = DEBUG_ENABLED ? process.env.MCP_BRIDGE_LOG_FILE || './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', 'MCP_AUTH_URL', 'MCP_TOKEN_URL',
'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
* Supports OAuth authentication with direct token storage
*/
class CursorHTTPBridge {
constructor() {
this.stdioServer = null;
this.stdioTransport = null;
this.mcpSessionId = null; // MCP protocol session ID (assigned by server)
this.authSessionId = this.generateAuthSessionId(); // Our auth session ID
this.authCompleted = false;
this.accessToken = null; // Store the actual OAuth token
this.refreshToken = null; // Store refresh token if available
this.tokenExpiry = null; // Store token expiry time
this.authCancelled = false; // Track if auth was cancelled
this.shuttingDown = false; // Track if bridge is shutting down
}
/**
* Generate a unique auth session ID for OAuth flow
*/
generateAuthSessionId() {
const timestamp = Date.now();
const uuid = randomUUID().split('-')[0]; // Use first part of UUID
return `mcp-bridge-${timestamp}-${uuid}`;
}
/**
* 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 headers including access token for authenticated requests
* Combines environment variables with the OAuth access token
*/
getHeadersWithAuth() {
const headers = this.envToHeaders();
// Add access token as MCP_AUTH_TOKEN environment variable header
if (this.accessToken) {
headers['X-MCP-MCP-AUTH-TOKEN'] = this.accessToken;
log(`๐ซ Added access token as X-MCP-MCP-AUTH-TOKEN header`);
}
return headers;
}
/**
* Build complete HTTP request headers for MCP requests
* @param {boolean} requireAuth - Whether to include authentication headers
* @param {boolean} isInitialization - Whether this is an initialization request
* @returns {Object} Complete headers object ready for HTTP request
*/
buildRequestHeaders(requireAuth = true, isInitialization = false) {
// Get environment variable headers based on auth requirement
let envHeaders;
if (requireAuth && this.accessToken) {
envHeaders = this.getHeadersWithAuth();
log(`๐ Using authenticated headers with access token`);
} else {
envHeaders = this.envToHeaders();
log(`๐ Using basic headers for ${requireAuth ? 'unauthenticated' : 'discovery'} request`);
}
// Build base headers
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
'Accept-Language': '*',
'Sec-Fetch-Mode': 'cors',
'User-Agent': 'node',
...envHeaders
};
// Add OAuth Authorization header if we have a token and auth is required
if (requireAuth && this.accessToken) {
headers['Authorization'] = `Bearer ${this.accessToken}`;
log(`๐ Added OAuth Bearer token to Authorization header`);
}
// Add MCP session ID if we have one (for protocol)
if (!isInitialization && this.mcpSessionId) {
headers['mcp-session-id'] = this.mcpSessionId;
log(`๐ Added MCP session ID: ${this.mcpSessionId}`);
} else if (isInitialization) {
log(`๐ Initialization request - no MCP session ID yet`);
}
return headers;
}
/**
* Get list of environment variables to pass as headers
* Only passes variables explicitly listed in MCP_PASS_VARS
*/
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);
}
// No MCP_PASS_VARS defined - pass nothing
log('๐ No MCP_PASS_VARS defined - passing no environment variables');
log('๐ก Set MCP_PASS_VARS to explicitly define which variables to pass as headers');
return [];
}
/**
* Ensure authentication is completed (lazy authentication)
* Only authenticates when actually needed (e.g., for tool calls)
*/
async ensureAuthenticated() {
const authUrl = process.env.MCP_AUTH_URL;
const authType = process.env.MCP_AUTH_TYPE;
if (authUrl && authType === 'oauth' && !this.authCompleted) {
log(`๐ Authentication required for tool call. Auth Session ID: ${this.authSessionId}`);
// Check if we already have a valid token
if (this.isTokenValid()) {
log(`โ
Existing valid token found`);
this.authCompleted = true;
return;
}
// Initiate OAuth flow
await this.initiateOAuthFlow(authUrl);
// Wait for authentication completion and token retrieval
await this.waitForAuthCompletion();
this.authCompleted = true;
log(`โ
Authentication completed for session ${this.authSessionId}`);
} else if (!authUrl) {
log(`๐ No MCP_AUTH_URL configured - skipping authentication`);
this.authCompleted = true;
} else if (authType === 'pat') {
log(`๐ Using PAT authentication - skipping OAuth flow`);
this.authCompleted = true;
} else if (authType && authType !== 'oauth') {
log(`๐ Using ${authType} authentication - skipping OAuth flow`);
this.authCompleted = true;
} else {
log(`โ
Authentication already completed for session ${this.authSessionId}`);
}
}
/**
* Check if authentication is required and initiate OAuth flow
* @deprecated Use ensureAuthenticated() instead for lazy authentication
*/
async checkAndInitiateAuth() {
const authUrl = process.env.MCP_AUTH_URL;
const authType = process.env.MCP_AUTH_TYPE;
if (authUrl && authType === 'oauth' && !this.authCompleted) {
log(`๐ Authentication required. Auth Session ID: ${this.authSessionId}`);
// Check if we already have a valid token
if (this.isTokenValid()) {
log(`โ
Existing valid token found`);
this.authCompleted = true;
return;
}
// Initiate OAuth flow
await this.initiateOAuthFlow(authUrl);
// Wait for authentication completion and token retrieval
await this.waitForAuthCompletion();
this.authCompleted = true;
log(`โ
Authentication completed for session ${this.authSessionId}`);
} else if (!authUrl) {
log(`๐ No MCP_AUTH_URL configured - skipping authentication`);
this.authCompleted = true;
} else if (authType === 'pat') {
log(`๐ Using PAT authentication - skipping OAuth flow`);
this.authCompleted = true;
} else if (authType && authType !== 'oauth') {
log(`๐ Using ${authType} authentication - skipping OAuth flow`);
this.authCompleted = true;
} else {
log(`โ
Authentication already completed for session ${this.authSessionId}`);
}
}
/**
* Check if current token is valid and not expired
*/
isTokenValid() {
if (!this.accessToken) {
return false;
}
if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
log(`โฐ Token expired at ${new Date(this.tokenExpiry).toISOString()}`);
return false;
}
return true;
}
/**
* Extract service name from MCP_AUTH_URL
*/
getServiceFromAuthUrl() {
const authUrl = process.env.MCP_AUTH_URL;
if (!authUrl) {
return 'devops'; // fallback
}
// Extract service from URL like: /api/auth/mcp-session/devops
const match = authUrl.match(/\/mcp-session\/([^/?]+)/);
return match ? match[1] : 'devops';
}
/**
* Initiate OAuth flow by opening browser
*/
async initiateOAuthFlow(authUrl) {
const fullAuthUrl = `${authUrl}?session_id=${this.authSessionId}`;
log(`๐ Opening browser for authentication...`);
log(`๐ Auth Session ID: ${this.authSessionId}`);
log(`๐ Auth URL: ${fullAuthUrl}`);
try {
// Try to open browser automatically
await this.openBrowser(fullAuthUrl);
log(`โ
Browser opened successfully`);
} catch (error) {
log(`โ ๏ธ Could not open browser automatically: ${error.message}`);
console.error(`\n๐ AUTHENTICATION REQUIRED`);
console.error(`๐ Auth Session ID: ${this.authSessionId}`);
console.error(`๐ Please visit: ${fullAuthUrl}`);
console.error(`โณ Waiting for authentication completion...\n`);
}
}
/**
* Open browser with the auth URL
*/
async openBrowser(url) {
const platform = process.platform;
let command;
switch (platform) {
case 'darwin': // macOS
command = `open "${url}"`;
break;
case 'win32': // Windows
command = `start "" "${url}"`;
break;
default: // Linux and others
command = `xdg-open "${url}"`;
break;
}
await execAsync(command);
}
/**
* Wait for authentication completion and retrieve token
*/
async waitForAuthCompletion() {
const tokenEndpoint = process.env.MCP_TOKEN_URL;
if (!tokenEndpoint) {
throw new Error('MCP_TOKEN_URL required for authentication');
}
const maxAttempts = 60; // Wait up to 5 minutes (60 * 5 seconds)
const timeoutMs = 5 * 60 * 1000; // 5 minutes total timeout
const startTime = Date.now();
let attempts = 0;
log(`โณ Waiting for authentication completion...`);
log(`โฐ Timeout in ${maxAttempts * 5} seconds. Press Ctrl+C to cancel.`);
// Set up cancellation handlers
const cancelHandler = () => {
log(`๐ Authentication cancelled by user`);
this.authCancelled = true;
};
process.on('SIGINT', cancelHandler);
process.on('SIGTERM', cancelHandler);
try {
while (attempts < maxAttempts && !this.authCancelled && !this.shuttingDown) {
// Check total elapsed time
const elapsed = Date.now() - startTime;
if (elapsed >= timeoutMs) {
log(`โฐ Authentication timeout after ${Math.round(elapsed / 1000)}s`);
break;
}
try {
// Poll for token using the auth session ID
const tokenResponse = await this.retrieveToken(tokenEndpoint);
if (tokenResponse) {
log(`โ
Authentication completed successfully!`);
return;
}
// Wait 5 seconds before next attempt, but check for cancellation during wait
await this.interruptibleWait(5000);
attempts++;
if (attempts % 6 === 0) { // Log every 30 seconds
const remainingTime = Math.max(0, Math.round((timeoutMs - elapsed) / 1000));
log(`โณ Still waiting for authentication... (${Math.round(elapsed / 1000)}s elapsed, ${remainingTime}s remaining)`);
log(`๐ก Complete OAuth in browser or press Ctrl+C to cancel`);
}
} catch (error) {
// Handle specific error types
if (error.message.includes('fetch') || error.message.includes('ECONNREFUSED')) {
log(`๐ Connection error: ${error.message}`);
log(`โ ๏ธ Token endpoint may be unreachable. Check MCP_TOKEN_URL configuration.`);
} else {
log(`๐ Token retrieval error: ${error.message}`);
}
await this.interruptibleWait(5000);
attempts++;
}
}
// Handle different exit conditions
if (this.authCancelled) {
throw new Error('Authentication cancelled by user');
} else if (this.shuttingDown) {
throw new Error('Bridge is shutting down');
} else {
throw new Error(`Authentication timeout after ${Math.round((Date.now() - startTime) / 1000)} seconds. Please try again.`);
}
} finally {
// Clean up event listeners
process.removeListener('SIGINT', cancelHandler);
process.removeListener('SIGTERM', cancelHandler);
}
}
/**
* Wait for specified time but allow interruption
*/
async interruptibleWait(ms) {
return new Promise((resolve) => {
const checkInterval = 100; // Check every 100ms
let elapsed = 0;
const interval = setInterval(() => {
elapsed += checkInterval;
if (this.authCancelled || this.shuttingDown || elapsed >= ms) {
clearInterval(interval);
resolve();
}
}, checkInterval);
});
}
/**
* Retrieve token from the token endpoint
*/
async retrieveToken(tokenEndpoint) {
try {
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
grant_type: 'authorization_code',
session_id: this.authSessionId
})
});
if (!response.ok) {
if (response.status === 404 || response.status === 401) {
// Token not ready yet, continue polling
return null;
}
throw new Error(`Token endpoint returned ${response.status}: ${response.statusText}`);
}
const tokenData = await response.json();
if (tokenData.access_token) {
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
// Calculate token expiry
if (tokenData.expires_in) {
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
log(`๐ซ Token expires at: ${new Date(this.tokenExpiry).toISOString()}`);
}
log(`๐ซ Access token retrieved successfully`);
log(`๐ Refresh token available: ${!!this.refreshToken}`);
return tokenData;
}
return null;
} catch (error) {
log(`โ Token retrieval failed: ${error.message}`);
throw error;
}
}
/**
* Refresh the access token using refresh token
*/
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const tokenEndpoint = process.env.MCP_TOKEN_URL;
if (!tokenEndpoint) {
throw new Error('MCP_TOKEN_URL required for token refresh');
}
try {
log(`๐ Refreshing access token...`);
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: this.refreshToken
})
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
}
const tokenData = await response.json();
if (tokenData.access_token) {
this.accessToken = tokenData.access_token;
// Update refresh token if provided
if (tokenData.refresh_token) {
this.refreshToken = tokenData.refresh_token;
}
// Calculate new token expiry
if (tokenData.expires_in) {
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
log(`๐ซ Refreshed token expires at: ${new Date(this.tokenExpiry).toISOString()}`);
}
log(`โ
Access token refreshed successfully`);
return true;
}
throw new Error('No access token in refresh response');
} catch (error) {
log(`โ Token refresh failed: ${error.message}`);
// Clear tokens on refresh failure
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
throw error;
}
}
/**
* 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}`);
log(`๐ Auth Session ID: ${this.authSessionId}`);
this.serverUrl = serverUrl;
log(`โ
Custom HTTP bridge configured`);
// 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)
}, false, false); // Don't require auth for tool discovery
return response.result || response;
});
// Call tool
this.stdioServer.setRequestHandler(CallToolRequestSchema, async (request) => {
// Ensure authentication is completed before making tool calls
await this.ensureAuthenticated();
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)
}, false, false); // Don't require auth for resource discovery
return response.result || response;
});
// Read resource
this.stdioServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
// Ensure authentication is completed before reading resources
await this.ensureAuthenticated();
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)
}, false, false); // Don't require auth for prompt discovery
// Ensure we return the expected format: { prompts: [...] }
const result = response.result || response;
if (result && typeof result === 'object' && Array.isArray(result.prompts)) {
return result;
} else {
// Fallback to empty prompts if format is unexpected
log(`โ ๏ธ Unexpected prompts response format: ${JSON.stringify(result)}`);
return { prompts: [] };
}
});
// Get prompt
this.stdioServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
// Ensure authentication is completed before getting prompts
await this.ensureAuthenticated();
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 and OAuth authentication
*/
async makeHttpRequest(jsonrpcRequest, isInitialization = false, requireAuth = true) {
try {
log(`๐ Making HTTP request: ${jsonrpcRequest.method}`);
// Check if token needs refresh before making request (only if auth is required)
if (!isInitialization && requireAuth && this.accessToken && this.tokenExpiry && Date.now() >= this.tokenExpiry - 60000) {
// Refresh token if it expires within 1 minute
try {
await this.refreshAccessToken();
} catch (error) {
log(`โ ๏ธ Token refresh failed, continuing with existing token: ${error.message}`);
}
}
// Build request headers using helper function
const requestHeaders = this.buildRequestHeaders(requireAuth, isInitialization);
const response = await fetch(this.serverUrl, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(jsonrpcRequest)
});
// Extract MCP session ID from response headers if this is initialization
if (isInitialization) {
log(`๐ Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
const mcpSessionId = response.headers.get('mcp-session-id');
if (mcpSessionId) {
this.mcpSessionId = mcpSessionId;
log(`๐ Server assigned MCP session ID: ${this.mcpSessionId}`);
} else {
log(`โ ๏ธ No MCP session ID found in response headers - server may not use sessions`);
}
}
// Handle authentication errors (only if auth is required)
if (response.status === 401 && requireAuth && this.accessToken) {
log(`๐ Received 401 Unauthorized, attempting token refresh...`);
try {
await this.refreshAccessToken();
// Retry the request with new token
log(`๐ Retrying request with refreshed token...`);
// Rebuild headers with new access token using helper function
const retryHeaders = this.buildRequestHeaders(requireAuth, isInitialization);
const retryResponse = await fetch(this.serverUrl, {
method: 'POST',
headers: retryHeaders,
body: JSON.stringify(jsonrpcRequest)
});
if (retryResponse.ok) {
log(`โ
Request succeeded after token refresh`);
return await this.parseResponse(retryResponse, jsonrpcRequest.method);
} else {
throw new Error(`Request failed after token refresh: ${retryResponse.status} ${retryResponse.statusText}`);
}
} catch (refreshError) {
log(`โ Token refresh failed: ${refreshError.message}`);
throw new Error(`Authentication failed and token refresh unsuccessful: ${refreshError.message}`);
}
}
if (!response.ok) {
const errorText = await response.text();
log(`โ HTTP ${response.status}: ${errorText.substring(0, 200)}`);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await this.parseResponse(response, jsonrpcRequest.method);
} catch (error) {
log(`โ HTTP request failed: ${error.message}`);
throw error;
}
}
/**
* Parse HTTP response (JSON or SSE)
*/
async parseResponse(response, method) {
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: ${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): ${method}`);
return { jsonrpc: '2.0', result: null };
}
try {
const result = JSON.parse(responseText);
log(`โ
HTTP request successful: ${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 (method && method.startsWith('notifications/')) {
log(`โ
Notification sent successfully (non-JSON response): ${method}`);
return { jsonrpc: '2.0', result: null };
}
throw new Error(`Invalid JSON response: ${parseError.message}`);
}
}
}
/**
* Cleanup on exit
*/
async cleanup() {
if (this.shuttingDown) {
return; // Already cleaning up
}
this.shuttingDown = true;
log('๐งน Cleaning up bridge...');
// Cancel any ongoing authentication
if (!this.authCompleted) {
this.authCancelled = true;
log('๐ Cancelling ongoing authentication...');
}
// Close stdio server
if (this.stdioServer) {
try {
await this.stdioServer.close();
log('โ
Stdio server closed');
} catch (error) {
log(`โ ๏ธ Error closing stdio server: ${error.message}`);
}
}
// Clear tokens
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
log('โ
Bridge cleanup completed');
}
}
// Handle graceful shutdown
const bridge = new CursorHTTPBridge();
let isShuttingDown = false;
const gracefulShutdown = async (signal) => {
if (isShuttingDown) {
log(`๐ฅ Force exit on second ${signal}`);
process.exit(1);
}
isShuttingDown = true;
log(`๐ก Received ${signal}, shutting down gracefully...`);
try {
await bridge.cleanup();
log(`โ
Graceful shutdown completed`);
process.exit(0);
} catch (error) {
log(`โ Error during shutdown: ${error.message}`);
process.exit(1);
}
};
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
log(`๐ฅ Uncaught exception: ${error.message}`);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
log(`๐ฅ Unhandled rejection at: ${promise}, reason: ${reason}`);
gracefulShutdown('unhandledRejection');
});
// Start the bridge
bridge.start().catch(async (error) => {
log(`โ Failed to start bridge: ${error.message}`);
await bridge.cleanup();
process.exit(1);
});