Skip to main content
Glama
WilliamSmithEdward

ssh-for-agents

ssh-for-agents

An MCP (Model Context Protocol) server that lets AI agents talk to SSH servers — run commands, read files, and list directories on remote hosts — behind a configurable command-safety policy.

Because it speaks MCP, any MCP-capable agent (Claude Desktop, Claude Code, or your own client) can use it with no glue code. Connections are async (built on asyncssh) and pooled per host.

AI agent ──MCP(stdio)──▶ ssh-for-agents ──asyncssh──▶ remote host(s)
                              │
                         guardrails policy
                    (readonly / guarded / unrestricted)

Quickstart

git clone https://github.com/WilliamSmithEdward/pySSHForAgents.git
cd pySSHForAgents
python -m venv .venv
.venv\Scripts\Activate.ps1          # macOS/Linux: source .venv/bin/activate
pip install -e .

Store your SSH key's passphrase in an environment variable (it's never written to a config file) — Windows setx NAME "value" then open a new terminal, macOS/Linux export NAME=value — then register a host and test it end to end:

ssh-for-agents-config add myserver --hostname 203.0.113.10 --user deploy \
    --passphrase-env MYSERVER_SSH_PASSPHRASE
ssh-for-agents-run myserver "uptime"

That's it. Now point your agent at it — see Connect to Claude below, or Codex / a local Ollama model. The full walkthrough (keys, host-key verification, every agent) is in SETUP.md.

Related MCP server: MCP SSH Server

Tools exposed to the agent

Tool

What it does

list_hosts

List configured hosts and the active safety policy.

check_command

Dry-run the policy for a command (allow / needs_confirmation / block).

run_command

Run a shell command on a host. Destructive commands need confirm=true.

read_file

Read a remote file over SFTP (size-capped).

list_dir

List a remote directory over SFTP.

Requirements

  • Python 3.10+

  • An SSH key (password auth is not wired up by design — use keys).

Install

python -m venv .venv
.venv\Scripts\activate          # Windows
# source .venv/bin/activate     # macOS/Linux
pip install -e .

asyncssh needs bcrypt to decrypt passphrase-protected OpenSSH keys; it's a declared dependency, so the install above pulls it in.

Configure

Copy the example and edit it:

cp hosts.example.json hosts.json
{
  "hosts": {
    "myserver": {
      "hostname": "203.0.113.10",
      "username": "deploy",
      "port": 22,
      "private_key": "~/.ssh/id_ed25519",
      "passphrase_env": "MYSERVER_SSH_PASSPHRASE",
      "known_hosts": "~/.ssh/known_hosts",
      "verify_host_key": true
    }
  },
  "policy": {
    "mode": "guarded",
    "extra_denylist": [],
    "extra_readonly_allow": [],
    "max_output_bytes": 100000,
    "default_timeout": 60
  }
}

Host fields:

  • private_key — path to the private key (~ is expanded).

  • passphrase_envname of an environment variable holding the key's passphrase. The passphrase itself is never stored in the config. Omit if the key has no passphrase.

  • known_hosts — path to a known_hosts file used to verify the server's host key. Omit to use the default (~/.ssh/known_hosts + system files).

  • verify_host_key — set false to skip host-key verification (insecure; only for throwaway hosts).

The server looks for hosts.json in the working directory, or wherever SSH_AGENT_CONFIG points.

hosts.json is git-ignored since it describes your infrastructure. Commit hosts.example.json instead.

Adding & managing hosts

The config holds any number of hosts — add as many as you like under hosts. Manage them with the bundled CLI instead of hand-editing JSON:

# add or update a host
ssh-for-agents-config add prod \
  --hostname 203.0.113.10 --user deploy \
  --key ~/.ssh/id_ed25519 --passphrase-env PROD_SSH_PASSPHRASE

ssh-for-agents-config list                 # show configured hosts + policy
ssh-for-agents-config remove prod          # drop a host
ssh-for-agents-config import-ssh-config    # pull hosts from ~/.ssh/config

--config <path> (or $SSH_AGENT_CONFIG) targets a specific hosts.json.

Hot-reload: a running server watches hosts.json and reloads it when it changes, so a newly added host is usable on the next tool call — no restart needed. Live connections to unchanged hosts are kept; a half-written or invalid edit is ignored (the last good config stays active).

Shell CLI — for agents that can't call MCP tools

Some agents run shell commands but can't reliably call MCP tools — notably a local (Ollama) model in Codex, which emits tool-call names the host rejects. For those, the same hosts and safety policy are exposed as a plain command, ssh-for-agents-run:

ssh-for-agents-run hosts                     # list aliases + policy
ssh-for-agents-run linode "df -h"            # run a command (default verb)
ssh-for-agents-run linode "rm /tmp/x" --confirm
ssh-for-agents-run check linode "rm -rf /"   # dry-run the policy
ssh-for-agents-run read linode /etc/os-release
ssh-for-agents-run ls linode /var/log

It reuses hosts.json and the guardrails, and on Windows resolves the key passphrase from the persisted user environment even when the launching shell filters env vars. See SETUP.md for wiring it into Codex (AGENTS.md + giving the sandbox read access to ~/.ssh).

Run

ssh-for-agents          # console script (serves over stdio)
python -m ssh_agent_mcp # equivalent

The server communicates over stdio — it's normally launched by an MCP client rather than run by hand.

Connect to Claude Code

claude mcp add ssh-for-agents -- /abs/path/to/.venv/Scripts/python.exe -m ssh_agent_mcp

Set the working directory (or SSH_AGENT_CONFIG) so it finds hosts.json.

Connect to Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "ssh-for-agents": {
      "command": "C:\\path\\to\\.venv\\Scripts\\python.exe",
      "args": ["-m", "ssh_agent_mcp"],
      "env": {
        "SSH_AGENT_CONFIG": "C:\\path\\to\\hosts.json",
        "MYSERVER_SSH_PASSPHRASE": "..."
      }
    }
  }
}

Use it from a local Ollama model

Ollama runs models and does function calling, but it is not an MCP client — so a small host/bridge sits between them: it launches this server, hands the model the tools, runs the tool-calling loop, and routes tool calls back. A working one is included at examples/ollama_bridge.py:

pip install -e ".[examples]"      # adds the `ollama` client library
ollama pull qwen3-coder:30b       # any tool-calling model works
python examples/ollama_bridge.py "what is the uptime and disk usage on linode?"

The model discovers hosts via list_hosts and runs commands via run_command. When the policy returns needs_confirmation, the bridge prompts you on the terminal rather than letting the model authorize its own destructive command. Pick a model with OLLAMA_MODEL=...; it must support tool calling (qwen3-coder, llama3.1/3.2, mistral-nemo, …).

Ollama model ◀──function calling──▶  bridge  ◀──MCP(stdio)──▶ ssh-for-agents ──▶ host

Safety model

The policy runs before any command reaches the server. Pick a mode:

Mode

Behavior

readonly

Only commands on a read-only allowlist run; everything else is blocked.

guarded

(default) Most commands run freely. Destructive/state-changing commands return needs_confirmation until the agent re-calls with confirm=true. A few catastrophic patterns are hard-blocked.

unrestricted

Anything runs. Only sensible for disposable/sandboxed hosts.

In guarded mode:

  • Needs confirmation: rm, dd, mkfs, shutdown/reboot, kill, mount, firewall edits, crontab, package installs/removals (apt install, …), service changes (systemctl stop, …), git push/reset, curl … | sh, recursive chmod/chown, redirects into system paths, and more.

  • Hard-blocked (cannot be confirmed away): fork bombs, recursive deletes of root paths (rm -rf /, /home, …), and writing/formatting raw disk devices (dd of=/dev/sda, mkfs … /dev/nvme0n1).

  • Commands are parsed into segments across ;, &&, ||, and pipes, and sudo/env wrappers are seen through, so cd /x && sudo rm y is still flagged.

Extend it without editing code via extra_denylist (more binaries that need confirmation) and extra_readonly_allow (more binaries allowed in readonly mode).

Important — this is a safety net, not a sandbox. String-based inspection of shell commands can always be evaded by a determined caller (base64 … | sh, exotic quoting, writing then running a script, etc.). It exists to prevent accidents and obvious mistakes. For a real trust boundary, connect as a dedicated unprivileged user and constrain it with OS permissions and/or sshd ForceCommand. Treat the mode as defense-in-depth, not as containment of an adversarial agent.

Develop & test

pip install -e ".[dev]"
pytest                  # guardrail policy tests (no SSH needed)

Project layout

ssh_agent_mcp/
  guardrails.py   # command-safety policy engine (pure, well-tested)
  config.py       # hosts.json loading + validation
  ssh_client.py   # asyncssh connection pool (run / read_file / list_dir)
  server.py       # FastMCP server + tools (with hosts.json hot-reload)
  manage.py       # `ssh-for-agents-config` CLI: add / list / remove / import hosts
  run_cli.py      # `ssh-for-agents-run` CLI: run SSH from the shell (Codex/local models)
tests/
  test_guardrails.py
  test_manage.py
  test_run_cli.py
examples/
  ollama_bridge.py  # drive a local Ollama model with these tools
hosts.example.json
SETUP.md            # step-by-step reproduction guide

Available Tools

5 tools
check_commandA

Dry-run the safety policy for a command without executing it.

Returns the verdict (allow / needs_confirmation / block), a human-readable reason, and which rules matched. Use this to understand why a command would be refused before calling run_command.

ParametersJSON Schema
NameRequiredDescriptionDefault
hostYes
commandYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. It discloses that the tool does not execute the command (non-destructive), returns a verdict, reason, and matched rules. However, it does not mention any side effects or whether it requires specific permissions, but for a dry-run tool this is sufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the key action and outcome. Every sentence adds value with no waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the presence of an output schema (implied from context signals), the description provides adequate high-level information about return values. It could mention pagination or error cases, but for a simple check tool this is complete enough.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the description does not explain the parameters 'host' and 'command'. While the names are somewhat self-explanatory, the description should add context like format or examples to compensate for the lack of schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: dry-run safety policy without executing. It specifies the verb 'check' and resource 'command safety policy', and distinguishes from sibling run_command by contrasting execution vs dry-run.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says 'Use this to understand why a command would be refused before calling run_command.' This provides clear guidance on when to use this tool and implies not to use it when you intend to execute the command.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_dirB

List the entries of a remote directory over SFTP.

ParametersJSON Schema
NameRequiredDescriptionDefault
hostYes
pathNo.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. It indicates a read-only listing operation, which is inherently non-destructive. However, it does not mention permissions, error handling, or any side effects, leaving some ambiguity.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, clear sentence with no unnecessary words. It is front-loaded and efficiently conveys the tool's purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the simplicity of the tool and the presence of an output schema (for return format), the description is minimally complete. However, it lacks details about hidden files, sorting, error conditions, or path handling.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the description does not explain the parameters (host, path) beyond their names. While the names are somewhat self-explanatory, the description adds no additional semantic context about expected values or formats.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it lists directory entries over SFTP, with a specific verb and resource. It implicitly differentiates from sibling tools like read_file (file content) and run_command (command execution), though it doesn't explicitly compare.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is given on when to use this tool versus alternatives, nor are there prerequisites or exclusions provided. The description lacks any when-to-use or when-not-to-use context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_hostsA

List the SSH hosts this server can reach and the active safety policy.

Call this first: use the returned alias values as the host argument for the other tools. Never guess a hostname or IP — only configured aliases work.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but the description explains the output usage (aliases for other tools) and implies a read-only operation. It does not detail side effects or idempotency, but the context is sufficient for a simple list tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences, front-loaded with purpose and immediate usage guidance. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no parameters, output schema exists, and sibling tools are provided, the description fully covers the tool's role and integration with other tools. It tells the agent exactly when and how to use it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so baseline is 4. The description adds value by explaining the purpose of the output, which goes beyond the empty schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it lists SSH hosts and active safety policy, with a specific verb and resource. Differentiates from siblings by indicating it's the first call to provide aliases for other tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Call this first' and instructs to use returned alias values as host argument for other tools. Also warns against guessing hostnames, providing clear when-to-use and what not to do.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_fileA

Read a remote file's contents over SFTP (size-capped).

Args: host: Alias of a configured host. path: Absolute or relative remote path. max_bytes: Max bytes to read (defaults to policy max_output_bytes).

ParametersJSON Schema
NameRequiredDescriptionDefault
hostYes
pathYes
max_bytesNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden. It discloses key behaviors: reading over SFTP, size cap via max_bytes, and default policy. However, it omits error handling, authentication needs, and handling of missing files or binary content.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two paragraphs: a one-sentence summary followed by a concise argument list. Every sentence adds value, no fluff, and the purpose is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the presence of an output schema (which explains return values), the description covers purpose, parameters, and behavior adequately. It lacks guidance on error conditions or security, but for a simple read tool it is nearly complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Since schema property descriptions are missing (0% coverage), the description adds meaning by explaining each parameter: host as alias, path as absolute/relative, max_bytes with default behavior. This compensates well, though some details (e.g., host alias format) are absent.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb (Read), resource (remote file), method (over SFTP), and constraint (size-capped). It distinguishes this tool from siblings like list_dir and run_command by specifying its exact function.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is for reading files, but does not explicitly state when to use alternatives (e.g., list_dir for directory listings) or when not to use it. No prerequisites mentioned, but the purpose is clear enough for basic selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_commandA

Run one shell command on an SSH host, subject to the safety policy.

host must be an alias from list_hosts (not a raw hostname/IP). Read exit_status, stdout, and stderr from the result. The result status:

  • "ok": executed.

  • "needs_confirmation": destructive command — explain it to the user and only re-call with confirm=true after they approve; do not retry blindly.

  • "blocked": catastrophic; cannot run — use a safer approach.

  • "error": see reason (unknown host, timeout, connection failure).

Args: host: Alias of a configured host (call list_hosts to discover them). command: A single shell command to execute. confirm: Authorize a command previously flagged needs_confirmation. timeout: Per-command timeout in seconds (defaults to policy setting).

ParametersJSON Schema
NameRequiredDescriptionDefault
hostYes
commandYes
confirmNo
timeoutNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It details the behavior: subject to safety policy, possible statuses (ok, needs_confirmation, blocked, error), and how to handle destructive commands. It could mention more about the safety policy, but it's adequately transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a main sentence, a bullet list for result status, and a numbered list for parameters. It is concise yet comprehensive, with no wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (4 parameters, output schema exists), the description covers all essential aspects: host source, command execution, status handling, and parameter details. It explains the output structure indirectly by mentioning exit_status, stdout, stderr, which is sufficient for an agent to use the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description provides rich semantics for each parameter beyond the schema: host must be an alias (not raw), command is a single shell command, confirm authorizes previously flagged commands, and timeout has a default and per-command context. With 0% schema coverage, this fully compensates.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Run one shell command on an SSH host', which is a specific verb-resource pair. It distinguishes from sibling tools like check_command and list_hosts by specifying that the host parameter must be an alias from list_hosts, and outlines the output structure.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear guidance on when to use the tool, including that host must be an alias from list_hosts, and explains the result statuses and how to handle the 'needs_confirmation' status with a confirmation flow. It lacks explicit exclusion of other cases but is sufficient.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 5 tool updatesv0.1.0
    • First observedcheck_command
    • First observedlist_dir
    • First observedlist_hosts
    • First observedread_file
    • First observedrun_command

TDQS

A4.1/5.0
Disambiguation5/5

Each tool has a clearly distinct purpose: check_command is for policy dry-run, list_dir for directory listing, list_hosts for host discovery, read_file for file reading, and run_command for command execution. There is no overlap.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (e.g., check_command, list_dir) using snake_case. No mixing of conventions.

Tool Count5/5

With 5 tools, the server is well-scoped for its purpose of secure SSH command execution with file reading and directory listing. Each tool serves a necessary role without redundancy.

Completeness4/5

The tools cover the core workflow of host discovery, command safety checking, command execution, and file reading. However, there is a notable gap: no tool for writing or uploading files, which may limit some use cases.

Maintenance

ActivityStale
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables 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.
    10
    29
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to securely execute commands, transfer files, and manage port forwarding on remote servers via SSH.
    168
    36
    Apache 2.0
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI to execute commands on remote hosts via SSH, supporting password and key authentication.
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/WilliamSmithEdward/pySSHForAgents'

If you have feedback or need assistance with the MCP directory API, please join our Discord server