{"id":51009172,"url":"https://github.com/ak40u/mt4ctl","last_synced_at":"2026-06-21T00:35:54.268Z","repository":{"id":359781955,"uuid":"1247444388","full_name":"ak40u/mt4ctl","owner":"ak40u","description":"MCP server for managing headless MetaTrader 4 terminals over SSH (Wine + systemd) — status, logs, screenshots, lifecycle control, headless login","archived":false,"fork":false,"pushed_at":"2026-05-24T09:36:06.000Z","size":404,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-21T00:35:53.147Z","etag":null,"topics":["devops","mcp","metatrader","model-context-protocol","mt4","python","ssh","systemd","trading","wine"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ak40u.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T10:23:22.000Z","updated_at":"2026-06-01T16:22:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ak40u/mt4ctl","commit_stats":null,"previous_names":["ak40u/mt4ctl"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ak40u/mt4ctl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ak40u%2Fmt4ctl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ak40u%2Fmt4ctl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ak40u%2Fmt4ctl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ak40u%2Fmt4ctl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ak40u","download_url":"https://codeload.github.com/ak40u/mt4ctl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ak40u%2Fmt4ctl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34590214,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["devops","mcp","metatrader","model-context-protocol","mt4","python","ssh","systemd","trading","wine"],"created_at":"2026-06-21T00:35:53.790Z","updated_at":"2026-06-21T00:35:54.263Z","avatar_url":"https://github.com/ak40u.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# mt4ctl\n\n**An MCP server for operating headless MetaTrader terminals — over SSH, from your agent.**\n\nManage MetaTrader 4 terminals running under **Wine + systemd** on remote hosts\n(native Linux *or* WSL2) entirely through the [Model Context Protocol](https://modelcontextprotocol.io):\ncheck status, read logs, capture screenshots, control the systemd lifecycle, and\nperform the tricky **headless first-login** — all as clean, typed tools.\n\n[![CI](https://github.com/ak40u/mt4ctl/actions/workflows/ci.yml/badge.svg)](https://github.com/ak40u/mt4ctl/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/mt4ctl)](https://pypi.org/project/mt4ctl/)\n[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)\n[![MCP](https://img.shields.io/badge/MCP-server-7C3AED)](https://modelcontextprotocol.io)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\n\u003c/div\u003e\n\n---\n\n## Why\n\nAlgo traders increasingly run MetaTrader 4 **headless on Linux** — Wine under\n`Xvfb`, supervised by `systemd`, no GUI. That's great for uptime and terrible for\nday-to-day operations: every \"is it connected?\", \"restart that one\", or \"log this\nnew account in\" turns into a fragile chain of\n`ssh → (Windows cmd → wsl) → bash → systemctl → wine`, with quoting hazards at\nevery hop.\n\n`mt4ctl` collapses that chain into a handful of MCP tools. Point it at a registry\nof your hosts and terminals, wire it into Claude (or any MCP client), and operate\nthe whole farm conversationally:\n\n\u003e *\"Which demo terminals are down?\"* · *\"Restart demo2.\"* ·\n\u003e *\"Log demo2 into account 1000002 on ExampleBroker-Demo.\"* ·\n\u003e *\"Screenshot the live terminal so I can see the AutoTrading state.\"*\n\n## Quickstart (5 minutes)\n\nThe `init` → `list` → `doctor` commands let you set up and verify everything\n**before** wiring an MCP client:\n\n```bash\n# 1. write a starter registry, then fill in your hosts + terminals\nuvx mt4ctl init                 # creates ~/.config/mt4ctl/terminals.yaml\n$EDITOR ~/.config/mt4ctl/terminals.yaml\n\n# 2. verify — offline, then over SSH (no MCP client needed)\nuvx mt4ctl list                 # confirms the registry parses\nuvx mt4ctl doctor               # checks SSH, remote tools, units, data dirs\n\n# 3. wire into Claude Code\nclaude mcp add --scope user mt4ctl \\\n  --env MT4CTL_CONFIG=\"$HOME/.config/mt4ctl/terminals.yaml\" \\\n  -- uvx mt4ctl\n```\n\nThen ask Claude: **\"Use mt4_list to show my configured terminals,\"** then\n**\"mt4_status,\"** and **\"mt4_doctor\"** if anything looks off. Full setup and\nother clients are below.\n\n## Features\n\n- **Per-terminal connection detection** — attributes established broker sockets\n  to each terminal's `systemd` cgroup, so terminals sharing a host (and a Wine\n  prefix) are reported independently — not guessed from a host-wide count.\n- **Headless first-login** — automates the one-time bootstrap a migrated terminal\n  needs (MetaTrader's saved password is machine-bound), then hands control back\n  to `systemd` for automatic reconnection on every restart.\n- **Idempotent strategy deploy** — *kubectl-apply for one terminal*: push a local\n  bundle of charts + experts and reconcile a terminal to it, touching only what\n  mt4ctl deployed (foreign files like a watchdog's chart stay untouched), with a\n  backup-and-restore-on-failure apply and a **polling**, report-only health verify\n  that waits out the broker reconnect instead of guessing from a single snapshot.\n- **Native *and* WSL2 hosts** — one registry, two execution models; commands are\n  base64-shipped so nothing breaks in the `cmd.exe → wsl.exe → bash` gauntlet.\n- **Live-trading guardrails** — terminals tagged `env: live` reject mutating\n  operations unless you pass `confirm=true`.\n- **Concurrent status** — hosts are polled in parallel via `asyncio`.\n- **Secrets stay secret** — passwords resolve from arg → env → secrets file,\n  are never logged, and the transient remote login config is `shred`-ed after use.\n\n## How it works\n\n```\n┌────────────┐   MCP/stdio   ┌──────────────────┐\n│ MCP client │ ────────────► │     mt4ctl       │\n│ (Claude…)  │               │  FastMCP server  │\n└────────────┘               └────────┬─────────┘\n                                       │ asyncio SSH (base64-framed)\n                 ┌─────────────────────┼─────────────────────┐\n                 ▼                                           ▼\n        ┌─────────────────┐                        ┌──────────────────┐\n        │  native Linux   │                        │  Windows + WSL2  │\n        │  sudo systemctl │                        │  wsl -u root --  │\n        ├─────────────────┤                        ├──────────────────┤\n        │ mt4-live-main…  │  systemd units running │ mt4-demo1…       │\n        │ wine terminal.exe (Xvfb display)         │ wine terminal.exe│\n        └─────────────────┘                        └──────────────────┘\n```\n\nA thin, typed core (`models` → `config` → `ssh` → `scripts` → `deploy` →\n`operations`/`login`) sits under the `server` adapter, so the logic is testable\nwithout a network and the MCP layer stays a one-line-per-tool shell.\n\n## Install\n\nThe fastest path needs no clone and no global install — [`uv`](https://docs.astral.sh/uv/)\nruns `mt4ctl` straight from the repo and fetches a matching Python itself:\n\n```bash\nuvx mt4ctl   # runs the stdio server\n```\n\nNo `uv` yet? `curl -LsSf https://astral.sh/uv/install.sh | sh` — or skip it and use\nthe `pipx` path below.\n\nPrefer a persistent `mt4ctl` command? Install it with `uv` or `pipx`:\n\n```bash\nuv tool install mt4ctl\n# or\npipx install mt4ctl\n```\n\nFor development:\n\n```bash\ngit clone https://github.com/ak40u/mt4ctl.git \u0026\u0026 cd mt4ctl\npython -m venv venv \u0026\u0026 source venv/bin/activate\npip install -e \".[dev]\"\n```\n\nThe server machine needs either `uv` or Python 3.11+, plus SSH access to your\nhosts. The remote hosts need the usual tools `mt4ctl` shells out to: `systemctl`,\n`ss`, `getent`, (for screenshots) `imagemagick`/`scrot` + `xdotool`, and (for\ndeploy/adopt) GNU `tar` + a sha256 tool.\n\n## Configure\n\nCopy the example registry and fill in your real hosts and terminals:\n\n```bash\nmkdir -p ~/.config/mt4ctl\ncp examples/terminals.example.yaml ~/.config/mt4ctl/terminals.yaml\n```\n\nThe registry is resolved from `MT4CTL_CONFIG`, then\n`~/.config/mt4ctl/terminals.yaml`, then `./terminals.yaml`. See\n[`examples/terminals.example.yaml`](examples/terminals.example.yaml) for the full\nschema and [`docs/configuration.md`](docs/configuration.md) for details.\n\n\u003e **Keep your populated registry private.** It maps your accounts and\n\u003e infrastructure. The default `.gitignore` excludes `terminals.yaml`.\n\n## Setting up terminal hosts\n\n`mt4ctl` manages terminals; it doesn't install them. To stand up a host that runs\nMT4 headless (Wine + Xvfb + `systemd`) so `mt4ctl` has something to drive:\n\n- **[Ubuntu / Linux server](docs/install-linux-ubuntu.md)** — Wine, the Xvfb +\n  window-manager display, fonts (incl. the real Wingdings the MT4 smiley needs),\n  `systemd` units, and the one-time headless login.\n- **[Windows via WSL2](docs/install-windows-wsl.md)** — the same stack inside\n  WSL2, plus enabling WSL + `systemd`, copying fonts from the Windows C: drive,\n  boot autostart, and the WSL-specific gotchas.\n\n## Connect to an MCP client\n\n**Claude Code** — one command wires it up (user scope = available in every project):\n\n```bash\nclaude mcp add --scope user mt4ctl \\\n  --env MT4CTL_CONFIG=\"$HOME/.config/mt4ctl/terminals.yaml\" \\\n  -- uvx mt4ctl\n```\n\nOr commit a project `.mcp.json` to share with a team (Claude Code expands `${HOME}`):\n\n```json\n{\n  \"mcpServers\": {\n    \"mt4ctl\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mt4ctl\"],\n      \"env\": { \"MT4CTL_CONFIG\": \"${HOME}/.config/mt4ctl/terminals.yaml\" }\n    }\n  }\n}\n```\n\n**Claude Desktop** — Settings → Developer → Edit Config (`claude_desktop_config.json`),\nsame shape but use an **absolute** config path (Desktop does not expand `${HOME}`),\nand an absolute `command` path if `uvx` is not on the GUI app's `PATH` (`which uvx`):\n\n```json\n{\n  \"mcpServers\": {\n    \"mt4ctl\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mt4ctl\"],\n      \"env\": { \"MT4CTL_CONFIG\": \"/Users/you/.config/mt4ctl/terminals.yaml\" }\n    }\n  }\n}\n```\n\n\u003e Installed `mt4ctl` persistently (uv/pipx)? Replace `command`/`args` with just\n\u003e `\"command\": \"mt4ctl\"`.\n\n## Tools\n\n| Tool | Mutates | Description |\n| --- | :---: | --- |\n| `mt4_list` | – | List configured terminals (offline). |\n| `mt4_status` | – | Per-terminal service state + broker connection + log age. |\n| `mt4_logs` | – | Tail / grep a terminal's newest log file. |\n| `mt4_screenshot` | – | Capture a terminal window as PNG. |\n| `mt4_control` | ✓ | `start` / `stop` / `restart` a unit (live needs `confirm`). |\n| `mt4_login` | ✓ | One-time headless login for auto-reconnect (live needs `confirm`). |\n| `mt4_doctor` | – | Diagnose registry, SSH, remote tools, units, and data dirs. |\n| `mt4_ea_list` | – | List the experts (strategies) attached per terminal. |\n| `mt4_autotrading` | – | AutoTrading master switch + per-EA live-trading status. |\n| `mt4_info` | – | Terminal build, broker server, and last broker ping. |\n| `mt4_deploy` | ✓ | Reconcile a terminal to a local strategy bundle (live needs `confirm`). |\n| `mt4_adopt` | ✓ | Record an already-running bundle as managed — the brownfield first cutover. |\n| `mt4_verify` | – | Poll a terminal until it is healthy after a restart (or report the failure). |\n\nFull reference: [`docs/tools.md`](docs/tools.md).\n\n## CLI\n\nThe subcommands **mirror the MCP tool surface**, so you can operate — and script —\nthe whole farm without an MCP client:\n\n```bash\n# setup\nmt4ctl init [path]   # write a starter terminals.yaml (default: XDG config path)\nmt4ctl list          # list configured terminals (offline)\nmt4ctl doctor        # check registry, SSH, remote tools, units, data dirs\n\n# read / inspect\nmt4ctl status [terminal]                 # service + broker per terminal (exit 1 if unhealthy)\nmt4ctl logs \u003cterminal\u003e [--pattern RE] [--lines N]\nmt4ctl ea-list [terminal]                # experts attached per terminal\nmt4ctl autotrading [terminal]            # AutoTrading master + per-EA live status\nmt4ctl info [terminal]                   # build / broker server / last ping\nmt4ctl screenshot \u003cterminal\u003e [--out-dir DIR]\n\n# control / lifecycle (env=live needs --confirm)\nmt4ctl control \u003cterminal\u003e {start|stop|restart} [--confirm]\nmt4ctl login \u003cterminal\u003e \u003cserver\u003e [--account A] [--password P] [--confirm]\nmt4ctl verify \u003cterminal\u003e [--timeout SECONDS]                # poll until healthy after a restart\nmt4ctl deploy \u003cterminal\u003e \u003cbundle\u003e [--dry-run] [--confirm] [--reset-market-watch]\nmt4ctl adopt \u003cterminal\u003e \u003cbundle\u003e [--confirm]                # adopt an already-running farm\n\nmt4ctl serve         # run the MCP stdio server (the default with no subcommand)\n```\n\nHealth-oriented commands (`status`, `verify`, `doctor`) **exit non-zero when\nsomething is unhealthy**, so a shell health-check can rely on the exit code rather\nthan grepping the output.\n\n## Deploy\n\nPush a local **bundle** of charts + experts onto a terminal and reconcile it to\nthat desired set — idempotently, touching only what mt4ctl deployed. The bundle\nmirrors the MT4 layout:\n\n```\n\u003cbundle\u003e/\n  profiles/default/\u003cname\u003e.chr        # ready charts (one expert each)\n  MQL4/Experts/\u003cfolder\u003e/\u003cea\u003e.ex4     # the experts those charts reference\n```\n\n```bash\nmt4ctl deploy demo3 ./bundle --dry-run   # preview the add/update/remove/foreign plan\nmt4ctl deploy demo3 ./bundle             # apply (env=live terminals need --confirm)\n```\n\nIt is **apply-only** (no selection, lot sizing, chart generation, or compilation —\nyou build the bundle), idempotent (a re-run is a no-op that still verifies health),\nand managed-subset (foreign files like a watchdog's chart are never touched). The\nwrite order is **stop → drain → backup → apply → start**; after the restart verify\n**polls** until the terminal is healthy (report-only — it never reverts), and there\nis no rollback command — recovery is to re-deploy the previous bundle. Add\n`--reset-market-watch` to rebuild the terminal's Market Watch in the stopped window\n(deletes `symbols.sel`, backed up first) and cap unbounded symbol carry-over.\n\nAlready running strategies on the farm? Take it under management first with\n`mt4ctl adopt \u003cterminal\u003e \u003cbundle\u003e` (records the current footprint, changes\nnothing), then deploy as usual. Full model and caveats: [`docs/deploy.md`](docs/deploy.md).\n\n## Security\n\n- Mutations on `env: live` terminals require explicit `confirm=true`.\n- Credentials resolve from argument → `MT4CTL_PASSWORD_\u003caccount\u003e` →\n  secrets file; they are never written to logs and the transient remote login\n  config is shredded after use.\n- All remote execution goes through your existing SSH config and key-based auth;\n  `mt4ctl` stores no credentials of its own.\n- During `mt4_login` the password is embedded in the base64-framed script handed\n  to `ssh`, so it is briefly visible in the local process list to your own user.\n  On the remote side it is written only to a fresh `mktemp` config (mode 600) that\n  a cleanup trap `shred`s on any exit path. On POSIX, the local secrets file is\n  rejected if it is readable by group/other.\n\n## Deep dive\n\n- **[The MT4 \"32 terminals per Windows user\" limit](docs/the-32-terminal-limit.md)** —\n  reproducing the cap on a clean box, locating the exact kernel object that enforces\n  it (a per-instance **Mutant** in the session-local `\\Sessions\\\u003cid\u003e\\BaseNamedObjects`),\n  and why running headless under Wine on Linux — what `mt4ctl` drives — sidesteps it\n  entirely.\n\n## Development\n\n```bash\nruff check src tests      # lint\nmypy                      # type-check (strict)\npytest                    # tests\n```\n\nSee [`docs/architecture.md`](docs/architecture.md) for the module boundaries.\n\n## License\n\nMIT © Pavel Volkov. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fak40u%2Fmt4ctl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fak40u%2Fmt4ctl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fak40u%2Fmt4ctl/lists"}