mt5ctl
by Chust-dev
README.md
<div align="center">
# 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](https://modelcontextprotocol.io):
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](https://github.com/ak40u/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`](docs/ANALISIS-VIABILIDAD.md)
> for the feasibility analysis and [`docs/PORT-TODO.md`](docs/PORT-TODO.md) for the
> remaining MT5-specific work. Not yet usable against a real MT5 terminal.
[](https://github.com/Chust-dev/mt5ctl/actions/workflows/ci.yml)
[](https://pypi.org/project/mt5ctl/)
[](https://www.python.org/)
[](https://modelcontextprotocol.io)
[](LICENSE)
</div>
---
## 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 `init` → `list` → `doctor` commands let you set up and verify everything
**before** wiring an MCP client:
```bash
# 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 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 → 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 (`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.
## Install
The fastest path needs no clone and no global install — [`uv`](https://docs.astral.sh/uv/)
runs `mt5ctl` straight from the repo and fetches a matching Python itself:
```bash
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`:
```bash
uv tool install mt5ctl
# or
pipx install mt5ctl
```
For development:
```bash
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:
```bash
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`](examples/terminals.example.yaml) for the full
schema and [`docs/configuration.md`](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
MetaTrader 5 headless (Wine + Xvfb + `systemd`) so `mt5ctl` has something to drive:
- **[Ubuntu / Linux server](docs/install-linux-ubuntu.md)** — 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](docs/install-windows-wsl.md)** — 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):
```bash
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}`):
```json
{
"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`):
```json
{
"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`](docs/tools.md).
## CLI
The subcommands **mirror the MCP tool surface**, so you can operate — and script —
the whole farm without an MCP client:
```bash
# 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]
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
```
```bash
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.
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`](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 `shred`s 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](docs/the-32-terminal-limit.md)** —
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
```bash
ruff check src tests # lint
mypy # type-check (strict)
pytest # tests
```
See [`docs/architecture.md`](docs/architecture.md) for the module boundaries.
## License
MIT © Pavel Volkov. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues