Protocol-level reference for developers building an MCP client against Medved Trader.
If you just want to connect Claude Desktop or a similar assistant, you don't need this page — see the MCP Server help page for setup. This page is for writing a client yourself, or for understanding exactly what a tool call does.
The MCP server, like all of Medved Trader's APIs, requires a Premium subscription. A trial subscription includes it. See Medved Trader APIs for the other APIs.
| Protocol | Model Context Protocol, version 2025-06-18 |
|---|---|
| Transport | Streamable HTTP — JSON-RPC 2.0 request objects over POST |
| Endpoint | POST http://localhost:16240/mcp |
| Server name | medved-trader |
| Content type | application/json both ways |
The port is configurable in Settings → App → API; 16240 is the default. Any path
other than /mcp returns 404. Medved Trader must be running, the MCP server must be
enabled, and at least one API key must exist — with no enabled key the port is never opened.
GET /mcp opens a text/event-stream that is held for 30 seconds and then
closed. It exists so clients that probe for a server-initiated SSE channel don't error out. The server
never pushes events or notifications, so a client can ignore it entirely.
No TLS. The endpoint is plain HTTP. On loopback that's fine; bound to a private network address, the API key is transmitted in the clear. Never expose the port to the internet.
Every request must carry a valid API key, created in Settings → App → API. Send it either way:
Authorization: Bearer <key>
X-API-Key: <key>
A key is the literal string mt_ followed by 43 base64url characters
(A–Z a–z 0–9 - _), 46 in total — for example
mt_kQ7pR2vN8xLmT4wYcF6gHbJ3sD9aZeU1oI5nX0yV7tE. It is an opaque token: nothing is encoded
in it, and the comparison is exact and case-sensitive. Don't parse it, and don't normalise the case.
Authorization is checked first and is what most MCP clients send. A missing, unknown or
disabled key returns HTTP 401 with a JSON-RPC error, code -32001:
{"jsonrpc":"2.0","id":null,"error":{"code":-32001,"message":"Unauthorized: a valid API key is required"}}
Each key carries a set of scopes, chosen by the user in the settings UI. Every tool declares the one scope it requires:
| Scope | Tools |
|---|---|
| MarketData | get_quote, get_history,
get_option_chain, enum_symbols, get_linked_symbol,
get_linked_symbol_colors |
| PortfoliosRead | enum_portfolios,
get_portfolio_symbols |
| PortfoliosWrite | add_symbols_to_portfolio,
remove_symbols_from_portfolio, set_linked_symbol |
| AccountsRead | enum_trading_accounts,
start_trading_account, stop_trading_account,
get_account_balances, get_positions, get_transactions |
| TradingExecute | place_order, modify_order,
cancel_order |
| AppControl | get_layout, set_layout,
save_layout, load_layout, get_layout_commands,
set_layout_commands, update_charts |
| — | get_app_version is unrestricted |
New keys default to read-only: MarketData + PortfoliosRead +
AccountsRead.
tools/list is filtered to the caller's scopes, so the tool set you get
back depends on the key. Build your client against whatever tools/list returns rather than
against this page — a hardcoded assumption that place_order exists will break on a
read-only key. Calling an out-of-scope tool is a tool error, not a protocol error (see below).
| Method | Behavior |
|---|---|
| initialize | Returns protocolVersion, a tools
capability, and serverInfo with the running MT name and version. |
| ping | Empty result. |
| tools/list | All tools with name, description and
inputSchema. Not paginated. |
| tools/call | Invokes a tool. See below. |
| resources/list | Always an empty array — MT exposes no resources. |
| prompts/list | Always an empty array — MT exposes no prompts. |
| notifications/initialized notifications/cancelled | Accepted and ignored. |
A request with no id is treated as a notification: it returns HTTP 202
with an empty body and never produces a JSON-RPC response, including on error.
Results come back as a single text content block whose text is the tool's JSON payload
— you have to parse the string to get the data:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [ { "type": "text", "text": "[{\"symbol\":\"AAPL\",\"last\":214.29, ...}]" } ],
"isError": false
}
}
A full exchange to read a quote:
POST /mcp
Authorization: Bearer mt_kQ7pR2vN8xLmT4wYcF6gHbJ3sD9aZeU1oI5nX0yV7tE
Content-Type: application/json
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_quote","arguments":{"symbols":"AAPL,MSFT"}}}
Two distinct kinds, and clients need to handle both.
Protocol errors use the JSON-RPC error member:
| Code | HTTP | Meaning |
|---|---|---|
| -32001 | 401 | Missing or invalid API key |
| -32700 | 200 | Body is not valid JSON |
| -32601 | 200 | Unknown method |
| -32603 | 200 | Unhandled internal error |
Only the auth failure changes the HTTP status. Once a request is authenticated, protocol errors are reported in the body with a 200 — don't drive your error handling off the status line.
Tool errors are not JSON-RPC errors at all. A tool that rejects its arguments or
can't find what you asked for returns a normal result with isError true and
the message as the text block:
{"jsonrpc":"2.0","id":3,
"result":{"content":[{"type":"text","text":"no such portID"}],"isError":true}}
Unknown tool names come back this way too, not as -32601. So does a scope refusal:
{"jsonrpc":"2.0","id":4,
"result":{"content":[{"type":"text","text":"Not permitted: this API key does not have the TradingExecute scope required by place_order."}],"isError":true}}
Filtering tools/list is not the enforcement — calling a tool you were never
listed is refused here, not silently accepted.
Latest quote for one or more symbols.
| Argument | Type | Description |
|---|---|---|
| symbols req | string | Comma-separated
symbols, or CURRENT (symbols in the active portfolio window), ACTIVE
(symbols across all open portfolio windows), or * (every symbol MT has data
for). |
Returns an array. Symbols MT has no data for are silently skipped, so the result may be shorter than the request — or empty.
[{ "symbol":"AAPL", "timestamp":"2026-07-15T19:42:11Z",
"last":214.29, "bid":214.28, "ask":214.30, "change":1.87,
"volume":42150300, "high":215.10, "low":211.94, "open":212.50,
"bidSize":300, "askSize":100, "lastVolume":200,
"avgTrade":186.4, "numTrades":226091, "low52":164.08, "high52":237.49 }]
Timestamps are UTC, ISO 8601 (yyyy-MM-ddTHH:mm:ssZ), the same as the WebSocket API.
Prices are numbers, rounded to a sensible precision for the price magnitude.
Historical OHLCV bars for one symbol.
| Argument | Type | Description |
|---|---|---|
| symbol req | string | Symbol to fetch. |
| start req | string | UTC, as an ISO 8601 date
(2026-07-15) or date-time (2026-07-15T13:30:00Z), or "0" for the
earliest data available. |
| end req | string | UTC, as an ISO 8601 date or
date-time, or "0" for the latest. A date on its own includes that whole UTC day. |
| frequency req | string | A plain number =
that many minutes, for intraday bars. Otherwise NxD for N-day bars, or 1W,
1M, 1Y. W, M and Y support only 1. |
| regularHoursOnly opt | bool | Exclude extended-hours bars. Intraday only. |
| newestFirst opt | bool | Reverse the result order. Default is oldest first. |
Intraday bars carry a timestamp (the bar's start, UTC, ISO 8601); end-of-day bars
carry a date (yyyy-MM-dd) instead. That's the only shape difference:
// frequency "5" -> { "timestamp":"2026-07-15T13:35:00Z", "open":212.5, "high":213.0,
// "low":212.4, "close":212.9, "volume":184300 }
// frequency "1D" -> { "date":"2026-07-15", "open":212.5, "high":215.1,
// "low":211.9, "close":214.3, "volume":42150300 }
History is served from what MT has stored and from its data source; intraday depth is bounded by your configured max stored days.
| Argument | Type | Description |
|---|---|---|
| symbol req | string | Underlying symbol. |
| force opt | bool | Request a fresh chain from the provider instead of using the cached one. |
[{ "symbol":".AAPL_260918C220", "expiration":"2026-09-18", "strike":220.0, "type":"C" }]
symbol is the option in MT's own symbol format, usable in any other tool;
expiration is yyyy-MM-dd.
This call waits for the data provider to answer, for up to 60 seconds. After that it returns a tool error saying it timed out. Chains can run to thousands of rows.
| Argument | Type | Description |
|---|---|---|
| active opt | bool | Only symbols in currently open portfolio windows. Default returns every symbol MT knows. |
["AAPL","MSFT","SPY"]
| Argument | Type | Description |
|---|---|---|
| color req | string | Link colour group,
from get_linked_symbol_colors. |
{ "symbol":"AAPL", "color":"Red" }
Lists the link colour groups. Active is the group that follows the active window: it
can be read with get_linked_symbol but not set.
Takes no arguments.
["Blue","Green","Red","Cyan", ... ,"Chocolate","Active"]
Sets the symbol for a link colour group, retargeting every window linked to that colour. This changes what the user is looking at.
| Argument | Type | Description |
|---|---|---|
| symbol req | string | Symbol to link. |
| color req | string | Link colour group.
Active cannot be set. |
{ "status":"OK", "symbol":"AAPL", "color":"Red" }
Portfolios come in two kinds, and it matters here. Simple portfolios hold per-row detail
— quantity, cost basis, commission, notes — and each row has an itemID.
Watchlists hold symbols only. Tools that take detail work on simple portfolios; against a
watchlist they either ignore the detail or return an error, as noted.
No arguments.
[{ "portID":"A3F2C1", "name":"Tech" }]
| Argument | Type | Description |
|---|---|---|
| portID req | string | Portfolio ID, or
CURRENT for the active portfolio window. |
| detailed opt | bool | Return per-row detail. Ignored for watchlists, which always return a plain symbol list. |
// detailed: false -> ["AAPL","MSFT"]
// detailed: true -> [{ "sym":"AAPL", "itemID":"K7X2", "qty":100,
// "paid":185.5, "comm":1.0, "note":"core" }]
In detailed mode, zero-valued and empty fields are omitted rather than sent as 0 or "". This is the
call that gives you the itemID values that
remove_symbols_from_portfolio needs.
Adds symbols to a portfolio, updates existing rows, or creates a new portfolio.
| Argument | Type | Description |
|---|---|---|
| portID opt | string | Portfolio to add to, or
CURRENT. Required unless new is set. With new, assigns a
specific ID; omit to have one generated. |
| new opt | bool | Create a new simple
portfolio. Cannot be combined with portID: "CURRENT". |
| name opt | string | Name for a new portfolio, or renames an existing one. |
| symbols opt | string[] | Plain symbols. Ones already present are skipped. |
| items opt | object[] | Detailed rows. Simple portfolios only — an error against a watchlist. |
Each entry in items takes sym, qty, paid,
comm, note, purchaseDate, and optionally itemID.
Supplying an itemID that already exists updates that row in place;
anything else appends a new one.
{ "status":"OK", "portID":"A3F2C1", "created":false }
On an in-place update, every field is written from what you send — fields you omit are set to zero or null, not preserved. Read the row first and send it back whole.
| Argument | Type | Description |
|---|---|---|
| portID req | string | Portfolio ID, or
CURRENT. |
| itemID req | string[] | Row IDs to remove,
from get_portfolio_symbols with detailed: true. |
{ "status":"OK", "portID":"A3F2C1" }
These place real orders against real money. There is no paper-trading or dry-run mode, and no confirmation step in the protocol.
Every tool here takes an accountKey from enum_trading_accounts. All of
them except enum_trading_accounts and start_trading_account require the
account to have been started first.
The read tools need AccountsRead; the three order tools need
TradingExecute on top. A key with only AccountsRead can connect an account
and read everything about it but cannot send an order, and won't see the order tools listed at all.
No arguments. Lists enabled accounts only.
[{ "accountKey":"IB:U1234567", "accountID":"U1234567",
"loginNick":"IB Main", "accountNick":"Margin" }]
Connects an account so balances, positions, transactions and orders become available. Call once before anything else in this group.
| Argument | Type | Description |
|---|---|---|
| accountKey req | string | From
enum_trading_accounts. |
{ "status":"OK", "message":"Trading account IB:U1234567 started" }
This is the one tool that reports failure as {"status":"fail"} inside a normal success
result rather than as a tool error — a broker declining to connect isn't a caller mistake. Check
status, not just isError. If API trading isn't permitted for the account's
data source, that is a tool error.
Disconnects an account. Takes accountKey. Returns {"status":"OK", ...}.
| Argument | Type | Description |
|---|---|---|
| accountKey req | string | |
| forceRefresh opt | bool | Ask the broker for fresh data instead of using MT's cache. |
Returns the broker's balance items. Fields vary by broker — don't hardcode a schema.
Same arguments as get_account_balances. Returns open positions; fields vary by
broker.
| Argument | Type | Description |
|---|---|---|
| accountKey req | string | |
| forceRefresh opt | bool | Ask the broker for fresh data. |
| changesOnly opt | bool | Only what changed
since your last call. Ignored when forceRefresh is set. |
Orders and transactions, including live order status. This is how you find out what happened to an order you sent.
| Argument | Type | Description |
|---|---|---|
| accountKey req | string | |
| order req | object | Order object with a
Legs array of at least one leg. |
Each leg takes Symbol, Action (Buy, Sell, SellShort, BuyToCover),
Quantity, OrderType (Market, Limit, Stop, StopLimit), and
LimitPrice / StopPrice as the type requires. TIF,
Routing, DisplaySize and SpInstructions are optional.
{ "accountKey": "IB:U1234567",
"order": { "Legs": [ { "Symbol":"AAPL", "Action":"Buy", "Quantity":100,
"OrderType":"Limit", "LimitPrice":210.00, "TIF":"Day" } ] } }
{ "status":"OK", "message":"new order sent", "reqID":"3f2a...-...-..." }
"status":"OK" means sent, not accepted and not filled. The call
returns as soon as the order goes to the broker; it does not wait for acknowledgement. Correlate the
returned reqID against get_transactions to find out what actually
happened. A rejected order still returns OK here.
Same arguments as place_order, but order must include
OrderID.
{ "accountKey": "IB:U1234567",
"order": { "OrderID":"00051f7d.6743a1e2", "Legs": [ { "LimitPrice": 211.50 } ] } }
Leg fields you omit are filled in from the existing order — quantity, order type, prices, TIF,
routing and so on — so you only send what changes. Symbol and action always come from the
original order and can't be changed by modifying; cancel and re-place instead. Same send-only
semantics as place_order.
| Argument | Type | Description |
|---|---|---|
| accountKey req | string | |
| orderID req | string[] | One or more order IDs. |
{ "status":"OK", "message":"cancel sent", "reqID":"7b1c...-...-..." }
Send-only, as above. Confirm via get_transactions.
No arguments. Unrestricted — any valid key may call it.
{ "app":"Medved Trader", "version":"1.2.3.4" }
A layout file can carry layout commands: settings that are applied every time the file is loaded
(load_layout, File > Load Layout, the Layout Library), before its windows open. Each is one
string, VERB arg arg. The settings they change are saved and stay changed after another layout
is loaded. Unknown or invalid commands in a file are logged and skipped when it loads. The tools below
check them first and write nothing if any is invalid. The full list of commands is in the
set_layout_commands tool description returned by tools/list. Currently:
| Command | Effect |
|---|---|
| SourcePriority <SourceID> <Never|Low|Normal|High> | Smart Source Priority of a data source the user has set up — the same setting as Settings > Sources. Skipped if the source is not set up. |
| ChartOddLots <On|Off> | Includes or excludes odd-lot trades on intraday charts — the same setting as Settings > Charts “Include Odd Lots on intraday charts”. Affects US stocks and warrants only. Open charts reload when it changes. |
Saving the layout again from the application (File > Save Layout) drops its commands.
get_layout never returns any. set_layout runs commands in its XML once, but
stores nothing.
| Argument | Type | Description |
|---|---|---|
| name | string | Layout name (saved in the user's layouts folder with the
.layout extension) or a full file path. |
| encrypted opt | bool | Encrypt the file instead of saving clear XML. Default false. |
| commands opt | string[] | Layout commands to store in the file. |
Saves the current window layout. Needs AppControl.
{ "name": "Futures", "commands": ["SourcePriority SCHWAB High", "SourcePriority DXFEED Normal"] }
{ "status":"OK", "path":"C:\\...\\Futures.layout", "commands":["SourcePriority SCHWAB High","SourcePriority DXFEED Normal"] }
| Argument | Type | Description |
|---|---|---|
| name | string | Layout name or full file path. The file must exist. |
{ "path":"C:\\...\\Futures.layout", "commands":["SourcePriority SCHWAB High"] }
| Argument | Type | Description |
|---|---|---|
| name | string | Layout name or full file path. The file must exist. |
| commands | string[] | The commands, replacing any the file has. An empty array removes them. |
Rewrites only the commands. The file's windows, the screens it was saved on, and its clear or encrypted
format are kept. It does not run the commands: call load_layout to apply the layout and its
commands. Layout Library files that carry a version header are refused. Needs AppControl.
{ "status":"OK", "path":"C:\\...\\Futures.layout", "commands":["SourcePriority SCHWAB High"] }
| Argument | Type | Description |
|---|---|---|
| symbol opt | string | Only charts showing this
symbol. Omit or "*" for all open charts. |
| drawings opt | bool | Also re-read the charts' drawings from the drawings database. Default false. |
Makes open chart windows recalculate and repaint. It does not download history again. With
drawings, the charts also re-read their drawings (trendlines, boxes, fibs, text and so
on). Use that after changing the drawings database from outside the application: open charts don't
otherwise notice such changes until they are closed and reopened. Needs AppControl.
{ "symbol": "AAPL", "drawings": true }
{ "status":"OK", "charts":2, "symbols":["AAPL"] }
charts counts the chart windows flagged; symbols lists their distinct
symbols. The call returns before the charts refresh — each one acts on its next refresh cycle,
normally within a second.
There is no shutdown tool. MCP does not expose one, and no scope enables one. The WebSocket and HTTP APIs have their own shutdown commands; if you need to close the application programmatically, use those. Application lifetime is deliberately outside what an MCP client can reach.
tools/list reflects the key's
scopes, so the same client sees different tool sets with different keys. Handle a tool simply
not being there.get_option_chain can take up to 60 seconds before it
gives up, and get_history over a long range is not fast.Z; date-only
values are yyyy-MM-dd. Arguments are read as UTC too.isError on every tools/call result — tool
failures come back with HTTP 200 and no JSON-RPC error member.