Skip to content

Commit b0bb2ee

Browse files
authored
Merge pull request #21 from zlowred/read-only-mode
Add read-only mode (& fix SSE server mode).
2 parents df15966 + 7dccd90 commit b0bb2ee

9 files changed

Lines changed: 202 additions & 86 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ For a complete guide on creating and customizing Flex Queries, see the [IB Flex
185185
| Paper Trading | `IB_PAPER_TRADING` | `--ib-paper-trading` |
186186
| Auth Timeout | `IB_AUTH_TIMEOUT` | `--ib-auth-timeout` |
187187
| Flex Token | `IB_FLEX_TOKEN` | N/A |
188+
| Read-only mode | `IB_READ_ONLY_MODE` | `--ib-read-only-mode` |
188189

189190
## Available MCP Tools
190191

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

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js && chmod +x dist/index.js",
2121
"prepublishOnly": "npm run build",
2222
"start": "node dist/index.js",
23-
"start:http": "MCP_HTTP_SERVER=true node dist/index.js",
23+
"start:http": "MCP_HTTP_SERVER=true node dist/index-http.js",
2424
"dev": "tsx src/index.ts",
25-
"dev:http": "MCP_HTTP_SERVER=true tsx src/index.ts",
25+
"dev:http": "MCP_HTTP_SERVER=true tsx src/index-http.ts",
2626
"watch": "tsc --watch",
2727
"clean": "rm -rf dist",
2828
"test": "vitest run",
@@ -67,4 +67,4 @@
6767
"typescript": "^5.3.0",
6868
"vitest": "^3.2.4"
6969
}
70-
}
70+
}

smithery.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ startCommand:
1010
IB_USERNAME: config.IB_USERNAME,
1111
IB_PASSWORD_AUTH: config.IB_PASSWORD_AUTH,
1212
IB_AUTH_TIMEOUT: config.IB_AUTH_TIMEOUT,
13+
IB_READ_ONLY_MODE: config.IB_READ_ONLY_MODE
1314
} })
1415
configSchema:
1516
# JSON Schema defining the configuration options for the MCP.
@@ -30,8 +31,13 @@ startCommand:
3031
type: number
3132
description: "Authentication timeout in milliseconds (default: 60000)"
3233
default: 60000
34+
IB_READ_ONLY_MODE:
35+
type: boolean
36+
description: "Enable read-only mode (default: false)"
37+
default: false
3338
exampleConfig:
3439
IB_HEADLESS_MODE: true
3540
IB_USERNAME: "your_ib_username"
3641
IB_PASSWORD_AUTH: "your_ib_password"
37-
IB_AUTH_TIMEOUT: 60000
42+
IB_AUTH_TIMEOUT: 60000
43+
IB_READ_ONLY_MODE: false

src/config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ export const config = {
88
IB_GATEWAY_PORT: parseInt(process.env.IB_GATEWAY_PORT || "5000"),
99
IB_ACCOUNT: process.env.IB_ACCOUNT || "",
1010
IB_PASSWORD: process.env.IB_PASSWORD || "",
11-
11+
1212
// Headless authentication configuration
1313
IB_USERNAME: process.env.IB_USERNAME || "",
1414
IB_PASSWORD_AUTH: process.env.IB_PASSWORD_AUTH || process.env.IB_PASSWORD || "",
1515
IB_AUTH_TIMEOUT: parseInt(process.env.IB_AUTH_TIMEOUT || "60000"),
1616
IB_HEADLESS_MODE: process.env.IB_HEADLESS_MODE === "true",
17-
17+
1818
// Paper trading configuration
1919
IB_PAPER_TRADING: process.env.IB_PAPER_TRADING === "true",
20-
20+
21+
// Read-only mode configuration
22+
IB_READ_ONLY_MODE: process.env.IB_READ_ONLY_MODE === "true",
23+
2124
// Flex Query configuration
2225
IB_FLEX_TOKEN: process.env.IB_FLEX_TOKEN || "",
2326

src/index.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ import { Logger } from "./logger.js";
1212
function parseArgs(): z.infer<typeof configSchema> {
1313
const args: any = {};
1414
const argv = process.argv.slice(2);
15-
15+
1616
// Log raw arguments for debugging
1717
Logger.info(`🔍 Raw command line arguments: ${JSON.stringify(argv)}`);
18-
18+
1919
for (let i = 0; i < argv.length; i++) {
2020
const arg = argv[i];
21-
21+
2222
if (arg.startsWith('--')) {
2323
const key = arg.slice(2);
2424
const nextArg = argv[i + 1];
25-
25+
2626
Logger.debug(`🔍 Processing flag: ${key}, nextArg: ${nextArg}`);
27-
27+
2828
switch (key) {
2929
case 'ib-username':
3030
args.IB_USERNAME = nextArg;
@@ -63,15 +63,25 @@ function parseArgs(): z.infer<typeof configSchema> {
6363
args.IB_PAPER_TRADING = true;
6464
Logger.debug(`🔍 Set IB_PAPER_TRADING to: true (flag only)`);
6565
}
66+
case 'ib-read-only-mode':
67+
// Support both --ib-read-only-mode (boolean flag) and --ib-read-only-mode=true/false
68+
if (nextArg && !nextArg.startsWith('--')) {
69+
args.IB_READ_ONLY_MODE = nextArg.toLowerCase() === 'true';
70+
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: ${nextArg.toLowerCase() === 'true'} (from arg: ${nextArg})`);
71+
i++;
72+
} else {
73+
args.IB_READ_ONLY_MODE = true;
74+
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: true (flag only)`);
75+
}
6676
break;
6777

6878
}
6979
} else if (arg.includes('=')) {
7080
const [key, value] = arg.split('=', 2);
7181
const cleanKey = key.startsWith('--') ? key.slice(2) : key;
72-
82+
7383
Logger.debug(`🔍 Processing key=value: ${cleanKey}=${value}`);
74-
84+
7585
switch (cleanKey) {
7686
case 'ib-username':
7787
args.IB_USERNAME = value;
@@ -94,11 +104,15 @@ function parseArgs(): z.infer<typeof configSchema> {
94104
args.IB_PAPER_TRADING = value.toLowerCase() === 'true';
95105
Logger.debug(`🔍 Set IB_PAPER_TRADING to: ${value.toLowerCase() === 'true'} (from value: ${value})`);
96106
break;
107+
case 'ib-read-only-mode':
108+
args.IB_READ_ONLY_MODE = value.toLowerCase() === 'true';
109+
Logger.debug(`🔍 Set IB_READ_ONLY_MODE to: ${value.toLowerCase() === 'true'} (from value: ${value})`);
110+
break;
97111

98112
}
99113
}
100114
}
101-
115+
102116
Logger.info(`🔍 Parsed args: ${JSON.stringify(args, null, 2)}`);
103117
return args;
104118
}
@@ -110,10 +124,12 @@ export const configSchema = z.object({
110124
IB_PASSWORD_AUTH: z.string().optional(),
111125
IB_AUTH_TIMEOUT: z.number().optional(),
112126
IB_HEADLESS_MODE: z.boolean().optional(),
113-
127+
114128
// Paper trading configuration
115129
IB_PAPER_TRADING: z.boolean().optional(),
116130

131+
// Read-only mode configuration
132+
IB_READ_ONLY_MODE: z.boolean().optional(),
117133
});
118134

119135
// Global gateway manager instance
@@ -123,12 +139,12 @@ let gatewayManager: IBGatewayManager | null = null;
123139
async function initializeGateway(ibClient?: IBClient) {
124140
if (!gatewayManager) {
125141
gatewayManager = new IBGatewayManager();
126-
142+
127143
try {
128144
Logger.info('⚡ Quick Gateway initialization for MCP plugin...');
129145
await gatewayManager.quickStartGateway();
130146
Logger.info('✅ Gateway initialization completed (background startup if needed)');
131-
147+
132148
// Update client port if provided
133149
if (ibClient) {
134150
ibClient.updatePort(gatewayManager.getCurrentPort());
@@ -147,7 +163,7 @@ async function cleanupAll(signal?: string) {
147163
if (signal) {
148164
Logger.info(`🛑 Received ${signal}, cleaning up temp files only...`);
149165
}
150-
166+
151167
// Only cleanup temp files - don't shutdown gateway (leave it running for next npx process)
152168
if (gatewayManager) {
153169
try {
@@ -169,7 +185,7 @@ const gracefulShutdown = (signal: string) => {
169185
return; // Silent return to avoid log spam
170186
}
171187
isShuttingDown = true;
172-
188+
173189
// Don't use async/await here to avoid potential hanging
174190
cleanupAll(signal).finally(() => {
175191
process.exit(0);
@@ -199,11 +215,11 @@ process.on('unhandledRejection', (reason, promise) => {
199215

200216
// Check if this module is being run directly (for stdio compatibility)
201217
// This handles direct execution, npx, and bin script execution
202-
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
203-
process.argv[1]?.endsWith('index.js') ||
204-
process.argv[1]?.endsWith('dist/index.js') ||
205-
process.argv[1]?.endsWith('ib-mcp') ||
206-
process.argv[1]?.includes('/.bin/ib-mcp');
218+
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
219+
process.argv[1]?.endsWith('index.js') ||
220+
process.argv[1]?.endsWith('dist/index.js') ||
221+
process.argv[1]?.endsWith('ib-mcp') ||
222+
process.argv[1]?.includes('/.bin/ib-mcp');
207223

208224
function IBMCP({ config: userConfig }: { config: z.infer<typeof configSchema> }) {
209225
// Merge user config with environment config
@@ -250,20 +266,20 @@ if (isMainModule) {
250266
// Suppress known problematic outputs that might interfere with JSON-RPC
251267
process.env.SUPPRESS_LOAD_MESSAGE = '1';
252268
process.env.NO_UPDATE_NOTIFIER = '1';
253-
269+
254270
// Log environment info for debugging MCP plugin issues
255271
Logger.info(`🔍 Environment: PWD=${process.cwd()}, NODE_ENV=${process.env.NODE_ENV || 'undefined'}`);
256272
Logger.info(`🔍 Process: npm_execpath=${process.env.npm_execpath || 'undefined'}, npm_command=${process.env.npm_command || 'undefined'}`);
257-
273+
258274
// Check if we're running in npx/MCP plugin context
259275
const isNpx = process.env.npm_execpath?.includes('npx') || process.cwd().includes('.npm');
260276
if (isNpx) {
261277
Logger.info('📦 Detected npx execution - likely running via MCP community plugin');
262278
}
263-
279+
264280
// Log startup information
265281
Logger.logStartup();
266-
282+
267283
// Parse command line arguments and merge with environment variables
268284
// Priority: args > env > defaults
269285
const argsConfig = parseArgs();
@@ -272,39 +288,40 @@ if (isMainModule) {
272288
IB_PASSWORD_AUTH: process.env.IB_PASSWORD_AUTH || process.env.IB_PASSWORD,
273289
IB_AUTH_TIMEOUT: process.env.IB_AUTH_TIMEOUT ? parseInt(process.env.IB_AUTH_TIMEOUT) : undefined,
274290
IB_HEADLESS_MODE: process.env.IB_HEADLESS_MODE === 'true',
291+
IB_READ_ONLY_MODE: process.env.IB_READ_ONLY_MODE === 'true',
275292

276293
};
277-
294+
278295
// Log environment config for debugging
279296
const logEnvConfig = { ...envConfig };
280297
if (logEnvConfig.IB_PASSWORD_AUTH) logEnvConfig.IB_PASSWORD_AUTH = '[REDACTED]';
281298
Logger.info(`🔍 Environment config: ${JSON.stringify(logEnvConfig, null, 2)}`);
282-
299+
283300
// Merge configs with priority: args > env > defaults
284301
const finalConfig = {
285302
...envConfig,
286303
...argsConfig,
287304
};
288-
305+
289306
// Log final config before cleanup
290307
const logFinalConfig = { ...finalConfig };
291308
if (logFinalConfig.IB_PASSWORD_AUTH) logFinalConfig.IB_PASSWORD_AUTH = '[REDACTED]';
292309
Logger.info(`🔍 Final config before cleanup: ${JSON.stringify(logFinalConfig, null, 2)}`);
293-
310+
294311
// Remove undefined values
295312
Object.keys(finalConfig).forEach(key => {
296313
if (finalConfig[key as keyof typeof finalConfig] === undefined) {
297314
delete finalConfig[key as keyof typeof finalConfig];
298315
}
299316
});
300-
317+
301318
// Log final config after cleanup
302319
const logFinalConfigAfter = { ...finalConfig };
303320
if (logFinalConfigAfter.IB_PASSWORD_AUTH) logFinalConfigAfter.IB_PASSWORD_AUTH = '[REDACTED]';
304321
Logger.info(`🔍 Final config after cleanup: ${JSON.stringify(logFinalConfigAfter, null, 2)}`);
305-
322+
306323
const stdioTransport = new StdioServerTransport();
307-
const server = IBMCP({config: finalConfig})
324+
const server = IBMCP({ config: finalConfig })
308325
server.connect(stdioTransport);
309326
}
310327

src/server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ export const configSchema = z.object({
1212
IB_PASSWORD_AUTH: z.string().optional(),
1313
IB_AUTH_TIMEOUT: z.number().optional(),
1414
IB_HEADLESS_MODE: z.boolean().optional(),
15-
15+
1616
// Paper trading configuration
1717
IB_PAPER_TRADING: z.boolean().optional(),
18+
19+
// Read-only mode configuration
20+
IB_READ_ONLY_MODE: z.boolean().optional(),
1821
});
1922

2023
// Global gateway manager instance
@@ -24,12 +27,12 @@ let gatewayManager: IBGatewayManager | null = null;
2427
async function initializeGateway(ibClient?: IBClient) {
2528
if (!gatewayManager) {
2629
gatewayManager = new IBGatewayManager();
27-
30+
2831
try {
2932
Logger.info('⚡ Quick Gateway initialization for MCP plugin...');
3033
await gatewayManager.quickStartGateway();
3134
Logger.info('✅ Gateway initialization completed (background startup if needed)');
32-
35+
3336
// Update client port if provided
3437
if (ibClient) {
3538
ibClient.updatePort(gatewayManager.getCurrentPort());

0 commit comments

Comments
 (0)