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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ For a complete guide on creating and customizing Flex Queries, see the [IB Flex
| Paper Trading | `IB_PAPER_TRADING` | `--ib-paper-trading` |
| Auth Timeout | `IB_AUTH_TIMEOUT` | `--ib-auth-timeout` |
| Flex Token | `IB_FLEX_TOKEN` | N/A |
| Read-only mode | `IB_READ_ONLY_MODE` | `--ib-read-only-mode` |

## Available MCP Tools

Expand All @@ -195,7 +196,7 @@ For a complete guide on creating and customizing Flex Queries, see the [IB Flex
| `get_account_info` | Retrieve account information and balances |
| `get_positions` | Get current positions and P&L |
| `get_market_data` | Real-time market data for symbols |
| `place_order` | Place market, limit, or stop orders |
| `place_order` | Place market, limit, or stop orders (only if read-only mode is disabled) |
| `get_order_status` | Check order execution status |
| `get_live_orders` | Get all live/open orders for monitoring |

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js && chmod +x dist/index.js",
"prepublishOnly": "npm run build",
"start": "node dist/index.js",
"start:http": "MCP_HTTP_SERVER=true node dist/index.js",
"start:http": "MCP_HTTP_SERVER=true node dist/index-http.js",
"dev": "tsx src/index.ts",
"dev:http": "MCP_HTTP_SERVER=true tsx src/index.ts",
"dev:http": "MCP_HTTP_SERVER=true tsx src/index-http.ts",
"watch": "tsc --watch",
"clean": "rm -rf dist",
"test": "vitest run",
Expand Down Expand Up @@ -67,4 +67,4 @@
"typescript": "^5.3.0",
"vitest": "^3.2.4"
}
}
}
8 changes: 7 additions & 1 deletion smithery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ startCommand:
IB_USERNAME: config.IB_USERNAME,
IB_PASSWORD_AUTH: config.IB_PASSWORD_AUTH,
IB_AUTH_TIMEOUT: config.IB_AUTH_TIMEOUT,
IB_READ_ONLY_MODE: config.IB_READ_ONLY_MODE
} })
configSchema:
# JSON Schema defining the configuration options for the MCP.
Expand All @@ -30,8 +31,13 @@ startCommand:
type: number
description: "Authentication timeout in milliseconds (default: 60000)"
default: 60000
IB_READ_ONLY_MODE:
type: boolean
description: "Enable read-only mode (default: false)"
default: false
exampleConfig:
IB_HEADLESS_MODE: true
IB_USERNAME: "your_ib_username"
IB_PASSWORD_AUTH: "your_ib_password"
IB_AUTH_TIMEOUT: 60000
IB_AUTH_TIMEOUT: 60000
IB_READ_ONLY_MODE: false
9 changes: 6 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ export const config = {
IB_GATEWAY_PORT: parseInt(process.env.IB_GATEWAY_PORT || "5000"),
IB_ACCOUNT: process.env.IB_ACCOUNT || "",
IB_PASSWORD: process.env.IB_PASSWORD || "",

// Headless authentication configuration
IB_USERNAME: process.env.IB_USERNAME || "",
IB_PASSWORD_AUTH: process.env.IB_PASSWORD_AUTH || process.env.IB_PASSWORD || "",
IB_AUTH_TIMEOUT: parseInt(process.env.IB_AUTH_TIMEOUT || "60000"),
IB_HEADLESS_MODE: process.env.IB_HEADLESS_MODE === "true",

// Paper trading configuration
IB_PAPER_TRADING: process.env.IB_PAPER_TRADING === "true",


// Read-only mode configuration
IB_READ_ONLY_MODE: process.env.IB_READ_ONLY_MODE === "true",

// Flex Query configuration
IB_FLEX_TOKEN: process.env.IB_FLEX_TOKEN || "",

Expand Down
75 changes: 46 additions & 29 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import { Logger } from "./logger.js";
function parseArgs(): z.infer<typeof configSchema> {
const args: any = {};
const argv = process.argv.slice(2);

// Log raw arguments for debugging
Logger.info(`🔍 Raw command line arguments: ${JSON.stringify(argv)}`);

for (let i = 0; i < argv.length; i++) {
const arg = argv[i];

if (arg.startsWith('--')) {
const key = arg.slice(2);
const nextArg = argv[i + 1];

Logger.debug(`🔍 Processing flag: ${key}, nextArg: ${nextArg}`);

switch (key) {
case 'ib-username':
args.IB_USERNAME = nextArg;
Expand Down Expand Up @@ -63,15 +63,25 @@ function parseArgs(): z.infer<typeof configSchema> {
args.IB_PAPER_TRADING = true;
Logger.debug(`🔍 Set IB_PAPER_TRADING to: true (flag only)`);
}
case 'ib-read-only-mode':
// Support both --ib-read-only-mode (boolean flag) and --ib-read-only-mode=true/false
if (nextArg && !nextArg.startsWith('--')) {
args.IB_READ_ONLY_MODE = nextArg.toLowerCase() === 'true';
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: ${nextArg.toLowerCase() === 'true'} (from arg: ${nextArg})`);
i++;
} else {
args.IB_READ_ONLY_MODE = true;
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: true (flag only)`);
}
break;

}
} else if (arg.includes('=')) {
const [key, value] = arg.split('=', 2);
const cleanKey = key.startsWith('--') ? key.slice(2) : key;

Logger.debug(`🔍 Processing key=value: ${cleanKey}=${value}`);

switch (cleanKey) {
case 'ib-username':
args.IB_USERNAME = value;
Expand All @@ -94,11 +104,15 @@ function parseArgs(): z.infer<typeof configSchema> {
args.IB_PAPER_TRADING = value.toLowerCase() === 'true';
Logger.debug(`🔍 Set IB_PAPER_TRADING to: ${value.toLowerCase() === 'true'} (from value: ${value})`);
break;
case 'ib-read-only-mode':
args.IB_READ_ONLY_MODE = value.toLowerCase() === 'true';
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: ${value.toLowerCase() === 'true'} (from value: ${value})`);
break;

}
}
}

Logger.info(`🔍 Parsed args: ${JSON.stringify(args, null, 2)}`);
return args;
}
Expand All @@ -110,10 +124,12 @@ export const configSchema = z.object({
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(),

// Read-only mode configuration
IB_READ_ONLY_MODE: z.boolean().optional(),
});

// Global gateway manager instance
Expand All @@ -123,12 +139,12 @@ let gatewayManager: IBGatewayManager | null = null;
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());
Expand All @@ -147,7 +163,7 @@ async function cleanupAll(signal?: string) {
if (signal) {
Logger.info(`🛑 Received ${signal}, cleaning up temp files only...`);
}

// Only cleanup temp files - don't shutdown gateway (leave it running for next npx process)
if (gatewayManager) {
try {
Expand All @@ -169,7 +185,7 @@ const gracefulShutdown = (signal: string) => {
return; // Silent return to avoid log spam
}
isShuttingDown = true;

// Don't use async/await here to avoid potential hanging
cleanupAll(signal).finally(() => {
process.exit(0);
Expand Down Expand Up @@ -199,11 +215,11 @@ process.on('unhandledRejection', (reason, promise) => {

// Check if this module is being run directly (for stdio compatibility)
// This handles direct execution, npx, and bin script execution
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
process.argv[1]?.endsWith('index.js') ||
process.argv[1]?.endsWith('dist/index.js') ||
process.argv[1]?.endsWith('ib-mcp') ||
process.argv[1]?.includes('/.bin/ib-mcp');
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
process.argv[1]?.endsWith('index.js') ||
process.argv[1]?.endsWith('dist/index.js') ||
process.argv[1]?.endsWith('ib-mcp') ||
process.argv[1]?.includes('/.bin/ib-mcp');

function IBMCP({ config: userConfig }: { config: z.infer<typeof configSchema> }) {
// Merge user config with environment config
Expand Down Expand Up @@ -250,20 +266,20 @@ if (isMainModule) {
// Suppress known problematic outputs that might interfere with JSON-RPC
process.env.SUPPRESS_LOAD_MESSAGE = '1';
process.env.NO_UPDATE_NOTIFIER = '1';

// Log environment info for debugging MCP plugin issues
Logger.info(`🔍 Environment: PWD=${process.cwd()}, NODE_ENV=${process.env.NODE_ENV || 'undefined'}`);
Logger.info(`🔍 Process: npm_execpath=${process.env.npm_execpath || 'undefined'}, npm_command=${process.env.npm_command || 'undefined'}`);

// Check if we're running in npx/MCP plugin context
const isNpx = process.env.npm_execpath?.includes('npx') || process.cwd().includes('.npm');
if (isNpx) {
Logger.info('📦 Detected npx execution - likely running via MCP community plugin');
}

// Log startup information
Logger.logStartup();

// Parse command line arguments and merge with environment variables
// Priority: args > env > defaults
const argsConfig = parseArgs();
Expand All @@ -272,39 +288,40 @@ if (isMainModule) {
IB_PASSWORD_AUTH: process.env.IB_PASSWORD_AUTH || process.env.IB_PASSWORD,
IB_AUTH_TIMEOUT: process.env.IB_AUTH_TIMEOUT ? parseInt(process.env.IB_AUTH_TIMEOUT) : undefined,
IB_HEADLESS_MODE: process.env.IB_HEADLESS_MODE === 'true',
IB_READ_ONLY_MODE: process.env.IB_READ_ONLY_MODE === 'true',

};

// Log environment config for debugging
const logEnvConfig = { ...envConfig };
if (logEnvConfig.IB_PASSWORD_AUTH) logEnvConfig.IB_PASSWORD_AUTH = '[REDACTED]';
Logger.info(`🔍 Environment config: ${JSON.stringify(logEnvConfig, null, 2)}`);

// Merge configs with priority: args > env > defaults
const finalConfig = {
...envConfig,
...argsConfig,
};

// Log final config before cleanup
const logFinalConfig = { ...finalConfig };
if (logFinalConfig.IB_PASSWORD_AUTH) logFinalConfig.IB_PASSWORD_AUTH = '[REDACTED]';
Logger.info(`🔍 Final config before cleanup: ${JSON.stringify(logFinalConfig, null, 2)}`);

// Remove undefined values
Object.keys(finalConfig).forEach(key => {
if (finalConfig[key as keyof typeof finalConfig] === undefined) {
delete finalConfig[key as keyof typeof finalConfig];
}
});

// Log final config after cleanup
const logFinalConfigAfter = { ...finalConfig };
if (logFinalConfigAfter.IB_PASSWORD_AUTH) logFinalConfigAfter.IB_PASSWORD_AUTH = '[REDACTED]';
Logger.info(`🔍 Final config after cleanup: ${JSON.stringify(logFinalConfigAfter, null, 2)}`);

const stdioTransport = new StdioServerTransport();
const server = IBMCP({config: finalConfig})
const server = IBMCP({ config: finalConfig })
server.connect(stdioTransport);
}

Expand Down
9 changes: 6 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ export const configSchema = z.object({
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(),

// Read-only mode configuration
IB_READ_ONLY_MODE: z.boolean().optional(),
});

// Global gateway manager instance
Expand All @@ -24,12 +27,12 @@ let gatewayManager: IBGatewayManager | null = null;
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());
Expand Down
Loading