Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mcp-inspector.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"interactive-brokers": {
"type": "sse",
"url": "http://localhost:8123/mcp"
}
}
}
5 changes: 4 additions & 1 deletion src/ib-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
102 changes: 102 additions & 0 deletions src/index-http.ts
Original file line number Diff line number Diff line change
@@ -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);
}
88 changes: 88 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -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<typeof configSchema> }) {
// 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 };