MCP server
The API also speaks the Model Context Protocol at /mcp, in the same process and on the same port as REST. It uses the streamable HTTP transport in stateless mode with JSON responses, so any MCP client that supports remote HTTP servers can connect without a bridge. For clients that only run local stdio servers, the OpenClaw plugin is a stdio-to-HTTP bridge.
The MCP server lives in ibkrapi/mcp_server.py.
Connecting
- URL:
http://localhost:8889/mcp. That is the app root, not the/v1REST prefix. A bare/mcpworks; the app rewrites it to/mcp/so clients that do not follow redirects on POST still connect. - Auth: the same bearer token as REST, enforced by the same middleware. An empty
api_tokenleaves/mcpopen as well. - Host checks: the SDK’s DNS rebinding protection is off, so the endpoint answers on any
Host. Network access control belongs to your proxy or firewall.
curl -s http://localhost:8889/mcp \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ping",
"arguments": {}
}
}'
How tools work
Each tool builds a REST request and sends it to the same FastAPI app in process through httpx.ASGITransport, adding the server’s own API_TOKEN. Validation, pacing, caching and error envelopes are exactly those of the REST API. Every tool returns:
{
"status": 200,
"body": {
"status": "ok",
"connected": true
}
}
status is the HTTP status the REST handler produced and body is its JSON (or {"raw": "<text>"} for a non-JSON body). An error shows up as a 4xx or 5xx status with the usual {"code", "message", "details"} body, not as an MCP protocol error. The in-process call times out after 120 seconds.
Empty strings, 0 for strike, lmt_price, aux_price, recent_bars, client_id and underlying_con_id, and None are dropped before the request is built, so the server-side defaults apply.
Tools
asset_class is one of stock, option, future, cfd, forex, crypto. For forex, symbol is the pair, such as EURUSD.
Market data
| Tool | Parameters | REST call |
|---|---|---|
get_contract | asset_class, symbol, exchange, currency, expiry, strike, right | GET /v1/<class>/{symbol} |
get_quote | same as get_contract | GET /v1/<class>/{symbol}/tick |
get_rates | asset_class, symbol, duration, bar_size, end_date_time, what_to_show, use_rth (default true), exchange, currency, expiry, strike, right | GET /v1/<class>/{symbol}/rates |
get_rates_ta | get_rates parameters plus indicators (required object) and recent_bars | POST /v1/<class>/{symbol}/rates/ta |
get_stock_ticks | symbol, start_date_time, end_date_time, number_of_ticks (default 1000), what_to_show, use_rth (default true), exchange, currency, primary_exchange | GET /v1/stocks/{symbol}/ticks |
Options need expiry, strike and right; futures need expiry and exchange. Note that use_rth defaults to true here while the REST useRTH defaults to false.
Options and futures
| Tool | Parameters | REST call |
|---|---|---|
get_option_chain | symbol, underlying_sec_type (default STK), fut_fop_exchange, underlying_con_id | GET /v1/options/{symbol}/chain |
place_option_combo | body: a combo order object as in POST /options/combo | POST /v1/options/combo |
exercise_option | body: conid, action, quantity, account, override | POST /v1/options/exercise |
get_future_continuous | symbol, exchange, currency | GET /v1/futures/{symbol}/continuous |
list_future_contracts | symbol, exchange, currency, include_expired | GET /v1/futures/{symbol}/contracts |
get_future_continuous and list_future_contracts take exchange as a parameter but do not send it, and both REST routes require it, so they come back with 422. Use request with an exchange query parameter instead.
Orders
| Tool | Parameters | REST call |
|---|---|---|
list_orders | none | GET /v1/orders |
get_order | order_id | GET /v1/orders/{orderId} |
place_order | asset_class, symbol, action, quantity, order_type (default MKT), lmt_price, aux_price, tif (default DAY), expiry, strike, right, outside_rth, account | POST /v1/orders |
cancel_order | order_id | DELETE /v1/orders/{orderId} |
cancel_all_orders | none | DELETE /v1/orders |
place_order has no exchange, currency or conid parameter, so it cannot place a futures order (the REST handler requires exchange) or pick a non-default exchange. Use request with a full order body for those.
Account, positions and history
| Tool | Parameters | REST call |
|---|---|---|
get_account | account | GET /v1/account |
get_account_values | account | GET /v1/account/values |
list_accounts | none | GET /v1/accounts |
list_positions | account | GET /v1/positions |
get_executions | account, client_id, sec_type, symbol, exchange, side, time_after | GET /v1/history/executions |
get_completed_orders | api_only | GET /v1/history/completed_orders |
ping | none | GET /v1/ping |
Discovery and fallback
| Tool | Parameters | What it does |
|---|---|---|
endpoints | none | Lists every route (method, path, summary) from the app’s live OpenAPI schema. |
request | method, path, query, body | Calls any route, such as GET /v1/stocks/AAPL/rates. Use it for anything the typed tools do not cover. |
Live account warning
place_order, place_option_combo, exercise_option, cancel_order, cancel_all_orders, and request with POST or DELETE, act on the real account the gateway is logged into. Their descriptions tell the agent to act only on an explicit user request, and the server-level instructions repeat it. That is text an agent reads, not a technical control: for a hard stop, set READ_ONLY_API=yes in .env.ibkr or give the agent a paper login.