Skip to content

MCP Server

Eric Kochen edited this page Jun 16, 2026 · 78 revisions

purple ships a built-in MCP server for SSH: AI agents can list your SSH hosts, run commands and manage containers through typed tools with input validation, a read-only mode and a JSON Lines audit log. No API keys needed, and every call your assistant makes is on the record.

What is MCP?

MCP (Model Context Protocol) is a standard for connecting AI coding assistants to external tools. purple implements an MCP server so AI agents like Claude Code, Cursor, Windsurf and Claude Desktop can query your SSH hosts, run commands and manage containers programmatically.

Starting the server

purple mcp

The AI client spawns purple mcp automatically as a child process. You do not need to start it manually. Just add the config below and restart your AI client.

With a custom SSH config:

purple --config ~/other/ssh_config mcp

Safety flags

Flag Effect
--read-only Restricts the server to list_hosts, get_host and list_containers. State-changing tools (run_command, container_action) are denied and removed from tools/list. Recommended when exposing purple to autonomous agents.
--no-audit Disables the audit log. By default every tool call is appended to ~/.purple/mcp-audit.log as a JSON line.
--audit-log <PATH> Custom audit log path. Default: ~/.purple/mcp-audit.log. Ignored when --no-audit is set.

Example: read-only mode for an autonomous agent, audit log to a custom path:

purple mcp --read-only --audit-log /var/log/purple-mcp.log

Available tools

Tool Description Read-only
list_hosts List all SSH hosts. Optional tag filter (substring match). yes
get_host Detailed info for one host: provider, tags, metadata, tunnels, askpass. yes
list_containers List Docker/Podman containers on a remote host. Returns runtime, engine version (when reported by the daemon) and the parsed container list. yes
run_command Run a command on a remote host via SSH. Timeout clamped to 1-300 seconds. no
container_action Start, stop or restart a container. no

Client setup

Claude Desktop (one-click install)

The simplest path. Download the latest .mcpb (MCP Bundle) from the GitHub releases page and double-click to install. Claude Desktop handles the rest.

The bundled installation runs purple in --read-only mode by default for safety. If you need run_command or container_action from Claude Desktop, install via Homebrew or cargo and configure claude_desktop_config.json directly (see below).

Claude Code

Add to ~/.claude/settings.json:

{
  "mcpServers": {
    "purple": {
      "command": "purple",
      "args": ["mcp"]
    }
  }
}

Restart Claude Code. The purple tools appear automatically.

Cursor

Add to your MCP configuration (Settings > MCP Servers):

{
  "purple": {
    "command": "purple",
    "args": ["mcp"]
  }
}

Claude Desktop (manual config)

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

{
  "mcpServers": {
    "purple": {
      "command": "purple",
      "args": ["mcp"]
    }
  }
}

For read-only mode, add the flag:

{
  "mcpServers": {
    "purple": {
      "command": "purple",
      "args": ["mcp", "--read-only"]
    }
  }
}

Custom SSH config path

If you use a non-default SSH config, pass --config before mcp:

{
  "mcpServers": {
    "purple": {
      "command": "purple",
      "args": ["--config", "/path/to/ssh_config", "mcp"]
    }
  }
}

Other MCP clients

Any MCP-compatible client that supports stdio transport can use purple. Point it to the purple mcp command.

Audit log

Every tool call is appended to ~/.purple/mcp-audit.log as a single JSON line:

{"ts":"2026-05-19T09:32:11Z","tool":"list_hosts","args":{"tag":"prod"},"outcome":"allowed","reason":null}
{"ts":"2026-05-19T09:32:18Z","tool":"run_command","args":{"alias":"web-1","command":"<redacted>"},"outcome":"allowed","reason":null}
{"ts":"2026-05-19T09:35:02Z","tool":"container_action","args":{"alias":"web-1","container_id":"abc","action":"start"},"outcome":"denied","reason":"read-only mode"}

Fields:

  • ts: ISO 8601 UTC timestamp, second precision
  • tool: the tool name that was called
  • args: the call arguments. The command field of run_command is replaced by <redacted> so secrets passed as shell arguments do not land in the log
  • outcome: allowed, denied (read-only blocked the call) or error (the tool returned an error result)
  • reason: present and non-null only when outcome is denied

The log file is created with mode 0o600 on Unix (owner read/write only). Append-only: writes never truncate prior entries. The MCP server holds a single file handle protected by a mutex, so concurrent writers cannot interleave lines.

To disable: purple mcp --no-audit. To redirect: purple mcp --audit-log /path/to/log.

If --audit-log points at a path that already exists as a symlink, purple refuses to open it (defense against an attacker pre-creating a symlink to a sensitive file).

Security

The MCP server validates every alias against the SSH config before executing SSH commands. Only hosts in your config can be targeted. Container IDs are validated with an ASCII alphanumeric allowlist to prevent injection. All SSH operations use BatchMode=yes (no interactive prompts) and timeouts (clamped to 1-300 seconds for run_command).

The --read-only flag is the recommended posture when exposing purple to autonomous agents. It enforces the allowlist at dispatch time, before any argument validation, so a probing agent cannot distinguish "tool denied" from "tool would have worked but args were bad".

Purple does not implement its own approval gate. Approval behavior depends on your AI client. Claude Code prompts for approval on tool calls by default. Verify your client's settings before enabling run_command in production.

Example session

What the AI client sends and receives:

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude","version":"1.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"purple","version":"3.22.1"}}}

→ {"jsonrpc":"2.0","method":"notifications/initialized"}
(no response)

→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_hosts","arguments":{"tag":"prod"}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"[{\"alias\":\"web-1\",\"hostname\":\"10.0.1.5\", ...}]"}]}}

→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_command","arguments":{"alias":"web-1","command":"uptime"}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"exit_code\":0,\"stdout\":\" 14:32 up 42 days\",\"stderr\":\"\"}"}]}}

In --read-only mode the same run_command call gets:

← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Tool denied. Server started with --read-only. Restart without --read-only to enable state-changing tools."}],"isError":true}}

Troubleshooting

"No such file or directory" on startup

MCP clients spawn purple as a child process with a minimal PATH (typically /usr/local/bin, /opt/homebrew/bin, /usr/bin, /bin). If you installed purple via curl the binary lives in ~/.local/bin which is not in that PATH.

Fix: use the full path in your MCP config:

{
  "mcpServers": {
    "purple": {
      "command": "/Users/you/.local/bin/purple",
      "args": ["mcp"]
    }
  }
}

Find your path with which purple.

Homebrew installs (/opt/homebrew/bin/purple) are usually found automatically.

The .mcpb bundle for Claude Desktop ships the purple binary inside the bundle, so this issue does not apply there.

Audit log not being written

Check ~/.purple/mcp-audit.log exists and is writable. If purple cannot open the log it prints a one-line warning to stderr at startup and continues without audit logging (so a broken log path never blocks the server).

If the path is a symlink, purple refuses to open it and the same warning is printed. Remove the symlink or point --audit-log at a regular file.

Read-only mode and an unexpected Invalid action error

If you see Invalid action: nuke. Must be start, stop or restart even though you started with --read-only, the server is NOT in read-only mode (the read-only guard fires before argument validation, so a denied call returns the read-only message, never the validation error). Check that your client config actually passes --read-only to purple mcp.

Limitations

  • Stdio transport only. No HTTP or SSE.
  • Read and execute only. No host mutations (add, edit, delete) in this version.
  • No provider sync, tunnel management or file transfer via MCP.
  • One request at a time per server instance. The Mutex<File> audit log is concurrency-safe but the JSON-RPC loop itself is single-threaded.

Clone this wiki locally