Content
# Infrastructure MCP Server
A self-hosted [Model Context Protocol](https://modelcontextprotocol.io) server that lets AI agents (Claude, Cursor, Hermes, OpenAI Agents, ...) safely operate real infrastructure — Linux servers, Docker, Kubernetes, Proxmox, Grafana, network gear, and more — through high-level, auditable tools instead of raw shell access.
> **Status:** early, actively developed. See [Roadmap](#roadmap) for what's implemented and what's coming.
---
## Table of Contents
- [Why](#why)
- [Goals](#goals)
- [Non-Goals](#non-goals)
- [Architecture](#architecture)
- [Current Features](#current-features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Connecting an AI Agent](#connecting-an-ai-agent)
- [Tool Design Philosophy](#tool-design-philosophy)
- [Security](#security)
- [Development](#development)
- [CI/CD](#cicd)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [A Note on How This Project Is Built](#a-note-on-how-this-project-is-built)
---
## Why
AI agents are increasingly asked to help operate infrastructure — check disk space, restart a container, look at recent errors. The naive way to enable this is to hand the agent raw SSH/shell access. That's fast to build and dangerous to run: no audit trail, no input validation, no blast-radius control, and every agent has to independently learn how to safely operate every kind of system.
Infrastructure MCP Server is the alternative: a single, self-hosted service that exposes **meaningful, structured, inventory-driven tools** (`disk_usage()`, `docker_restart()`, `grafana_alerts()`) instead of `execute(command)`. Agents reason about infrastructure; this server is the only thing that actually touches it.
## Goals
- Self-hosted
- Production-ready
- Secure by default
- Extensible
- Vendor agnostic
- Infrastructure as Code friendly
- Stateless where possible
- Structured outputs only
- Human-auditable
- Open Source
## Non-Goals
The project is **not** intended to become:
- another SSH wrapper
- another Ansible replacement
- another monitoring system
- another Kubernetes operator
Instead, it provides AI-friendly infrastructure primitives built on top of those things.
---
## Architecture
```
AI Agent
(Hermes / Claude / Cursor)
│
│ MCP
▼
Infrastructure MCP Server
│
┌───────────┴────────────┐
│ │
Infrastructure APIs SSH / Telnet Layer
│ │
▼ ▼
Proxmox Linux Servers
Grafana Cisco / MikroTik
Prometheus UniFi
Docker Home Assistant
Kubernetes Storage / Bare Metal
```
Three layers, wired top-down:
1. **MCP layer** (`mcp/tools/`, `mcp/prompts/`, `mcp/resources/`) — registers tools with the MCP SDK. Tools call adapters; they never talk to infrastructure directly.
2. **Adapter layer** (`internal/linux/`, `internal/docker/`, `internal/proxmox/`, `internal/grafana/`, ...) — one isolated package per integration, all implementing a common interface. Authentication (SSH keys, API tokens, kubeconfig) lives **inside adapters** and is never exposed to AI agents.
3. **Foundation** — `internal/inventory/` (YAML config, validated, env-var expansion for secrets) and `internal/ssh` / `internal/telnet` (pooled connections to targets, including legacy devices with obsolete crypto or no SSH at all).
**Everything is inventory-driven.** No IP addresses, hostnames, or credentials appear in tool implementations — targets are resolved by name/tag from the inventory YAML.
### Technology Stack
| Concern | Choice |
|---|---|
| Language | Go 1.26+ |
| MCP SDK | Official [Model Context Protocol Go SDK](https://github.com/modelcontextprotocol/go-sdk) |
| Configuration | YAML (`go-playground/validator`) |
| Logging | `log/slog` (structured, JSON) |
| SSH | `golang.org/x/crypto/ssh` |
| HTTP | `net/http` |
| Serialization | `encoding/json` |
| Testing | Go testing, Testcontainers, Docker Compose |
| CI | GitHub Actions |
### Project Structure
```
infrastructure-mcp/
├── cmd/
│ └── server/ # entry point (stdio MCP transport)
├── internal/
│ ├── inventory/ # YAML config, validation, secret expansion
│ ├── ssh/ # pooled SSH connectivity
│ ├── telnet/ # legacy device transport
│ ├── remote/ # SSH/Telnet dispatch by target protocol
│ ├── linux/ # Linux diagnostics adapter
│ ├── docker/ # Docker CLI-over-SSH adapter
│ ├── toolerr/ # structured error contract
│ ├── kubernetes/
│ ├── grafana/
│ ├── proxmox/ # planned
│ ├── prometheus/, loki/ # planned
│ ├── cisco/, mikrotik/, unifi/ # planned
│ └── homeassistant/ # planned
├── mcp/
│ ├── tools/ # one file per MCP tool
│ ├── prompts/
│ └── resources/
├── configs/ # example inventory
├── docs/
├── examples/
├── scripts/
└── tests/
```
---
## Current Features
Tools implemented and tested against the real MCP protocol so far:
**Core (v0.1)**
- `list_servers` — list inventory targets, optionally filtered by tag
- `run_command` — run an arbitrary command against an inventory target
- `uptime` — uptime and load averages
- `disk_usage` — per-filesystem usage with warning/critical severity
- `memory_usage` — memory utilization with warning/critical severity
**Linux (v0.2)**
- `failed_services` — systemd units in the failed state
- `cpu_usage` — aggregate CPU utilization (Proxmox/KVM guest-time aware)
- `reboot_required` — pending-reboot detection (marker file + kernel comparison)
- `running_processes` — top processes by CPU usage
- `journal_errors` — recent systemd journal entries at error priority+
- `kernel_version` — running kernel release
**Docker (v0.3)**
- `docker_ps` — list containers (running or all)
- `docker_images` — list local images
- `docker_stats` — point-in-time resource usage snapshot
- `docker_logs` — recent combined stdout/stderr log lines
- `docker_restart` — restart a container (destructive; requires `confirm: true`)
**Kubernetes (v0.4)**
- `kubectl_get_pods` — list pods, optionally scoped to a namespace
- `kubectl_logs` — fetch a pod/container's log lines
- `kubectl_events` — recent cluster events, most recent first
- `kubectl_describe` — structured pod summary (spec/status highlights + recent events)
- `kubectl_nodes` — node list with readiness, roles, and capacity
**Grafana (v0.5)**
- `grafana_alerts` — currently firing/resolved alert instances from the alerting API
- `grafana_dashboards` — search dashboards by title text and/or tag
- `grafana_annotations` — annotations, optionally scoped to a time range and/or tags
- `grafana_query` — raw query against one datasource (PromQL/LogQL/SQL/...); response is a datasource-specific passthrough, not normalized
**Cross-cutting, since v0.1**
- Legacy network device support: Telnet transport, SSH legacy-crypto negotiation, transparent per-target protocol dispatch
- Structured error contract on every tool (`message`, `recommendation`, `retryable`, `category`)
- Structured per-execution logging (`tool`, `user`, `target`, `duration`, `result`, `error`)
- Host-key verification fail-closed by default, with explicit opt-outs for lab use
- Dual MCP transport: stdio (local subprocess clients) and Streamable HTTP (remote clients), from the same binary — HTTP is fail-closed by default, requiring a bearer token (`MCP_HTTP_TOKEN`) unless explicitly opted out for lab use
- Docker image (multi-arch, GHCR) + Docker Compose, with a self-check `-healthcheck` mode for shell-less containers
- CI/CD: PR-gated build/vet/lint/race-tested-CI, automatic semantic versioning + changelog + image publish + Trivy scan on merge to `main` — see [CI/CD](#cicd)
See [Roadmap](#roadmap) for what's next.
---
## Installation
### Prerequisites
- Go 1.26 or newer (bumped from 1.24 in v0.4 — required by `k8s.io/client-go`'s current release train)
- SSH access (key- or password-based) to the servers/devices you want to manage
- An MCP-capable client (Claude Desktop, Claude Code, Cursor, or any other MCP client)
### Build from source
```bash
git clone https://github.com/<your-org>/infrastructure-mcp.git
cd infrastructure-mcp
go build -o bin/infrastructure-mcp ./cmd/server
```
### Or run via Docker
Pre-built multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on every merge to `main` (see [CI/CD](#cicd)):
```bash
docker pull ghcr.io/<your-org>/infrastructure-mcp:latest
```
Or with Docker Compose, which wires up the inventory and SSH volume mounts for you — see [`docker-compose.yaml`](docker-compose.yaml):
```bash
cp configs/inventory.example.yaml inventory.yaml # fill in your real targets
cp .env.example .env # fill in real secrets, never commit
docker compose up -d
```
### Run tests
```bash
go build ./... # build
go vet ./... # static checks
go test ./... -race -cover # all tests, race detector, coverage
go test ./internal/linux -run TestName # a single test
```
---
## Configuration
The server is entirely inventory-driven — copy the example inventory and fill in your own targets:
```bash
cp configs/inventory.example.yaml configs/inventory.yaml
```
```yaml
servers:
archive:
hostname: 10.0.0.5
user: hermes
key: ~/.ssh/archive
tags:
- linux
- ethereum
pve01:
hostname: 10.0.0.2
user: hermes
proxyjump: jumpbox # reached only via another inventory target
tags:
- proxmox
routers:
core:
hostname: 10.0.0.10
vendor: cisco
user: admin
password: ${CORE_ROUTER_PASSWORD} # resolved from the environment at load time
legacy-edge: # no SSH server at all — Telnet only
hostname: 10.0.0.12
vendor: cisco
user: admin
password: ${LEGACY_EDGE_PASSWORD}
protocol: telnet
switches:
ancient-sw: # obsolete SSH key exchange/cipher/MAC only
hostname: 10.0.0.21
vendor: cisco
user: admin
password: ${ANCIENT_SW_PASSWORD}
legacy_crypto: true
# Every category, including single-instance-feeling ones like Grafana and
# Proxmox, is a map keyed by name — so a second Grafana or a second Proxmox
# cluster is just another key, not a schema change.
grafana:
main:
url: https://grafana.lab.local
token: ${GRAFANA_TOKEN}
proxmox:
lab:
url: https://pve.lab.local:8006
token: ${PROXMOX_TOKEN}
# All auth (client cert, bearer token, exec plugin, ...) lives inside the
# kubeconfig file itself — nothing is inlined into the inventory.
kubernetes:
home:
kubeconfig: /etc/infrastructure-mcp/kubeconfig-home
context: home-admin
```
**Secrets are never written in plaintext.** Any `${VAR}` in the YAML is resolved from the process environment at load time, and load fails closed if the variable is unset — real credentials are never committed to the repo.
### Server flags
```bash
./bin/infrastructure-mcp \
-inventory configs/inventory.yaml \
-known-hosts ~/.ssh/known_hosts \
-insecure-ignore-host-key=false \
-transport stdio
```
| Flag | Default | Description |
|---|---|---|
| `-inventory` | `configs/inventory.yaml` | Path to the inventory YAML file |
| `-known-hosts` | `$HOME/.ssh/known_hosts` | Path used to verify target SSH host keys |
| `-insecure-ignore-host-key` | `false` | Skip host key verification entirely — **lab/dev only, never production** |
| `-transport` | `stdio` | `stdio` (local subprocess clients) or `http` (remote Streamable HTTP clients) |
| `-http-addr` | `:8080` | Address to listen on when `-transport=http` |
| `-healthcheck` | `false` | Instead of starting the server, GET `-healthcheck-url` and exit 0/1 — used as the container `HEALTHCHECK` |
| `-allow-anonymous-http` | `false` | Allow `-transport=http` to serve without a bearer token — **lab/dev only**, every tool becomes reachable to anyone who can reach the port |
Host key verification is **fail-closed by default**: an unrecognized target's connection is refused unless it's in `known_hosts` or you explicitly opt into insecure mode. `-transport=http` is fail-closed the same way: it refuses to start unless the `MCP_HTTP_TOKEN` environment variable is set (see below) or `-allow-anonymous-http` is passed explicitly.
---
## Connecting an AI Agent
Infrastructure MCP Server supports two transports from the same binary, so it works with both local-subprocess and remote-network MCP clients:
- **stdio** (default) — the client spawns the binary itself and talks over its stdin/stdout. This is what most desktop IDEs and agent CLIs (Claude Desktop, Claude Code, Cursor) expect.
- **Streamable HTTP** (`-transport http`) — the server listens on the network (`/mcp` endpoint, plus `/healthz`) and any remote MCP client connects over HTTP. Use this when the server runs elsewhere (a container, a VM, behind a reverse proxy) rather than on the same machine as the client.
### stdio (local)
```json
{
"mcpServers": {
"infrastructure": {
"command": "/path/to/bin/infrastructure-mcp",
"args": ["-inventory", "/path/to/configs/inventory.yaml"]
}
}
}
```
### Remote HTTP
`-transport=http` requires a bearer token — `/mcp` exposes every registered tool, including `run_command`, so the server refuses to start without one (fail-closed, like host key verification above). Generate one and set it as `MCP_HTTP_TOKEN`:
```bash
export MCP_HTTP_TOKEN=$(openssl rand -hex 32)
./bin/infrastructure-mcp -inventory configs/inventory.yaml -transport http -http-addr :8080
```
(or `docker compose up -d`, which does this by default once `MCP_HTTP_TOKEN` is set in `.env` — see [Installation](#installation)). Every request to `/mcp` must then send `Authorization: Bearer $MCP_HTTP_TOKEN`; `/healthz` stays open for container/load-balancer health checks. `-allow-anonymous-http` disables this requirement — lab/dev only, never expose an anonymous instance to anything but `localhost`.
Point any remote-MCP-capable client at `http://<host>:8080/mcp` with that header set. For clients that only support stdio, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) or an equivalent local proxy:
```json
{
"mcpServers": {
"infrastructure": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://<host>:8080/mcp", "--header", "Authorization: Bearer ${MCP_HTTP_TOKEN}"]
}
}
}
```
Put the HTTP endpoint behind TLS and network-level access control (a reverse proxy, VPN, or private network) before exposing it beyond localhost — the server itself does not add its own transport-layer authentication.
Once connected, over either transport, the agent sees only the tools listed under [Current Features](#current-features) — never raw shell access, never credentials.
---
## Tool Design Philosophy
Never expose raw infrastructure:
```
execute(command) # avoid
```
Prefer meaningful, structured actions:
```
check_disk()
check_memory()
docker_list()
grafana_alerts()
backup_switch()
```
Every tool:
- validates its inputs
- never panics
- returns structured JSON
- includes severity where relevant
- includes a recommendation on warning/critical states
- includes a timestamp
```json
{
"status": "warning",
"server": "archive",
"disk_usage": 91,
"recommendation": "Free disk space or expand storage.",
"timestamp": "2026-07-23T19:10:00Z"
}
```
### Logging
Every tool execution produces a structured log line with:
`tool`, `user`, `duration`, `target`, `result`, `error`
### Error Handling
Tools never return a raw Go error. Every failure is shaped as:
`message`, `recommendation`, `retryable`, `category`
(`category` is one of `not_found`, `auth`, `network`, `timeout`, `invalid_input`, `internal`.)
---
## Security
- No secrets in logs
- No secrets in prompts
- No shell interpolation — any user-supplied value that reaches a remote command is validated against a strict whitelist pattern first
- Validate all inputs
- Whitelist inventory — no target is reachable unless it's named in the inventory
- Least privilege
- Read-only by default
- Dangerous actions require explicit confirmation (e.g. `docker_restart` requires `confirm: true` and is a no-op otherwise)
- `-transport=http` is fail-closed: it refuses to start without a bearer token (`MCP_HTTP_TOKEN`) unless `-allow-anonymous-http` is explicitly passed
Found a security issue? Please report it privately rather than opening a public issue — see [Contributing](#contributing).
---
## Development
### Testing standards
- Unit tests target ~100% coverage for inventory, adapters, parsing, and validation
- Integration tests use Testcontainers / Docker Compose (Ubuntu, Alpine, Grafana, Prometheus, Loki)
- Every tool is exercised through a real MCP protocol round-trip (`mcp.NewInMemoryTransports()`), not just unit-tested in isolation
### Performance goals
- SSH/Telnet connection pooling
- Parallel execution
- Context cancellation everywhere
- Streaming responses
- Timeout and retry support
### Code conventions
- Small packages, dependency injection, no globals, prefer interfaces
- `context.Context` everywhere
- `log/slog` for logging
- Avoid reflection when possible
- Functions under ~50 lines when practical
- Structured errors, never raw ones
- Document exported functions
- Never expose credentials to the MCP layer
- Never hardcode infrastructure — always resolve through inventory
---
## CI/CD
Two GitHub Actions workflows, both required for anyone contributing:
### `ci.yml` — required for every PR into `main`
Runs on every pull request and on push to `main`:
- **test** — `go build ./...`, `go vet ./...`, a `gofmt -l` formatting check, and `go test ./... -race -cover`
- **lint** — [`golangci-lint`](https://golangci-lint.run) against [`.golangci.yml`](.golangci.yml) (errcheck, staticcheck, govet, gosec, and more)
Branch protection on `main` **must be configured by a repo admin** (this is a repository setting, not something a workflow file can enforce) to require both the `test` and `lint` jobs to pass, and to require a pull request before merging — that gate only takes effect once the setting is turned on.
If "require a pull request" is enabled, also add `github-actions[bot]` to that rule's bypass list (or exempt admins, since the release workflow runs as `github-actions[bot]`) — otherwise `release.yml`'s version-bump commit, which pushes straight to `main`, will be rejected by the same rule it's meant to satisfy.
### `release.yml` — runs on every merge to `main`
1. **Version + changelog** — [Cocogitto](https://docs.cocogitto.io) reads [Conventional Commits](https://www.conventionalcommits.org) since the last tag (config: [`cog.toml`](cog.toml)), computes the next semver, updates [`CHANGELOG.md`](CHANGELOG.md), commits, and pushes both the commit and the `vX.Y.Z` tag to `main`. A merge with no releasable commits (e.g. docs/chore-only) skips the rest of the pipeline.
2. **Build & scan** — builds the Docker image and scans it with [Trivy](https://trivy.dev) *before* publishing; a CRITICAL/HIGH finding blocks the release. Results are uploaded to the repo's Security tab either way.
3. **Publish** — once the scan is clean, builds and pushes the multi-arch (`linux/amd64`, `linux/arm64`) image to `ghcr.io/<org>/infrastructure-mcp`, tagged `latest` and `vX.Y.Z`.
Commit messages therefore need to follow Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, ...) — see [Contributing](#contributing).
---
## Roadmap
Implemented versions are listed under [Current Features](#current-features) and tracked feature-by-feature in [`PROGRESS.md`](PROGRESS.md). Everything below is **not started**.
### v0.6 — Proxmox
- `proxmox_nodes`
- `proxmox_vms`
- `proxmox_tasks`
- `proxmox_start_vm`
- `proxmox_stop_vm`
- `proxmox_snapshot`
### v0.7 — Networking
Cisco: `backup`, `show version`, `interfaces`, `inventory`, `logs`
MikroTik: `backup`, `interfaces`, `routes`, `firewall`, `system resources`
UniFi: `devices`, `clients`, `AP status`, `firmware`, `statistics`
> Note: Telnet transport and SSH legacy-crypto support for these vendors already shipped ahead of schedule in v0.1 — only the vendor-specific tools above remain.
### v0.8 — Monitoring
Prometheus, Loki, Tempo, Alertmanager
### v0.9 — Home Assistant
- `entities`
- `devices`
- `automations`
- `sensors`
- `cameras`
### v1.0 — AI-oriented composite tools
Instead of calling 15 low-level tools, the agent calls one expert tool that composes them:
- `diagnose_linux()`
- `diagnose_docker()`
- `diagnose_kubernetes()`
- `diagnose_network()`
- `diagnose_storage()`
- `morning_brief()`
- `incident_summary()`
- `root_cause_analysis()`
- `backup_everything()`
- `check_infrastructure()`
### Further out
AWS, Azure, GCP, VMware, TrueNAS, Ceph, Synology, IPMI, Redfish, OpenStack, Nomad, Consul.
---
## Contributing
Contributions are welcome — this is an early-stage project and there's a lot of surface area left on the roadmap above.
1. Fork the repo and create a feature branch.
2. Follow the existing package/adapter pattern: one isolated package per integration under `internal/`, a matching set of tools under `mcp/tools/`, wired into `cmd/server/main.go`.
3. Match the existing conventions: structured errors via `internal/toolerr`, structured logging via the `withLogging` wrapper, inventory-driven targets, no shell interpolation of user input.
4. Add tests — unit tests for adapters/parsing, and at least one test that exercises new tools through the real MCP protocol.
5. Run `go build ./... && go vet ./... && gofmt -l . && go test ./... -race -cover` and, if you have it installed, `golangci-lint run ./...` before opening a PR — this is exactly what `ci.yml` runs, and it's a **required, non-optional check** on every PR into `main` (see [CI/CD](#cicd)).
6. Use [Conventional Commits](https://www.conventionalcommits.org) for your commit messages (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`, `ci:`, ...) — the version bump, `CHANGELOG.md`, and release on merge are generated automatically from them.
7. Open a pull request describing what changed and why.
For larger changes (a new adapter, a new version's worth of tools), consider opening an issue first to discuss the approach.
---
## License
Licensed under the [MIT License](LICENSE) — free to use, modify, and distribute, including commercially.
---
## A Note on How This Project Is Built
This project is **vibe-coded**: the vast majority of the implementation — code, tests, commit-by-commit progress — is written by AI (Claude Code), iterating directly against this README as the specification.
The **design and architecture are human-authored** by the repository owner: the layering (MCP → adapters → foundation), the inventory-driven philosophy, the tool-design rules, the security posture, and the version roadmap were all decided by a person before any code was written. AI implements against that spec; it doesn't set the direction.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available