Skip to content

Chust-dev/mt5ctl

Repository files navigation

mt5ctl

An MCP server for operating headless MetaTrader 5 terminals — over SSH, from your agent.

Manage MetaTrader 5 terminals running under Wine + systemd on remote hosts (native Linux or WSL2) entirely through the Model Context Protocol: check status, read logs, capture screenshots, control the systemd lifecycle, and perform the tricky headless first-login — all as clean, typed tools.

⚠️ Port in progress. mt5ctl is a port of mt4ctl (MIT, © Pavel Volkov) from MetaTrader 4 to MetaTrader 5. The tool's own identity is already renamed, but the runtime logic still speaks MT4 (data paths, file encodings, log phrasing). See docs/ANALISIS-VIABILIDAD.md for the feasibility analysis and docs/PORT-TODO.md for the remaining MT5-specific work. Not yet usable against a real MT5 terminal.

CI PyPI Python MCP License: MIT


Why

Algo traders increasingly run MetaTrader 4 headless on Linux — Wine under Xvfb, supervised by systemd, no GUI. That's great for uptime and terrible for day-to-day operations: every "is it connected?", "restart that one", or "log this new account in" turns into a fragile chain of ssh → (Windows cmd → wsl) → bash → systemctl → wine, with quoting hazards at every hop.

mt5ctl collapses that chain into a handful of MCP tools. Point it at a registry of your hosts and terminals, wire it into Claude (or any MCP client), and operate the whole farm conversationally:

"Which demo terminals are down?" · "Restart demo2." · "Log demo2 into account 1000002 on ExampleBroker-Demo." · "Screenshot the live terminal so I can see the AutoTrading state."

Quickstart (5 minutes)

The initlistdoctor commands let you set up and verify everything before wiring an MCP client:

# 1. write a starter registry, then fill in your hosts + terminals
uvx mt5ctl init                 # creates ~/.config/mt5ctl/terminals.yaml
$EDITOR ~/.config/mt5ctl/terminals.yaml

# 2. verify — offline, then over SSH (no MCP client needed)
uvx mt5ctl list                 # confirms the registry parses
uvx mt5ctl doctor               # checks SSH, remote tools, units, data dirs

# 3. wire into Claude Code
claude mcp add --scope user mt5ctl \
  --env MT5CTL_CONFIG="$HOME/.config/mt5ctl/terminals.yaml" \
  -- uvx mt5ctl

Then ask Claude: "Use mt5_list to show my configured terminals," then "mt5_status," and "mt5_doctor" if anything looks off. Full setup and other clients are below.

Features

  • Per-terminal connection detection — attributes established broker sockets to each terminal's systemd cgroup, so terminals sharing a host (and a Wine prefix) are reported independently — not guessed from a host-wide count.
  • Headless first-login — automates the one-time bootstrap a migrated terminal needs (MetaTrader's saved password is machine-bound), then hands control back to systemd for automatic reconnection on every restart.
  • Idempotent strategy deploykubectl-apply for one terminal: push a local bundle of charts + experts and reconcile a terminal to it, touching only what mt5ctl deployed (foreign files like a watchdog's chart stay untouched), with a backup-and-restore-on-failure apply and a polling, report-only health verify that waits out the broker reconnect instead of guessing from a single snapshot.
  • Native and WSL2 hosts — one registry, two execution models; commands are base64-shipped so nothing breaks in the cmd.exe → wsl.exe → bash gauntlet.
  • Live-trading guardrails — terminals tagged env: live reject mutating operations unless you pass confirm=true.
  • Concurrent status — hosts are polled in parallel via asyncio.
  • Secrets stay secret — passwords resolve from arg → env → secrets file, are never logged, and the transient remote login config is shred-ed after use.

How it works

┌────────────┐   MCP/stdio   ┌──────────────────┐
│ MCP client │ ────────────► │     mt5ctl       │
│ (Claude…)  │               │  FastMCP server  │
└────────────┘               └────────┬─────────┘
                                       │ asyncio SSH (base64-framed)
                 ┌─────────────────────┼─────────────────────┐
                 ▼                                           ▼
        ┌─────────────────┐                        ┌──────────────────┐
        │  native Linux   │                        │  Windows + WSL2  │
        │  sudo systemctl │                        │  wsl -u root --  │
        ├─────────────────┤                        ├──────────────────┤
        │ mt5-live-main…  │  systemd units running │ mt5-demo1…       │
        │ wine terminal.exe (Xvfb display)         │ wine terminal.exe│
        └─────────────────┘                        └──────────────────┘

A thin, typed core (modelsconfigsshscriptsdeployoperations/login) sits under the server adapter, so the logic is testable without a network and the MCP layer stays a one-line-per-tool shell.

Install

The fastest path needs no clone and no global install — uv runs mt5ctl straight from the repo and fetches a matching Python itself:

uvx mt5ctl   # runs the stdio server

No uv yet? curl -LsSf https://astral.sh/uv/install.sh | sh — or skip it and use the pipx path below.

Prefer a persistent mt5ctl command? Install it with uv or pipx:

uv tool install mt5ctl
# or
pipx install mt5ctl

For development:

git clone https://github.com/Chust-dev/mt5ctl.git && cd mt5ctl
python -m venv venv && source venv/bin/activate
pip install -e ".[dev]"

The server machine needs either uv or Python 3.11+, plus SSH access to your hosts. The remote hosts need the usual tools mt5ctl shells out to: systemctl, ss, getent, (for screenshots) imagemagick/scrot + xdotool, and (for deploy/adopt) GNU tar + a sha256 tool.

Configure

Copy the example registry and fill in your real hosts and terminals:

mkdir -p ~/.config/mt5ctl
cp examples/terminals.example.yaml ~/.config/mt5ctl/terminals.yaml

The registry is resolved from MT5CTL_CONFIG, then ~/.config/mt5ctl/terminals.yaml, then ./terminals.yaml. See examples/terminals.example.yaml for the full schema and docs/configuration.md for details.

Keep your populated registry private. It maps your accounts and infrastructure. The default .gitignore excludes terminals.yaml.

Setting up terminal hosts

mt5ctl manages terminals; it doesn't install them. To stand up a host that runs MT4 headless (Wine + Xvfb + systemd) so mt5ctl has something to drive:

  • Ubuntu / Linux server — Wine, the Xvfb + window-manager display, fonts (incl. the real Wingdings the MT4 smiley needs), systemd units, and the one-time headless login.
  • Windows via WSL2 — the same stack inside WSL2, plus enabling WSL + systemd, copying fonts from the Windows C: drive, boot autostart, and the WSL-specific gotchas.

Connect to an MCP client

Claude Code — one command wires it up (user scope = available in every project):

claude mcp add --scope user mt5ctl \
  --env MT5CTL_CONFIG="$HOME/.config/mt5ctl/terminals.yaml" \
  -- uvx mt5ctl

Or commit a project .mcp.json to share with a team (Claude Code expands ${HOME}):

{
  "mcpServers": {
    "mt5ctl": {
      "command": "uvx",
      "args": ["mt5ctl"],
      "env": { "MT5CTL_CONFIG": "${HOME}/.config/mt5ctl/terminals.yaml" }
    }
  }
}

Claude Desktop — Settings → Developer → Edit Config (claude_desktop_config.json), same shape but use an absolute config path (Desktop does not expand ${HOME}), and an absolute command path if uvx is not on the GUI app's PATH (which uvx):

{
  "mcpServers": {
    "mt5ctl": {
      "command": "uvx",
      "args": ["mt5ctl"],
      "env": { "MT5CTL_CONFIG": "/Users/you/.config/mt5ctl/terminals.yaml" }
    }
  }
}

Installed mt5ctl persistently (uv/pipx)? Replace command/args with just "command": "mt5ctl".

Tools

Tool Mutates Description
mt5_list List configured terminals (offline).
mt5_status Per-terminal service state + broker connection + log age.
mt5_logs Tail / grep a terminal's newest log file.
mt5_screenshot Capture a terminal window as PNG.
mt5_control start / stop / restart a unit (live needs confirm).
mt5_login One-time headless login for auto-reconnect (live needs confirm).
mt5_doctor Diagnose registry, SSH, remote tools, units, and data dirs.
mt5_ea_list List the experts (strategies) attached per terminal.
mt5_autotrading AutoTrading master switch + per-EA live-trading status.
mt5_info Terminal build, broker server, and last broker ping.
mt5_deploy Reconcile a terminal to a local strategy bundle (live needs confirm).
mt5_adopt Record an already-running bundle as managed — the brownfield first cutover.
mt5_verify Poll a terminal until it is healthy after a restart (or report the failure).

Full reference: docs/tools.md.

CLI

The subcommands mirror the MCP tool surface, so you can operate — and script — the whole farm without an MCP client:

# setup
mt5ctl init [path]   # write a starter terminals.yaml (default: XDG config path)
mt5ctl list          # list configured terminals (offline)
mt5ctl doctor        # check registry, SSH, remote tools, units, data dirs

# read / inspect
mt5ctl status [terminal]                 # service + broker per terminal (exit 1 if unhealthy)
mt5ctl logs <terminal> [--pattern RE] [--lines N]
mt5ctl ea-list [terminal]                # experts attached per terminal
mt5ctl autotrading [terminal]            # AutoTrading master + per-EA live status
mt5ctl info [terminal]                   # build / broker server / last ping
mt5ctl screenshot <terminal> [--out-dir DIR]

# control / lifecycle (env=live needs --confirm)
mt5ctl control <terminal> {start|stop|restart} [--confirm]
mt5ctl login <terminal> <server> [--account A] [--password P] [--confirm]
mt5ctl verify <terminal> [--timeout SECONDS]                # poll until healthy after a restart
mt5ctl deploy <terminal> <bundle> [--dry-run] [--confirm] [--reset-market-watch]
mt5ctl adopt <terminal> <bundle> [--confirm]                # adopt an already-running farm

mt5ctl serve         # run the MCP stdio server (the default with no subcommand)

Health-oriented commands (status, verify, doctor) exit non-zero when something is unhealthy, so a shell health-check can rely on the exit code rather than grepping the output.

Deploy

Push a local bundle of charts + experts onto a terminal and reconcile it to that desired set — idempotently, touching only what mt5ctl deployed. The bundle mirrors the MT4 layout:

<bundle>/
  profiles/default/<name>.chr        # ready charts (one expert each)
  MQL4/Experts/<folder>/<ea>.ex4     # the experts those charts reference
mt5ctl deploy demo3 ./bundle --dry-run   # preview the add/update/remove/foreign plan
mt5ctl deploy demo3 ./bundle             # apply (env=live terminals need --confirm)

It is apply-only (no selection, lot sizing, chart generation, or compilation — you build the bundle), idempotent (a re-run is a no-op that still verifies health), and managed-subset (foreign files like a watchdog's chart are never touched). The write order is stop → drain → backup → apply → start; after the restart verify polls until the terminal is healthy (report-only — it never reverts), and there is no rollback command — recovery is to re-deploy the previous bundle. Add --reset-market-watch to rebuild the terminal's Market Watch in the stopped window (deletes symbols.sel, backed up first) and cap unbounded symbol carry-over.

Already running strategies on the farm? Take it under management first with mt5ctl adopt <terminal> <bundle> (records the current footprint, changes nothing), then deploy as usual. Full model and caveats: docs/deploy.md.

Security

  • Mutations on env: live terminals require explicit confirm=true.
  • Credentials resolve from argument → MT5CTL_PASSWORD_<account> → secrets file; they are never written to logs and the transient remote login config is shredded after use.
  • All remote execution goes through your existing SSH config and key-based auth; mt5ctl stores no credentials of its own.
  • During mt5_login the password is embedded in the base64-framed script handed to ssh, so it is briefly visible in the local process list to your own user. On the remote side it is written only to a fresh mktemp config (mode 600) that a cleanup trap shreds on any exit path. On POSIX, the local secrets file is rejected if it is readable by group/other.

Deep dive

  • The MT4 "32 terminals per Windows user" limit — reproducing the cap on a clean box, locating the exact kernel object that enforces it (a per-instance Mutant in the session-local \Sessions\<id>\BaseNamedObjects), and why running headless under Wine on Linux — what mt5ctl drives — sidesteps it entirely.

Development

ruff check src tests      # lint
mypy                      # type-check (strict)
pytest                    # tests

See docs/architecture.md for the module boundaries.

License

MIT © Pavel Volkov. See LICENSE.

About

MCP server for operating headless MetaTrader 5 terminals over SSH (Wine + systemd). Port of mt4ctl to MT5 — work in progress.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages