# opsgate
**The safety gate between AI agents and your servers.**
An MCP server that lets Claude, Cursor, or any AI agent operate real infrastructure —
without handing it a root shell.
[](https://github.com/polymatx/opsgate/actions/workflows/ci.yml)
[](https://goreportcard.com/report/github.com/polymatx/opsgate)
[](https://pkg.go.dev/github.com/polymatx/opsgate)
[](LICENSE)
[](https://go.dev)
[](https://modelcontextprotocol.io)
[What it looks like](#what-it-looks-like) ·
[30-second try](#try-it-in-30-seconds-no-server-needed) ·
[Break it yourself](#break-it-yourself) ·
[Tools](#tools) ·
[Security model](#security-model) ·
[Contributing](#contributing)
---
## What it looks like
You ask a question. The agent investigates with read-only tools and answers.
> **You:** Something's listening on port 8000 on my server and I don't remember what. Check it.
```
▸ listening_ports(host: "hetzner")
Netid State Recv-Q Send-Q Local Address:Port Process
tcp LISTEN 0 511 0.0.0.0:443 users:(("nginx",pid=249619),…)
tcp LISTEN 0 511 0.0.0.0:80 users:(("nginx",pid=249619),…)
tcp LISTEN 0 4096 0.0.0.0:22 users:(("sshd",pid=237597),…)
tcp LISTEN 0 151 127.0.0.1:3306 users:(("mysqld",pid=237695),…)
tcp LISTEN 0 5 127.0.0.1:8000 users:(("python3",pid=237608),…)
▸ service_list(host: "hetzner", state: "running")
docker.service loaded active running Docker Application Container Engine
mysql.service loaded active running MySQL Community Server
nginx.service loaded active running A high performance web server…
php8.3-fpm.service loaded active running The PHP 8.3 FastCGI Process Manager
pm2-root.service loaded active running PM2 process manager
```
> **Claude:** Port 8000 is a bare `python3` process (pid 237608) bound to localhost only, so
> it isn't reachable from outside. It's not managed by systemd — no unit owns it — which
> means it won't survive a reboot. It's most likely something started by hand in a shell.
Now something that changes state. In `operate` mode the agent cannot just do it:
> **You:** OK, nginx config changed. Reload it.
```
▸ nginx_test(host: "hetzner")
nginx: configuration file /etc/nginx/nginx.conf test is successful
▸ nginx_reload(host: "hetzner")
┌─ opsgate: approval required ─────────────────────────────────┐
│ allow nginx_reload on host "hetzner"? │
│ │
│ The nginx configuration passed validation. │
│ This will reload nginx. │
│ │
│ Command: 'nginx' '-s' 'reload' │
│ [ Approve ] [ Deny ] │
└─────────────────────────────────────────────────────────────┘
```
You click Deny, and nothing happens — the agent is told the operator refused. Approve, and
it runs and lands in the audit log. Either way you saw the exact command first.
*(Tool output above is real output from a live server; the prompt is rendered by your MCP
client, so it looks however that client draws it.)*
## Try it in 30 seconds (no server needed)
opsgate runs with **no config file at all** — local machine, read-only:
```bash
go install github.com/polymatx/opsgate/cmd/opsgate@latest
opsgate serve
```
```
opsgate v0.1.3 | mode=observe | default_host=local | audit=~/.opsgate/audit.jsonl
```
That's it. No config, no SSH, and it defaults to `observe` — so it can read your disk usage
and journal, and it will refuse to change anything:
```
▸ disk_usage() → works
▸ service_restart(...) → Refused by opsgate policy: host is in observe mode
```
Point your client at it and ask "what's using disk space on this machine?" When you're
ready for real hosts, `opsgate init` writes a starter config to `~/.opsgate/config.yaml`.
### Which clients can reach it
opsgate runs **on your machine** (or on the server itself) and holds your SSH key, so the
transport you need depends on where your client actually executes:
| Client | Transport | Works because |
|---|---|---|
| Claude Code (terminal) | stdio | Runs natively on your machine and launches the binary |
| Claude Desktop, Cursor, VS Code | stdio | Same — a local process |
| Cloud or sandboxed surfaces (web chat, VM-backed agents) | **HTTP** | They cannot launch a local process, or even see your SSH key — they can only reach a URL |
If your client reports it has no access to your servers, it is almost certainly one of the
sandboxed kind. Serve over [HTTP](#any-client-over-http) instead of stdio.
Before publishing that endpoint, read the warning there: it puts an
infrastructure-management API on whatever network you expose it to.
### Wire it up to your client