bravoric-ssh-mcp
The bravoric-ssh-mcp server exposes an SSH + tmux control plane over MCP, letting an agent manage hosts, sessions, commands, broadcasts, tunnels, snippets, rotations, and audit logs.
Hosts: list hosts, get host details, check TCP reachability (
ping,ping_all), get a dashboard summary, and verify tmux availability.tmux sessions: list sessions across all hosts, find text in panes, create/rename/kill sessions, kill whole tmux servers, detach clients, and manage windows (list, create, rename, kill).
Interactive control: capture pane output, send literal text, send Enter, and send raw tmux keys (e.g., C-c, Up).
Command execution: run commands on one host, many hosts in parallel, in detached tmux with wait, or broadcast with wait.
Snippets & broadcast: list, add, remove snippets; broadcast commands/snippets to multiple hosts in tmux or direct mode.
SSH tunnels: list, start, stop, and health-check
-L/-R/-Dtunnels, including all hosts.Rotations: list, add, and remove rotation profiles for cycling through tmux sessions.
Audit logs: list and read local or remote compressed audit logs.
Diagnostics: get MCP server status (config, credential provider, tmux, log directory).
Manages tmux sessions on remote hosts (over SSH) and on the local machine: listing hosts and their sessions, attaching (including read-only attach), creating, renaming and killing sessions, listing/managing windows, detaching clients, capturing pane content, and broadcasting commands into new detached tmux sessions per host.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bravoric-ssh-mcp list the tmux sessions on web-prod"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
bravoric-ssh-client
A full-screen SSH + tmux TUI client.
I wrote this for myself. It is a personal tool, not a product: I'm publishing it on GitHub mainly to show how I reason and how I build the tools I use every day. It lists your hosts, shows the tmux sessions running on each of them, and lets you attach, create, rename and kill sessions without leaving the terminal. All network operations run in background threads, so the UI always stays responsive. No external services, nothing to install on your servers.
Italiano? Vedi README.it.md.
About this project
This is a personal tool. I built it to scratch my own itch and I use it every day. I'm sharing it as-is, for reference and curiosity, to show the way I think and how I assemble my own tooling. It is not a supported product: no roadmap, no SLA, no promise of feature parity. Issues and suggestions are welcome, but I only merge changes that fit how I use it.
Related MCP server: 2native-ssh-mcp
Features
Host list → tmux sessions, keyboard driven, with live text filter.
Host management: add, edit, delete, duplicate, group, per-host credentials.
Connectivity: TCP ping on the SSH port, mass ping of all visible hosts.
Session management: attach, read-only attach (
tmux attach -r), create, rename, kill, kill whole server, details, windows, new window, detach clients.Credential providers: system keyring (GNOME/KDE/macOS/Windows) or a local
plainfile with0600permissions; per-host override (keyring,plain,key,prompt).Import: from
~/.ssh/configand from Remmina profiles, plus a helper to copy passwords from Remmina into the app keyring.File exchange:
scpcopy and a two-panel Midnight Commander session (local ↔ remote or remote ↔ remote) with automatic password handling.SSH tunnels:
-L/-R/-Dport forwards, started in the background and shared between the TUI and the MCP server.Jump host / bastion (
-J/ ProxyJump) with a multi-host askpass helper.Broadcast snippets: run a command on many hosts at once, either in a new detached tmux session per host or directly over SSH (parallel).
Rotation profiles: auto-cycle through the panes of several sessions, with an optional interactive mode to send keys to the focused session.
Audit trail: record interactive sessions to gzip logs (tmux
pipe-paneorscript).Local tmux: add an host with
host = "localhost"to manage the tmux on the local machine without SSH.MCP server: the whole client is exposed as a Model Context Protocol server on stdio, so an agent (e.g. opencode) can drive hosts, sessions, commands, broadcasts, tunnels and audit logs.
Requirements
Python 3.11+
OpenSSH
ssh/scpclienttmuxon the servers you manage (and locally for local hosts)Optional:
mc(Midnight Commander) for the file-exchange viewOptional:
script(util-linux) to record plain SSH shellsOptional: a graphical terminal (
kitty,ptyxis,gnome-terminal,konsole,alacritty) to open sessions in separate windows. The client detects the ones available inPATHand asks which to use each time you attach to or create a session — so you can pick a terminal that supports zooming out for a session that was resized elsewhere (kitty, for example, does not let you zoom out, while ptyxis does). If a single graphical terminal is installed there is nothing to choose and the prompt is skipped; setBRAVORIC_TERMINALto pin one and skip the prompt even with several.kittyalso enables the kitty-unicode avatar renderer in pi-emote whenTERM_PROGRAM=kittyis set in kitty.conf
Installation
git clone https://github.com/BravoRicDev/bravoric-ssh-client.git
cd bravoric-ssh-client
python -m venv .venv
.venv/bin/pip install -e ".[dev]" # omit [dev] for a runtime-only installRun it:
.venv/bin/bravoric-sshor, if installed on your PATH:
bravoric-sshConfiguration
Config file: ~/.config/bravoric-ssh-client/config.toml
(override the path with the BRAVORIC_CONFIG environment variable).
[credentials]
provider = "keyring" # or "plain" (local file, permissions 0600)
[[hosts]]
alias = "web-prod"
host = "server.example.com"
user = "deploy"
# group = "clients" # group hosts together (g key in the list)
# jump_host = "bastion" # alias of the bastion for ProxyJump (-J)
# auth = "" # default: use the global provider
# auth = "key" # SSH key / agent (no password)
# auth = "prompt" # ask every timeGeneral options ([general]):
[general]
restart_after_ssh = false # reopen the TUI after an ssh session ends
history_size = 10 # how many recent sessions to keep
audit_log = false # record sessions to compressed logs (pipe-pane/script)
# snippets_file = "..." # snippet catalog path (default: <config_dir>/snippets.json)
# tunnels_file = "..." # tunnel state path (default: <config_dir>/tunnels.json)Import from your ssh config
.venv/bin/python scripts/import_ssh_config.py # -> ~/.config/.../config.toml
.venv/bin/python scripts/import_ssh_config.py --merge # add only new hostsImport from Remmina
Press i on the host screen to import both ~/.ssh/config and Remmina SSH
profiles (flatpak and legacy) without duplicates. For a new Remmina host, set
the password the first time with p.
Import credentials from Remmina / SSH Pilot
.venv/bin/python scripts/import_keyring_credentials.py # copy passwords into the app keyringUsing the TUI
Host screen: arrows +
Enterto open a host.qquits.aadd a host ·eedit ·ddelete (with confirmation) ·Dduplicatepset/remove the host password in the providertconnectivity test (TCP ping on the SSH port) ·Tping all visible hostsccopy a file (scp) ·Ffile exchange via Midnight CommanderuSSH tunnels for the selected host (forward-L/-R/-D, live status)Bbroadcast a snippet to multiple hosts (default: a new tmux session per host)iimport hosts from~/.ssh/configand Remmina (merge)gcycle groups ·hrecent sessions ·Rsaved rotations ·oobserve a hostrreload config from disk · text field on top = live filtera
🔑next to a host means a password is stored
Session screen:
Enterattach ·Rread-only attach (tmux attach -r)nnew session ·rrename ·kkill (with confirmation) ·Kkill alldsession details ·wlist/manage windows (renamer, closek)Ddetach other clients ·Wnew window ·grefreshsopen a plain SSH shell ·Escback to hosts
Attach, read-only attach, new session and plain shell open in a new window of
a terminal you pick from the list; the TUI stays open, so you can launch more
sessions from it without leaving it. The same happens when you attach from the
observe screen, the quick-launch panel or the agent launcher. The new window runs
a full bravoric-ssh process, so it behaves exactly like a direct launch:
password, jump host, rotation, audit log, session history and window title all
work the same way.
Window title: when you attach to (or create) a session, the terminal window
title becomes HOST - SESSION. The client sets the remote tmux title string
during the attach and restores it afterwards, so it works even with
set-titles on on the server.
Automatic restart: with restart_after_ssh = true in [general], the TUI
reopens automatically after you exit an ssh session.
Rotation profiles
If several agents/processes work in distinct tmux sessions (possibly across
servers), create a rotation profile: the TUI shows each session's content
(capture-pane) and, after cycle_interval seconds of inactivity, moves to the
next one automatically. Any input stops the rotation on the current session.
Ron the host screen → saved rotations (Enterto observe,nto create,ato reopen all sessions of the profile in separate windows).Host config:
auto_cycle = trueandcycle_interval = 120to enable automatic selection; profiles are saved inrotations.json.
Local tmux
Add a host with host = "localhost" (or 127.0.0.1, or set local = true):
the TUI manages the local tmux directly without SSH. A localhost host is
already included in the default config.
Every host change is written to config.toml with an automatic backup
(config.toml.bak.<timestamp>).
SSH tunnels (u)
From the selected host press u: the list of configured tunnels with their
status (🟢 active / 🔴 off). n adds one (kind L/R/D, port, destination),
s/x start/stop a single tunnel, a/X start/stop all, d removes one.
Tunnels run as background ssh -N -T processes (password from the keyring is
injected automatically) and their definitions are saved in the config:
[[hosts]]
alias = "db-host"
host = "server.example.com"
[[hosts.tunnels]]
name = "postgres"
kind = "L"
local_port = 5432
remote_host = "localhost"
remote_port = 5432
[[hosts.tunnels]]
kind = "D" # SOCKS
local_port = 1080Jump host / bastion (jump_host)
A host that is not directly reachable can be reached through a bastion: set the
bastion alias in the config and the client injects -J (ProxyJump) into the
connections, using the multi-host SSH_ASKPASS helper for both passwords
(bastion and final host):
[[hosts]]
alias = "internal-app"
host = "10.0.0.9"
user = "deploy"
jump_host = "bastion"Broadcasting snippets (B)
Press B on the host screen: snippet catalog (snippets.json in the config
dir, n to add one). Pick a snippet, mark hosts with Space (or a for all)
and press Enter. Two modes (t toggles, default: tmux):
tmux session (default): a detached tmux session named
bcast-<snippet>-<host>-<YYYYMMDD-HHMMSS>is created on each host and runs the command. If tmux is not installed on a host, it falls back to direct mode.Direct (ssh batch): the script runs in parallel (thread pool) and the results (stdout/exit code) appear immediately in the grid.
MCP server (bravoric-ssh-mcp)
The whole client is exposed as an MCP server on stdio: an agent (e.g. opencode) can control hosts, tmux sessions, commands, broadcasts, tunnels and audit logs exactly like the TUI.
bravoric-ssh-mcp # start the server (stdio)Exposed tools (47), grouped:
Host:
list_hosts,get_host,get_status,ping,ping_all,hosts_summary,tmux_presentOverview:
list_sessions_all,find_in_sessions,run_command_all,session_historytmux sessions:
list_sessions,create_session,session_details,rename_session,kill_session,kill_server,detach_clients,list_windows,new_window,rename_window,kill_window,capture_pane,send_keys,send_enter,send_rawRun and wait:
run_and_wait,broadcast_waitCommands:
run_command,run_command_manySnippets/broadcast:
list_snippets,add_snippet,remove_snippet,broadcast(tmux|direct)Tunnels:
list_tunnels,list_tunnels_all,start_tunnel,stop_tunnel,stop_tunnels,tunnel_healthRotations:
list_rotations,add_rotation,remove_rotationAudit:
list_audit_logs,read_audit_log,list_remote_audit_logs,read_remote_audit_log
Registration for opencode (global, ~/.config/opencode/opencode.jsonc):
"mcp": {
"bravoric-ssh": {
"type": "local",
"command": ["bravoric-ssh-mcp"],
"enabled": true,
"environment": {}
}
}Use the absolute path to the executable if it is not on your
PATH, e.g./path/to/venv/bin/bravoric-ssh-mcp.
Agent skill
The repo ships a skill (docs/skill/SKILL.md) with the tool inventory, usage
patterns (create detached → send-keys → capture-pane → kill), naming
conventions and error handling.
# Claude Code
mkdir -p ~/.claude/skills/bravoric-ssh
cp docs/skill/SKILL.md ~/.claude/skills/bravoric-ssh/
# opencode
mkdir -p ~/.agents/skills/bravoric-ssh
cp docs/skill/SKILL.md ~/.agents/skills/bravoric-ssh/Headless usage (no GUI)
The MCP server is a Python process on stdio and does not require a desktop:
Credentials: without a system keyring, use the
plainprovider (secrets.tsv, permissions 0600):[credentials] provider = "plain"Config: point
BRAVORIC_CONFIGat the file before startingbravoric-ssh-mcp(or create~/.config/bravoric-ssh-client/config.toml).The
SSH_ASKPASShelpers work without a display (SSH_ASKPASS_REQUIRE=force). Typical systemd daemon:[Service] Environment=BRAVORIC_CONFIG=/etc/bravoric-ssh-client/config.toml ExecStart=/opt/bravoric-ssh-client/.venv/bin/bravoric-ssh-mcp
Audit trail (audit_log = true)
Interactive sessions are recorded to compressed logs:
attach / new tmux session:
tmux pipe-pane -o 'gzip -c >> file'is injected during the attach and closed on detach. For local hosts the file is on this machine (<config_dir>/logs/YYYYMMDD_ALIAS_SESSION.log.gz), for remote hosts it lives in~/.bravoric-ssh-client/logs/on the server.interactive shell (
s): ifscriptis installed, I/O is recorded locally; otherwise the shell starts without logging.
The .log.gz files are ready for offline parsing (zgrep, regex, ...).
When you pick an action (attach/new/shell) the session opens in a new window of the terminal you choose: the TUI stays alive, so you can launch more sessions from it, and the terminal running the TUI is never left dirty. Close the new window (or detach) to go back to the TUI.
Testing
.venv/bin/python -m pytestWith coverage and lint:
.venv/bin/python -m pytest --cov=bravoric_ssh_client
.venv/bin/ruff check .
.venv/bin/ruff format --check .Contributing
This is a personal project, shared to show how I build my own tools. See CONTRIBUTING.md: it's small and modular on purpose, so bug reports and focused pull requests with tests are welcome — but I only take changes that fit the spirit of the tool.
License
MIT © Riccardo (bravoric) — Made by me, for me.
Available Tools
47 toolsadd_rotationC
Aggiunge (o sostituisce per nome) un profilo di rotazione.
Inputs: name (required), entries (required). Returns: result
add_snippetC
Aggiunge uno snippet al catalogo.
Inputs: name (required), command (required), description. Returns: result
broadcastC
Broadcast di un comando/snippet su più host (mode=tmux|direct).
Inputs: aliases (required), mode, command, snippet. Returns: result
broadcast_waitC
Broadcast tmux su più host con attesa di completamento e raccolta output.
Inputs: aliases (required), command (required), timeout. Returns: result
capture_paneC
Cattura l'output di una sessione (ultime lines righe).
Inputs: alias (required), session (required), lines, window. Returns: result
create_sessionC
Crea una sessione tmux detached su un host (comando opzionale).
Inputs: alias (required), name, command. Returns: result
detach_clientsC
Stacca gli altri client dalla sessione.
Inputs: name (required), alias (required). Returns: result
find_in_sessionsC
Cerca una regex nelle pane delle sessioni tmux di un host.
Inputs: alias (required), pattern (required), lines. Returns: result
get_hostC
Dettagli di un host.
Inputs: alias (required). Returns: result
get_statusC
Diagnostica del server MCP (config, provider, tmux, dir log).
Inputs: no named parameters. Returns: result
hosts_summaryC
Dashboard: per ogni host raggiungibilità, tmux e numero sessioni.
Inputs: timeout. Returns: result
kill_serverC
Termina il server tmux di un host (tutte le sessioni).
Inputs: alias (required). Returns: result
kill_sessionC
Termina una sessione tmux.
Inputs: name (required), alias (required). Returns: result
kill_windowD
Chiude una finestra.
Inputs: alias (required), session (required), window_id (required). Returns: result
list_audit_logsB
Elenca i log audit compressi locali.
Inputs: no named parameters. Returns: result
list_hostsB
Elenca gli host configurati.
Inputs: no named parameters. Returns: result
list_remote_audit_logsC
Elenca i log audit compressi sul server remoto dell'host.
Inputs: alias (required). Returns: result
list_rotationsB
Elenca i profili di rotazione salvati.
Inputs: no named parameters. Returns: result
list_sessionsC
Elenca le sessioni tmux di un host.
Inputs: alias (required). Returns: result
list_sessions_allB
Elenca le sessioni tmux di tutti gli host in parallelo.
Inputs: no named parameters. Returns: result
list_snippetsB
Elenca gli snippet del catalogo.
Inputs: no named parameters. Returns: result
list_tunnelsB
Elenca i tunnel configurati di un host.
Inputs: alias (required). Returns: result
list_tunnels_allB
Elenca tunnel e stato attivo su tutti gli host.
Inputs: no named parameters. Returns: result
list_windowsC
Elenca le finestre di una sessione.
Inputs: alias (required), session (required). Returns: result
new_windowC
Crea una finestra in una sessione.
Inputs: alias (required), session (required), name. Returns: result
pingC
Verifica la raggiungibilità TCP di un host.
Inputs: alias (required), timeout. Returns: result
ping_allC
Ping TCP di tutti gli host.
Inputs: timeout. Returns: result
read_audit_logB
Legge un log audit .gz locale (max_lines=0 = tutto).
Inputs: filename (required), max_lines. Returns: result
read_remote_audit_logC
Legge un log audit .gz remoto (max_lines=0 = tutto).
Inputs: alias (required), filename (required), max_lines. Returns: result
remove_rotationB
Rimuove un profilo di rotazione.
Inputs: name (required). Returns: result
remove_snippetC
Rimuove uno snippet dal catalogo.
Inputs: name (required). Returns: result
rename_sessionC
Rinomina una sessione tmux.
Inputs: alias (required), new_name (required), old_name (required). Returns: result
rename_windowD
Rinomina una finestra.
Inputs: alias (required), session (required), new_name (required), window_id (required). Returns: result
run_and_waitB
Esegue un comando in tmux detached e attende il completamento (sentinel).
Inputs: alias (required), command (required), timeout. Returns: result
run_commandC
Esegue un comando batch su un host (ssh).
Inputs: alias (required), command (required), timeout. Returns: result
run_command_allC
Esegue un comando batch su tutti gli host raggiungibili.
Inputs: command (required), timeout. Returns: result
run_command_manyC
Esegue un comando batch in parallelo su più host.
Inputs: aliases (required), command (required), timeout, max_workers. Returns: result
send_enterC
Invia Invio alla sessione.
Inputs: alias (required), session (required). Returns: result
send_keysC
Invia testo letterale alla sessione.
Inputs: text (required), alias (required), session (required). Returns: result
send_rawC
Invia tasti tmux non letterali (es. C-c, Up).
Inputs: keys (required), alias (required), session (required). Returns: result
session_detailsC
Dettagli di una sessione tmux.
Inputs: name (required), alias (required). Returns: result
session_historyC
Cronologia delle sessioni recenti (history.json).
Inputs: no named parameters. Returns: result
start_tunnelC
Avvia un tunnel SSH (kind L/R/D).
Inputs: kind (required), alias (required), local_port (required), bind, +3 more. Returns: result
stop_tunnelC
Ferma il tunnel su una porta.
Inputs: alias (required), local_port (required). Returns: result
stop_tunnelsC
Ferma tutti i tunnel di un host.
Inputs: alias (required). Returns: result
tmux_presentC
True se tmux è installato sull'host.
Inputs: alias (required). Returns: result
tunnel_healthC
Verifica che le porte locali dei tunnel attivi ascoltino davvero.
Inputs: alias (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 47 tools
Many tools are clearly distinct (e.g., ping vs list_hosts vs create_session), but several overlapping pairs exist: run_command vs run_command_all vs run_command_many, broadcast vs broadcast_wait, list_tunnels vs list_tunnels_all, and list_audit_logs vs list_remote_audit_logs. Descriptions help but don't fully eliminate ambiguity, especially around batch vs single-host variants.
The vast majority of tools follow a consistent verb_noun convention (list_hosts, get_host, create_session, kill_session, etc.). Minor deviations appear in compound names like hosts_summary (noun_verb) and ping_all, but overall it's predictable.
47 tools is excessive for the apparent scope and goes well beyond the typical 3-15 range. Many tools are thin wrappers (send_enter, send_keys, send_raw) that could be consolidated, making the surface feel heavy and unwieldy.
Coverage is broad: host management, tmux sessions/windows/panes, tunnels, snippets, rotations, audit logs, and diagnostics are all present. A few gaps exist (e.g., no explicit update_host or snippet editing), but the lifecycle for most resources is covered.
Maintenance
Related MCP Connectors
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
Remote shell and detached long-running jobs on your own machines — no SSH, open ports or VPN.
Scoped, audited SSH exec, sessions, and SFTP on your saved servers without exposing credentials
Manage remote servers, SSH terminals, files, databases and proxies with scoped access and approvals.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables programmatic control over tmux terminal sessions for SSH access, command execution, and terminal automation. Supports creating sessions, sending commands, capturing output, and managing multiple panes for interactive debugging and monitoring.810 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to remotely execute shell commands and transfer files on SSH hosts, including jump-host tunneling, background tasks, resumable transfers, and optional approval of destructive commands, while SSH credentials remain in the local process. It runs over stdio or a streamable HTTP daemon and supports auditing, allow/deny lists, and multiple authentication methods.ISC
- AlicenseNot gradedqualityCmaintenanceEnables agents to spawn, monitor, and control CLI coding agents running in isolated tmux sessions, with verified input injection, redacted pane capture, fleet and per-session state classification, lineage tracking, blocking exec with exit codes, cross-agent messaging, waste analytics, and log/ledger reading. Ownership checks, policy controls, and audit events keep every control action safe by default.3 npmApache 2.0
- AlicenseNot gradedqualityBmaintenanceExposes interactive Claude Code CLI sessions running inside tmux across one or more machines as MCP tools, enabling agents to list, create, read, and drive those sessions over stdio.1MIT