iaas_mcp
Model Context Protocol server

Give an AI agent the whole IaaS platform.

iaas-mcp-server is a remote, stateless Streamable-HTTP MCP server that exposes the Hypervisor.io platform to any MCP client. 364 tools cover instances, VPCs, Kubernetes, managed databases, load balancers, DNS, VPN, object storage and more, backed by the same tested API client as the iaas OpenTofu provider. Bearer pass-through auth, confirm-gated deletes, async convergence built in.

364Tools
302User
62Admin (curated)
~/mcp - run & smoke test
# required: the platform API base URL
export IAAS_API_ENDPOINT="https://panel.example.com/api"
export IAAS_MCP_LISTEN=":8080"   # optional (default)

$ go build -o iaas-mcp-server . && ./iaas-mcp-server
listening on :8080  POST /mcp  GET /healthz

# list the tools (Bearer pass-through)
$ curl -sN localhost:8080/mcp \
    -H "Authorization: Bearer $IAAS_API_TOKEN" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
  → 364 tools  (302 user.* + 62 admin.*)
  user.instance.create   user.vpc.attach_instance   ...
What it is

One MCP server over the whole platform

A single binary that speaks the modern Streamable-HTTP MCP transport in stateless mode, so any request lands on any replica and it scales horizontally with no session store.

Remote & stateless

Tools run over Streamable-HTTP in stateless mode. There is no per-connection state and no session store, so it sits behind a load balancer and scales out cleanly. It exposes exactly two HTTP surfaces: POST /mcp (the MCP endpoint, Bearer required) and GET /healthz (a plain, unauthenticated liveness probe that returns ok). The process drains in-flight requests on SIGINT / SIGTERM.

Shared, tested client

It reuses the API client and async waiter from the OpenTofu provider github.com/hypervisor-io/terraform-provider-iaas, so the IaC provider and this server call the platform through one shared, tested implementation. The REST API is the single source of truth; the provider and this server are kept in lockstep with it by a CI gate (see Tri-sync).

Two endpoints, nothing else

POST /mcp is the JSON-RPC MCP endpoint; a request without an Authorization header returns HTTP 401 (an MCP auth error). GET /healthz needs no auth and answers ok. Requires Go 1.25+ to build.

Authentication

Bearer pass-through, nothing stored

The server does not verify or store tokens. Every MCP request must carry Authorization: Bearer <platform-api-token>; the server relays that token to the platform API, which authorizes it. Token handling lives behind one seam (internal/iaasauth) so a later OAuth 2.1 facade can drop in without touching a single tool.

User token

Powers the user.* tools. Create one in the platform panel under API tokens. By default it works from any IP; if you set allowed_ips on the token, the MCP server's egress IP must be in that list. Present a user token and the admin.* tools return an authorization error with a hint.

Admin token

Unlocks the curated admin.* tools. Generate it on the Master host with php artisan api:admin-token generate. Admin tokens are IP-locked to a registered IP, so the operator must register the MCP server's egress IP for the token. There is no client-side scope check: the platform decides.

🔒 Admin tokens are IP-locked

A user token works from any source IP unless you pin it with allowed_ips. An admin token is always IP-locked: the platform validates it against the exact egress IP it was registered with, and a call from any other source IP is rejected with 401 / 403 regardless of whether the token is valid. Register the MCP server's stable egress IP before you expect any admin.* tool to work. Never hard-code a token; read it from the client's environment (the examples use $IAAS_API_TOKEN).

Environment variables

VariableRequiredDefaultMeaning
IAAS_API_ENDPOINTyesnoneBase URL of the platform API, e.g. https://panel.example.com/api
IAAS_MCP_LISTENno:8080Address the HTTP server binds to
IAAS_API_TIMEOUTno30sPer-request timeout to the platform API (Go duration)
IAAS_API_INSECUREnofalseSkip TLS verification on calls to the platform API (staging only)
export IAAS_API_ENDPOINT="https://panel.example.com/api"   # required
export IAAS_MCP_LISTEN=":8080"                             # optional (default)
export IAAS_API_TIMEOUT="30s"                              # optional (default)
export IAAS_API_INSECURE="false"                           # optional (default)

go build -o iaas-mcp-server .   # requires Go 1.25+
./iaas-mcp-server               # or: go run .
Quick start

Point a client at /mcp

The examples assume the server is reachable at https://mcp.example.com/mcp and your token is in $IAAS_API_TOKEN. Never paste a real token into a config you commit.

Run the server

A single static binary, or a distroless container. Set the required IAAS_API_ENDPOINT and go.

# container
docker run --rm -p 8080:8080 \
  -e IAAS_API_ENDPOINT="https://panel.example.com/api" \
  iaas-mcp-server

# or straight from source (Go 1.25+)
go build -o iaas-mcp-server . && ./iaas-mcp-server

Remote connector (hosted MCP client)

In any client that supports remote (Streamable-HTTP) connectors, add a custom connector pointing at the server's /mcp URL and supply the Authorization header. The connector then lists the tools and lets the agent call them.

# URL
https://mcp.example.com/mcp
# Header
Authorization: Bearer <your platform API token>

Use a user token for user.* tools, or an admin token (IP-locked; the server's egress IP must be registered) to also reach admin.* tools.

Stdio-only client (via the mcp-remote bridge)

Some clients launch servers only as local stdio processes. Bridge them to the remote Streamable-HTTP endpoint with mcp-remote, as a stdio server entry:

{
  "mcpServers": {
    "hypervisor": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.example.com/mcp",
        "--header", "Authorization: Bearer ${IAAS_API_TOKEN}"
      ],
      "env": { "IAAS_API_TOKEN": "your-platform-api-token" }
    }
  }
}

Generic mcp.json-style client

Clients that speak Streamable-HTTP directly (VS Code, custom agents) take the URL and header inline:

{
  "servers": {
    "hypervisor": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${env:IAAS_API_TOKEN}" }
    }
  }
}

curl smoke test

The transport is JSON-RPC over HTTP; responses come back as Server-Sent Events, so send Accept: application/json, text/event-stream. Because the server is stateless, each request is independent.

# initialize
curl -sN https://mcp.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $IAAS_API_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"smoke","version":"0.0.1"}}}'

# list the tools
curl -sN https://mcp.example.com/mcp \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $IAAS_API_TOKEN" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# liveness (no auth)
curl https://mcp.example.com/healthz    # → ok

A POST /mcp without an Authorization header returns HTTP 401.

Your first task

A typical flow: discover what to deploy, create an instance (async), then attach it to a VPC. Tool calls use tools/call with {"name": ..., "arguments": ...}.

  • Discover: user.catalog.locations, then user.catalog.plan_groups, user.catalog.plans, user.catalog.images to resolve ids by name.
  • Create (async): user.instance.create waits until the instance is deployed, then returns it.
  • Attach: user.vpc.attach_instance auto-assigns the lowest free IP as primary and returns the attached IPs.
  • Clean up (confirm-gated): user.instance.delete with {"id": "...", "confirm": true}; without "confirm": true it refuses.
Reference

Every tool family

All 364 tools, named user.<family>.<action> and admin.<family>.<action>, grouped by domain. Search narrows by name or purpose; the chips narrow by group. Families backed by a platform task are tagged ASYNC.

Compute & instances · 8 families
user.instanceuser4 toolsASYNC

Full VM lifecycle. create enqueues a deploy task and waits until the instance is deployed; delete is confirm-gated and waits until it is fully removed.

Tools
user.instance.create   # async: waits for "deployed"
user.instance.list
user.instance.get
user.instance.delete   # confirm-gated
user.ssh_keyuser5 tools

Register SSH public keys for injection into instances at provision time. create / list / get / update / delete.

user.user_scriptuser5 tools

Manage reusable cloud-init / startup scripts attachable to instances. create / list / get / update / delete.

user.imageuser4 toolsASYNC

Capture a custom image (snapshot) from an instance; creation converges to available. create / list / get / delete.

user.docker_deploymentuser5 tools

Deploy a catalog Docker app or a custom Compose file onto an instance, installing the engine first if needed. deploy_app / deploy_compose / list / get / delete.

user.instance_backup_policyuser6 tools

Named backup schedule & retention policy; instances attach and detach in place. create / list / get / attach_instance / detach_instance / delete.

user.autoscaling_groupuser7 toolsASYNC

A self-healing fleet kept between min and max instances. create / list / get / update / pause / resume / delete.

user.autoscaling_policyuser4 tools

A CPU or memory-driven scale up/down policy attached to an autoscaling group. create / get / update / delete.

Networking - VPC & IP · 5 families
user.vpcuser4 tools

Isolated layer-2 private networks. attach_instance auto-assigns the lowest free IP as primary. create / list / get / attach_instance.

user.instance_vpcuser6 tools

Manage an instance's VPC interface and its IPs. add_ip / remove_ip / set_primary_ip / list_ips / list_available_ips / disable.

user.nat_gatewayuser8 toolsASYNC

The single egress gateway for a VPC's private subnets. create / get / get_for_vpc / attach_subnet / detach_subnet / enable / disable / delete.

user.static_ipuser4 tools

Reserve a long-lived public IPv4 from a location's pool. allocate / list / get / deallocate (confirm-gated).

user.ip_setuser7 tools

Named, version-scoped sets of CIDR entries referenced from security-group rules. create / list / get / add_entry / bulk_add / remove_entry / delete.

Security · 1 family
user.security_groupuser8 tools

Named, stateful firewall rule sets attachable to instances. Rules and attachments update in place. create / list / get / add_rule / remove_rule / attach_instances / detach_instances / delete.

Load balancing · 1 family
user.load_balanceruser22 toolsASYNC

HAProxy load balancers plus their frontends, backends, targets, routing rules and certificates as child tools. The balancer itself deploys asynchronously.

Tools
user.load_balancer.create / list / get / delete   # async
user.load_balancer.frontend_create / _get / _update / _delete
user.load_balancer.backend_create / _get / _delete
user.load_balancer.target_create / _get / _update / _delete
user.load_balancer.routing_rule_create / _get / _update / _delete
user.load_balancer.certificate_create / _get / _delete
VPN · 2 families
user.vpn_gatewayuser8 tools

A VPN gateway and its peers. create / get / delete plus add_peer / get_peer / update_peer / remove_peer / peer_config.

user.vpn_peeringuser3 tools

Peering between VPN gateways. create / get / delete.

DNS · 3 families
user.dns_zoneuser7 tools

Authoritative DNS zones, attachable to VPCs for private resolution. create / list / get / update / attach_vpc / detach_vpc / delete.

user.dns_record_setuser4 tools

Record sets (RRsets) within a zone. create / get / update / delete.

user.dns_recorduser4 tools

Individual records inside a record set. create / get / update / delete.

Managed databases · 4 families
user.managed_databaseuser11 toolsASYNC

Managed database instances. create / list / get / resize / upgrade / restart / reset_password / resync_replicas / acknowledge_error / retry / delete.

user.db_replicauser1 tool

Provision a read replica of a managed database. create.

user.db_parameter_groupuser5 tools

Reusable database parameter (tuning) groups. create / list / get / update / delete.

user.db_backup_policyuser6 tools

Database backup schedule & retention policies. create / list / get / attach_database / detach_database / delete.

Storage & object storage · 3 families
user.volumeuser10 tools

Block-storage volumes and their snapshots. create / list / get / resize / attach / detach / snapshot_create / snapshot_get / snapshot_delete / delete.

user.s3_bucketuser9 tools

S3-compatible buckets, ACLs and key attachments. create / list / get / set_acl / list_keys / attach_key / update_key / detach_key / delete.

user.s3_access_keyuser4 tools

S3 access keys; the secret is returned only once at create. create / list / get / update.

Kubernetes · 4 families
user.kubernetes_clusteruser10 toolsASYNC

Managed clusters. create waits until running. create / list / get / kubeconfig / upgrade_control_plane / upgrade_workers / upgrade_ccm / retry_upgrade / autoscaler_manifest / delete.

user.kubernetes_node_pooluser5 tools

Worker node pools within a cluster. create / list / get / update / delete.

user.kubernetes_security_group_ruleuser4 tools

Firewall rules on a cluster's security group. create / list / get / delete.

user.kubernetes_ssl_certificateuser4 tools

TLS certificates managed for a cluster. create / list / get / delete.

Ops & account · 4 families
user.cataloguser12 tools

Read-only discovery: resolve ids by name before you build. locations / plan_groups / plans / images / isos and the k8s_* catalogs (regions, versions, worker/control-plane/load-balancer plans, subnets, vpcs).

user.projectuser7 tools

Group resources into projects. create / list / get / update / assign_resource / unassign_resource / delete.

user.notification_channeluser5 tools

Delivery channels for monitoring alerts. create / list / get / update / delete.

user.alert_ruleuser5 tools

Metric-threshold alert rules routed to notification channels. create / list / get / update / delete.

Admin - curated allowlist · 62 tools, reads + 2 safe mutations
admin.instanceadmin7 tools

Fleet-wide instance inventory, read-only. list / get / list_by_user / ips / disks / backups / metrics.

admin.hypervisoradmin11 tools

Hypervisor, group and storage reads, plus the one reversible mutation set_maintenance (confirm-gated). list / get / metrics / instance_stats / set_maintenance and the hypervisor_group / hypervisor_storage reads.

admin.subnet / ip / vpcadmin8 tools

Network inventory, read-only. subnet.list / get / ips / available_ips / statistics, ip.list, vpc.list / get.

admin.task / migrationadmin6 tools

Operations reads. task.list / get / logs and migration.list / get / logs.

admin.rdns_requestadmin2 tools

Reverse-DNS request queue. list, plus the reversible mutation process (confirm-gated).

admin.user / session / systemadmin6 tools

Identity & audit reads. user.list / get, session.list, system.admin_logs / email_log / ip_log.

admin.* catalog & plansadmin22 tools

Read-only catalog, billing and inventory: backup_plan, backup_storage, cs_location, cs_plan_group, currency, db_plan, image, instance_plan, iso, lb_plan, s3_plan, s3_server, s3_bucket, s3_access_key, self_provisioning_pack, volume_plan.

No matching tool families. Try a different search term or choose "All".

Cross-cutting behavior

What every tool inherits

Four behaviors are wired into the framework, so every tool in the catalog above behaves predictably. Learn them once.

Confirm gate on destructive ops

The 42 destructive tools (delete / deallocate / detach and the two reversible admin mutations) refuse unless the call passes "confirm": true, so an agent cannot destroy on a slip. A missing or false confirm fails closed.

user.instance.delete { "id": "..." }
  # refused: confirmation required

user.instance.delete { "id": "...", "confirm": true }
  # proceeds, then waits until fully removed

Idempotency keys

Mutating tools accept an optional idempotency_key. Where the platform endpoint supports it (for example Kubernetes create / update / delete) it is threaded through, so a retried call is deduplicated server-side rather than acting twice.

user.kubernetes_cluster.create {
  "name": "prod",
  # ...
  "idempotency_key": "prod-cluster-2026-07-01"
}

Async convergence via a waiter

Create tools that enqueue a platform task poll to a terminal state and return the converged object, so the agent gets a ready resource, not a pending id. user.instance.create waits until deployed; user.kubernetes_cluster.create waits until running. The waiter is shared with the OpenTofu provider.

Error mapping

Platform errors become actionable MCP results, not raw HTTP.

PlatformBecomes
401 / 403A scope / IP-lock hint
422Surfaced field errors
404Not-found
429Surfaced as retryable

The curated admin allowlist (spec 17, decision D3)

admin.* is a deliberately narrow, safe allowlist: admin reads (list / get / inspect / stats across instances, users, hypervisors, subnets, tasks, plans, storages and more) plus exactly two reversible mutations, admin.hypervisor.set_maintenance and admin.rdns_request.process.

Deliberately not exposed

The admin surface intentionally omits every irreversible or fleet-wide-risk operation:

  • Any billing or credit mutation
  • User deletion and impersonation-token minting
  • Hypervisor destroy / decommission
  • Bulk IP / subnet deletion, and other high-blast-radius actions
Tri-sync

API, provider and MCP, in lockstep

The platform REST API is the source of truth for three consumers that must not drift: the API itself, the OpenTofu provider, and this MCP server.

A checked-in api-manifest.json records, per endpoint, whether the provider and MCP cover or explicitly exclude it. Three CI checks, one per repo, fail the build on drift, and a release cannot ship with an endpoint that this server neither covers nor explicitly excludes.

This repo's leg is internal/tools/manifest_coverage_test.go: it asserts that every endpoint marked mcp.status == "covered" names a tool this server actually registers, against a vendored copy of the manifest refreshed with make sync-manifest. See spec 17 (17-opentofu-mcp-api-trisync.md) in the platform repo.

          platform REST API   # single source of truth
                 │
        api-manifest.json      # per-endpoint: covered / excluded
          ╭─────┴─────╮
   OpenTofu provider     iaas-mcp-server
   (terraform-           (this repo:
    provider-iaas)        manifest_coverage_test.go)
          ╰─────┬─────╯
                 │
        3 CI gates fail on drift