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). Seedocs/ANALISIS-VIABILIDAD.mdfor the feasibility analysis anddocs/PORT-TODO.mdfor the remaining MT5-specific work. Not yet usable against a real MT5 terminal.
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."
The init → list → doctor 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 mt5ctlThen 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.
- Per-terminal connection detection — attributes established broker sockets
to each terminal's
systemdcgroup, 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
systemdfor automatic reconnection on every restart. - Idempotent strategy deploy — kubectl-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 → bashgauntlet. - Live-trading guardrails — terminals tagged
env: livereject mutating operations unless you passconfirm=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.
┌────────────┐ 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 (models → config → ssh → scripts → deploy →
operations/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.
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 serverNo 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 mt5ctlFor 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.
Copy the example registry and fill in your real hosts and terminals:
mkdir -p ~/.config/mt5ctl
cp examples/terminals.example.yaml ~/.config/mt5ctl/terminals.yamlThe 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
.gitignoreexcludesterminals.yaml.
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),
systemdunits, 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.
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 mt5ctlOr 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
mt5ctlpersistently (uv/pipx)? Replacecommand/argswith just"command": "mt5ctl".
| 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.
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.
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.
- Mutations on
env: liveterminals require explicitconfirm=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;
mt5ctlstores no credentials of its own. - During
mt5_loginthe password is embedded in the base64-framed script handed tossh, so it is briefly visible in the local process list to your own user. On the remote side it is written only to a freshmktempconfig (mode 600) that a cleanup trapshreds on any exit path. On POSIX, the local secrets file is rejected if it is readable by group/other.
- 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 — whatmt5ctldrives — sidesteps it entirely.
ruff check src tests # lint
mypy # type-check (strict)
pytest # testsSee docs/architecture.md for the module boundaries.
MIT © Pavel Volkov. See LICENSE.