eitri gives Claude explicit tools for creating and controlling VMs on a fleet.
There is one place it speaks MCP: /mcp, served by the control plane itself.
MCP streamable HTTP, authenticated with a personal access token. No install and
no config file: a PAT is enough to connect. Creating VMs additionally needs an
SSH user CA you register once with ca_upload; every other tool needs only the
token.
On the hosted plane that endpoint is https://api.eitri.sh/mcp. A plane you run
yourself serves the same endpoint on its own address, from the same binary — a
self-hosted eitri is a self-hosted MCP server, with nothing extra to install.
| Tool | Purpose |
|---|---|
vm_create |
Create a VM; by default waits for lifecycle=ready + IP, then for guest SSH and cloud-init status --wait to finish. Takes an optional network, the named host network to give the guest a second NIC on; a host serves only the names it advertises, and naming another refuses the create. Returns network and network_ip—the address that network's own DHCP granted, empty at ready when it has not answered yet. Refused while the tenant has no registered SSH user CA—call ca_upload first, or the guest would trust nothing. An optional cloud_init is the guest's own user-data, taken verbatim: eitri's own write_files and runcmd (the CA trust file, the sshd drop-in, the clock drop-in, the root grow) are prepended to yours rather than replaced by them, and every scalar you set—growpart included—still wins. |
vm_list |
List all VMs on the fleet (id, name, lifecycle, IP, size, network and network_ip). |
vm_info |
Show one VM's state, both addresses, plus a ready-to-use ssh command. Where to read network_ip back when it was still empty at create. |
vm_exec |
Run a shell command in a VM over SSH; returns stdout, stderr, exit code. |
vm_write_file |
Write content to a path in a VM over SFTP (parent dirs created). |
vm_read_file |
Read a file from a VM over SFTP (capped at 1 MiB, truncation flagged). |
vm_expose |
Publish a guest port on the VM's host, protocol tcp (the default) or udp; returns the exposure id, both ports, the protocol, the address to dial and the socket's state. Omit host_port to allocate one from 30000–32767. |
vm_exposures |
List a VM's published ports, same shape. |
vm_unexpose |
Stop publishing a guest port (host_port or protocol disambiguates when one guest port is published twice). |
vm_destroy |
Destroy a VM by id or exact name. Explicit-only—never called automatically. |
ca_upload |
Register your SSH user CA's public key with your tenant, with an optional label. Only the public half is sent. A guest trusts the CA set it was created with, so VMs that already exist will not accept certificates from a CA uploaded now. |
tenant_info |
Show this tenant's setup: registered CAs (fingerprint and label), whether a delegation is live and when it expires, and the gate address. Read-only. |
delegate_begin |
Get the ephemeral public key eitri will authenticate with, the principal your certificate must carry, and the ssh-keygen line that signs it. |
delegate_complete |
Hand back the signed certificate. eitri can then reach your VMs until it expires. |
Deliberately absent: any host or fleet-level operation (enroll, decommission, power management, image/firmware knobs). The worst case from a confused model is VM churn, never fleet damage—and Claude Code's per-tool permission prompts gate every call regardless.
Mint a personal access token in the console Settings page and point an MCP client at the endpoint with that token as a bearer credential:
claude mcp add --transport http eitri https://api.eitri.sh/mcp \
--header "Authorization: Bearer $EITRI_TOKEN"
Then delegate access, which is what lets vm_exec, vm_write_file,
vm_read_file and vm_create's boot wait reach a guest. It is two calls and
one ssh-keygen:
delegate_begin returns a public key, the principal (ubuntu) and the
exact command to run.
Run it. It signs eitri's public key with your CA, on your TTL:
ssh-keygen -s ~/.ssh/my-ca -I eitri-delegation -n ubuntu -V +8h eitri-delegation.pub
delegate_complete with the contents of eitri-delegation-cert.pub.
eitri holds no signing key at any point. Its half is an ephemeral keypair held in memory, useless without a certificate it cannot produce for itself. The certificate chains to a CA your tenant has already registered, so VMs created before you delegate accept it too.
The delegation lives in memory only. A control-plane restart drops it and you
delegate again — the public key is stable, so that is one ssh-keygen and one
delegate_complete. A delegate_begin you never complete is held for an hour
and then abandoned, so a certificate signed long after the fact is refused; call
delegate_begin again for the current key. GET /api/v1/delegations reports the
expiry; DELETE ends it now.
Delegation requires the tenant to have at least one registered SSH user CA
(eitri ca upload), because a certificate signed by a CA your guests do not
trust would be refused by them anyway.
The endpoint answers POST only; a GET is 405. There are no sessions—each
call carries its own credential. vm_create with the default wait: true
blocks for as long as the guest takes to boot, and emits MCP progress
notifications while it waits (for clients that ask for progress).
eitri reaches VMs by name over SSH, with certificates in both directions and no TOFU anywhere. The user signing key stays with you: eitri is delegated a credential, never a key.
eitri generates an ephemeral ed25519 keypair per tenant, in memory only, and
hands you the public half; you sign it with your own CA, on your own TTL and
principals, and post the certificate back. eitri then authenticates as that
key-plus-certificate: it reaches the guest's sshd over the VM host's own sync
tunnel and verifies the guest's host certificate under <tenant>.<vm>:22
against the host CA. No signing key ever exists on the server, the certificate
expires, and a restart drops it.
Because the certificate chains to a CA the tenant already registered, it is accepted by every guest that trusts that CA — including ones created long before the delegation. Exec without a live delegation is refused with the three-step recipe rather than a generic authentication failure.
VMs are addressed by their namespaced connect name, not IP, so recycled IPs and
host-key churn are not a concern. The guest trusts the tenant's registered user
CAs via TrustedUserCAKeys (provisioned by eitri's own cloud-config, merged
with yours), so no per-VM
authorized_key injection is needed. The PAT authenticates API calls only; SSH
traffic uses the certificate, and the token itself is never surfaced in a tool
result or error.
vm_destroy
(exact id or name, no wildcards, no bulk) is the only kill path, and Claude
is instructed to treat it as explicit-only, never automatic cleanup.vm_expose binds a host port and
pipes it to the guest; whoever can reach the host on that port reaches the
service. A UDP one goes further: with no handshake to prove where a datagram
came from, the host answers the address the datagram claimed, so the port can
be aimed at a third party. The tool descriptions say so, so the model treats
publishing as a deliberate act. DNS, TLS certs and routing remain out of
scope—the tools hand back a host address and a port.IMPORTANT—"ready" is not "booted."
vm_create'slifecycle=readymeans cloud-hypervisor is up and the VM has an allocated IP; it does not mean the guest has finished booting Linux, brought up its NIC, or startedsshd.vm_create(with the defaultwait: true) accounts for this: it polls forready+ IP, then retries SSH until it connects, then runscloud-init status --waitbefore returning—so a normalvm_createcall only returns once the guest is genuinely usable. But if you reach a just-created VM some other way (e.g. its IP fromvm_list/vm_infoimmediately after creation, orwait: false), it may still be mid-boot and refuse connections for a short window.