heyo-mcp is a Model Context Protocol server that gives coding agents such as Claude Code tools for HWS sandboxes, app-lb deployments, app-obs logs and metrics, ci runs and the artifact store.
What it is
The server lives in mcp/ (TypeScript, Node 22). It exposes two kinds of tool:
- Sandbox tools use the Heyo cloud API to boot a microVM, run commands in it, and move files in and out.
- Fleet tools operate on app-lb, app-obs, ci and the artifact store. Several diagnostic tools answer questions that span services. For example,
diagnose_empty_poolcombines app-lb's pool state, app-obs's logs and the metrics that explain why a pool won't fill.
It runs in two modes:
| Mode | When | How callers authenticate |
|---|---|---|
| stdio (default) | An agent launches it locally | Credentials come from the environment the agent host starts it in |
| HTTP | HEYO_MCP_HTTP_PORT is set. It runs as an app-lb deployment, shared by many callers. |
app-lb's gate in front, plus the caller's own bearer token forwarded upstream |
Every service is optional. A service you don't configure still lists its tools, and calling one returns an error naming the variable to set. Sandbox tools are the exception: they are listed only when a usable cloud key is available.
Install
cd mcp
npm install
npm run build # compiles to mcp/dist/
npm test # optional: node --test over dist/*.test.js
Claude Code, hosted server
Heyo runs the server at https://mcp.us2.heyo.work/mcp (streamable HTTP). Point Claude Code at it with an app-lb token:
claude mcp add --transport http heyo https://mcp.us2.heyo.work/mcp \
--header "Authorization: Bearer applb_…"
A namespace user mints that token from the app-lb dashboard's "Get started" card (or App-tokens → New token). It is an admin-tier token confined to their namespace and expires within 90 days; see app-lb auth. The applb_* tools work with it. The app-obs and ci tools do not yet, because those gates live in the default namespace (see Limits and known gaps).
Claude Code, local server
claude mcp add heyo \
-e HEYO_API_KEY=heyo_api_… \
-- node /path/to/hws/mcp/dist/index.js
Or in a project's .mcp.json:
{
"mcpServers": {
"heyo": {
"command": "node",
"args": ["/path/to/hws/mcp/dist/index.js"],
"env": {
"HEYO_API_KEY": "heyo_api_…",
"APPLB_URL": "http://127.0.0.1:9090",
"APPLB_TOKEN": "applb_…"
}
}
}
}
Other MCP hosts work the same way: run node /path/to/hws/mcp/dist/index.js over stdio with the variables below in its environment. The server writes logs to stderr only, because stdout carries the protocol. At startup it prints a ready line listing the configured services, plus a CREDENTIAL FAULT banner if a key has the wrong shape (for example an applb_ token in HEYO_API_KEY).
After connecting, call heyo_status first. It reports which services the server can reach and what each says about itself.
Configuration
| Variable | Default | Meaning |
|---|---|---|
HEYO_API_KEY |
unset | Heyo cloud API key (heyo_api_…). Enables the sandbox tools, and is app-lb's default credential in managed mode. |
HEYO_BASE_URL |
https://server.heyo.computer |
Cloud API base URL. |
APPLB_URL |
cloud base (managed mode) | app-lb base URL: its own admin listener (for example http://127.0.0.1:9090) or the cloud base. |
APPLB_NAMESPACE |
discovered from the key | Managed mode: the base becomes ${APPLB_URL}/namespaces/<ns>/lb. |
APPLB_TOKEN |
falls back to HEYO_API_KEY in managed mode |
Bearer token for app-lb: an applb_… app token (self-hosted) or a heyo_api_… key (managed). |
APPLB_BASIC |
unset | user:pass, or a complete Basic … header, for a password-gated app-lb. Sent byte for byte. |
APP_OBS_URL |
unset | app-obs base URL. |
APP_OBS_API_TOKEN |
unset | Bearer token for app-obs query routes. |
CI_URL |
unset | ci base URL. Use ci's own listener (see ci behind a gate). |
CI_TOKEN |
unset | A repository submit token (git config ci.token), used by ci_run_status and ci_run_logs. |
ART_URL |
unset | Artifact store base URL. |
ART_API_KEY |
unset | The store's own key, sent as x-api-key. |
ART_GATE_TOKEN |
APPLB_TOKEN |
App token for an app-lb gate in front of the store, sent as Authorization. |
HEYO_MCP_TIMEOUT_MS |
30000 |
Timeout per upstream request. |
HEYO_MCP_HTTP_PORT |
unset | Set this to serve HTTP instead of stdio. |
HEYO_MCP_HTTP_HOST |
127.0.0.1 |
HTTP bind address. |
HEYO_MCP_REQUIRE_IDENTITY |
on | HTTP mode: refuse requests without an app-lb forwarded identity. Set 0 to disable (see Hosted mode). |
Minimal configurations
Managed app-lb through Heyo cloud. One key covers sandboxes and deployments, and the namespace is discovered from the key on first use:
HEYO_API_KEY=heyo_api_…
If the key can see several namespaces, discovery fails and lists them. Set APPLB_NAMESPACE to choose one.
Self-hosted HWS fleet, run on the app-lb host:
APPLB_URL=http://127.0.0.1:9090
APPLB_TOKEN=applb_…
APP_OBS_URL=http://127.0.0.1:9600
APP_OBS_API_TOKEN=…
CI_URL=http://127.0.0.1:9555
CI_TOKEN=…
ART_URL=http://127.0.0.1:8090
ART_API_KEY=…
Credentials
Four kinds of credential are involved. They are not interchangeable.
| Credential | Looks like | Accepted by |
|---|---|---|
| Heyo cloud API key | heyo_api_… |
Heyo cloud (sandboxes), and app-lb only through cloud's /namespaces/{ns}/lb path |
| app-lb app token | applb_… |
app-lb's admin API, and app-lb auth gates in front of deployments. Not accepted by cloud. |
| Service tokens | anything | Only the service they belong to (APP_OBS_API_TOKEN, CI_TOKEN, ART_API_KEY) |
| JWT | eyJ… |
app-lb jwt gates (see app-lb auth) |
App token scopes
An app-lb app token has two independent scopes, checked in different places:
- Admin tier (
none,view,admin) is checked by app-lb's admin API.viewcovers only/metrics,/disks,/feeds,/security,/ingressand/storage, which serveapplb_metrics,applb_disks,applb_security_eventsand the feed tools. Everything else needsadmin. - Deployment scope (a
deploymentslist and/or anamespace) is checked by each deployment's own gate.
There is no read-only tier for deployments. GET /deployments requires admin, because specs can hold secrets. So a token that can run applb_get_deployment can also run applb_delete_deployment. To narrow a token that needs deployment reads, restrict its deployments list or namespace.
When a call returns 401 or 403, run heyo_whoami. It reports the current credential's admin tier, namespace, deployment scope and expiry, which tells an authentication failure apart from a scope failure.
Tools
65 tools in total. Read-only tools only issue GET requests, which is checked by a test. Destructive tools have descriptions starting with DESTRUCTIVE, and that prefix is what sets destructiveHint. The full generated list with descriptions is in the component README (npm run catalogue regenerates it).
Diagnostics
| Tool | Notes |
|---|---|
heyo_status |
read-only. Which services are reachable and configured. Also resolves the managed namespace. |
heyo_whoami |
read-only. The current credential's scope and expiry. |
fleet_overview |
read-only. app-obs rows plus app-lb topology and ingest counters. |
diagnose_deployment |
read-only. Args id, window. app-lb record, pool, series, recent errors. |
deployment_logs |
read-only. Args id, window or from/to, level, backend, q, before. |
diagnose_empty_pool |
read-only. Why a VM pool is empty or won't fill. |
diagnose_ci_job |
read-only. Why a ci job isn't running. Needs ci's own listener. |
Deploying
| Tool | Notes |
|---|---|
applb_deploy |
Entry point. Arg spec. Validates cross-field rules, creates or updates, starts the right job, waits, and reports TLS. |
applb_spec_schema |
read-only. Arg block (for example VmSpec, AuthGate, JwtSpec, MountSpec, ScalingPolicy, BuildSpec). Omit it for the index. |
applb_create_deployment |
POST /deployments: replaces the deployment and recycles its pool. |
applb_update_deployment |
PUT: keeps the pool when the vm block is unchanged. |
applb_delete_deployment |
destructive. |
applb_scale |
Args id, scaling. |
applb_build |
vm deployments with a build block. Arg git_ref. |
applb_pull |
vm or site deployments, from an artifact store. Arg artifact_ref. |
applb_pull_mounts |
Re-unpack a vm deployment's mounts. |
applb_host_update |
Static (upstreams) or site deployments only: runs their update.commands. |
applb_job, applb_deployment_jobs |
read-only. Job status. |
Fleet and pools
| Tool | Notes |
|---|---|
applb_list_deployments, applb_get_deployment |
read-only (but need admin tier). |
applb_metrics, applb_disks, applb_certs, applb_security_events |
read-only. |
applb_feeds, applb_feed |
read-only. Per-namespace deployment event feed. |
applb_drain_upstream |
destructive. Take a static upstream out of rotation. |
applb_uncordon_upstream |
Put it back. |
applb_evict_vm |
destructive. Destroy one pool VM. |
applb_exec |
destructive. Run a command in a deployment's guest. Args id, command, cwd, env, sandbox_id. |
applb_purge_disk, applb_purge_orphan_disks, applb_sweep_disks |
destructive. "Orphaned" is app-lb's inference, so review before purging. |
Sandboxes
Listed only when a usable heyo_api_… key is available, either configured or forwarded by the caller.
| Tool | Notes |
|---|---|
sandbox_create |
Args include image, size_class, region, driver, name, archive_id, start_command, working_directory, env_vars, open_ports, setup_hooks, wait_seconds (default 120), retries (503 retries, default 3). |
sandbox_list, sandbox_info |
List sandboxes, or show one. |
sandbox_exec |
Runs sh -c and returns stdout, stderr and exit code. |
sandbox_read_file, sandbox_write_file |
Writes are limited to 512 KiB. Use the archive route for larger files. |
sandbox_upload_url, sandbox_finalize_upload, sandbox_attach_archive |
Large uploads through a presigned URL. |
sandbox_set_ttl, sandbox_stop, sandbox_start, sandbox_restart |
Lifecycle. A stopped sandbox keeps its disk. |
sandbox_kill |
destructive. Deletes the sandbox and its disk. |
heyo_capacity |
read-only. Daemons registered to this key, and running sandboxes. |
Artifact store
| Tool | Notes |
|---|---|
art_publish |
Args tag, content_base64 or path (stdio only), name, kind, annotations. Uploads the blob, writes a manifest, and points the tag at the manifest digest. |
art_list_tags, art_get_tag, art_get_manifest, art_list_blobs, art_usage |
read-only. |
ci
| Tool | Notes |
|---|---|
ci_run_status, ci_run_logs |
read-only. Use CI_TOKEN. Work through the public gated hostname. Check run.finished, not the status string. |
ci_cancel_run, ci_destroy_vm, ci_cleanup_failed_vms |
destructive. |
Raw requests
heyo_request (sandboxes only), applb_request, obs_request, ci_request and art_request send arbitrary HTTP to each service. Prefer a named tool: a raw call's intent, including a DELETE, is hidden in its arguments.
Resources and prompts
| URI | Contents |
|---|---|
heyo://applb/deployment-spec |
Full deployment spec schema, generated from app-lb's Rust types, plus cross-field rules |
heyo://applb/deploy-guide |
The deploy sequence end to end |
heyo://applb/tls |
Why exact host routes get certificates and host_suffix routes need a wildcard |
heyo://applb/examples/{name} |
Each spec in app-lb/examples, for example heyo://applb/examples/git-build.json |
The server has one prompt, deploy_a_service(kind, id, host?), where kind is vm, site or static. It returns the ordered plan for that backend type.
Deploying with an agent
A typical request is "deploy this service". The agent should do the following:
- Call
applb_spec_schema(or readheyo://applb/deployment-spec) and pick the closest example. - Call
applb_deploywith the full spec. In managed or namespace-scoped use, include"namespace": "<ns>": a spec without one meansdefault. - Watch the job it returns with
applb_job.
A minimal vm deployment that builds from a Dockerfile:
{
"id": "hello",
"namespace": "team-a",
"routes": [{ "host": "hello.example.com" }],
"vm": {
"driver": "firecracker",
"image": "hello",
"port": 8080,
"size_class": "micro",
"start_command": "setsid nohup /usr/local/bin/hello </dev/null >/var/log/hello.log 2>&1 &"
},
"build": {
"repo": "https://github.com/example/hello.git",
"ref": "main",
"dockerfile": "Dockerfile"
},
"scaling": { "min_replicas": 1, "max_replicas": 3 },
"health": { "path": "/health", "timeout_secs": 2 }
}
This follows heyo://applb/examples/git-build.json, which also shows private-repo auth and the remaining scaling fields. app-lb documents the spec in full.
Which job applies depends on the backend:
| Backend | Update with |
|---|---|
vm with a build block |
applb_build |
vm or site with bytes in an artifact store |
art_publish, then applb_pull |
static upstreams |
applb_host_update (runs the spec's update.commands on the app-lb host) |
Choosing the wrong job is refused, not ignored. applb_host_update never updates a managed vm deployment.
TLS: an exact host route gets a certificate automatically within seconds. A host_suffix route needs a fleet wildcard certificate, and without one it is served a fallback certificate that won't validate.
Publishing a build
art_publish performs three requests in order: PUT /blobs/{sha256}, PUT /manifests, then PUT /tags/{tag} with the manifest's digest. The store accepts a tag that points at a blob digest, but nothing can resolve it, so publish with this tool rather than with raw requests. Re-publishing identical bytes only moves the tag.
When the store is behind an app-lb gate, one request carries two credentials: Authorization: Bearer applb_… for the gate (ART_GATE_TOKEN) and x-api-key for the store (ART_API_KEY). On the store's own listener, ART_API_KEY alone is enough.
Hosted mode
Set HEYO_MCP_HTTP_PORT to serve MCP over streamable HTTP at /mcp, with an open GET /healthz that reports the tool count, configured services and credential faults. The transport is stateless (a new server per request), so it works behind a load-balanced pool. Paths other than /mcp and /healthz return 404.
Credential forwarding
A hosted instance can act as each caller rather than as itself:
Caller's Authorization |
What the server uses upstream |
|---|---|
Bearer applb_… |
Always the caller's token for app-lb, even when APPLB_TOKEN is configured. Also used for app-obs, ci and the store when their configured credential is empty or is itself an applb_ token, which means an app-lb gate fronts them. Never used for cloud. |
Bearer heyo_api_… |
Used for cloud only when HEYO_API_KEY is unset. Used for app-lb only when no app-lb credential is configured. |
| anything else | The configured credentials |
This prevents a narrowly scoped app token from borrowing the server's broader credential. app-lb re-checks the caller's scope on every request.
For forwarding to app-lb, APPLB_URL must still be set (with no token), so the server knows where app-lb is.
Behind an app-lb gate
HEYO_MCP_REQUIRE_IDENTITY (on by default) makes the server refuse any /mcp request that lacks x-auth-request-user or x-auth-request-email, the headers app-lb sets after a google or jwt gate admits a caller. Each call is logged to stderr with that identity. The server does not re-verify the JWT.
| Gate on the deployment | HEYO_MCP_REQUIRE_IDENTITY |
Notes |
|---|---|---|
jwt or google |
leave on | The gate forwards an identity. |
app-token |
0 |
app-lb forwards no identity for token callers. The gate does the authentication. |
/mcp in public_paths |
0 |
Safe only if the instance holds no credentials, so every call carries the caller's own token. |
| none | never 0 |
That leaves an unauthenticated process that can delete deployments. |
In HTTP mode, art_publish accepts only content_base64: a path would name a file on the server, not the caller's machine.
Deploying it
The repository ships two shapes:
- Host process:
mcp/deploy/supervisor/heyo-mcp.confrunsnode …/mcp/dist/index.jsbound to loopback, andmcp/deploy/heyo-mcp.jsonregisters it with app-lb as a static deployment behind ajwtgate. Make the deployment'supstreamsport matchHEYO_MCP_HTTP_PORT(the conf uses9650). - MicroVM:
mcp/deploy/image/Dockerfilebuilds a rootfs (heyvm mvm build --local-only -f deploy/image/Dockerfile -c . -n heyo-mcpfrommcp/). Itsinit.shfirewalls the listener to the host gateway, and app-lb starts node with the deployment'sstart_commandand env.mcp/deploy/vm.mddescribes the zero-credential, app-token-gated posture this shape is designed for.
An instance with HEYO_API_KEY set gives every caller the gate admits the same cloud account. Leave it unset for a fleet-operations instance. An app-token gate cannot pass a cloud key through, so sandbox tools won't be listed.
ci behind an app-lb gate
When ci is behind an app-lb gate, its public_paths are only /healthz, /api/submit, /api/runs/, /api/stream/ and /__ui/. Its HTML pages (runs, jobs, runners, VMs, repos) admit browsers only. For that reason:
ci_run_statusandci_run_logswork against the public hostname withCI_TOKEN. A token for another repository gets 404.diagnose_ci_joband the VM tools need ci's own listener. Run the server on the ci host, or tunnel to it:
ssh -N -L 8081:127.0.0.1:8081 ci-host # then CI_URL=http://127.0.0.1:8081
Pointed at the gated host, those tools fail with this explanation rather than a bare 401.
Limits and known gaps
| Limit | Details |
|---|---|
| Cloud request bodies are capped at 1 MB | sandbox_write_file refuses more than 512 KiB. For larger files, use sandbox_upload_url, then PUT the .tar.gz (Content-Type: application/gzip, no Authorization), then sandbox_finalize_upload, then sandbox_create {archive_id} or sandbox_attach_archive. |
503 from sandbox_create |
Region capacity, not a fault. It is retried with backoff (3 attempts from 2 s). Try another driver or region. Cloud publishes no free-capacity figure, so heyo_capacity can't predict it. |
| Namespace tokens and default-namespace gates | app-obs and ci gates normally live in default, so a token confined to another namespace gets 403 from them while app-lb tools still work. |
| Managed mode route set | Through cloud's namespace path, fleet-wide operator tools (applb_disks, applb_certs, applb_purge_disk, applb_purge_orphan_disks, applb_sweep_disks) return 404 route not exposed through the namespace proxy. |
Guest start_command failures |
Written to /var/log/heyvm-start.log in the guest, never to app-obs. Read the file with applb_exec. An empty log beside an empty pool points here. |
| Untyped fallbacks | The raw *_request tools bypass schema validation and read-only guarantees. |
| No JWT re-verification | In HTTP mode the server trusts app-lb's forwarded identity headers, so it must only be reachable through app-lb. |
Troubleshooting
| Symptom | Fix |
|---|---|
<service> is not configured — set <VAR> |
Set that variable. heyo_status shows everything that's missing. |
No sandbox_* tools listed |
No usable cloud key. HEYO_API_KEY is unset, or it holds an applb_ token. |
401 or 403 from an applb_* tool |
Run heyo_whoami. The admin tier is usually too low (view can't read deployments) or the namespace is wrong. |
401 from cloud with an applb_ token |
Wrong kind of credential. Cloud needs heyo_api_…. |
| Managed mode reports an ambiguous namespace | The key sees several namespaces. Set APPLB_NAMESPACE. |
| 401 from ci tools against the public hostname | The expected gate behaviour. Point CI_URL at ci's own listener. |
| Art writes return 401 while reads work | ART_API_KEY is missing. |
| HTTP mode returns 401 "did not arrive through app-lb's gate" | Either a direct request to the port, or an app-token or public gate with HEYO_MCP_REQUIRE_IDENTITY still on. |
| A tool call times out after 30 s | The upstream accepted the connection and never answered. Raise HEYO_MCP_TIMEOUT_MS only if the service is really that slow. |
See also
- app-lb and app-lb auth: the deployment spec, gates and app tokens
- heyctl: the CLI for the same APIs
- developer-tools
- Component README