A Model Context Protocol (MCP) server for Anka macOS virtualization. It runs as a streamable HTTP server with bearer-token auth and exposes two curated, purpose-built tool sets that auto-enable based on configuration:
- Controller backend - talks to the Anka Build Cloud Controller REST API to request a VM from a fleet and hand back SSH connection details. No
ankaCLI is used. - Local backend - drives the local
ankaCLI with a small, safe set of lifecycle commands, guarded by a running-VM limit.
There is intentionally no generic "run any anka command" tool.
| Backend | Enabled when | Tools |
|---|---|---|
| Controller | ANKA_CONTROLLER_URL is set |
controller_list_templates, controller_request_vm, controller_get_vm, controller_terminate_vm |
| Local | ANKA_LOCAL=on, or auto when no controller is configured (detects the anka CLI) |
local_list_templates, local_start_vm, local_show_vm, local_ssh_access, local_delete_vm |
When ANKA_CONTROLLER_URL is set, the local backend defaults to off so only controller tools are exposed. Set ANKA_LOCAL=on or ANKA_LOCAL=auto to enable both. The server refuses to start if neither backend is enabled.
- Node.js >= 18
- For the local backend: the
ankaCLI installed and onPATH(or pointANKA_BINat it) - For the controller backend: network access to an Anka Build Cloud Controller
Requires Node.js >= 18.
# run without a global install
npx @veertu/anka-mcp
# or install globally
npm install -g @veertu/anka-mcp
anka-mcpSet auth and backend env vars before starting (see Configuration). Example:
export ANKA_MCP_ADMIN_TOKEN="$(openssl rand -hex 32)"
echo "ANKA_MCP_ADMIN_TOKEN=$ANKA_MCP_ADMIN_TOKEN"
anka-mcp
# Create a client MCP token (plaintext shown once):
curl -s -X POST "http://localhost:9111/admin/tokens" \
-H "Authorization: Bearer $ANKA_MCP_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label":"my-client"}'npm install
npm run build
export ANKA_MCP_ADMIN_TOKEN="$(openssl rand -hex 32)"
echo "ANKA_MCP_ADMIN_TOKEN=$ANKA_MCP_ADMIN_TOKEN"
npm startFor multi-client deployments, use the admin API to issue per-client tokens:
export ANKA_MCP_ADMIN_TOKEN="$(openssl rand -hex 32)"
export ANKA_CONTROLLER_URL="http://your-controller:8090"
npm start
# Create a per-client MCP token (plaintext shown once):
curl -s -X POST "http://localhost:9111/admin/tokens" \
-H "Authorization: Bearer $ANKA_MCP_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label":"team-a"}'The MCP endpoint is served at http://<host>:<port>/mcp. An unauthenticated status endpoint at http://<host>:<port>/status returns { "version": "<semver>" } for health checks. For local dev without auth (never expose beyond localhost):
ANKA_MCP_ALLOW_NO_AUTH=1 npm run devThe agent generates an SSH keypair locally and passes a base64-encoded OpenSSH public key line as ssh_public_key_base64 to controller_request_vm. The MCP server installs that key on the VM via the controller startup_script, polls until the instance is Started with a forwarded SSH port, then returns { host, port, username }. The agent connects with its private key. If ssh_public_key_base64 is omitted, the tool returns ssh_key_instructions instead of starting a VM.
flowchart LR
agent["AI agent"] -->|"controller_request_vm {templateId, ssh_public_key_base64}"| mcp["anka-mcp"]
mcp -->|"POST /api/v1/vm"| ctl["Controller API"]
mcp -->|"poll GET /api/v1/vm?id="| ctl
mcp -->|"{host, port, username}"| agent
agent -->|"ssh -i local private key"| vm["macOS VM"]
The VM template must expose port forwarding for the SSH guest port (default 22, e.g. a port-forward-22 tag), or pass addSshPortForward: true to controller_request_vm to add a rule at start time.
ANKA_MCP_SSH_KEY=/tmp/anka_mcp_ssh_key
rm -f "$ANKA_MCP_SSH_KEY" "$ANKA_MCP_SSH_KEY.pub"
ssh-keygen -t ed25519 -N "" -C "anka-mcp" -f "$ANKA_MCP_SSH_KEY" -q
# Linux:
base64 -w0 < "$ANKA_MCP_SSH_KEY.pub"
# macOS:
base64 < "$ANKA_MCP_SSH_KEY.pub" | tr -d '\n'
# Pass the output as ssh_public_key_base64.
# If controller_request_vm returns status ready with ssh, use it — do not call controller_get_vm.
# Only if status is pending, poll controller_get_vm every 30s until ready — do not SSH yet
# Wait ~20s after status becomes ready (startup_script installs your key at boot), then:
SSH_AUTH_SOCK= ssh -i "$ANKA_MCP_SSH_KEY" -p <port> \
-o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
<username>@<host>
# If auth fails, wait 20s and retry. IdentitiesOnly + SSH_AUTH_SOCK= avoids "Too many authentication failures".
# When done or on session termination:
rm -f "$ANKA_MCP_SSH_KEY" "$ANKA_MCP_SSH_KEY.pub"The agent manages VMs on the developer's own machine through a limited command set: list templates, start (which always clones the chosen template into a fresh VM so the original is never touched), show, prepare SSH access, delete. A running-VM limit (default 2) prevents exceeding the local Anka concurrency limit. The delete tool always requires a specific VM name and can never delete all VMs.
For SSH, local_ssh_access installs the caller's base64-encoded public key into the running VM's ~/.ssh/authorized_keys (via anka run) and returns { ip, port, user }. The VM template must have Remote Login (sshd) enabled.
All configuration is via environment variables.
| Variable | Default | Description |
|---|---|---|
ANKA_MCP_HTTP_PORT |
9111 |
Port the HTTP server listens on. |
ANKA_MCP_HTTP_HOST |
127.0.0.1 |
Interface to bind to. Defaults to localhost; set 0.0.0.0 for remote access behind TLS. |
ANKA_MCP_ADMIN_TOKEN |
(none) | Admin bearer token for /admin/* routes. Required to start the server. |
ANKA_MCP_DB_PATH |
./anka-mcp.db |
SQLite database for client tokens and instance ownership. |
ANKA_MCP_REVOKE_CLEANUP |
on |
When a token is revoked, terminate its controller VMs (set off to skip). |
ANKA_MCP_ALLOW_NO_AUTH |
false |
Set to 1 to run unauthenticated (local dev only). |
ANKA_MCP_ALLOWED_ORIGINS |
(none) | Comma-separated Origin allow-list for DNS-rebinding protection. |
ANKA_MCP_LOG |
on |
Request logging to stderr. Set to off (or 0/false/no) to disable. |
ANKA_MCP_AUDIT_LOG |
(none) | Optional append-only audit log file (duplicates stderr log lines). |
ANKA_MCP_MAX_BODY_BYTES |
1048576 |
Max JSON request body size (1 MiB). |
ANKA_MCP_RATE_LIMIT_RPM |
120 |
Max requests per client IP per minute (0 = disabled). |
ANKA_MCP_SESSION_IDLE_MS |
3600000 |
Idle MCP session eviction threshold (1 hour). |
ANKA_MCP_MAX_SESSIONS |
50 |
Max concurrent MCP sessions. |
ANKA_MCP_MAX_RESPONSE_CHARS |
32768 |
Max serialized tool response size (32 KiB). |
When logging is enabled, each MCP request is written to stderr with the client source (IP and user-agent), JSON-RPC method, tool name and arguments, tool response (with passwords and private keys redacted), and any underlying anka or controller API calls. Example:
anka-mcp: 2026-06-22T16:52:15.021Z [127.0.0.1 (Cursor/1.x)] mcp tools/call {"tool":"local_list_templates","args":{}}
anka-mcp: 2026-06-22T16:52:15.059Z [127.0.0.1 (Cursor/1.x)] anka anka -j list -> ok
anka-mcp: 2026-06-22T16:52:15.059Z [127.0.0.1 (Cursor/1.x)] tool local_list_templates args={} -> {"vms":[...]}
| Variable | Default | Description |
|---|---|---|
ANKA_CONTROLLER_URL |
(none) | Base URL, e.g. http://anka.controller:8090. Enables the backend. |
ANKA_CONTROLLER_AUTH |
(none) | Raw Authorization header value (root token / UAK / Basic). |
ANKA_CONTROLLER_TLS_INSECURE |
false |
Set to 1 to skip TLS certificate verification. |
ANKA_CONTROLLER_POLL_INTERVAL_MS |
3000 |
Interval between instance-status polls. |
ANKA_CONTROLLER_START_TIMEOUT_MS |
180000 |
Max time to wait for a VM to become SSH-ready. |
| Variable | Default | Description |
|---|---|---|
ANKA_LOCAL |
auto* |
auto (detect the binary), on, or off. *Defaults to off when ANKA_CONTROLLER_URL is set. |
ANKA_BIN |
anka |
Path to (or name of) the anka binary. |
ANKA_TIMEOUT_MS |
300000 |
Max time a single anka invocation may run. |
ANKA_LOCAL_MAX_VMS |
2 |
Max running VMs allowed before start is refused. |
ANKA_LOCAL_POLL_INTERVAL_MS |
2000 |
Interval between status polls while waiting for a VM's IP. |
ANKA_LOCAL_IP_TIMEOUT_MS |
120000 |
Max time local_start_vm/local_ssh_access wait for an IP. |
| Variable | Default | Description |
|---|---|---|
ANKA_VM_SSH_USER |
anka |
Username returned for SSHing into a VM. |
ANKA_VM_SSH_PASSWORD |
admin |
Password returned for SSHing into a VM. |
ANKA_VM_SSH_GUEST_PORT |
22 |
Guest port that maps to SSH (matched in port forwarding). |
The MCP server never generates or stores SSH private keys. The agent creates a keypair locally, passes the base64-encoded public key line to controller_request_vm or local_ssh_access, and connects with the matching private key once the endpoint is ready.
Important: The controller may expose an SSH port before startup_script finishes installing your public key. If controller_request_vm returns status: "ready", use that response — do not call controller_get_vm. Poll controller_get_vm only when request returned status: "pending". Wait ~20 seconds after status is "ready", then SSH with -o IdentitiesOnly=yes and SSH_AUTH_SOCK= to avoid early auth failures.
For production, terminate TLS in front of this server so the bearer token is not sent in cleartext.
See SECURITY.md for the full operator security guide.
By default the server binds to localhost only (127.0.0.1). To expose it on a network:
- Set
ANKA_MCP_HTTP_HOST=0.0.0.0(or a specific interface). - Terminate TLS in a reverse proxy in front of anka-mcp.
- Firewall to trusted clients only.
- Issue per-client tokens via the admin API — do not reuse the admin token for MCP clients.
- Never use
ANKA_MCP_ALLOW_NO_AUTHon non-localhost hosts.
The server prints a warning at startup when bound to a non-loopback address.
When ANKA_MCP_ADMIN_TOKEN is set, the server exposes an admin API to create and revoke per-client MCP bearer tokens. Tokens are stored in SQLite (ANKA_MCP_DB_PATH); only hashed secrets are persisted.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/admin/tokens |
Bearer ANKA_MCP_ADMIN_TOKEN |
Create a client token |
GET |
/admin/tokens |
admin bearer | List tokens (no secrets) |
DELETE |
/admin/tokens/:id |
admin bearer | Revoke token and clean up VMs |
Admin and MCP tokens are separate: the admin token never works on /mcp, and client tokens never work on /admin/*. The /status endpoint does not require authentication.
Controller VM isolation: each client token can only controller_get_vm / controller_terminate_vm instances it created via controller_request_vm. Revoking a token blocks MCP access immediately and, by default (ANKA_MCP_REVOKE_CLEANUP=on), best-effort terminates all controller instances owned by that token. The revoke response includes cleanup.terminated and cleanup.failed arrays.
Back up anka-mcp.db for disaster recovery; it is created automatically on first start.
controller_list_templates- list registry templates (id,name,arch) to find atemplateId.controller_request_vm{ templateId, ssh_public_key_base64, tag?, name?, externalId?, addSshPortForward? }- start one VM and install the caller's SSH public key viastartup_script. Requiresssh_public_key_base64(base64 of a single-line OpenSSH public key). If omitted, returns{ error, ssh_key_instructions }without starting a VM. When SSH-ready:{ instance_id, status: "ready", ssh: { host, port, username } }— use this directly; do not callcontroller_get_vm. While pulling:{ status: "pending", ssh: null, message }— pollcontroller_get_vmevery 30 seconds.controller_get_vm{ instance_id }- poll an owned instance only whencontroller_request_vmreturnedstatus: "pending". When SSH-ready, returns{ status: "ready", ssh: { host, port, username } }.controller_terminate_vm{ instance_id }- terminate an instance (must be owned by the caller's token). Returns{ instance_id, terminated: true, ssh_key_cleanup }; run the cleanup command to remove the agent keypair.
local_list_templates- list the local VM library to find a template to clone from.local_start_vm{ template, name?, wait?, timeoutSeconds? }- clone the given template into a fresh, disposable VM and start it. The original template is never started or modified.namedefaults to an auto-generated name (mcp-<id>). Subject to the running-VM limit. By default waits for the new VM to boot and obtain an IP, then returns{ ok, name, source, ip }; passwait: falseto return immediately. Delete withlocal_delete_vmwhen done.local_show_vm{ name }- get a VM's IP address.local_ssh_access{ name, ssh_public_key_base64 }- install the caller's public key into a running VM; returns{ ip, port, user }. Requiresssh_public_key_base64; if omitted, returns{ error, ssh_key_instructions }.local_delete_vm{ name }- delete one specific VM.
VM/template names that start with - are rejected so a name can never be reinterpreted as a CLI flag.
Point any MCP client that supports streamable HTTP at http://<host>:<port>/mcp with the bearer token. For example, Cursor's mcp.json:
{
"mcpServers": {
"anka": {
"url": "http://your-host:9111/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}npm test # run the suite once
npm run test:watch # watch modeThe suite (Vitest) is hermetic - it needs neither a real Anka install nor a controller:
- Unit tests cover config parsing, token store, the controller client +
extractSsh/isSshReady(against an in-process mock controller), and input validation / output shaping. - End-to-end tests spawn the real server over HTTP and exercise tools through the MCP protocol, using a fake
ankabinary (test/fixtures/fake-anka.mjs) and a mock controller (test/helpers/controllerMock.ts). They verify auth, admin token API, per-token controller VM isolation, revoke cleanup, backend gating, per-backend tool exposure, the controller request->SSH flow, the local 2-VM guard, flag-injection rejection, and the SSH key-injection flow.
- Create a file under
src/tools/controller/orsrc/tools/local/exporting a tool viadefineTool({ name, config, handler }). - Add it to the array in that backend's
index.ts(controllerToolsorlocalTools). It is then auto-registered whenever that backend is enabled.
src/
index.ts # entry: starts the HTTP server
auth.ts # MCP bearer token resolution
config.ts # env-driven config + backend detection
anka.ts # runAnka(): execFile wrapper + JSON-envelope parsing
controller.ts # Anka Build Cloud Controller API client
server.ts # createServer(): McpServer + registerTools
tokens/
store.ts # SQLite token + instance ownership store
schema.ts # DB migrations
ownership.ts # controller instance access helpers
cleanup.ts # revoke-time controller VM termination
tools/
define-tool.ts # defineTool helper + jsonResult
index.ts # registers enabled backends' tools
controller/ # controller_* tools
local/ # local_* tools (+ vms.ts: list/count/guard/name schema)
transports/
http.ts # streamable HTTP transport + auth/origin middleware
status.ts # unauthenticated GET /status (version)
admin.ts # /admin/tokens routes
test/
fixtures/fake-anka.mjs # fake anka CLI for hermetic local-backend tests
helpers/ # mock controller + MCP-over-HTTP client
unit/ # config, controller client, validation/shaping
e2e/ # full server over HTTP (auth, controller, local)


