mcp2term
by soobik
README.md
# mcp2term
Expose un terminal interactif via **MCP (Model Context Protocol)** sur Internet grâce à **ngrok**, pour permettre à ChatGPT, Claude Desktop ou tout client MCP d'exécuter des commandes shell à distance.
## Architecture
```
[Client MCP] <--HTTPS--> [ngrok (Basic Auth)] <--> [FastMCP Server] <--> [Shell (cmd/bash)]
```
## Prérequis
- Python ≥ 3.11
ngrok est optionnel : s'il n'est pas installé, il est téléchargé automatiquement.
## Installation
```bash
pip install -r requirements.txt
```
Ou via pip (rend la commande `mcp2term` disponible) :
```bash
pip install .
```
## Configuration
Copier et éditer le fichier `.env` :
```bash
cp .env.example .env
```
Variables disponibles :
| Variable | Obligatoire | Description |
|---|---|---|
| `NGROK_AUTH_TOKEN` | Non* | Token d'authentification ngrok |
| `NGROK_BASIC_AUTH` | Non* | Identifiants au format `user:password` |
| `NGROK_CONFIG_PATH` | Non | Chemin vers une config ngrok existante (`ngrok.yml`), pour réutiliser un domaine réservé ou une région déjà configurés |
| `NGROK_TUNNEL_NAME` | Non | Nom d'un tunnel défini dans `NGROK_CONFIG_PATH` à réutiliser tel quel |
| `HOST` | Non | Adresse d'écoute locale (défaut: `127.0.0.1`) |
| `PORT` | Non | Port local (défaut: `8765`) |
| `LOG_LEVEL` | Non | `DEBUG`, `INFO` (défaut), `WARNING`, `ERROR` |
| `LOG_FILE` | Non | `1` pour écrire aussi dans `mcp2term.log` |
\* Sans token ni auth, le tunnel ngrok ne s'ouvre pas ; le serveur reste accessible en local uniquement.
Obtenir un token : https://dashboard.ngrok.com/authentication
## Utilisation
```bash
# Démarrage normal (ngrok en arrière-plan)
python mcp2term.py
# Mode local uniquement (pas de tunnel ngrok)
python mcp2term.py --local-only
# Options personnalisées
python mcp2term.py --port 9000 --host 0.0.0.0 --log-level DEBUG
# Réutiliser une config ngrok déjà en place (domaine réservé, région...)
python mcp2term.py --ngrok-config ~/.config/ngrok/ngrok.yml
python mcp2term.py --ngrok-config ~/.config/ngrok/ngrok.yml --ngrok-tunnel-name mon-tunnel
```
La version du schéma (`tunnels:` v2 ou `endpoints:` v3) est détectée automatiquement à partir du champ
`version:` du fichier de config.
**Limitation avec `--ngrok-tunnel-name` sur une config v3** : `NGROK_BASIC_AUTH` est ignoré (l'API v3
rejette ce champ pour un endpoint nommé). Pour protéger un tunnel nommé v3, configurer l'authentification
directement sur l'endpoint dans le fichier `ngrok.yml` (`traffic_policy`), ou ne pas utiliser
`--ngrok-tunnel-name`.
### Options CLI
| Option | Description |
|---|---|
| `--host HOST` | Adresse d'écoute (défaut: `127.0.0.1`) |
| `--port PORT` | Port d'écoute (défaut: `8765`) |
| `--log-level LEVEL` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `--local-only` | Désactive le tunnel ngrok |
| `--guest` | Tunnel ngrok sans Basic Auth (accès public complet) |
| `--ngrok-config PATH` | Chemin vers une config ngrok existante (`ngrok.yml`) |
| `--ngrok-tunnel-name NAME` | Nom d'un tunnel défini dans cette config à réutiliser |
| `--help` | Affiche l'aide |
### Ce qui se passe au lancement
1. Démarre un shell interactif (cmd.exe sur Windows, bash sur Unix)
2. Lance le serveur MCP en local **(immédiatement, sans attendre ngrok)**
3. **En arrière-plan** : télécharge ngrok si nécessaire, configure le token, tente d'ouvrir un tunnel (2 tentatives avec retry)
4. Si le tunnel est établi : affiche l'URL publique
5. Si le tunnel échoue : le serveur reste accessible en local
6. **Surveillance** : un thread vérifie l'état du tunnel toutes les 30s (backoff x2 en cas d'erreur) et le reconnecte automatiquement si perdu
## Outils MCP
| Outil | Paramètres | Description |
|---|---|---|
| `execute_command` | `command`, `timeout=15` | Exécute une commande shell (état persistant) |
| `change_directory` | `path` | Change le répertoire courant |
| `get_shell_state` | — | Retourne le dossier courant, le type de shell et le PID |
| `reset_shell` | — | Tue et relance le shell |
| `interrupt_command` | — | Envoie Ctrl+C à la commande en cours |
| `send_stdin` | `data` | Envoie du texte à l'entrée standard (commandes interactives) |
| `read_file` | `path` | Lit un fichier (protection anti-traversal, max 10 MB) |
| `write_file` | `path`, `content` | Écrit un fichier (création des dossiers parents si nécessaire, max 10 MB) |
| `start_interactive` | `command`, `cols=80`, `rows=24` | Démarre un programme interactif (PTY réel) : nano, vim, top, htop… |
| `send_keys` | `keys` | Envoie des touches à la session interactive (texte + touches spéciales) |
| `read_screen` | — | Retourne l'écran rendu (grille texte) et la position du curseur |
| `resize_interactive` | `cols`, `rows` | Redimensionne le terminal de la session interactive |
| `stop_interactive` | — | Arrête la session interactive |
| `health_check` | — | Statut : uptime, URL ngrok, nombre de commandes, état du shell |
### Détail des outils
**`execute_command(command, timeout=15)`**
Le shell conserve l'état (répertoire courant, variables d'environnement) entre les commandes.
- `timeout` : temps max d'attente de la sortie (défaut: 15s). Passer à 60s+ pour les commandes longues.
- Le shell est automatiquement réinitialisé après 10 minutes d'inactivité.
**`read_file(path)` et `write_file(path, content)`**
Les chemins sont résolus relativement au répertoire courant du shell.
Les traversées de répertoire (`../`) sont bloquées.
Taille max : 10 MB.
### Mode interactif (PTY)
`execute_command` utilise de simples pipes : les programmes plein écran/curses
(nano, vim, top, htop) ne peuvent pas s'y afficher correctement. Le mode
interactif alloue un vrai PTY à un processus séparé et restitue l'écran rendu
en texte (via [`pyte`](https://pypi.org/project/pyte/), un émulateur de
terminal VT100 pur Python) au lieu des séquences ANSI brutes.
**Exemple** : `start_interactive("nano fichier.txt")` → `send_keys("hello")`
→ `send_keys("<CTRL-X>")` → `send_keys("y<ENTER>")` → `stop_interactive()`.
**Syntaxe des touches** (`send_keys`) : le texte littéral est envoyé tel quel ;
les touches spéciales s'écrivent entre chevrons : `<ENTER>` `<ESC>` `<TAB>`
`<BACKSPACE>` `<UP>` `<DOWN>` `<LEFT>` `<RIGHT>` `<HOME>` `<END>` `<PGUP>`
`<PGDN>` et `<CTRL-X>` (n'importe quelle lettre).
**Limitations** :
- Une seule session interactive à la fois — `stop_interactive` avant d'en
démarrer une autre encore en cours.
- Modèle "instantané" : `read_screen` retourne l'état actuel de l'écran, pas un
flux vidéo continu. Pour un programme qui se rafraîchit tout seul (`top`,
`htop`), il faut interroger `read_screen` à nouveau pour voir l'évolution.
- Linux/macOS uniquement (POSIX) — retourne une erreur explicite sur Windows.
## Configuration client (ChatGPT / Claude Desktop)
Ajouter un serveur MCP distant :
- **URL** : `https://votre-sous-domaine.ngrok.io/mcp`
- **Type** : Streamable HTTP
- **Authentification** : Basic Auth (utilisateur/mot de passe du `.env`)
## Sécurité
### Rate Limiting
Le serveur limite chaque session à **30 appels d'outils par minute**. Au-delà, une erreur est retournée.
### Protection anti-traversal
`read_file` et `write_file` vérifient que le chemin résolu reste dans le répertoire de base du shell. Les tentatives de `../` sont bloquées.
### Masquage des credentials
Les tokens et mots de passe sont systématiquement masqués dans les logs (`****`).
### Journalisation des sessions
Chaque appel d'outil est horodaté avec un identifiant de session, permettant le traçage des actions.
## Docker
```bash
docker build -t mcp2term .
docker run -it --rm -p 8765:8765 -v ./.env:/app/.env mcp2term --local-only
```
Le shell s'exécute **dans le conteneur** : bash, curl, git et nano sont préinstallés.
Les fichiers créés par les commandes sont perdus à l'arrêt du conteneur sauf si tu montes un volume :
```bash
docker run -it --rm -p 8765:8765 -v "$PWD/data:/workspace" -w /workspace mcp2term
```
### Ajouter des outils au conteneur
Édite le `Dockerfile` et ajoute la ligne dans la section dédiée :
```dockerfile
RUN apt-get install -y --no-install-recommends nodejs
# ou via pip
RUN pip install --no-cache-dir poetry
```
Reconstruis l'image : `docker build -t mcp2term .`
## Tests
```bash
pip install pytest httpx
python -m pytest tests/ -v
```
Tests d'intégration : initialisation, liste des outils, exécution de commande,
health check, mode interactif (démarrage/envoi de touches/redimensionnement/
arrêt, refus d'une seconde session concurrente).
Le serveur est automatiquement démarré/arrêté par les fixtures pytest.
La fidélité de rendu de programmes complets comme nano/vim/htop (dépendante de
`TERM`/terminfo) n'est pas testée en CI — à vérifier manuellement avec un
client réel après toute modification du mode interactif.
## Dépendances
- `mcp` — SDK MCP officiel (Anthropic)
- `pyngrok` — Client Python pour ngrok (auto-installation du binaire)
- `python-dotenv` — Chargement du `.env`
- `pyte` — Émulateur de terminal VT100 pur Python, utilisé pour restituer un
écran texte lisible en mode interactif
## Logs
| Niveau | Usage |
|---|---|
| `INFO` | Démarrage, arrêt, URL ngrok, commandes exécutées, fichiers écrits |
| `WARNING` | Timeouts, rate limit, erreurs tunnel, traversées de répertoire |
| `DEBUG` | Appels d'outils, stdin/stdout brut, état du tunnel, reconnexion, send_stdin |
Activer avec `LOG_LEVEL=DEBUG` dans `.env` ou `--log-level DEBUG`.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues