mt4ctl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mt4ctl-0.1.0/.github/workflows/ci.yml +42 -0
- mt4ctl-0.1.0/.github/workflows/release.yml +75 -0
- mt4ctl-0.1.0/.gitignore +35 -0
- mt4ctl-0.1.0/.pre-commit-config.yaml +17 -0
- mt4ctl-0.1.0/CHANGELOG.md +30 -0
- mt4ctl-0.1.0/CONTRIBUTING.md +63 -0
- mt4ctl-0.1.0/LICENSE +21 -0
- mt4ctl-0.1.0/PKG-INFO +266 -0
- mt4ctl-0.1.0/README.md +234 -0
- mt4ctl-0.1.0/docs/architecture.md +76 -0
- mt4ctl-0.1.0/docs/configuration.md +90 -0
- mt4ctl-0.1.0/docs/tools.md +80 -0
- mt4ctl-0.1.0/examples/mcp.json.example +11 -0
- mt4ctl-0.1.0/examples/terminals.example.yaml +52 -0
- mt4ctl-0.1.0/pyproject.toml +84 -0
- mt4ctl-0.1.0/src/mt4ctl/__init__.py +12 -0
- mt4ctl-0.1.0/src/mt4ctl/__main__.py +8 -0
- mt4ctl-0.1.0/src/mt4ctl/auth.py +57 -0
- mt4ctl-0.1.0/src/mt4ctl/config.py +203 -0
- mt4ctl-0.1.0/src/mt4ctl/errors.py +47 -0
- mt4ctl-0.1.0/src/mt4ctl/login.py +171 -0
- mt4ctl-0.1.0/src/mt4ctl/models.py +139 -0
- mt4ctl-0.1.0/src/mt4ctl/operations.py +171 -0
- mt4ctl-0.1.0/src/mt4ctl/py.typed +0 -0
- mt4ctl-0.1.0/src/mt4ctl/scripts.py +168 -0
- mt4ctl-0.1.0/src/mt4ctl/server.py +231 -0
- mt4ctl-0.1.0/src/mt4ctl/ssh.py +120 -0
- mt4ctl-0.1.0/tests/conftest.py +43 -0
- mt4ctl-0.1.0/tests/test_auth.py +60 -0
- mt4ctl-0.1.0/tests/test_config.py +187 -0
- mt4ctl-0.1.0/tests/test_login.py +120 -0
- mt4ctl-0.1.0/tests/test_models.py +55 -0
- mt4ctl-0.1.0/tests/test_operations.py +80 -0
- mt4ctl-0.1.0/tests/test_scripts.py +82 -0
- mt4ctl-0.1.0/tests/test_server.py +113 -0
- mt4ctl-0.1.0/tests/test_ssh.py +65 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
cache: pip
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: pip install -e ".[dev]"
|
|
31
|
+
|
|
32
|
+
- name: Lint (ruff)
|
|
33
|
+
run: ruff check src tests
|
|
34
|
+
|
|
35
|
+
- name: Format check (ruff)
|
|
36
|
+
run: ruff format --check src tests
|
|
37
|
+
|
|
38
|
+
- name: Type-check (mypy)
|
|
39
|
+
run: mypy
|
|
40
|
+
|
|
41
|
+
- name: Test (pytest)
|
|
42
|
+
run: pytest --cov=mt4ctl --cov-report=term-missing
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag a release to publish: git tag v0.1.0 && git push origin v0.1.0
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Verify tag matches project version
|
|
22
|
+
run: |
|
|
23
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
24
|
+
VER=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
25
|
+
echo "tag=$TAG project=$VER"
|
|
26
|
+
test "$TAG" = "$VER" || { echo "::error::tag v$TAG does not match pyproject version $VER"; exit 1; }
|
|
27
|
+
|
|
28
|
+
- name: Build sdist and wheel
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade build twine
|
|
31
|
+
python -m build
|
|
32
|
+
python -m twine check dist/*
|
|
33
|
+
|
|
34
|
+
- uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
publish-pypi:
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
# Must match the environment registered with the PyPI Trusted Publisher.
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/mt4ctl
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write # OIDC token for PyPI Trusted Publishing (no API token needed)
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish to PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
|
|
57
|
+
github-release:
|
|
58
|
+
needs: build
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write # create the GitHub Release and attach artifacts
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: dist
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Create GitHub Release
|
|
69
|
+
env:
|
|
70
|
+
GH_TOKEN: ${{ github.token }}
|
|
71
|
+
run: |
|
|
72
|
+
gh release create "${GITHUB_REF_NAME}" dist/* \
|
|
73
|
+
--repo "${GITHUB_REPOSITORY}" \
|
|
74
|
+
--title "${GITHUB_REF_NAME}" \
|
|
75
|
+
--generate-notes
|
mt4ctl-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# --- private infrastructure: never commit real hosts/accounts/secrets ---
|
|
2
|
+
/terminals.yaml
|
|
3
|
+
terminals.local.yaml
|
|
4
|
+
credentials.json
|
|
5
|
+
*.secret
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
|
|
9
|
+
# --- python ---
|
|
10
|
+
venv/
|
|
11
|
+
.venv/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*.egg-info/
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
.eggs/
|
|
18
|
+
|
|
19
|
+
# --- tooling caches ---
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
25
|
+
coverage.xml
|
|
26
|
+
|
|
27
|
+
# --- local scratch ---
|
|
28
|
+
.cache/
|
|
29
|
+
*.png
|
|
30
|
+
plans/
|
|
31
|
+
|
|
32
|
+
# --- editors / OS ---
|
|
33
|
+
.idea/
|
|
34
|
+
.vscode/
|
|
35
|
+
.DS_Store
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Run `pre-commit install` once; hooks then run on every commit.
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
4
|
+
rev: v0.15.14
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff
|
|
7
|
+
args: [--fix]
|
|
8
|
+
- id: ruff-format
|
|
9
|
+
|
|
10
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
11
|
+
rev: v5.0.0
|
|
12
|
+
hooks:
|
|
13
|
+
- id: end-of-file-fixer
|
|
14
|
+
- id: trailing-whitespace
|
|
15
|
+
- id: check-yaml
|
|
16
|
+
- id: check-added-large-files
|
|
17
|
+
- id: detect-private-key
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-05-23
|
|
10
|
+
|
|
11
|
+
Initial release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- MCP stdio server exposing six tools: `mt4_list`, `mt4_status`, `mt4_logs`,
|
|
16
|
+
`mt4_screenshot`, `mt4_control`, `mt4_login`.
|
|
17
|
+
- YAML registry with validation; supports native Linux and WSL2 hosts.
|
|
18
|
+
- Concurrent, per-terminal status with cgroup-based broker-connection attribution
|
|
19
|
+
(gated on root or matching service user, otherwise reported as unknown).
|
|
20
|
+
- Headless first-login bootstrap with process-group-scoped cleanup and
|
|
21
|
+
credential shredding via a cleanup trap.
|
|
22
|
+
- Live-trading guardrails (`confirm=true`) on mutating operations.
|
|
23
|
+
- Credential resolution chain (argument → env var → secrets file) with a
|
|
24
|
+
permission check on the secrets file.
|
|
25
|
+
- Human-friendly entry point (`--help`/`--version` and a TTY guard) plus
|
|
26
|
+
fail-fast config resolution and actionable SSH-failure errors.
|
|
27
|
+
- Test suite, strict `mypy`, `ruff`, and a 3.11–3.13 CI matrix.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/ak40u/mt4ctl/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/ak40u/mt4ctl/releases/tag/v0.1.0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in `mt4ctl`.
|
|
4
|
+
|
|
5
|
+
## Dev setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m venv venv && source venv/bin/activate
|
|
9
|
+
pip install -e ".[dev]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Before opening a PR
|
|
13
|
+
|
|
14
|
+
All three must pass (CI enforces them on Python 3.11–3.13):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
ruff check src tests
|
|
18
|
+
mypy
|
|
19
|
+
pytest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Conventions
|
|
23
|
+
|
|
24
|
+
- **Keep the core network-free.** Shell logic belongs in `scripts.py` /
|
|
25
|
+
`login.py` as pure builders; `ssh.py` is the only module that executes
|
|
26
|
+
commands. New behavior should be testable without a live host.
|
|
27
|
+
- **Type everything.** `mypy` runs in strict mode.
|
|
28
|
+
- **Actionable errors.** Raise the typed errors in `errors.py`; messages should
|
|
29
|
+
tell the caller how to recover.
|
|
30
|
+
- **Never log secrets.** Passwords flow only through `auth.py` and the transient
|
|
31
|
+
remote login config, which is shredded after use.
|
|
32
|
+
- **Conventional commits** (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`,
|
|
33
|
+
`chore:`).
|
|
34
|
+
|
|
35
|
+
## Releasing
|
|
36
|
+
|
|
37
|
+
Tagging a version publishes to **PyPI** via Trusted Publishing (OIDC — no stored
|
|
38
|
+
tokens) and creates a **GitHub Release** with the wheel + sdist attached.
|
|
39
|
+
|
|
40
|
+
One-time PyPI setup (account owner): add a pending publisher at
|
|
41
|
+
https://pypi.org/manage/account/publishing/ for project `mt4ctl`, owner `ak40u`,
|
|
42
|
+
repo `mt4ctl`, workflow `release.yml`, environment `pypi`.
|
|
43
|
+
|
|
44
|
+
To cut a release:
|
|
45
|
+
|
|
46
|
+
1. Bump `version` in `pyproject.toml` and `__version__` in
|
|
47
|
+
`src/mt4ctl/__init__.py`; update `CHANGELOG.md`.
|
|
48
|
+
2. Commit, then tag and push:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`release.yml` verifies the tag matches the project version, builds and
|
|
55
|
+
`twine check`s the artifacts, publishes to PyPI, and creates the GitHub Release.
|
|
56
|
+
|
|
57
|
+
## Adding a tool
|
|
58
|
+
|
|
59
|
+
1. Implement the operation in `operations.py` (or a focused module) against the
|
|
60
|
+
`Registry`.
|
|
61
|
+
2. Add a thin `@mcp.tool()` wrapper in `server.py` with a clear docstring.
|
|
62
|
+
3. Cover the parsing / command construction with unit tests.
|
|
63
|
+
4. Document it in `docs/tools.md`.
|
mt4ctl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Volkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mt4ctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mt4ctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for managing headless MetaTrader terminals over SSH (Wine + systemd)
|
|
5
|
+
Project-URL: Homepage, https://github.com/ak40u/mt4ctl
|
|
6
|
+
Project-URL: Repository, https://github.com/ak40u/mt4ctl
|
|
7
|
+
Project-URL: Issues, https://github.com/ak40u/mt4ctl/issues
|
|
8
|
+
Author-email: Pavel Volkov <pvolkov@live.ru>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: devops,mcp,metatrader,model-context-protocol,mt4,ssh,systemd,trading,wine
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: mcp>=1.2
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
34
|
+
|
|
35
|
+
# mt4ctl
|
|
36
|
+
|
|
37
|
+
**An MCP server for operating headless MetaTrader terminals — over SSH, from your agent.**
|
|
38
|
+
|
|
39
|
+
Manage MetaTrader 4 terminals running under **Wine + systemd** on remote hosts
|
|
40
|
+
(native Linux *or* WSL2) entirely through the [Model Context Protocol](https://modelcontextprotocol.io):
|
|
41
|
+
check status, read logs, capture screenshots, control the systemd lifecycle, and
|
|
42
|
+
perform the tricky **headless first-login** — all as clean, typed tools.
|
|
43
|
+
|
|
44
|
+
[](https://github.com/ak40u/mt4ctl/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/mt4ctl/)
|
|
46
|
+
[](https://www.python.org/)
|
|
47
|
+
[](https://modelcontextprotocol.io)
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
Algo traders increasingly run MetaTrader 4 **headless on Linux** — Wine under
|
|
57
|
+
`Xvfb`, supervised by `systemd`, no GUI. That's great for uptime and terrible for
|
|
58
|
+
day-to-day operations: every "is it connected?", "restart that one", or "log this
|
|
59
|
+
new account in" turns into a fragile chain of
|
|
60
|
+
`ssh → (Windows cmd → wsl) → bash → systemctl → wine`, with quoting hazards at
|
|
61
|
+
every hop.
|
|
62
|
+
|
|
63
|
+
`mt4ctl` collapses that chain into a handful of MCP tools. Point it at a registry
|
|
64
|
+
of your hosts and terminals, wire it into Claude (or any MCP client), and operate
|
|
65
|
+
the whole farm conversationally:
|
|
66
|
+
|
|
67
|
+
> *"Which demo terminals are down?"* · *"Restart demo2."* ·
|
|
68
|
+
> *"Log demo2 into account 1000002 on ExampleBroker-Demo."* ·
|
|
69
|
+
> *"Screenshot the live terminal so I can see the AutoTrading state."*
|
|
70
|
+
|
|
71
|
+
## Quickstart (5 minutes)
|
|
72
|
+
|
|
73
|
+
`mt4_list` works **offline** (no SSH/MT4 needed), so you can confirm the wiring
|
|
74
|
+
before anything else lines up:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 1. create a minimal registry
|
|
78
|
+
mkdir -p ~/.config/mt4ctl
|
|
79
|
+
cat > ~/.config/mt4ctl/terminals.yaml <<'YAML'
|
|
80
|
+
hosts:
|
|
81
|
+
box: { ssh: my-ssh-alias, kind: native }
|
|
82
|
+
terminals:
|
|
83
|
+
t1: { host: box, service: mt4-t1, data_dir: /home/trader/mt4/t1, account: "1000001" }
|
|
84
|
+
YAML
|
|
85
|
+
|
|
86
|
+
# 2. add to Claude Code (uvx runs the server straight from git — no install)
|
|
87
|
+
claude mcp add --scope user mt4ctl \
|
|
88
|
+
--env MT4CTL_CONFIG="$HOME/.config/mt4ctl/terminals.yaml" \
|
|
89
|
+
-- uvx --from git+https://github.com/ak40u/mt4ctl mt4ctl
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then ask Claude: **"Use mt4_list to show my configured terminals."** You should
|
|
93
|
+
see your `t1` row. Once the SSH alias and `systemd` unit line up, ask for
|
|
94
|
+
**"mt4_status t1"**. Full setup and other clients are below.
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- **Per-terminal connection detection** — attributes established broker sockets
|
|
99
|
+
to each terminal's `systemd` cgroup, so terminals sharing a host (and a Wine
|
|
100
|
+
prefix) are reported independently — not guessed from a host-wide count.
|
|
101
|
+
- **Headless first-login** — automates the one-time bootstrap a migrated terminal
|
|
102
|
+
needs (MetaTrader's saved password is machine-bound), then hands control back
|
|
103
|
+
to `systemd` for automatic reconnection on every restart.
|
|
104
|
+
- **Native *and* WSL2 hosts** — one registry, two execution models; commands are
|
|
105
|
+
base64-shipped so nothing breaks in the `cmd.exe → wsl.exe → bash` gauntlet.
|
|
106
|
+
- **Live-trading guardrails** — terminals tagged `env: live` reject mutating
|
|
107
|
+
operations unless you pass `confirm=true`.
|
|
108
|
+
- **Concurrent status** — hosts are polled in parallel via `asyncio`.
|
|
109
|
+
- **Secrets stay secret** — passwords resolve from arg → env → secrets file,
|
|
110
|
+
are never logged, and the transient remote login config is `shred`-ed after use.
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
┌────────────┐ MCP/stdio ┌──────────────────┐
|
|
116
|
+
│ MCP client │ ────────────► │ mt4ctl │
|
|
117
|
+
│ (Claude…) │ │ FastMCP server │
|
|
118
|
+
└────────────┘ └────────┬─────────┘
|
|
119
|
+
│ asyncio SSH (base64-framed)
|
|
120
|
+
┌─────────────────────┼─────────────────────┐
|
|
121
|
+
▼ ▼
|
|
122
|
+
┌─────────────────┐ ┌──────────────────┐
|
|
123
|
+
│ native Linux │ │ Windows + WSL2 │
|
|
124
|
+
│ sudo systemctl │ │ wsl -u root -- │
|
|
125
|
+
├─────────────────┤ ├──────────────────┤
|
|
126
|
+
│ mt4-live-main… │ systemd units running │ mt4-demo1… │
|
|
127
|
+
│ wine terminal.exe (Xvfb display) │ wine terminal.exe│
|
|
128
|
+
└─────────────────┘ └──────────────────┘
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A thin, typed core (`models` → `config` → `ssh` → `scripts` → `operations`/`login`)
|
|
132
|
+
sits under the `server` adapter, so the logic is testable without a network and
|
|
133
|
+
the MCP layer stays a one-line-per-tool shell.
|
|
134
|
+
|
|
135
|
+
## Install
|
|
136
|
+
|
|
137
|
+
The fastest path needs no clone and no global install — [`uv`](https://docs.astral.sh/uv/)
|
|
138
|
+
runs `mt4ctl` straight from the repo and fetches a matching Python itself:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uvx --from git+https://github.com/ak40u/mt4ctl mt4ctl # runs the stdio server
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
No `uv` yet? `curl -LsSf https://astral.sh/uv/install.sh | sh` — or skip it and use
|
|
145
|
+
the `pipx` path below.
|
|
146
|
+
|
|
147
|
+
Prefer a persistent `mt4ctl` command? Install it with `uv` or `pipx`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv tool install git+https://github.com/ak40u/mt4ctl
|
|
151
|
+
# or
|
|
152
|
+
pipx install git+https://github.com/ak40u/mt4ctl
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For development:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git clone https://github.com/ak40u/mt4ctl.git && cd mt4ctl
|
|
159
|
+
python -m venv venv && source venv/bin/activate
|
|
160
|
+
pip install -e ".[dev]"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The server machine needs either `uv` or Python 3.11+, plus SSH access to your
|
|
164
|
+
hosts. The remote hosts need the usual tools `mt4ctl` shells out to: `systemctl`,
|
|
165
|
+
`ss`, `getent`, and (for screenshots) `imagemagick`/`scrot` + `xdotool`.
|
|
166
|
+
|
|
167
|
+
## Configure
|
|
168
|
+
|
|
169
|
+
Copy the example registry and fill in your real hosts and terminals:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
mkdir -p ~/.config/mt4ctl
|
|
173
|
+
cp examples/terminals.example.yaml ~/.config/mt4ctl/terminals.yaml
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The registry is resolved from `MT4CTL_CONFIG`, then
|
|
177
|
+
`~/.config/mt4ctl/terminals.yaml`, then `./terminals.yaml`. See
|
|
178
|
+
[`examples/terminals.example.yaml`](examples/terminals.example.yaml) for the full
|
|
179
|
+
schema and [`docs/configuration.md`](docs/configuration.md) for details.
|
|
180
|
+
|
|
181
|
+
> **Keep your populated registry private.** It maps your accounts and
|
|
182
|
+
> infrastructure. The default `.gitignore` excludes `terminals.yaml`.
|
|
183
|
+
|
|
184
|
+
## Connect to an MCP client
|
|
185
|
+
|
|
186
|
+
**Claude Code** — one command wires it up (user scope = available in every project):
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
claude mcp add --scope user mt4ctl \
|
|
190
|
+
--env MT4CTL_CONFIG="$HOME/.config/mt4ctl/terminals.yaml" \
|
|
191
|
+
-- uvx --from git+https://github.com/ak40u/mt4ctl mt4ctl
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or commit a project `.mcp.json` to share with a team (Claude Code expands `${HOME}`):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"mt4ctl": {
|
|
200
|
+
"command": "uvx",
|
|
201
|
+
"args": ["--from", "git+https://github.com/ak40u/mt4ctl", "mt4ctl"],
|
|
202
|
+
"env": { "MT4CTL_CONFIG": "${HOME}/.config/mt4ctl/terminals.yaml" }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Claude Desktop** — Settings → Developer → Edit Config (`claude_desktop_config.json`),
|
|
209
|
+
same shape but use an **absolute** config path (Desktop does not expand `${HOME}`),
|
|
210
|
+
and an absolute `command` path if `uvx` is not on the GUI app's `PATH` (`which uvx`):
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"mcpServers": {
|
|
215
|
+
"mt4ctl": {
|
|
216
|
+
"command": "uvx",
|
|
217
|
+
"args": ["--from", "git+https://github.com/ak40u/mt4ctl", "mt4ctl"],
|
|
218
|
+
"env": { "MT4CTL_CONFIG": "/Users/you/.config/mt4ctl/terminals.yaml" }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
> Installed `mt4ctl` persistently (uv/pipx)? Replace `command`/`args` with just
|
|
225
|
+
> `"command": "mt4ctl"`.
|
|
226
|
+
|
|
227
|
+
## Tools
|
|
228
|
+
|
|
229
|
+
| Tool | Mutates | Description |
|
|
230
|
+
| --- | :---: | --- |
|
|
231
|
+
| `mt4_list` | – | List configured terminals (offline). |
|
|
232
|
+
| `mt4_status` | – | Per-terminal service state + broker connection + log age. |
|
|
233
|
+
| `mt4_logs` | – | Tail / grep a terminal's newest log file. |
|
|
234
|
+
| `mt4_screenshot` | – | Capture a terminal window as PNG. |
|
|
235
|
+
| `mt4_control` | ✓ | `start` / `stop` / `restart` a unit (live needs `confirm`). |
|
|
236
|
+
| `mt4_login` | ✓ | One-time headless login for auto-reconnect (live needs `confirm`). |
|
|
237
|
+
|
|
238
|
+
Full reference: [`docs/tools.md`](docs/tools.md).
|
|
239
|
+
|
|
240
|
+
## Security
|
|
241
|
+
|
|
242
|
+
- Mutations on `env: live` terminals require explicit `confirm=true`.
|
|
243
|
+
- Credentials resolve from argument → `MT4CTL_PASSWORD_<account>` →
|
|
244
|
+
secrets file; they are never written to logs and the transient remote login
|
|
245
|
+
config is shredded after use.
|
|
246
|
+
- All remote execution goes through your existing SSH config and key-based auth;
|
|
247
|
+
`mt4ctl` stores no credentials of its own.
|
|
248
|
+
- During `mt4_login` the password is embedded in the base64-framed script handed
|
|
249
|
+
to `ssh`, so it is briefly visible in the local process list to your own user.
|
|
250
|
+
On the remote side it is written only to a fresh `mktemp` config (mode 600) that
|
|
251
|
+
a cleanup trap `shred`s on any exit path. On POSIX, the local secrets file is
|
|
252
|
+
rejected if it is readable by group/other.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
ruff check src tests # lint
|
|
258
|
+
mypy # type-check (strict)
|
|
259
|
+
pytest # tests
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
See [`docs/architecture.md`](docs/architecture.md) for the module boundaries.
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT © Pavel Volkov. See [LICENSE](LICENSE).
|