ib-mcp 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ib_mcp-0.2.5/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, David Hellekalek
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
11
+
12
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ib_mcp-0.2.5/PKG-INFO ADDED
@@ -0,0 +1,296 @@
1
+ Metadata-Version: 2.1
2
+ Name: ib-mcp
3
+ Version: 0.2.5
4
+ Summary: Model Context Protocol (MCP) server exposing Interactive Brokers data via ib_async + FastMCP
5
+ Home-page: https://github.com/Hellek1/ib-mcp
6
+ License: BSD-3-Clause
7
+ Keywords: interactive-brokers,ib,ibapi,tws,asyncio,mcp,fastmcp,llm
8
+ Author: David Hellekalek
9
+ Author-email: hellekalek@gmail.com
10
+ Requires-Python: >=3.12,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Office/Business :: Financial :: Investment
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: defusedxml (>=0.7.1)
21
+ Requires-Dist: fastmcp (>=2.11.0)
22
+ Requires-Dist: ib_async (>=2.0.1,<3.0.0)
23
+ Project-URL: Documentation, https://github.com/Hellek1/ib-mcp#readme
24
+ Project-URL: Repository, https://github.com/Hellek1/ib-mcp
25
+ Description-Content-Type: text/markdown
26
+
27
+ # IB Async MCP Server
28
+
29
+ [![CI](https://github.com/Hellek1/ib-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Hellek1/ib-mcp/actions/workflows/ci.yml)
30
+
31
+ Lightweight Model Context Protocol (MCP) server exposing read-only Interactive Brokers data (contracts, historical data, fundamentals, news, portfolio, account) via the asynchronous [`ib_async`](https://ib-api-reloaded.github.io/ib_async/) library and [`FastMCP`](https://github.com/modelcontextprotocol/fastmcp). Ideal for feeding financial data into LLM workflows and autonomous agents while keeping trading disabled.
32
+
33
+ ## Overview
34
+
35
+ This directory contains an MCP (Model Context Protocol) server that wraps the ib_async library to allow LLMs to interact with Interactive Brokers data.
36
+
37
+ ## Features
38
+
39
+ The MCP server provides the following tools for LLM interaction:
40
+
41
+ ### 1. Contract Lookup and Conversion
42
+ - **lookup_contract**: Look up contract details by ticker symbol and optional exchange/currency
43
+ - **ticker_to_conid**: Convert ticker symbol to contract ID (conid)
44
+
45
+ ### 2. Market Data
46
+ - **get_historical_data**: Retrieve historical market data with configurable duration, bar size, and data type
47
+
48
+ ### 3. News
49
+ - **get_news**: Retrieve current news articles for a contract
50
+ - **get_historical_news**: Retrieve historical news articles within a date range
51
+
52
+ ### 4. Fundamental Data
53
+ - **get_fundamental_data**: Retrieve fundamental data including financial summaries, ownership, financial statements, and more
54
+
55
+ ### 5. Portfolio and Account Information
56
+ - **get_portfolio**: Retrieve portfolio positions and details
57
+ - **get_account_summary**: Retrieve account summary information
58
+ - **get_positions**: Retrieve current positions
59
+
60
+ ## Prerequisites
61
+
62
+ 1. **Interactive Brokers Account**: You need an active IB account
63
+ 2. **IB Gateway or TWS**: Download and install either:
64
+ - [IB Gateway (Stable)](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) - Recommended for API-only use
65
+ - [IB Gateway (Latest)](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) - Latest features
66
+ - [Trader Workstation (TWS)](https://www.interactivebrokers.com/en/trading/tws.php) - Full trading platform
67
+
68
+ 3. **API Configuration**:
69
+ - Enable API access in TWS/Gateway: `Configure → API → Settings` and check "Enable ActiveX and Socket Clients"
70
+ - Set appropriate port (default: 7497 for TWS, 4001 for Gateway)
71
+ - Add `127.0.0.1` to trusted IPs if connecting locally
72
+
73
+ ## Installation
74
+
75
+ ### From source (development)
76
+ ```bash
77
+ git clone https://github.com/Hellek1/ib-mcp.git
78
+ cd ib-mcp
79
+ pip install poetry
80
+ poetry install
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ ### Running the MCP Server
86
+
87
+ ```bash
88
+ # Using default settings (TWS on localhost:7497)
89
+ poetry run ib-mcp-server
90
+
91
+ # Custom IB Gateway connection
92
+ poetry run ib-mcp-server --host 127.0.0.1 --port 4001 --client-id 1
93
+
94
+ # Help with all options
95
+ poetry run ib-mcp-server --help
96
+ ```
97
+
98
+ ### Command Line Options
99
+
100
+ - `--host`: IB Gateway/TWS host (default: 127.0.0.1)
101
+ - `--port`: IB Gateway/TWS port (default: 7497 for TWS, use 4001 for Gateway)
102
+ - `--client-id`: Unique client ID for the connection (default: 1)
103
+
104
+ You can also use environment variables instead of flags:
105
+
106
+ - `IB_HOST`
107
+ - `IB_PORT`
108
+ - `IB_CLIENT_ID`
109
+
110
+ Flags override environment variables if both are provided.
111
+
112
+ ## Docker
113
+
114
+ ### Build
115
+
116
+ ```bash
117
+ docker build -t ib-mcp .
118
+ ```
119
+
120
+ ### Run (connect to TWS running on host)
121
+
122
+ On macOS/Windows Docker Desktop you can reach host via `host.docker.internal` (already the default):
123
+
124
+ ```bash
125
+ docker run --rm -it \
126
+ -e IB_HOST=host.docker.internal \
127
+ -e IB_PORT=7497 \
128
+ -e IB_CLIENT_ID=1 \
129
+ ghcr.io/hellek1/ib-mcp
130
+ ```
131
+
132
+ On Linux you may need to add `--add-host host.docker.internal:host-gateway` and ensure the TWS/Gateway port is accessible:
133
+
134
+ ```bash
135
+ docker run --rm -it \
136
+ --add-host host.docker.internal:host-gateway \
137
+ -e IB_HOST=host.docker.internal \
138
+ -e IB_PORT=7497 \
139
+ ghcr.io/hellek1/ib-mcp
140
+ ```
141
+
142
+ Override arguments directly if preferred:
143
+
144
+ ```bash
145
+ docker run --rm -it ghcr.io/hellek1/ib-mcp --host host.docker.internal --port 4001 --client-id 2
146
+ ```
147
+
148
+
149
+ ### MCP Client Integration
150
+
151
+ The server communicates via stdio using the MCP protocol. It can be integrated with MCP-compatible tools and LLM applications.
152
+
153
+ Example MCP client configuration (e.g. Claude Desktop) using Docker:
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "ib-async": {
158
+ "command": "docker",
159
+ "args": [
160
+ "run",
161
+ "--rm",
162
+ "--add-host","host.docker.internal:host-gateway",
163
+ "-e","IB_HOST=host.docker.internal",
164
+ "-e","IB_PORT=7497",
165
+ "-e","IB_CLIENT_ID=1",
166
+ "ghcr.io/hellek1/ib-mcp:latest"
167
+ ]
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ Notes:
174
+ 1. Remove the `--add-host` line on macOS/Windows Docker Desktop (it's only needed on Linux).
175
+
176
+ ## Available Tools
177
+
178
+ ### Contract Lookup
179
+ ```
180
+ lookup_contract(symbol, sec_type="STK", exchange="SMART", currency="USD")
181
+ ticker_to_conid(symbol, sec_type="STK", exchange="SMART", currency="USD")
182
+ ```
183
+
184
+ ### Market Data
185
+ ```
186
+ get_historical_data(symbol, duration="1 M", bar_size="1 day", data_type="TRADES", exchange="SMART", currency="USD")
187
+ ```
188
+
189
+ ### News
190
+ ```
191
+ get_news(symbol, provider_codes="", exchange="SMART", currency="USD")
192
+ get_historical_news(symbol, start_date, end_date, provider_codes="", max_count=10, exchange="SMART", currency="USD")
193
+ ```
194
+
195
+ ### Fundamentals
196
+ ```
197
+ get_fundamental_data(symbol, report_type="ReportsFinSummary", exchange="SMART", currency="USD")
198
+ ```
199
+
200
+ Available report types:
201
+ - `ReportsFinSummary`: Financial summary
202
+ - `ReportsOwnership`: Ownership information
203
+ - `ReportsFinStatements`: Financial statements
204
+ - `RESC`: Research reports
205
+ - `CalendarReport`: Calendar events
206
+
207
+ ### Portfolio & Account
208
+ ```
209
+ get_portfolio(account="")
210
+ get_account_summary(account="")
211
+ get_positions(account="")
212
+ ```
213
+
214
+ ## Example Usage
215
+
216
+ Once connected to an LLM through MCP, you can ask questions like:
217
+
218
+ - "Look up the contract details for AAPL"
219
+ - "Get the last month of daily historical data for TSLA"
220
+ - "What are the recent news articles for Microsoft?"
221
+ - "Show me the financial summary for Google"
222
+ - "What positions do I currently have in my portfolio?"
223
+
224
+ ## Data Formats
225
+
226
+ ### XML to Markdown Conversion
227
+
228
+ The server automatically converts XML-formatted fundamental data to markdown for better readability in LLM interactions.
229
+
230
+ ### Error Handling
231
+
232
+ The server includes comprehensive error handling and will provide meaningful error messages when:
233
+ - IB connection fails
234
+ - Invalid symbols are requested
235
+ - Market data is not available
236
+ - Authentication issues occur
237
+
238
+ ## Troubleshooting
239
+
240
+ ### Connection Issues
241
+
242
+ 1. **"Cannot connect to Interactive Brokers"**
243
+ - Ensure TWS/Gateway is running
244
+ - Check that API is enabled in settings
245
+ - Verify port numbers match (7497 for TWS, 4001 for Gateway)
246
+ - Check firewall settings
247
+
248
+ 2. **"No contract found"**
249
+ - Verify symbol spelling
250
+ - Try different exchanges (NYSE, NASDAQ vs SMART)
251
+ - Check if security type is correct
252
+
253
+ 3. **"No market data"**
254
+ - Ensure you have appropriate market data subscriptions
255
+ - Check if markets are open for real-time data
256
+ - Try delayed data mode if real-time is not available
257
+
258
+ ### Performance Tips
259
+
260
+ 1. Use specific exchanges when possible instead of "SMART" routing
261
+ 2. Limit historical data requests to reasonable time ranges
262
+ 3. Cache contract IDs for frequently accessed symbols
263
+
264
+ ## Security Considerations
265
+
266
+ - The MCP server operates in read-only mode - no order placement capabilities
267
+ - Credentials are handled by the IB Gateway/TWS application
268
+ - The server only accesses data you have permission to view in your IB account
269
+
270
+ ## Contributing
271
+
272
+ 1. Fork & branch: `feat/xyz`
273
+ 2. Install dev deps: `poetry install`
274
+ 3. Activate pre-commit: `pre-commit install`
275
+ 4. Run tests: `poetry run pytest -q`
276
+ 5. Open a PR with a concise description.
277
+
278
+ ### Release (maintainers)
279
+ ```bash
280
+ poetry version patch # or minor / major
281
+ poetry build
282
+ poetry publish --username __token__ --password <pypi-token>
283
+ git tag v$(poetry version -s)
284
+ git push --tags
285
+ ```
286
+
287
+ ## Support & References
288
+
289
+ - IB API functionality: [ib_async docs](https://ib-api-reloaded.github.io/ib_async/)
290
+ - MCP protocol: [MCP spec](https://spec.modelcontextprotocol.io/)
291
+ - Interactive Brokers: [IB API docs](https://ibkrcampus.com/ibkr-api-page/twsapi-doc/)
292
+
293
+ ---
294
+
295
+ Licensed under the BSD 3-Clause License. Contributions welcome.
296
+
ib_mcp-0.2.5/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # IB Async MCP Server
2
+
3
+ [![CI](https://github.com/Hellek1/ib-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Hellek1/ib-mcp/actions/workflows/ci.yml)
4
+
5
+ Lightweight Model Context Protocol (MCP) server exposing read-only Interactive Brokers data (contracts, historical data, fundamentals, news, portfolio, account) via the asynchronous [`ib_async`](https://ib-api-reloaded.github.io/ib_async/) library and [`FastMCP`](https://github.com/modelcontextprotocol/fastmcp). Ideal for feeding financial data into LLM workflows and autonomous agents while keeping trading disabled.
6
+
7
+ ## Overview
8
+
9
+ This directory contains an MCP (Model Context Protocol) server that wraps the ib_async library to allow LLMs to interact with Interactive Brokers data.
10
+
11
+ ## Features
12
+
13
+ The MCP server provides the following tools for LLM interaction:
14
+
15
+ ### 1. Contract Lookup and Conversion
16
+ - **lookup_contract**: Look up contract details by ticker symbol and optional exchange/currency
17
+ - **ticker_to_conid**: Convert ticker symbol to contract ID (conid)
18
+
19
+ ### 2. Market Data
20
+ - **get_historical_data**: Retrieve historical market data with configurable duration, bar size, and data type
21
+
22
+ ### 3. News
23
+ - **get_news**: Retrieve current news articles for a contract
24
+ - **get_historical_news**: Retrieve historical news articles within a date range
25
+
26
+ ### 4. Fundamental Data
27
+ - **get_fundamental_data**: Retrieve fundamental data including financial summaries, ownership, financial statements, and more
28
+
29
+ ### 5. Portfolio and Account Information
30
+ - **get_portfolio**: Retrieve portfolio positions and details
31
+ - **get_account_summary**: Retrieve account summary information
32
+ - **get_positions**: Retrieve current positions
33
+
34
+ ## Prerequisites
35
+
36
+ 1. **Interactive Brokers Account**: You need an active IB account
37
+ 2. **IB Gateway or TWS**: Download and install either:
38
+ - [IB Gateway (Stable)](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) - Recommended for API-only use
39
+ - [IB Gateway (Latest)](https://www.interactivebrokers.com/en/trading/ibgateway-latest.php) - Latest features
40
+ - [Trader Workstation (TWS)](https://www.interactivebrokers.com/en/trading/tws.php) - Full trading platform
41
+
42
+ 3. **API Configuration**:
43
+ - Enable API access in TWS/Gateway: `Configure → API → Settings` and check "Enable ActiveX and Socket Clients"
44
+ - Set appropriate port (default: 7497 for TWS, 4001 for Gateway)
45
+ - Add `127.0.0.1` to trusted IPs if connecting locally
46
+
47
+ ## Installation
48
+
49
+ ### From source (development)
50
+ ```bash
51
+ git clone https://github.com/Hellek1/ib-mcp.git
52
+ cd ib-mcp
53
+ pip install poetry
54
+ poetry install
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Running the MCP Server
60
+
61
+ ```bash
62
+ # Using default settings (TWS on localhost:7497)
63
+ poetry run ib-mcp-server
64
+
65
+ # Custom IB Gateway connection
66
+ poetry run ib-mcp-server --host 127.0.0.1 --port 4001 --client-id 1
67
+
68
+ # Help with all options
69
+ poetry run ib-mcp-server --help
70
+ ```
71
+
72
+ ### Command Line Options
73
+
74
+ - `--host`: IB Gateway/TWS host (default: 127.0.0.1)
75
+ - `--port`: IB Gateway/TWS port (default: 7497 for TWS, use 4001 for Gateway)
76
+ - `--client-id`: Unique client ID for the connection (default: 1)
77
+
78
+ You can also use environment variables instead of flags:
79
+
80
+ - `IB_HOST`
81
+ - `IB_PORT`
82
+ - `IB_CLIENT_ID`
83
+
84
+ Flags override environment variables if both are provided.
85
+
86
+ ## Docker
87
+
88
+ ### Build
89
+
90
+ ```bash
91
+ docker build -t ib-mcp .
92
+ ```
93
+
94
+ ### Run (connect to TWS running on host)
95
+
96
+ On macOS/Windows Docker Desktop you can reach host via `host.docker.internal` (already the default):
97
+
98
+ ```bash
99
+ docker run --rm -it \
100
+ -e IB_HOST=host.docker.internal \
101
+ -e IB_PORT=7497 \
102
+ -e IB_CLIENT_ID=1 \
103
+ ghcr.io/hellek1/ib-mcp
104
+ ```
105
+
106
+ On Linux you may need to add `--add-host host.docker.internal:host-gateway` and ensure the TWS/Gateway port is accessible:
107
+
108
+ ```bash
109
+ docker run --rm -it \
110
+ --add-host host.docker.internal:host-gateway \
111
+ -e IB_HOST=host.docker.internal \
112
+ -e IB_PORT=7497 \
113
+ ghcr.io/hellek1/ib-mcp
114
+ ```
115
+
116
+ Override arguments directly if preferred:
117
+
118
+ ```bash
119
+ docker run --rm -it ghcr.io/hellek1/ib-mcp --host host.docker.internal --port 4001 --client-id 2
120
+ ```
121
+
122
+
123
+ ### MCP Client Integration
124
+
125
+ The server communicates via stdio using the MCP protocol. It can be integrated with MCP-compatible tools and LLM applications.
126
+
127
+ Example MCP client configuration (e.g. Claude Desktop) using Docker:
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "ib-async": {
132
+ "command": "docker",
133
+ "args": [
134
+ "run",
135
+ "--rm",
136
+ "--add-host","host.docker.internal:host-gateway",
137
+ "-e","IB_HOST=host.docker.internal",
138
+ "-e","IB_PORT=7497",
139
+ "-e","IB_CLIENT_ID=1",
140
+ "ghcr.io/hellek1/ib-mcp:latest"
141
+ ]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Notes:
148
+ 1. Remove the `--add-host` line on macOS/Windows Docker Desktop (it's only needed on Linux).
149
+
150
+ ## Available Tools
151
+
152
+ ### Contract Lookup
153
+ ```
154
+ lookup_contract(symbol, sec_type="STK", exchange="SMART", currency="USD")
155
+ ticker_to_conid(symbol, sec_type="STK", exchange="SMART", currency="USD")
156
+ ```
157
+
158
+ ### Market Data
159
+ ```
160
+ get_historical_data(symbol, duration="1 M", bar_size="1 day", data_type="TRADES", exchange="SMART", currency="USD")
161
+ ```
162
+
163
+ ### News
164
+ ```
165
+ get_news(symbol, provider_codes="", exchange="SMART", currency="USD")
166
+ get_historical_news(symbol, start_date, end_date, provider_codes="", max_count=10, exchange="SMART", currency="USD")
167
+ ```
168
+
169
+ ### Fundamentals
170
+ ```
171
+ get_fundamental_data(symbol, report_type="ReportsFinSummary", exchange="SMART", currency="USD")
172
+ ```
173
+
174
+ Available report types:
175
+ - `ReportsFinSummary`: Financial summary
176
+ - `ReportsOwnership`: Ownership information
177
+ - `ReportsFinStatements`: Financial statements
178
+ - `RESC`: Research reports
179
+ - `CalendarReport`: Calendar events
180
+
181
+ ### Portfolio & Account
182
+ ```
183
+ get_portfolio(account="")
184
+ get_account_summary(account="")
185
+ get_positions(account="")
186
+ ```
187
+
188
+ ## Example Usage
189
+
190
+ Once connected to an LLM through MCP, you can ask questions like:
191
+
192
+ - "Look up the contract details for AAPL"
193
+ - "Get the last month of daily historical data for TSLA"
194
+ - "What are the recent news articles for Microsoft?"
195
+ - "Show me the financial summary for Google"
196
+ - "What positions do I currently have in my portfolio?"
197
+
198
+ ## Data Formats
199
+
200
+ ### XML to Markdown Conversion
201
+
202
+ The server automatically converts XML-formatted fundamental data to markdown for better readability in LLM interactions.
203
+
204
+ ### Error Handling
205
+
206
+ The server includes comprehensive error handling and will provide meaningful error messages when:
207
+ - IB connection fails
208
+ - Invalid symbols are requested
209
+ - Market data is not available
210
+ - Authentication issues occur
211
+
212
+ ## Troubleshooting
213
+
214
+ ### Connection Issues
215
+
216
+ 1. **"Cannot connect to Interactive Brokers"**
217
+ - Ensure TWS/Gateway is running
218
+ - Check that API is enabled in settings
219
+ - Verify port numbers match (7497 for TWS, 4001 for Gateway)
220
+ - Check firewall settings
221
+
222
+ 2. **"No contract found"**
223
+ - Verify symbol spelling
224
+ - Try different exchanges (NYSE, NASDAQ vs SMART)
225
+ - Check if security type is correct
226
+
227
+ 3. **"No market data"**
228
+ - Ensure you have appropriate market data subscriptions
229
+ - Check if markets are open for real-time data
230
+ - Try delayed data mode if real-time is not available
231
+
232
+ ### Performance Tips
233
+
234
+ 1. Use specific exchanges when possible instead of "SMART" routing
235
+ 2. Limit historical data requests to reasonable time ranges
236
+ 3. Cache contract IDs for frequently accessed symbols
237
+
238
+ ## Security Considerations
239
+
240
+ - The MCP server operates in read-only mode - no order placement capabilities
241
+ - Credentials are handled by the IB Gateway/TWS application
242
+ - The server only accesses data you have permission to view in your IB account
243
+
244
+ ## Contributing
245
+
246
+ 1. Fork & branch: `feat/xyz`
247
+ 2. Install dev deps: `poetry install`
248
+ 3. Activate pre-commit: `pre-commit install`
249
+ 4. Run tests: `poetry run pytest -q`
250
+ 5. Open a PR with a concise description.
251
+
252
+ ### Release (maintainers)
253
+ ```bash
254
+ poetry version patch # or minor / major
255
+ poetry build
256
+ poetry publish --username __token__ --password <pypi-token>
257
+ git tag v$(poetry version -s)
258
+ git push --tags
259
+ ```
260
+
261
+ ## Support & References
262
+
263
+ - IB API functionality: [ib_async docs](https://ib-api-reloaded.github.io/ib_async/)
264
+ - MCP protocol: [MCP spec](https://spec.modelcontextprotocol.io/)
265
+ - Interactive Brokers: [IB API docs](https://ibkrcampus.com/ibkr-api-page/twsapi-doc/)
266
+
267
+ ---
268
+
269
+ Licensed under the BSD 3-Clause License. Contributions welcome.
@@ -0,0 +1,3 @@
1
+ from .server import IBMCPServer, main
2
+
3
+ __all__ = ["IBMCPServer", "main"]
@@ -0,0 +1,615 @@
1
+ """FastMCP-based MCP server for Interactive Brokers (alternate implementation).
2
+
3
+ This mirrors the tools and structure from the legacy server, but uses FastMCP
4
+ for simpler registration and JSON-schema generation from type hints.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Annotated, Any
11
+
12
+ import defusedxml.ElementTree as ET
13
+ import ib_async as ib
14
+ from fastmcp import FastMCP
15
+ from pydantic import Field
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class IBMCPServer:
21
+ """Interactive Brokers MCP Server (FastMCP edition)."""
22
+
23
+ def __init__(
24
+ self, host: str = "127.0.0.1", port: int = 7496, client_id: int = 1
25
+ ) -> None:
26
+ self.server = FastMCP("IBKR MCP Server")
27
+ self.ib = ib.IB()
28
+ self.host = host
29
+ self.port = port
30
+ self.client_id = client_id
31
+ self.connected = False
32
+ self.news_provider_codes: str = ""
33
+
34
+ # Register FastMCP tools
35
+ self._register_handlers()
36
+
37
+ def _register_handlers(self) -> None:
38
+ """Register tools using FastMCP decorators. Uses closures that capture self."""
39
+
40
+ async def _ensure_connected() -> None:
41
+ if self.connected:
42
+ return
43
+ try:
44
+ await self.ib.connectAsync(
45
+ self.host, self.port, self.client_id, readonly=True
46
+ )
47
+ self.connected = True
48
+ logger.info("Connected to IB at %s:%s", self.host, self.port)
49
+ news_providers = await self.ib.reqNewsProvidersAsync()
50
+ self.news_provider_codes = "+".join(np.code for np in news_providers)
51
+ logger.info("News providers retrieved: %s", self.news_provider_codes)
52
+ except Exception as e: # pragma: no cover - relies on external service
53
+ logger.error("Failed to connect to IB: %s", e)
54
+ raise ConnectionError(
55
+ f"Cannot connect to Interactive Brokers: {e}"
56
+ ) from e
57
+
58
+ def _create_contract(
59
+ symbol: str,
60
+ sec_type: str = "STK",
61
+ exchange: str = "SMART",
62
+ currency: str = "USD",
63
+ ) -> ib.Contract:
64
+ if symbol.isdigit():
65
+ return ib.Contract(conId=int(symbol))
66
+ if sec_type == "STK":
67
+ return ib.Stock(symbol=symbol, exchange=exchange, currency=currency)
68
+ if sec_type in ("FOREX", "CASH"):
69
+ return ib.Forex(pair=symbol)
70
+ if sec_type == "FUT":
71
+ return ib.Future(symbol=symbol, exchange=exchange)
72
+ if sec_type == "OPT":
73
+ # Option expects strike as float, not currency as 3rd arg
74
+ return ib.Option(symbol=symbol, exchange=exchange, currency=currency)
75
+ return ib.Contract(
76
+ symbol=symbol, secType=sec_type, exchange=exchange, currency=currency
77
+ )
78
+
79
+ def _flatten_contracts(contracts: list[Any]) -> list[ib.Contract]:
80
+ # Recursively flatten nested contract lists and filter out None
81
+ result: list[ib.Contract] = []
82
+ for c in contracts:
83
+ if isinstance(c, ib.Contract):
84
+ result.append(c)
85
+ elif isinstance(c, list):
86
+ result.extend(_flatten_contracts(c))
87
+ return result
88
+
89
+ def _xml_to_markdown(xml_data: str) -> str:
90
+ """Convert XML data to markdown; return as-is if not XML."""
91
+ try:
92
+ if not xml_data or not xml_data.strip().startswith("<"):
93
+ return xml_data
94
+ root = ET.fromstring(xml_data)
95
+ return _xml_element_to_markdown(root)
96
+ except ET.ParseError:
97
+ return xml_data
98
+
99
+ def _xml_element_to_markdown(element: ET.Element, level: int = 0) -> str:
100
+ markdown = ""
101
+ indent = " " * level
102
+ if level == 0:
103
+ markdown += f"# {element.tag}\n\n"
104
+ elif level == 1:
105
+ markdown += f"## {element.tag}\n\n"
106
+ elif level == 2:
107
+ markdown += f"### {element.tag}\n\n"
108
+ else:
109
+ markdown += f"{indent}**{element.tag}**\n\n"
110
+ if element.text and element.text.strip():
111
+ markdown += f"{indent}{element.text.strip()}\n\n"
112
+ if element.attrib:
113
+ for key, value in element.attrib.items():
114
+ markdown += f"{indent}- **{key}**: {value}\n"
115
+ markdown += "\n"
116
+ for child in element:
117
+ markdown += _xml_element_to_markdown(child, level + 1)
118
+ return markdown
119
+
120
+ @self.server.tool(
121
+ description="Look up contract details by ticker symbol and optional exchange/currency"
122
+ )
123
+ async def lookup_contract(
124
+ symbol: Annotated[str, "Stock symbol (e.g., AAPL, GOOGL, etc.)"],
125
+ sec_type: Annotated[
126
+ str, "Security type (e.g., STK, OPT, FUT, etc.)"
127
+ ] = "STK",
128
+ exchange: Annotated[
129
+ str, "Exchange (e.g., SMART, NYSE, NASDAQ, etc.)"
130
+ ] = "SMART",
131
+ currency: Annotated[str, "Currency (e.g., USD, EUR, etc.)"] = "USD",
132
+ ) -> str:
133
+ await _ensure_connected()
134
+ contract = _create_contract(symbol, sec_type, exchange, currency)
135
+ try:
136
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
137
+ contracts = _flatten_contracts(contracts_raw)
138
+ if not contracts:
139
+ return f"No contract found for {symbol}"
140
+ lines = [f"Found {len(contracts)} contract(s) for {symbol}:"]
141
+ for i, c in enumerate(contracts, 1):
142
+ if c is None:
143
+ continue
144
+ lines.extend(
145
+ [
146
+ f"Contract {i}:",
147
+ f" ConID: {getattr(c, 'conId', '')}",
148
+ f" Symbol: {getattr(c, 'symbol', '')}",
149
+ f" SecType: {getattr(c, 'secType', '')}",
150
+ f" Exchange: {getattr(c, 'exchange', '')}",
151
+ f" Primary Exchange: {getattr(c, 'primaryExchange', '')}",
152
+ f" Currency: {getattr(c, 'currency', '')}",
153
+ f" Trading Class: {getattr(c, 'tradingClass', '')}",
154
+ f" Local Symbol: {getattr(c, 'localSymbol', '')}",
155
+ "",
156
+ ]
157
+ )
158
+ return "\n".join(lines)
159
+ except Exception as e: # pragma: no cover - depends on network
160
+ return f"Error looking up contract: {e}"
161
+
162
+ @self.server.tool(description="Convert ticker symbol to contract ID (conid)")
163
+ async def ticker_to_conid(
164
+ symbol: str,
165
+ sec_type: str = "STK",
166
+ exchange: str = "SMART",
167
+ currency: str = "USD",
168
+ ) -> str:
169
+ await _ensure_connected()
170
+ contract = _create_contract(symbol, sec_type, exchange, currency)
171
+ try:
172
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
173
+ contracts = _flatten_contracts(contracts_raw)
174
+ if not contracts:
175
+ return f"No contract found for {symbol}"
176
+ conid = getattr(contracts[0], "conId", None)
177
+ result = [f"ConID for {symbol}: {conid}"]
178
+ if len(contracts) > 1:
179
+ result.append(
180
+ f"\nNote: Found {len(contracts)} contracts. Using first one."
181
+ )
182
+ result.append("All ConIDs found:")
183
+ for i, c in enumerate(contracts, 1):
184
+ if c is None:
185
+ continue
186
+ result.append(
187
+ f" {i}. {getattr(c, 'conId', '')} "
188
+ f"({getattr(c, 'exchange', '')}, {getattr(c, 'currency', '')})"
189
+ )
190
+ return "\n".join(result)
191
+ except Exception as e: # pragma: no cover
192
+ return f"Error converting ticker to conid: {e}"
193
+
194
+ @self.server.tool(description="Retrieve historical market data")
195
+ async def get_historical_data(
196
+ symbol: Annotated[str, "Stock symbol or conid"],
197
+ duration: Annotated[str, "Duration (e.g., '1 M', '1 Y', '5 D')"] = "1 M",
198
+ bar_size: Annotated[
199
+ str, "Bar size (e.g., '1 day', '1 hour', '5 mins')"
200
+ ] = "1 day",
201
+ data_type: Annotated[
202
+ str,
203
+ "Data type (TRADES, MIDPOINT, BID, ASK, FEE_RATE, OPTION_IMPLIED_VOLATILITY)",
204
+ ] = "TRADES",
205
+ max_bars: Annotated[
206
+ int,
207
+ Field(description="Maximum number of bars to retrieve", ge=1, le=500),
208
+ ] = 20,
209
+ exchange: str = "SMART",
210
+ currency: str = "USD",
211
+ ) -> str:
212
+ await _ensure_connected()
213
+ contract = _create_contract(symbol, "STK", exchange, currency)
214
+ try:
215
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
216
+ contracts = _flatten_contracts(contracts_raw)
217
+ if not contracts:
218
+ return f"No contract found for {symbol}"
219
+ c = contracts[0]
220
+ bars = await self.ib.reqHistoricalDataAsync(
221
+ contract=c,
222
+ endDateTime="",
223
+ durationStr=duration,
224
+ barSizeSetting=bar_size,
225
+ whatToShow=data_type,
226
+ useRTH=True,
227
+ )
228
+ if not bars:
229
+ return f"No historical data found for {symbol}"
230
+ header = [
231
+ f"Historical data for {symbol} ({getattr(c, 'conId', '')}):",
232
+ (
233
+ f"Duration: {duration}, Bar Size: {bar_size}, "
234
+ f"Data Type: {data_type}"
235
+ ),
236
+ "",
237
+ (
238
+ f"{'Date':<12} {'Open':<10} {'High':<10} {'Low':<10} "
239
+ f"{'Close':<10} {'Volume':<12}"
240
+ ),
241
+ "-" * 70,
242
+ ]
243
+ lines = []
244
+ for bar in bars[-max_bars:]:
245
+ if hasattr(bar.date, "strftime"):
246
+ date_str = bar.date.strftime("%Y-%m-%d") # type: ignore[attr-defined]
247
+ else:
248
+ date_str = str(bar.date)
249
+ lines.append(
250
+ f"{date_str:<12} {bar.open:<10.2f} {bar.high:<10.2f} "
251
+ f"{bar.low:<10.2f} {bar.close:<10.2f} {bar.volume:<12}"
252
+ )
253
+ if len(bars) > max_bars:
254
+ lines.append(
255
+ f"\n... showing last {max_bars} of {len(bars)} total bars"
256
+ )
257
+ return "\n".join(header + lines)
258
+ except Exception as e: # pragma: no cover
259
+ return f"Error getting historical data: {e}"
260
+
261
+ @self.server.tool(
262
+ description="Search for contracts by partial symbol or company name"
263
+ )
264
+ async def search_contracts(
265
+ pattern: Annotated[str, "Search pattern (symbol or company name)"],
266
+ ) -> str:
267
+ await _ensure_connected()
268
+ try:
269
+ results = await self.ib.reqMatchingSymbolsAsync(pattern)
270
+ if not results:
271
+ return f"No contracts found matching '{pattern}'"
272
+ lines = [f"Contracts matching '{pattern}':", ""]
273
+ for i, desc in enumerate(results[:10], 1):
274
+ c = desc.contract
275
+ if c is None:
276
+ continue
277
+ lines.extend(
278
+ [
279
+ f"{i}. {c.symbol} ({c.conId})",
280
+ f" - Security Type: {c.secType}",
281
+ f" - Exchange: {c.primaryExchange or c.exchange}",
282
+ f" - Currency: {c.currency}",
283
+ "",
284
+ ]
285
+ )
286
+ if len(results) > 10:
287
+ lines.append(f"... and {len(results) - 10} more results")
288
+ return "\n".join(lines)
289
+ except Exception as e: # pragma: no cover
290
+ return f"Error searching contracts: {e}"
291
+
292
+ @self.server.tool(description="Retrieve historical news articles")
293
+ async def get_historical_news(
294
+ symbol: Annotated[str, "Stock symbol or conid"],
295
+ start_date: Annotated[str, "Start date (YYYY-MM-DD)"],
296
+ end_date: Annotated[str, "End date (YYYY-MM-DD)"],
297
+ max_count: Annotated[int, "Maximum number of articles to retrieve"] = 10,
298
+ exchange: Annotated[
299
+ str, "Exchange (e.g., SMART, NYSE, NASDAQ, etc.)"
300
+ ] = "SMART",
301
+ currency: Annotated[str, "Currency (e.g., USD, EUR, etc.)"] = "USD",
302
+ ) -> str:
303
+ await _ensure_connected()
304
+ contract = _create_contract(symbol, "STK", exchange, currency)
305
+ try:
306
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
307
+ contracts = _flatten_contracts(contracts_raw)
308
+ if not contracts:
309
+ return f"No contract found for {symbol}"
310
+ c = contracts[0]
311
+ news = await self.ib.reqHistoricalNewsAsync(
312
+ getattr(c, "conId", 0),
313
+ self.news_provider_codes,
314
+ start_date,
315
+ end_date,
316
+ max_count,
317
+ )
318
+ if not news:
319
+ return f"No historical news found for {symbol}"
320
+ lines = [
321
+ f"Historical news for {symbol} ({getattr(c, 'conId', '')}):",
322
+ f"Period: {start_date} to {end_date}",
323
+ "",
324
+ ]
325
+ if isinstance(news, list):
326
+ for i, article in enumerate(news[:max_count], 1):
327
+ lines.extend(
328
+ [
329
+ f"{i}. {getattr(article, 'headline', '')}",
330
+ f" Time: {getattr(article, 'time', '')}",
331
+ f" Provider: {getattr(article, 'providerCode', '')}",
332
+ f" Article ID: {getattr(article, 'articleId', '')}",
333
+ "",
334
+ ]
335
+ )
336
+ return "\n".join(lines)
337
+ except Exception as e: # pragma: no cover
338
+ return f"Error getting historical news: {e}"
339
+
340
+ @self.server.tool(
341
+ description="Retrieve a full news article by ID and provider code"
342
+ )
343
+ async def get_article(
344
+ articleId: Annotated[str, "Article ID returned from historical news"],
345
+ providerCode: Annotated[str, "Provider code returned from historical news"],
346
+ as_markdown: Annotated[
347
+ bool,
348
+ "Attempt to convert XML content to markdown if the article is XML",
349
+ ] = True,
350
+ truncate: Annotated[
351
+ int,
352
+ "Optional max length of returned text (0 for no truncation)",
353
+ ] = 0,
354
+ ) -> str:
355
+ await _ensure_connected()
356
+ try:
357
+ # Prefer async variant if available
358
+ if hasattr(self.ib, "reqNewsArticleAsync"):
359
+ article_obj = await self.ib.reqNewsArticleAsync(providerCode, articleId) # type: ignore[attr-defined]
360
+ else: # pragma: no cover - fallback path
361
+ article_obj = self.ib.reqNewsArticle(providerCode, articleId) # type: ignore[attr-defined]
362
+ if article_obj is None:
363
+ return f"No article content found for {providerCode}:{articleId}"
364
+ raw_text = getattr(article_obj, "articleText", "") or getattr(
365
+ article_obj, "text", ""
366
+ )
367
+ if not raw_text:
368
+ return f"Article {providerCode}:{articleId} has no text content"
369
+ if as_markdown:
370
+ formatted = _xml_to_markdown(raw_text)
371
+ else:
372
+ formatted = raw_text
373
+ if truncate and truncate > 0 and len(formatted) > truncate:
374
+ formatted = formatted[:truncate].rstrip() + "... (truncated)"
375
+ return "\n".join(
376
+ [
377
+ f"Article {articleId} ({providerCode}):",
378
+ "",
379
+ formatted,
380
+ ]
381
+ )
382
+ except Exception as e: # pragma: no cover
383
+ return f"Error retrieving article {providerCode}:{articleId}: {e}"
384
+
385
+ @self.server.tool(description="Retrieve fundamental data for a contract")
386
+ async def get_fundamental_data(
387
+ symbol: Annotated[str, "Stock symbol or conid"],
388
+ report_type: Annotated[
389
+ str,
390
+ (
391
+ "Report type (ReportsFinSummary, ReportsOwnership, "
392
+ "ReportsFinStatements, RESC, CalendarReport)"
393
+ ),
394
+ ] = "ReportsFinSummary",
395
+ exchange: str = "SMART",
396
+ currency: str = "USD",
397
+ ) -> str:
398
+ await _ensure_connected()
399
+ contract = _create_contract(symbol, "STK", exchange, currency)
400
+ try:
401
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
402
+ contracts = _flatten_contracts(contracts_raw)
403
+ if not contracts:
404
+ return f"No contract found for {symbol}"
405
+ c = contracts[0]
406
+ data = await self.ib.reqFundamentalDataAsync(c, report_type)
407
+ if not data:
408
+ return f"No fundamental data found for {symbol}"
409
+ formatted = _xml_to_markdown(data)
410
+ lines = [
411
+ f"Fundamental data for {symbol} ({getattr(c, 'conId', '')}):",
412
+ f"Report Type: {report_type}",
413
+ "",
414
+ formatted,
415
+ ]
416
+ return "\n".join(lines)
417
+ except Exception as e: # pragma: no cover
418
+ return f"Error getting fundamental data: {e}"
419
+
420
+ @self.server.tool(description="Retrieve portfolio positions and details")
421
+ async def get_portfolio(
422
+ account: Annotated[str, "Account name (empty for all accounts)"] = "",
423
+ ) -> str:
424
+ await _ensure_connected()
425
+ try:
426
+ items = self.ib.portfolio(account)
427
+ if not items:
428
+ return "No portfolio items found"
429
+ head = [
430
+ f"Portfolio {'for account ' + account if account else '(all accounts)'}:",
431
+ "",
432
+ (
433
+ f"{'Symbol':<10} {'Position':<10} {'Avg Cost':<12} "
434
+ f"{'Market Value':<15} {'Unrealized PnL':<15}"
435
+ ),
436
+ "-" * 70,
437
+ ]
438
+ rows = [
439
+ f"{it.contract.symbol:<10} {it.position:<10} {it.averageCost:<12.2f} "
440
+ f"{it.marketValue:<15.2f} {it.unrealizedPNL:<15.2f}"
441
+ for it in items
442
+ ]
443
+ return "\n".join(head + rows)
444
+ except Exception as e: # pragma: no cover
445
+ return f"Error getting portfolio: {e}"
446
+
447
+ @self.server.tool(description="Retrieve account summary information")
448
+ async def get_account_summary(
449
+ account: Annotated[str, "Account name (empty for all accounts)"] = "",
450
+ ) -> str:
451
+ await _ensure_connected()
452
+ try:
453
+ vals = await self.ib.accountSummaryAsync(account)
454
+ if not vals:
455
+ return "No account data found"
456
+ by_acc: dict[str, list[Any]] = {}
457
+ for v in vals:
458
+ by_acc.setdefault(v.account, []).append(v)
459
+ lines: list[str] = [
460
+ f"Account Summary {'for ' + account if account else '(all accounts)'}:",
461
+ "",
462
+ ]
463
+ for acc, values in by_acc.items():
464
+ lines.append(f"Account: {acc}")
465
+ lines.append("-" * 40)
466
+ for v in values:
467
+ lines.append(f"{v.tag}: {v.value} {v.currency}")
468
+ lines.append("")
469
+ return "\n".join(lines)
470
+ except Exception as e: # pragma: no cover
471
+ return f"Error getting account summary: {e}"
472
+
473
+ @self.server.tool(description="Retrieve current positions")
474
+ async def get_positions(
475
+ account: Annotated[str, "Account name (empty for all accounts)"] = "",
476
+ ) -> str:
477
+ await _ensure_connected()
478
+ try:
479
+ positions = self.ib.positions(account)
480
+ if not positions:
481
+ return "No positions found"
482
+ head = [
483
+ f"Positions {'for account ' + account if account else '(all accounts)'}:",
484
+ "",
485
+ f"{'Account':<10} {'Symbol':<10} {'Position':<10} {'Avg Cost':<12}",
486
+ "-" * 50,
487
+ ]
488
+ rows = [
489
+ f"{p.account:<10} {p.contract.symbol:<10} {p.position:<10} {p.avgCost:<12.2f}"
490
+ for p in positions
491
+ ]
492
+ return "\n".join(head + rows)
493
+ except Exception as e: # pragma: no cover
494
+ return f"Error getting positions: {e}"
495
+
496
+ @self.server.tool(
497
+ description=(
498
+ "Get detailed contract information including dividends and corporate actions"
499
+ )
500
+ )
501
+ async def get_contract_details(
502
+ symbol: Annotated[str, "Stock symbol or conid"],
503
+ sec_type: str = "STK",
504
+ exchange: str = "SMART",
505
+ currency: str = "USD",
506
+ ) -> str:
507
+ await _ensure_connected()
508
+ contract = _create_contract(symbol, sec_type, exchange, currency)
509
+ try:
510
+ contracts_raw = await self.ib.qualifyContractsAsync(contract)
511
+ contracts = _flatten_contracts(contracts_raw)
512
+ if not contracts:
513
+ return f"No contract found for {symbol}"
514
+ c = contracts[0]
515
+ details_list = await self.ib.reqContractDetailsAsync(c)
516
+ if not details_list:
517
+ return f"No contract details found for {symbol}"
518
+ d = details_list[0]
519
+ lines = [
520
+ f"Contract Details for {symbol} ({getattr(c, 'conId', '')}):",
521
+ "",
522
+ ]
523
+ lines.extend(
524
+ [
525
+ "**Basic Information:**",
526
+ f"- Long Name: {getattr(d, 'longName', '')}",
527
+ f"- Industry: {getattr(d, 'industry', '')}",
528
+ f"- Category: {getattr(d, 'category', '')}",
529
+ f"- Subcategory: {getattr(d, 'subcategory', '')}",
530
+ f"- Market Name: {getattr(d, 'marketName', '')}",
531
+ f"- Trading Hours: {getattr(d, 'tradingHours', '')}",
532
+ f"- Liquid Hours: {getattr(d, 'liquidHours', '')}",
533
+ "",
534
+ "**Financial Information:**",
535
+ f"- Min Tick: {getattr(d, 'minTick', '')}",
536
+ f"- Price Magnifier: {getattr(d, 'priceMagnifier', '')}",
537
+ f"- Market Cap: {getattr(d, 'marketCap', 'N/A')}",
538
+ f"- Shares Outstanding: {getattr(d, 'sharesOutstanding', 'N/A')}",
539
+ ]
540
+ )
541
+ # Dividends if available
542
+ dividends = getattr(d, "dividends", None)
543
+ if dividends:
544
+ lines.append("")
545
+ lines.append("**Recent Dividends:**")
546
+ for div in dividends[:5]:
547
+ if div is not None:
548
+ lines.append(
549
+ f"- {getattr(div, 'date', '')}: ${getattr(div, 'amount', '')} "
550
+ f"({getattr(div, 'currency', '')})"
551
+ )
552
+ return "\n".join(lines)
553
+ except Exception as e: # pragma: no cover
554
+ return f"Error getting contract details: {e}"
555
+
556
+ # Keep references on self to make tools reachable in tests/REPL if needed
557
+ self.lookup_contract = lookup_contract # type: ignore[attr-defined]
558
+ self.ticker_to_conid = ticker_to_conid # type: ignore[attr-defined]
559
+ self.get_historical_data = get_historical_data # type: ignore[attr-defined]
560
+ self.search_contracts = search_contracts # type: ignore[attr-defined]
561
+ self.get_historical_news = get_historical_news # type: ignore[attr-defined]
562
+ self.get_fundamental_data = get_fundamental_data # type: ignore[attr-defined]
563
+ self.get_portfolio = get_portfolio # type: ignore[attr-defined]
564
+ self.get_account_summary = get_account_summary # type: ignore[attr-defined]
565
+ self.get_positions = get_positions # type: ignore[attr-defined]
566
+ self.get_contract_details = get_contract_details # type: ignore[attr-defined]
567
+
568
+ def run(self) -> None:
569
+ """Run the FastMCP stdio server (synchronous)."""
570
+ logging.basicConfig(level=logging.INFO)
571
+ try:
572
+ # FastMCP's run manages its own event loop using anyio.run internally.
573
+ self.server.run()
574
+ finally: # pragma: no cover - disconnect path is runtime-only
575
+ if self.connected:
576
+ self.ib.disconnect()
577
+ self.connected = False
578
+
579
+
580
+ def main() -> None:
581
+ """CLI entry point for running the server over stdio."""
582
+ import argparse
583
+ import os
584
+
585
+ parser = argparse.ArgumentParser(
586
+ description="Interactive Brokers MCP Server (FastMCP)"
587
+ )
588
+ parser.add_argument(
589
+ "--host",
590
+ default=os.getenv("IB_HOST", "127.0.0.1"),
591
+ help="IB Gateway/TWS host (env: IB_HOST)",
592
+ )
593
+ parser.add_argument(
594
+ "--port",
595
+ type=int,
596
+ default=int(os.getenv("IB_PORT", "7497")),
597
+ help="IB Gateway/TWS port (env: IB_PORT)",
598
+ )
599
+ parser.add_argument(
600
+ "--client-id",
601
+ type=int,
602
+ default=int(os.getenv("IB_CLIENT_ID", "1")),
603
+ help="Client ID (env: IB_CLIENT_ID)",
604
+ )
605
+ args = parser.parse_args()
606
+
607
+ server = IBMCPServer(args.host, args.port, args.client_id)
608
+ server.run()
609
+
610
+
611
+ __all__ = ["IBMCPServer", "main"]
612
+
613
+
614
+ if __name__ == "__main__":
615
+ main()
@@ -0,0 +1,73 @@
1
+ [tool.poetry]
2
+ name = "ib-mcp"
3
+ version = "0.2.5"
4
+ description = "Model Context Protocol (MCP) server exposing Interactive Brokers data via ib_async + FastMCP"
5
+ authors = ["David Hellekalek <hellekalek@gmail.com>"]
6
+ license = "BSD-3-Clause"
7
+ readme = "README.md"
8
+ homepage = "https://github.com/Hellek1/ib-mcp"
9
+ repository = "https://github.com/Hellek1/ib-mcp"
10
+ documentation = "https://github.com/Hellek1/ib-mcp#readme"
11
+ keywords = ["interactive-brokers", "ib", "ibapi", "tws", "asyncio", "mcp", "fastmcp", "llm"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Topic :: Office/Business :: Financial :: Investment",
16
+ "License :: OSI Approved :: BSD License",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ # Use flat layout (package lives at project root)
24
+ packages = [ { include = "ib_mcp" } ]
25
+
26
+ # Include type information marker
27
+ include = ["ib_mcp/py.typed"]
28
+
29
+ [tool.poetry.scripts]
30
+ ib-mcp-server = "ib_mcp.server:main"
31
+
32
+ [tool.poetry.dependencies]
33
+ python = ">=3.12,<4.0"
34
+ # Core runtime deps
35
+ ib_async = "^2.0.1"
36
+ fastmcp = ">=2.11.0"
37
+ # Low-level MCP protocol lib (kept separate; fastmcp currently builds on it)
38
+ defusedxml = ">=0.7.1"
39
+
40
+ # (Optional) pin pydantic major for stability if fastmcp upgrades; uncomment if needed
41
+ # pydantic = "^2.8"
42
+
43
+ [tool.poetry.group.dev.dependencies]
44
+ pytest = ">=8.0"
45
+ pytest-asyncio = ">=0.23"
46
+ mypy = ">=1.11.0"
47
+ ruff = ">=0.5.0"
48
+ pre-commit = ">=3.8.0"
49
+
50
+ [tool.mypy]
51
+ ignore_missing_imports = true
52
+ check_untyped_defs = true
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+ target-version = "py312"
57
+
58
+ [tool.ruff.lint]
59
+ select = ["E", "F", "I", "B", "UP", "ANN", "S", "ISC", "W"]
60
+ ignore = []
61
+
62
+ [tool.ruff.format]
63
+ quote-style = "double"
64
+ indent-style = "space"
65
+ skip-magic-trailing-comma = false
66
+
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ pythonpath = ["."]
70
+
71
+ [build-system]
72
+ requires = ["poetry-core>=1.9.0"]
73
+ build-backend = "poetry.core.masonry.api"