diff --git a/mcp-inspector.json b/mcp-inspector.json new file mode 100644 index 0000000..e52b270 --- /dev/null +++ b/mcp-inspector.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "interactive-brokers": { + "type": "sse", + "url": "http://localhost:8123/mcp" + } + } +} diff --git a/src/ib-client.ts b/src/ib-client.ts index 133d750..5ce79e3 100644 --- a/src/ib-client.ts +++ b/src/ib-client.ts @@ -337,8 +337,11 @@ export class IBClient { const conid = contract.conid; // Get market data snapshot + // Using corrected field IDs based on IB Client Portal API documentation: + // 31=Last Price, 70=Day High, 71=Day Low, 82=Change, 83=Change%, + // 84=Bid, 85=Ask Size, 86=Ask, 87=Volume, 88=Bid Size const response = await this.client.get( - `/iserver/marketdata/snapshot?conids=${conid}&fields=31,84,86,87,88,85,70,71,72,73,74,75,76,77,78` + `/iserver/marketdata/snapshot?conids=${conid}&fields=31,70,71,82,83,84,85,86,87,88` ); return { diff --git a/src/index-http.ts b/src/index-http.ts new file mode 100644 index 0000000..e71e0a7 --- /dev/null +++ b/src/index-http.ts @@ -0,0 +1,102 @@ +import express from "express"; +import cors from "cors"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { createIBMCPServer } from "./server.js"; + +try { + const PORT = process.env.PORT || process.env.MCP_PORT || 8000; + + console.log(`🚀 Starting Interactive Brokers MCP Server in HTTP/SSE mode on port ${PORT}`); + + const app = express(); + app.use(cors()); + app.use(express.json()); + + // Store active SSE transports + const transports: { [sessionId: string]: SSEServerTransport } = {}; + + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'ok', + service: 'interactive-brokers-mcp', + transport: 'http/sse' + }); + }); + + // SSE endpoint - establishes the server-to-client event stream + app.get('/mcp', async (req, res) => { + console.log('📡 New SSE connection request received'); + + try { + // Create SSE transport + const transport = new SSEServerTransport('/mcp/messages', res); + const sessionId = transport.sessionId; + transports[sessionId] = transport; + + console.log(`✅ SSE transport created with session ID: ${sessionId}`); + + // Create a new MCP server instance for this connection + const server = createIBMCPServer({ config: {} }); + + // Handle transport closure + res.on("close", () => { + console.log(`🔌 SSE connection closed for session: ${sessionId}`); + delete transports[sessionId]; + }); + + // Connect the server to the transport + await server.connect(transport); + console.log(`🔗 MCP server connected to SSE transport: ${sessionId}`); + } catch (error) { + console.error('❌ Failed to connect MCP server to SSE transport:', error); + if (!res.headersSent) { + res.status(500).send('Internal server error'); + } + } + }); + + // Message endpoint - receives client-to-server messages + app.post('/mcp/messages', async (req, res) => { + const sessionId = req.query.sessionId as string; + console.log(`📨 Received message for session: ${sessionId}`); + + const transport = transports[sessionId]; + if (transport) { + try { + await transport.handlePostMessage(req, res, req.body); + } catch (error) { + console.error('❌ Error handling POST message:', error); + if (!res.headersSent) { + res.status(500).json({ error: 'Internal server error' }); + } + } + } else { + console.warn(`⚠️ No transport found for session ID: ${sessionId}`); + res.status(400).json({ error: 'No transport found for sessionId' }); + } + }); + + // Start the server + const server = app.listen(PORT, () => { + console.log(`✅ HTTP/SSE server listening on http://localhost:${PORT}`); + console.log(`📡 SSE endpoint: http://localhost:${PORT}/mcp`); + console.log(`📨 Messages endpoint: http://localhost:${PORT}/mcp/messages`); + console.log(`🏥 Health check: http://localhost:${PORT}/health`); + }); + + // Handle server errors + server.on('error', (error: any) => { + if (error.code === 'EADDRINUSE') { + console.error(`❌ Port ${PORT} is already in use`); + process.exit(1); + } else { + console.error('❌ Server error:', error); + process.exit(1); + } + }); + +} catch (error) { + console.error('❌ Fatal error starting HTTP/SSE server:', error); + process.exit(1); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..5c308d9 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,88 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { IBClient } from "./ib-client.js"; +import { IBGatewayManager } from "./gateway-manager.js"; +import { config } from "./config.js"; +import { registerTools } from "./tools.js"; +import { Logger } from "./logger.js"; + +export const configSchema = z.object({ + // Authentication configuration + IB_USERNAME: z.string().optional(), + IB_PASSWORD_AUTH: z.string().optional(), + IB_AUTH_TIMEOUT: z.number().optional(), + IB_HEADLESS_MODE: z.boolean().optional(), + + // Paper trading configuration + IB_PAPER_TRADING: z.boolean().optional(), +}); + +// Global gateway manager instance +let gatewayManager: IBGatewayManager | null = null; + +// Initialize and start IB Gateway (fast startup for MCP plugin compatibility) +async function initializeGateway(ibClient?: IBClient) { + if (!gatewayManager) { + gatewayManager = new IBGatewayManager(); + + try { + Logger.info('⚡ Quick Gateway initialization for MCP plugin...'); + await gatewayManager.quickStartGateway(); + Logger.info('✅ Gateway initialization completed (background startup if needed)'); + + // Update client port if provided + if (ibClient) { + ibClient.updatePort(gatewayManager.getCurrentPort()); + } + } catch (error) { + Logger.error('❌ Failed to initialize Gateway:', error); + // Don't throw error during quick startup - tools will handle it + Logger.warn('⚠️ Gateway initialization failed, tools will attempt connection when called'); + } + } + return gatewayManager; +} + +export function createIBMCPServer({ config: userConfig }: { config: z.infer }) { + // Merge user config with environment config + const mergedConfig = { + ...config, + ...userConfig + }; + + // Log the merged config for debugging (but redact sensitive info) + const logConfig = { ...mergedConfig }; + if (logConfig.IB_PASSWORD_AUTH) logConfig.IB_PASSWORD_AUTH = '[REDACTED]'; + if (logConfig.IB_PASSWORD) logConfig.IB_PASSWORD = '[REDACTED]'; + Logger.info(`🔍 Final merged config: ${JSON.stringify(logConfig, null, 2)}`); + + // Create IB Client with default port initially - this will be updated once gateway starts + const ibClient = new IBClient({ + host: mergedConfig.IB_GATEWAY_HOST, + port: mergedConfig.IB_GATEWAY_PORT, + }); + + // Initialize gateway on first server creation and update client port + initializeGateway(ibClient).catch(error => { + Logger.error('Failed to initialize gateway:', error); + }); + + Logger.info('Gateway starting...'); + + // Create MCP server + const server = new McpServer({ + name: "interactive-brokers-mcp", + version: "1.0.0" + }); + + // Register all tools with merged config + registerTools(server, ibClient, gatewayManager || undefined, mergedConfig); + + Logger.info('Tools registered'); + + return server; +} + +export { gatewayManager }; + +