Skip to main content
Glama
sivaram311

mt5-mcp

by sivaram311

MT5-MCP

MT5-MCP is an MCP (Model Context Protocol) server exposing MetaTrader 5 — market data, live streaming with a durable log, and full order/position lifecycle management — as tools for AI agents (Claude, Cursor, or any MCP-aware client). Runs locally against the MT5 terminal already installed on this machine; MetaTrader 5 is the only backend connector for now.

Current status: Bolts 1–5 shipped. Market data, live streaming, and order/position tools (with a mandatory dry-run-by-default safety layer) are all live. See docs/aidlc/BOLTS.md for what's next (history/audit tools, the auth checkpoint).

Docs

Install

python -m venv .venv
.venv\Scripts\pip install -e ".[dev]"
.venv\Scripts\pytest -q

Copy .env.example to .env and set MT5_PATH to your terminal's terminal64.exe (auto-detection works if the terminal is already running, but an explicit path is more reliable — see diagnostics/FINDINGS.md for why relying on auto-detection is risky).

Run the server

.venv\Scripts\mt5-mcp
# or: .venv\Scripts\python.exe -m mt5_mcp

Default transport is streamable-http, bound to 127.0.0.1:3403 (loopback only, reserved in E:\MyAgent\workflow\ports\REGISTRY.md). This is deliberate — stdio was the original design but was found to hang indefinitely on any tool that calls into MetaTrader5 (root cause never identified despite extensive isolation; see diagnostics/FINDINGS.md). stdio is still available (MT5_MCP_TRANSPORT=stdio) but should not be trusted for anything beyond the ping tool.

Connect an MCP client

Claude Code:

claude mcp add --transport http mt5-mcp http://127.0.0.1:3403/mcp

Claude Desktop / other JSON-config clients, add to the client's MCP server config:

{
  "mcpServers": {
    "mt5-mcp": {
      "url": "http://127.0.0.1:3403/mcp"
    }
  }
}

(Exact key names for HTTP-transport servers vary by client — check that client's docs if this doesn't work as-is.)

The server must already be running (mt5-mcp in a terminal) before the client connects — this project doesn't yet register itself as a background/managed process.

Public access (client on a different machine)

A DEV instance of this server is reverse-proxied at https://mt5-mcp-dev.delena.buzz (nginx + Cloudflare, port 3403 on this host) so a client on another machine can connect without a VPN/SSH tunnel:

claude mcp add --transport http mt5-mcp https://mt5-mcp-dev.delena.buzz/mcp

⚠️ No authentication gate today — explicit, documented decision, not an oversight. Anyone with the URL can call every read-only tool and every execution tool. MT5_MCP_DRY_RUN is a server-side env var on this host, not something a remote caller can flip — but check its current value before relying on it as a safety net: it does not always default to dry-run-on in practice, only in the absence of an explicit override, and this host's .env is not committed (check E:\MyAgent\workflow\ports\REGISTRY.md's :3403 row for the live current status, since it changes and this file doesn't get re-edited on every toggle). When dry-run is on, execution tools always return simulated responses. When it's off, an unauthenticated caller can place/modify/cancel/close a real order on the connected account, bounded only by MT5_MCP_MAX_LOT_SIZE/MT5_MCP_MAX_OPEN_POSITIONS (always enforced regardless of dry-run) and the kill-switch (MT5_MCP_KILL_SWITCH_PATH — create that file to immediately block place_order). Either way, an unauthenticated caller can always read real market data and real open-position state (tickets/volumes/P&L). Full CSS integration is planned but not built — see docs/aidlc/INCEPTION.md's "Auth / security note" and E:\MyAgent\workflow\css\CLIENT-REGISTRY.md's mt5-mcp row (status waived-no-auth) for the reasoning and current status. Treat this URL accordingly until that changes.

If you run your own reverse proxy in front of this server, add its hostname to MT5_MCP_PUBLIC_HOSTNAMES (comma-separated) — FastMCP's built-in DNS-rebinding protection otherwise rejects any Host header besides 127.0.0.1/localhost/::1 with a 421.

Tools

All tools return the standard envelope: {success, error_code, error_message, retryable, request_id, data}.

Market data (read-only)

  • get_historical_ohlcv(symbol, timeframe, from_date?, to_date?, from_bar?, to_bar?, limit?, include_volume?, include_spread?, session_filter?, price_type?, only_completed_bars?)timeframe: M1/M5/M15/M30/H1/H4/D1/W1/MN1. price_type only supports "bid" (or omitted) — ask/mid/last not implemented.

  • get_symbol_info(symbol) — contract size, tick size/value, digits, swaps, margin, current bid/ask.

Live streaming (durable log, not true push — see below)

  • subscribe_live_data(symbol, data_types?)data_types: ["tick"] and/or ["bar"] (fixed M1). Returns a subscription_id.

  • unsubscribe_live_data(subscription_id?, symbol?) — stop by id or all subscriptions for a symbol.

  • get_stream_log(symbol, from_time?, to_time?, data_type?, limit?) — read back logged ticks/bars, including after unsubscribing.

Orders (execution — dry-run by default, see Safety below)

  • place_order(symbol, order_type, side, volume, price?, stop_loss?, take_profit?, comment?, magic_number?, deviation?, client_order_id?)order_type: market/limit/stop. side: buy/sell. price required for limit/stop. client_order_id is logged for traceability only — it does not deduplicate retries.

  • modify_order(ticket, price?, stop_loss?, take_profit?, volume?, expiration?) — modifies a pending order. expiration is not implemented — passing it raises unsupported_parameter.

  • cancel_order(ticket) — cancels a pending order.

Positions

  • get_open_positions(symbol?, magic_number?, comment?) — read-only.

  • modify_position(ticket, stop_loss?, take_profit?) — changes SL/TP on an open position.

  • close_position(ticket, volume?) — full or partial close.

  • close_all_positions(symbol?, side?, magic_number?) — bulk close.

Health

  • ping() — no MT5 connection required, works even if MT5_PATH is wrong.

Not implemented: stop_limit/trailing_stop order types, get_order_history/get_deal_history/get_position_history (Bolt 6), CSS auth (deferred, see INCEPTION.md).

Safety layer (order/position tools)

  • Dry-run is on by default. No real order ever gets placed unless you explicitly set MT5_MCP_DRY_RUN=false in the server's environment. It's an env var, not a tool parameter — an agent can't flip it mid-conversation. Dry-run responses use real current market prices, so they're shaped like a real fill would be.

  • Kill-switch: create a file at MT5_MCP_KILL_SWITCH_PATH (default: <repo root>\MT5_MCP_KILL_SWITCH) and place_order (only — the sole action that creates new exposure) is blocked immediately, independent of the dry-run setting. Delete the file to re-enable. modify_order/cancel_order/modify_position/close_position/close_all_positions are deliberately not blocked by the kill-switch — an emergency stop shouldn't also trap you inside an existing position.

  • Limits: MT5_MCP_MAX_LOT_SIZE (default 0.10) and MT5_MCP_MAX_OPEN_POSITIONS (default 3) — both enforced server-side on place_order, not just documented.

  • Audit log: every order/position attempt — dry-run or real, success or failure — is written to a local SQLite log (MT5_MCP_AUDIT_LOG_DB, default <repo root>\mt5_mcp_audit_log.db), before the tool call returns.

  • Nothing here authorizes a live (non-demo) account. The connector only ever points at whatever MT5_PATH's terminal is logged into — currently OctaFX-Demo. Pointing this at a real account is a distinct, explicit decision this project has not made.

Environment variables

Variable

Default

Purpose

MT5_PATH

auto-detect

Path to terminal64.exe

MT5_MCP_TRANSPORT

streamable-http

stdio | sse | streamable-http — see the stdio warning above

MT5_MCP_HTTP_HOST

127.0.0.1

Bind host for http/sse transports

MT5_MCP_HTTP_PORT

3403

Bind port (reserved in the port registry)

MT5_MCP_PUBLIC_HOSTNAMES

(none)

Comma-separated extra Host headers to accept from a reverse proxy — required for any public hostname to work, see "Public access" above

MT5_MCP_DRY_RUN

true

Must be exactly false to allow real order execution; anything else (including unset/typo) stays dry-run

MT5_MCP_KILL_SWITCH_PATH

<repo root>\MT5_MCP_KILL_SWITCH

If this file exists, place_order is blocked

MT5_MCP_MAX_LOT_SIZE

0.10

Max volume per place_order call

MT5_MCP_MAX_OPEN_POSITIONS

3

Max concurrent open positions before place_order is blocked

MT5_MCP_STREAM_LOG_DB

<repo root>\mt5_mcp_stream_log.db

SQLite path for subscribe_live_data/get_stream_log

MT5_MCP_AUDIT_LOG_DB

<repo root>\mt5_mcp_audit_log.db

SQLite path for the order/position audit trail

All .db files are gitignored — they're local runtime state, not source.