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.

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:

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:

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:

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:

  1. When to use it, the intent, not the mechanism. "Use this when the analysis needs price history" beats "returns candles".
  2. When not to use it, the upper bound on count, the warning about expensive calls. Models over-call tools that look cheap.
  3. Units and formats, the exact spelling of symbols, the valid timeframe strings, whether a quantity is lots, units or currency.
  4. 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:

  1. Start the server under Inspector.
  2. 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.
  3. 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.
  4. 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:

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.