linuxmcp
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., "@linuxmcpcheck disk usage on web01"
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.
linuxmcp
An MCP server that gives an AI agent shell access to a fixed, pre-registered set of Linux machines over SSH, and returns structured results.
The design point is the trust boundary: exec_command never takes a host, port or credential
inline. It takes a machine_id that must already resolve to an entry in the registry. Growing
that registry is its own explicit, audited tool call, never a side effect of running a command.
Transport: stdio only. It runs as a local subprocess launched by Claude Desktop / Claude Code and is not exposed over the network.
Audit: every tool call is appended to
~/.local/state/linuxmcp/audit.jsonl.Guardrails: server-enforced timeouts that kill the remote process group, output truncation with an explicit
truncatedflag, optional per-machine deny-pattern regexes, and connection verification before a machine can enter the registry.
Install
pip install -e .
# with the test dependencies
pip install -e ".[dev]"Python 3.11+. Works against both the 1.x (FastMCP) and 2.x (MCPServer) MCP Python
SDKs; both are exercised in development.
This installs a linuxmcp console script. Validate a config without serving. This is useful
before restarting a client, since a bad registry makes the server exit rather than start:
linuxmcp --check --config ~/.config/linuxmcp/machines.yamlRelated MCP server: ssh-mcp
Configure
Copy examples/machines.yaml to ~/.config/linuxmcp/machines.yaml:
machines:
- id: web01
host: 192.168.1.10
port: 22
user: admin
auth: key # key | agent
key_path: ~/.ssh/id_ed25519
tags: [prod, web]
description: "Primary nginx frontend"
deny_patterns:
- 'rm\s+-rf\s+/(\s|$)'
- '\bmkfs\b'
- 'dd\s+if=.*of=/dev/'
- id: db01
host: 10.0.0.5
port: 22
user: postgres
auth: agent # use the running ssh-agent, no key_path needed
tags: [prod, db]
description: "Postgres primary"
- id: switch01
host: 10.0.0.2
user: admin
auth: password
password_env: LINUXMCP_SWITCH01_PASSWORD # variable NAME, never the password
tags: [net]
defaults:
connect_timeout_sec: 10
command_timeout_sec: 60
max_output_bytes: 200000
idle_timeout_sec: 300
max_pool_size: 16
kill_grace_sec: 5
max_transfer_bytes: 20000000
strict_host_keys: true
env_file: ~/.config/linuxmcp/secrets.envSecrets. The YAML holds key paths, the string agent, or an environment variable name.
It never holds a password or private key material. Key files must exist and should be 0600;
loose permissions are a warning at startup and a hard rejection when adding or updating a
machine. Encrypted keys are not supported directly, so add them to ssh-agent and use
auth: agent.
Password auth
Prefer keys. Some hosts genuinely can't do key auth (appliances, switches, a box you don't
control), so auth: password exists for those. The registry stores the name of an environment
variable, and the server reads it at connect time:
- id: switch01
host: 10.0.0.2
user: admin
auth: password
password_env: LINUXMCP_SWITCH01_PASSWORDSupply the value either from the server process environment, or from an env file that
defaults.env_file points at:
# ~/.config/linuxmcp/secrets.env (chmod 0600)
LINUXMCP_SWITCH01_PASSWORD=correct horse battery stapleSee examples/secrets.env. Details worth knowing:
The env file must be
0600or the server refuses to start. That's stricter than the warning you get for a loose key file, because this one is plaintext passwords.A process env var beats the file, so you can override one password for a single run without editing anything.
The file is read at connect time, not cached at startup, so rotating a password takes effect on the next connection with no restart. There's a test for exactly that.
Everything after the first
=is the value. No inline-comment stripping, no shell interpolation. A password containing#, spaces, or another=survives intact. Quote the value to keep leading or trailing whitespace.A missing password warns at startup instead of aborting, unlike other config errors. One appliance's absent secret shouldn't take the rest of the fleet offline. The connection attempt itself then fails with a message naming the variable.
asyncssh sends the password over keyboard-interactive too when the server offers only that, which is the common PAM setup. Both paths are covered by integration tests against real servers.
The password never reaches the registry file, the audit log, a tool response, or the agent. Even
the variable name stays out of tool output, same as key_path. The server's instructions
tell the model not to ask you for a password or accept one as a tool argument; if a connection
fails, it should name the variable for you to set.
Password mode disables public key and agent auth for that machine. Otherwise a stale default key gets offered first and a wrong password comes back as a confusing key error.
Host keys. Host key verification is on by default against ~/.ssh/known_hosts. Point
defaults.known_hosts (or a machine's known_hosts) at another file, or set it to the literal
none to disable verification for that machine. defaults.strict_host_keys: false disables it
fleet-wide. Disabling is logged loudly at startup.
Fail closed. Any validation error at startup aborts the server rather than skipping the bad entry. That covers an unknown field, a duplicate id, a missing key file, and an invalid deny-pattern regex.
Persistence. add_machine / remove_machine / update_machine rewrite machines.yaml under
an exclusive flock on machines.yaml.lock, via a same-directory temp file and rename, so a
crash mid-write cannot leave a half-written registry and two concurrent calls cannot clobber each
other. Rewrites normalise the document: YAML comments are not preserved across a mutation.
Register with Claude Desktop / Claude Code
Add to claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/claude_desktop_config.json; Linux:
~/.config/Claude/claude_desktop_config.json), or to .mcp.json in a project for Claude Code:
{
"mcpServers": {
"linuxmcp": {
"command": "/absolute/path/to/venv/bin/linuxmcp",
"args": [
"--config", "/home/you/.config/linuxmcp/machines.yaml",
"--audit-log", "/home/you/.local/state/linuxmcp/audit.jsonl"
],
"env": {
"SSH_AUTH_SOCK": "/run/user/1000/keyring/ssh"
}
}
}
}Both args are optional; those are the defaults. Use the absolute path to the console script;
a bare linuxmcp only resolves if the launching process inherits the right PATH.
SSH_AUTH_SOCK is only needed for machines with auth: agent. GUI-launched Claude Desktop does
not inherit your shell environment, so an agent-auth machine will fail with a clear error unless
you set it here explicitly. The same applies to password variables: if you supply them from your
shell rather than an env file, a GUI-launched client won't see them. Use defaults.env_file and
you don't have to think about it.
Or, in Claude Code:
claude mcp add linuxmcp -- /absolute/path/to/venv/bin/linuxmcpTools
Tool | Blast radius |
| Read-only. Ids, tags, description, cached reachability. No auth details. |
| Read-only. Connects and authenticates, runs nothing. |
| Runs a shell command on one host. |
| Same command, several hosts, concurrent. |
| Trust-expanding: widens the reachable fleet. Persists. |
| Removes an entry and closes its pooled session. Persists. |
| Partial update, same validation as |
| SFTP, explicit path pairs, size-limited. |
exec_command returns:
{
"stdout": "...", "stderr": "...", "exit_code": 0,
"truncated": false, "duration_ms": 143, "timed_out": false,
"stdout_bytes": 812, "stderr_bytes": 0
}A non-zero exit_code is a normal result, not a tool error. truncated: true means output
exceeded max_output_bytes; stdout_bytes / stderr_bytes report the true sizes.
The server sets MCP instructions telling the agent to call list_machines first, to confirm the
target and the exact command before anything destructive, and to treat the three registry-mutation
tools with the same care.
Safety model
Timeouts are enforced, not documented. The command is wrapped in the remote host's
timeout -k, which puts it in its own process group and signals the whole group at the deadline, so pipelines and spawned children die instead of being orphaned. A client-side deadline sits behind that for hosts without coreutilstimeout.Output is capped but still drained. Past
max_output_bytesthe remainder is read and discarded rather than left unread. Abandoning the stream would stall the SSH window and hang a chatty command instead of letting it finish.Passwords are referenced, not stored.
auth: passwordholds an environment variable name; the value is resolved at connect time and never written to the registry, the audit log, or a tool response. The env file that can hold it must be0600.Deny patterns are regexes checked before any connection is made, so a denied command never reaches the host. This is a backstop for catastrophic commands (
rm -rf /,mkfs,dd of=/dev/...), not a security boundary. The tool grants shell access by design, and any allow-list would be trivially bypassable from inside a shell. The boundary is the machine list.Verification before registration.
add_machineandupdate_machineopen a real SSH connection by default and fail the call if it doesn't work, so a typo or a rotated key surfaces immediately instead of sitting in the registry until someone needs it.verify=falseis there for machines that aren't up yet.Connection reuse. A small LRU pool of live SSH sessions with idle-timeout eviction; a session is dropped when the machine's connection settings change or the machine is removed.
Audit log. One JSON object per line: timestamp, event, machine, command, exit code, duration, truncation flag, and a SHA-256 of the output. Command text is logged verbatim, which is the point. Command output is not, only its size and digest, so the log doesn't become a second copy of whatever a command happened to print.
Tests
pytest # everything
pytest -m "not integration" # unit tests only, no sockets
pytest -m integration # only the real-SSH testsUnit tests cover config validation and fail-closed behaviour, atomic/locked persistence, deny-pattern matching, audit-log contents, password resolution and redaction, and, against a mocked SSH layer, output truncation, timeout enforcement, remote command wrapping, and connection pooling.
The integration tests stand up a real asyncssh server on 127.0.0.1 on an ephemeral port and
run real commands through a real /bin/sh. They're what actually proves the timeout wrapper
fires, that it takes backgrounded children down with it instead of orphaning them, that SFTP
size limits reject before any bytes move, and that password auth works against both a
password-only and a keyboard-interactive-only server.
Not in scope
No network transport. stdio only, single local user. No SSE/HTTP server, no bind address, nothing listening.
No multi-user auth or permissions model. This is a personal admin tool. Anyone who can talk to this process can run anything the configured SSH users can run; there are no per-caller identities, roles, or scoped grants.
No command allow-list. Intentional; see the deny-pattern note above.
No secret management. Key discovery is your
~/.sshand your agent, and linuxmcp stores paths. Passwords are referenced by env var name; there's no keyring, vault, orpassintegration. If you want one, export the variable from your own wrapper before launching the server.No interactive commands. stdin is closed on the remote side, so anything that prompts fails rather than hangs. Use the non-interactive flags.
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Scoped, audited SSH exec, sessions, and SFTP on your saved servers without exposing credentials
- emisarOAuthdev.emisar
Let AI operate servers without SSH. Choose actions, approve risky changes, and audit every step.
Provides capabilities that let LLM agents perform a range of infrastructure management tasks.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI assistants to securely execute remote SSH commands, perform file transfers, and monitor system status through a standardized interface. It features robust security controls including command whitelisting, blacklisting, and credential isolation to prevent unauthorized operations.1022 npmMIT
- AlicenseAqualityAmaintenanceEnables AI assistants to execute shell commands and transfer files via SFTP across remote servers using existing SSH configurations. It supports parallel execution on server groups and provides built-in safety warnings for potentially destructive commands.62Mozilla Public 2.0
- AlicenseAqualityDmaintenanceEnables AI agents to execute SSH commands, read files, and list directories on remote hosts with a configurable command-safety policy.5MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to securely execute commands on remote hosts via SSH and SFTP, with persistent shells, file transfers, screenshots, and an audit log.1MIT