MCP server that turns your Tailscale network into an AI-native operations layer.
Instead of making your AI assistant craft ssh commands, parse raw text, and sleep-poll for results — give it semantic tools that understand infrastructure.
| Tool | What it does |
|---|---|
ts_discover |
Fleet overview or deep node probe — hardware, services, ports, installed tools. One call = full infrastructure map. |
ts_status |
Tailscale network state with SSH readiness |
ts_exec |
Raw command execution (escape hatch) |
ts_transfer |
SFTP push/pull files and directories |
ts_poll |
Execute a command repeatedly until a regex matches — replaces sleep+exec loops |
ts_service |
Structured systemd service status: state, health, pid, memory, recent logs |
ts_logs |
Journalctl with grep filtering — structured output |
ts_build_status |
CI/CD pipeline status from audit logs — no need to know service names or timestamps |
AI assistants using SSH-based tools waste tokens and time:
# Before: the AI has to know everything
ts_exec(node="forge", command="systemctl --user show cynic-kernel
--property=ActiveState,SubState,MainPID,MemoryCurrent 2>/dev/null
&& journalctl --user -u cynic-kernel --no-pager -n 10
--no-hostname -o short-iso 2>/dev/null")
# Then sleep 120...
# Then re-exec to check again...
# After: 2 params, structured output, zero guesswork
ts_service(node="forge", service="cynic-kernel", user=true)
# Before: sleep-poll loop (5 tool calls, 10 minutes wasted)
sleep 120
ts_exec(...) # check if done
sleep 120
ts_exec(...) # still not done...
# After: 1 call, MCP handles the loop
ts_poll(node="forge", command="...",
pattern="test result|error", interval=10, timeout=300)
Add to ~/.claude/mcp.json:
{
"mcpServers": {
"tailscale": {
"command": "/path/to/tailscale-mcp",
"args": []
}
}
}git clone https://github.com/zeyxx/tailscale-mcp.git
cd tailscale-mcp
make all # vet + test + build + cross-compileCheck Releases.
- Tailscale installed and running on the local machine
- SSH config (
~/.ssh/config) with entries for your Tailscale nodes - Nodes must be reachable via Tailscale IP with SSH key auth
Example ~/.ssh/config:
Host forge
HostName 100.75.118.79
User kairos
IdentityFile ~/.ssh/my_key
The MCP resolves SSH aliases automatically — ts_discover(node="forge") works even if the Tailscale hostname is different.
stdio/JSON-RPC
AI Client <=====================> tailscale-mcp
(Claude Code) (Go binary)
|
+----------------+----------------+
| | |
tailscale CLI SSH (x/crypto) SFTP (pkg/sftp)
| | |
Discovery Exec/Poll Push/Pull
- Zero agents on remote nodes — uses standard SSH, no daemon to install
- Persistent connections — SSH connections are pooled and reused with keepalive
- Automatic reconnection — dead connections are detected and replaced
- Context-aware timeouts — every operation is bounded
- Cross-platform — builds for Linux, macOS, Windows
An AI with no prior context calls:
ts_discover()
fleet_size: 2
self:
- DESKTOP: ip=100.1.1.1 os=windows status=online ssh=no
peers: 1
- kairos: ip=100.2.2.2 os=linux status=online ssh=yes (user: kairos)
ts_discover(node="forge")
node: kairos
kernel: Linux 6.8.0-101-generic x86_64
cpus: 3
ram: 11Gi total, 1.3Gi used, 10Gi available
disk: 193G total, 25G used, 169G free, 13% use%
tools: python3, surreal, protoc
user_services: 2 running
- cynic-kernel.service
- forgejo.service
listening_ports: 15
- 0.0.0.0:3030
- [::1]:50051
From zero to full infrastructure understanding in 2 calls.
tailscale-mcp/
main.go Entry point — MCP server over stdio
mcp/
tools.go Tool registration (8 tools)
discover.go Fleet discovery + deep node probe
poll.go Regex-based poll loop
service.go Systemd service status parser
logs.go Journalctl wrapper
build.go CI/CD build status from audit logs
ssh/
client.go SSH connect + exec with context timeouts
config.go SSH config parser (~/.ssh/config)
manager.go Persistent connection pool with keepalive
tailscale/
discovery.go tailscale status --json parser
types.go Network/node types
transfer/
sftp.go SFTP push/pull with size limits
MIT