Update, 16 September 2026. MetaTrader 5 Build 6060 (released 23 July 2026) added native MCP support and a built-in AI Assistant, with no setup required. Everything below still works and remains accurate for the third-party servers, but you may not need them. Read what shipped and when to use it instead first.
Our beginner guide covered what the Model Context Protocol is and how to wire an existing MetaTrader 5 server into an AI client so the model can read your account and place trades. This article assumes you read that one and decided the off-the-shelf server is not enough. You want your own.
Here is what you are actually going to build: a local MCP server, roughly 676 lines of Python, split into four layers, exposing 14 tools to any MCP-speaking client. The shape below follows the reference build walkthrough published on MQL5 (Connecting AI agents to MetaTrader 5 via MCP), which is also the fastest way to see the finished code before writing your own. It runs on the same Windows machine as your MT5 terminal, talks to the terminal through the official MetaTrader5 package, and speaks to the AI over stdout/stdin rather than over the network. No Flask. No HTTP server. No Docker. No ports.
And here is what it is not. It is not a strategy. It contains no signal, no forecast, no edge. It is plumbing, the pipe between a language model that decides things and a terminal that executes them. Every source of profit in your system lives somewhere else. If you remember one sentence from this article, make it that one, because the failure mode of this project is people building the pipe and then expecting the pipe to be profitable.
Why build your own instead of installing one
There are existing MCP servers for MT5, and for reading data they are perfectly good. They expose account balance, symbol metadata, historical candles and live ticks, and they do it competently. If your only goal is to let a model look at your charts, install one and stop reading. There is no prize for reimplementing it.
The case for building your own starts the moment an LLM is allowed to place an order.
- Risk limits live in your code, not in a prompt. An off-the-shelf server gives the model a
place_ordertool with whatever parameters its author chose to expose. If you want a maximum lot size, a daily loss ceiling, or a symbol allowlist, you cannot add them without forking the project. When real money moves on a model's decision, you want the reject logic in code you wrote and code you can read. A system prompt that says "never risk more than 1%" is a suggestion. Aif lots > MAX_LOTS: raiseis an enforcement. - You choose which tools exist at all. Twenty generic tools invite the wrong call; five sharp tools mapped onto your workflow invite correct ones. Tool inventory is a design decision, and it is yours.
- You own the schemas and the descriptions. The model does not read your source code. It reads tool names, parameter names, parameter types, and docstrings, that is the entire surface it reasons over. If those are vague, the model will call the right function with the wrong arguments and you will spend an evening debugging a prompt problem you mistook for a data problem.
- You control versioning. Brokers change symbol suffixes, contract sizes and margin rules. When a dependency you do not control breaks, you wait. When your own wrapper breaks, you fix it in ten minutes.
Install someone else's server if you are reading. Build your own if you are executing.
The four-layer architecture
The reference build separates concerns deliberately. The layering is not decoration, it is what lets you unit-test the parts that talk to MT5 without starting an MCP server or connecting an agent, and what lets you swap the MCP framework later without touching business logic.
mt5-mcp-server/
├── server.py # Layer 4, MCP tool registration
├── mt5client.py # Layer 2, connection + shared utilities
├── constants.py # Layer 1, string <-> numeric mappings
├── handlers/
│ ├── account.py # Layer 3, business logic
│ ├── symbols.py
│ ├── positions.py
│ ├── orders.py
│ └── history.py
├── config.json
└── requirements.txt
Layer 1: Constants
The MetaTrader5 Python API speaks in integers. TIMEFRAME_H1 is 16385. ORDER_TYPE_BUY is 0. Fill policies and time-in-force rules are likewise enumerated numbers. Humans and language models both work in strings: "H1", "BUY", "IOC", "GTC".
constants.py holds the mapping dictionaries that translate between the two worlds. It is a thin file and an easy one to skip, which is exactly why it matters: the moment conversion logic is scattered, every handler grows its own slightly different version of it, and one of them gets the fill policy wrong. Centralise it once, import it everywhere.
Layer 2: The MT5 client wrapper
mt5client.py owns the connection and the utilities every handler needs. Four responsibilities:
- Initialise the MT5 connection with timeout protection, see the next section, this is the part that bites people.
- Ensure the terminal is connected and logged in before each operation, so no handler has to think about connection state.
- Convert MT5's named tuples into plain Python dicts. The native return types are not JSON-serialisable and MCP is JSON-RPC all the way down.
- Build properly formatted order requests from human-readable parameters, so
"BUY"and"IOC"become the integer codes MT5 expects.
Layer 3: Handlers
Five modules with the actual business logic: account queries, symbol and market data, position management, order management, trade history. A handler takes plain Python parameters, calls the MT5 API, and returns a dict. Nothing else.
There are no framework dependencies here and no HTTP concepts. That is the point. You can import handlers.positions in a REPL and test it against a live terminal without a language model anywhere in the loop, which is how you should develop it.
Layer 4: The MCP server
server.py registers 14 tools using the FastMCP framework. Each tool is a thin wrapper: check the MT5 connection, delegate to the appropriate handler, return the result. The wrappers carry almost no logic by design, their real content is the names and docstrings, which is what the model actually reasons over.
Setting up
You need Python 3.10 or newer on Windows. Two dependencies, both installed with pip install -r requirements.txt:
MetaTrader5, the official MetaQuotes Python package, documented here. It communicates with the terminal through a COM interface and provides everything from candlestick history to order placement.fastmcp, the framework for building the server (gofastmcp.com). It handles the JSON-RPC protocol, tool registration, automatic parameter schema generation from your type hints, and the stdio transport. You do not write protocol code.
Configuration lives in config.json next to the code:
{
"mt5_path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe",
"login": 12345678,
"password": "your_password",
"server": "YourBroker-Server",
"timeout": 60
}
Every field is optional, and the behaviour when a field is missing is worth understanding:
mt5_path, if null, the library auto-finds the terminal. That works cleanly with a single installation; with several terminals installed you want to be explicit.login,password,server, if null, the server assumes the terminal is already logged in. This is usually true if MT5 is open on your desktop, and it is the configuration most people end up with.timeout, in seconds, and it bounds how long you are willing to wait for an MT5 operation before giving up.
Store it as plain JSON and keep it out of version control. More on that in the guardrails section.
Talking to the terminal safely
Here is the first genuinely unpleasant thing about this stack: the MetaTrader5 Python library can hang. Not raise, hang. The terminal is a separate process with its own event loop, and the Python side blocks waiting on it. If the terminal is starting up, updating, showing a modal dialog, or simply unresponsive, your call never returns and your MCP server stops answering the model forever.
The fix is to never call into MT5 directly. Wrap every call in a thread with a timeout:
def _run_with_timeout(fn, timeout=INIT_TIMEOUT):
"""Run fn() in a thread with a timeout. Returns None if timed out."""
result = [None]
def _worker():
result[0] = fn()
t = threading.Thread(target=_worker, daemon=True)
t.start()
t.join(timeout)
# If the thread is still alive after timeout, the MT5 call is wedged.
# Abandon it (daemon thread) and report failure instead of blocking.
if t.is_alive():
return None
return result[0]
Two details matter. The thread is a daemon, so a wedged call cannot prevent your server process from exiting. And the result container is a mutable list, so the worker writes into it without scoping tricks. A None return then means explicitly "the terminal did not answer in time". A handler can surface that as an error rather than a silent empty dataset.
The second detail is config discovery. If you resolve config.json relative to the current working directory, the server breaks the moment a client launches it from somewhere else. And MCP clients do exactly that. Resolve it relative to the script instead:
_CONFIG_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config.json"
)
If the file is missing, return an empty dict and let MT5 fall back to its defaults. A missing config should degrade, not crash, the terminal is often already logged in, and the server is much more useful starting up in reduced mode than not starting at all.
Registering tools with FastMCP
Tool declaration is almost aggressively simple: decorate a function, let FastMCP read its signature. Parameter names, type hints and defaults become the JSON schema the model receives. The docstring becomes the tool description. A representative tool from the reference server:
@mcp.tool()
def get_rates(symbol: str, timeframe: str = "H1", count: int = 200) -> dict:
"""Fetch historical OHLCV candles for a symbol.
Use this whenever the analysis needs price history: support and
resistance levels, moving averages, volatility, session ranges.
Prefer H1 or H4 for intraday structure, D1 for regime context.
Avoid counts above 5000, large pulls are slow and rarely needed.
Args:
symbol: Instrument name exactly as the broker lists it, e.g. "EURUSD".
timeframe: One of M1, M5, M15, M30, H1, H4, D1, W1, MN.
count: Number of most recent candles to return (default 200).
Returns:
{"symbol": str, "timeframe": str,
"bars": [{"time", "open", "high", "low", "close", "tick_volume"}, ...]}
"""
ensure_connected()
return handlers.symbols.get_rates(symbol, timeframe, count)
Look at how much of that function is docstring. That is not padding: it is the entire interface the model sees. The model never reads your handler and has no idea that count is expensive above a few thousand bars. It knows only what the description tells it.
This is the part of MCP server development that most people underestimate. Tool descriptions are prompt engineering for the tool layer. They are how the model decides when to call your function, which arguments to pass, and when to leave it alone. Getting them right matters more than almost any implementation detail, because a correct tool that is described badly will be called incorrectly or not at all.
In practice that means each description should say four things:
- When to use it, the intent, not the mechanism. "Use this when the analysis needs price history" beats "returns candles".
- When not to use it, the upper bound on
count, the warning about expensive calls. Models over-call tools that look cheap. - Units and formats, the exact spelling of symbols, the valid timeframe strings, whether a quantity is lots, units or currency.
- The shape of the return value, so the model can plan the next call without a trial-and-error round trip.
The reference server's 14 tools cover the surface you would expect: historical rates, account information (balance, equity, margin), symbol listing and metadata, real-time tick prices, and the order side, place, modify, close, list open positions, and query trade history. Every one of them follows the same pattern: thin wrapper, connection check, delegate to a handler, return a dict.
Testing with MCP Inspector
Do not connect a live agent to a brand-new server. The standard way to exercise an MCP server directly is MCP Inspector, which connects to your server, lists every registered tool with its generated schema, and lets you call them with hand-written parameters.
That gives you the debug loop you actually want:
- Start the server under Inspector.
- Confirm all 14 tools appear, with the parameter types you expect. Wrong types here mean your type hints are wrong, and the model would have received the same wrong schema.
- Call each tool with known-good arguments and inspect the raw JSON. Check that the return shape matches what the docstring promises, if it does not, the model will plan against a lie.
- Call them with bad arguments on purpose. What happens with an unknown symbol, a negative count, a timeframe string that does not exist? An unhandled exception here becomes a confusing tool failure for the agent later.
Only once every tool behaves under Inspector should you wire up a client, Claude Desktop, OpenClaw, Cursor, or VS Code Copilot. Because the transport is stdio, the client simply launches the process and reads the tool list: nothing to expose, no port to open.
Test the whole thing on a demo account first. Not because a demo will catch every bug, it will not, but because the failures you find there cost nothing.
Production guardrails you must add
The reference build is a working server, not a hardened one. Before an agent with order-placement rights touches a funded account, deal with each of these:
- Windows-only. The
MetaTrader5package talks to the terminal through a COM interface, so this runs on Windows and nowhere else. There is no Linux build. Plan your deployment accordingly. - The terminal must be running. Your server is a local companion process, not a hosted service. If MT5 is closed, updating or logged out, everything fails. Treat terminal health as a dependency you monitor, not an assumption.
- Thread safety. MT5 API calls are not thread-safe in general. If two tool calls arrive concurrently. And an agent that discovers your tools in parallel will do exactly that, you get inconsistent state results. Serialise access with a lock around every MT5 call, or run a single worker queue. The simplicity is worth the throughput.
- Order idempotency. This is the one that costs money. An LLM that calls your order tool, gets a timeout, and retries will place the order twice. A timeout does not mean the order failed, it means you do not know. Design for it: use a client-side order ID you supply and can check for existence before placing, or implement a check-before-place pattern that queries open orders and recent history for a matching order first. Make the retry safe rather than hoping the model will not retry.
- Risk limits, server-side. The model does not enforce your risk rules. It has no persistent memory of them and no way to verify them. If you want a maximum lot size, a maximum daily loss, a maximum number of open positions, or a symbol allowlist, you implement it in the handler, before the order reaches the terminal, with a clear error message back to the caller. Recommend this explicitly to yourself: every limit you genuinely care about must be a line of code that can refuse, not a sentence in a prompt that can be talked around.
- Credentials. Your login and password sit in
config.jsonin plain text on the local machine. Do not commit it. Add it to.gitignorebefore you create it, not after. Consider restricting the file's permissions to your user account. Remember that the terminal is already logged in most of the time, so the null-login configuration is both safer and simpler. - Description quality. Tool descriptions determine how often the model calls the right tool with the right arguments. Treat a vague docstring as a bug, because it behaves like one.
What this unlocks
What you have at the end of this is a reliable, controlled, local interface between a language model and your trading terminal. That unlocks a specific and useful set of things: asking questions about your account in plain language and getting exact numbers back, pulling structured market data into an analysis loop without hand-writing the MT5 API every time, monitoring positions and history conversationally, and, with the guardrails above in place, supervised order placement where a human reviews before capital moves.
What it does not do is give you an edge. The server contains no strategy, no model, no signal. It is a better-shaped API, and a better-shaped API makes the strategy work you already do faster to iterate on and easier to test. That is the whole value proposition, and it is a real one, but notice that it is leverage on your work, not a substitute for it.
The fit is research, monitoring, and semi-automated workflows with a human in the loop. The misfit is unattended high-frequency execution, where the layered latency of model, protocol and Python is measured in the wrong units entirely and where an unhandled timeout is not an inconvenience but a duplicated position.
Build it, test it under Inspector, add the guardrails, and run it against a demo account for long enough to be bored by it. That boredom is the signal that it is ready.
Where to run this
None of the techniques above require a specific broker, the Python API works against any MT5 terminal. We run our own testing on Axi, which we use because of its MT5 support, raw-spread Pro account, and the fact that it doesn't restrict DLL imports or WebRequest calls in EAs. If you're already with a broker you like, stay there.
If you do want to try Axi, our introducing-broker code is 4592551. It costs you nothing and supports the research we publish here. Open an Axi Account →
Risk notice. Trading leveraged instruments carries a high risk of loss. Nothing in this article is financial advice, and no strategy or integration described here is guaranteed to be profitable. Test everything on a demo account before risking capital.
AI Trade Lab is an Axi Introducing Broker (IB 4592551). We may receive compensation if you open an account through our links. This does not change what we publish or how we test.
