lava-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lava-mcpshow me the status of all devices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
lava-mcp
An MCP server that exposes a LAVA instance to agents — letting them query the board farm, submit and manage test jobs, and (in a later phase) open interactive sessions to a board.
It is a thin client over LAVA's REST API (v0.2/v0.3); point it at any LAVA instance.
Install
pip install -e .[dev]Related MCP server: jenkins-mcp-server
Credentials
The LAVA target (LAVA_URL) is normally pinned to the instance a deployment
fronts; the token is resolved per request:
Hosted (HTTP), pinned — set
LAVA_URLon the server to the LAVA instance it serves. Each connecting client then sends only its ownX-Lava-Tokenand acts as its own LAVA user; noX-Lava-Urlis needed. The server stores no per-user token.Hosted (HTTP), multi-tenant — leave
LAVA_URLunset; each client sends bothX-Lava-UrlandX-Lava-Token, so one server can front many LAVA instances.Local (stdio) mode — falls back to
LAVA_URL/LAVA_TOKENin the environment (single user).
Run (stdio, local)
export LAVA_URL=https://lava.example.com
export LAVA_TOKEN=<your-api-token>
lava-mcp # or: lava-mcp --read-onlyLaunch over stdio from your MCP client (claude_desktop_config.json / Claude Code):
{
"mcpServers": {
"lava": {
"command": "lava-mcp",
"env": { "LAVA_URL": "https://lava.example.com", "LAVA_TOKEN": "..." }
}
}
}For a hosted server pinned to a LAVA instance, point Claude Code at the HTTP endpoint and pass only your token:
claude mcp add --transport http lava https://mcp.example.com/mcp \
--header "X-Lava-Token: <your-api-token>"If the server is multi-tenant (no LAVA_URL set), also pass
--header "X-Lava-Url: https://lava.example.com" to choose the instance.
Run as a hosted service (HTTPS via Caddy)
For interactive board sessions the server must be reachable by lab workers, so run
it hosted. docker compose brings up the MCP server behind Caddy (automatic HTTPS)
and exposes the SSH board-session gateway:
cp .env.example .env # set LAVA_URL/LAVA_TOKEN/LAVA_MCP_DOMAIN/LAVA_MCP_GATEWAY_WS_URL
docker compose up -dAgents connect to
https://$LAVA_MCP_DOMAIN/mcp(streamable-HTTP transport).In-job containers and humans reach the SSH gateway over the WebSocket transport at
wss://$LAVA_MCP_DOMAIN/mcp/gateway-ssh— a WebSocket route on the MCP app itself (same port as/mcp, so Caddy's existing/mcproute serves it). SetLAVA_MCP_GATEWAY_WS_URLto that URL; clients usewebsocat.
Or run the HTTP transport directly:
lava-mcp --transport streamable-http --host 0.0.0.0 --port 8000 --gatewayInteractive board sessions (gateway)
In hosted mode with --gateway (or LAVA_MCP_GATEWAY_ENABLED=true), the server runs
an in-process SSH rendezvous fronted by a WebSocket bridge. open_board_session
submits a LAVA job that runs a device-attached container; the container dials out
(ssh -R, tunnelled over wss://.../mcp/gateway-ssh via websocat) to the gateway, so
no inbound access to the worker is needed. The asyncssh listener is loopback-only and
reachable exclusively through the bridge; there is no direct SSH port.
sequenceDiagram
actor Agent as Agent (MCP client)
participant MCP as lava-mcp + SSH gateway
participant LAVA as LAVA scheduler
participant Board as Board container<br/>(on lab worker)
Agent->>MCP: open_board_session(device_type)
Note over MCP: mint per-session ed25519 keypair,<br/>allocate reverse port, create session
MCP->>LAVA: submit_job (interactive job,<br/>key + gateway ws url + reverse port)
LAVA->>Board: schedule on a worker with the board,<br/>run device-attached container
Board->>MCP: ssh -R reverse_port:localhost:22<br/>(over wss via websocat, auth with session key)
Note over MCP: validate key, accept reverse forward,<br/>mark session "connected"
MCP-->>Agent: session connected
Agent->>MCP: run_in_session(session_id, command)
MCP->>Board: ssh back through tunnel<br/>(127.0.0.1:reverse_port), run command
Board-->>MCP: exit status + stdout/stderr
MCP-->>Agent: command output
Agent->>MCP: close_board_session(session_id)
MCP->>LAVA: cancel job (releases the board)Then:
run_in_session(session_id, command)runs a command on the board's container (e.g.qdl,fastboot,adb, shell).close_board_session(session_id)cancels the job and frees the board.
The container image + test definition live in this repo under interactive/
(published to ghcr.io/mattface/lava-mcp/interactive and fetched from this repo by
the lab worker); the parameter contract is in lava_mcp/jobs.py.
The container is Debian (running as root), so a session can apt-get or build
whatever tooling it needs at runtime — you're not limited to what's pre-installed.
For example, fetch and build qdl from source and detect the attached board (each
line is a run_in_session call, or run them over attach_shell):
apt-get update && apt-get install -y git build-essential pkg-config \
libusb-1.0-0-dev libxml2-dev
git clone https://github.com/linux-msm/qdl && make -C qdl
# With the board in EDL (Emergency Download) mode it enumerates as
# "Qualcomm HS-USB QDLoader 9008" (05c6:9008) — confirm it's attached:
lsusb | grep -i '05c6:9008'
# the freshly built ./qdl/qdl can now flash it (prog + rawprogram/patch XMLs)This is the point of the container-beside-the-board way: you control how the board is driven from the host — including trying a newer or custom flashing tool than the one baked into the image.
Optional allowlists gate the interactive features (all default to open). The general
LAVA-proxy tools are never gated here — they are equivalent to using your own LAVA
token, so /mcp is usable by any token holder.
LAVA_MCP_GATEWAY_ALLOW_IPS— comma/space-separated IPs or CIDRs permitted to reach the gateway. It is enforced at the WebSocket bridge against Caddy's forwarded client IP and applies to every connection — in-job containers and humans alike — dropped before authentication. Set it to your lab worker network plus any human/VPN source ranges.LAVA_MCP_HTTP_ALLOW_USERS— LAVA users (viawhoami) allowed the interactive use tools:open_board_session,run_in_session,close_*,list_*,open_console_session,check_serial_console_support.LAVA_MCP_SSH_ALLOW_USERS— users allowed the attach tools that hand out gateway/SSH keys:attach_shell,attach_console.
Interactive sessions are also gated per device: they only run on devices an admin
has opted in by tagging with allow-remote-access (override the tag name with
LAVA_MCP_REMOTE_ACCESS_TAG; set it empty to disable the gate). open_board_session
checks up front that the device-type has at least one such device and fails with an
actionable message if not, and every interactive job is pinned to the tag so LAVA only
schedules it on a permitted device.
The gateway tunnels into an isolated lab network, so its trust model matters — see
docs/security.md for the roles, enforced controls (loopback-only
reverse tunnels, per-session keys, ephemeral human keys, session ownership), and the
operator responsibilities (set LAVA_MCP_GATEWAY_ALLOW_IPS; Caddy's /mcp route already fronts the /mcp/gateway-ssh WebSocket).
For humans (without an agent)
The gateway has no dedicated human client — but the board-session tools are just MCP calls, so a person can drive the exact same open → run → close flow by hand. Point any generic MCP client at the hosted endpoint with your token; no LLM is involved.
The quickest is the MCP Inspector:
npx @modelcontextprotocol/inspector
# In the UI: Transport = Streamable HTTP
# URL = https://<LAVA_MCP_DOMAIN>/mcp
# Header = X-Lava-Token: <your-api-token>
# (add X-Lava-Url too if the server is multi-tenant)Then invoke the same tools the agent would:
open_board_sessionwithdevice_type(e.g.qcs6490-rb3gen2-core-kit) — reserves a board, submits the LAVA job, and waits for the container to dial back. The result includes thesession_idandconnected: true.run_in_sessionwith thatsession_idand acommand(qdl,fastboot,adb, any shell) — returns the exit status, stdout and stderr.close_board_sessionwith thesession_id— cancels the job and frees the board.
This is command-at-a-time execution over the gateway, not a live PTY.
Interactive SSH shell for humans (attach_shell)
For a live PTY in the board's container — not command-at-a-time — call
attach_shell(session_id). This is the interactive form of a board session: a shell
next to the board (not on it), for controlling how the board is driven from the host
— trying different flashing software/versions, custom fastboot/qdl/adb sequences, or
deeper USB debugging of a board that won't boot. (For the board's own console, use
attach_console.) It mints a short-lived keypair, authorises it both at the gateway
(for the tunnel)
and inside the board container (appended to its authorized_keys over the existing
session), and returns a private key plus a ready-to-use ssh_config. The config's jump
host tunnels to the gateway over wss://.../mcp/gateway-ssh (via websocat), then
ProxyJumps into the container's own sshd — the gateway forwards but offers no shell of
its own, and the container's key is never disclosed. Requires websocat on your PATH:
# save private_key to lava-shell-<id>.key (chmod 600) and ssh_config to
# lava-shell-<id>.conf, then:
ssh -F lava-shell-<id>.conf board-<id>sequenceDiagram
actor Human
participant MCP as lava-mcp + SSH gateway
participant Board as Board container<br/>(reverse tunnel up, runs sshd)
Human->>MCP: attach_shell(session_id)
Note over MCP: mint ephemeral human key,<br/>authorise it at the gateway AND in the container
MCP->>Board: append human key to authorized_keys<br/>(over the session)
MCP-->>Human: private key + ssh_config (websocat + ProxyJump)
Human->>MCP: ssh -F conf (wss via websocat, human key)
Note over MCP: human role — allow direct-tcpip to<br/>127.0.0.1:reverse_port only
MCP->>Board: tunnel to the container sshd
Human->>Board: authenticate as root (human key) → PTY
loop live interactive shell
Human->>Board: keystrokes (via gateway tunnel)
Board-->>Human: terminal output (via gateway tunnel)
end
Human->>MCP: close_board_session(session_id)
Note over MCP: revoke human key, cancel job (container destroyed)Human keys expire (LAVA_MCP_GATEWAY_HUMAN_KEY_TTL, default 1h) and are revoked on
close_board_session. Your source IP must be inside LAVA_MCP_GATEWAY_ALLOW_IPS if set.
See docs/security.md for the full model.
Direct serial console via ser2net (open_console_session / attach_console)
Where attach_shell gives you the board's userspace (it needs a booted, networked
board), a serial console is the board's actual UART — boot/kernel/panic logs, works
with no DUT networking, and the login prompt itself. Many LAVA labs front the UART with
ser2net (the device dict's connection_command
is telnet <ser2net-host> <port>), and LAVA drives boot over that same console. This is
Mode 2 — used with a LAVA job that deploys and boots an image and runs its test on
the board (no device-attached container), so the in-lab foothold comes from a LAVA
Test Services container (interactive/ser2net-proxy/) instead. It needs
allow_test_services: true in the device dict (check with check_serial_console_support).
Reach for the console to interact with the booted board directly — drive tests and
run commands live at the console without writing a LAVA test definition, watch the
boot, or work with the bootloader/login prompt — whereas a board session (attach_shell)
is for host-side work next to the board (flashing, fastboot/adb/qdl, USB debugging).
Don't hand-author the deploy+boot job — getting boot right per device is hard.
Always base it on a previous successful job whose deploy url closely matches the
artifacts you want to boot — flash method, rawprogram/patch, storage and auth headers
are image-specific, so only a job that flashed a similar URL is a safe template (use
list_jobs + get_job_definition to find and compare deploy url; don't use an
unrelated job such as a health-check). Keep its deploy+boot actions — swap in your URL
but keep the artifact authentication (HTTP headers such as Authorization, and any
credentials) so the fetch succeeds — then add
the console proxy on top. You don't need an example: open_console_session returns the
exact services action, the console-ready action, and the environment: values to add
(its add_to_job field).
Flow:
open_console_session()mints a session and returns ajob_environmentblock. Add it to your deploy-and-boot job's top-levelenvironment:, include theinteractive/ser2net-proxyservices block, and set theSER2NET_*vars for your lab. Submit the job.The proxy starts at the beginning of the job, relays the console read-only while LAVA drives the boot, and dials out (
ssh -R) to the gateway. When your console-ready test echoes the sentinel (LAVA_MCP_CONSOLE_WRITABLE), the proxy enables writes.attach_console(session_id)returns anssh -Wcommand that tunnels to the gateway overwss://.../mcp/gateway-ssh(viawebsocat; wrap withsocatfor a raw PTY) — you get the live UART, bridged through the gateway on a loopback-only port.close_console_session(session_id)revokes access; ending the job tears down the proxy.
sequenceDiagram
actor Human
participant MCP as lava-mcp + gateway
participant Proxy as ser2net-proxy<br/>(Test Services, in lab)
participant Ser2net as ser2net → board UART
Human->>MCP: open_console_session() → job_environment
Note over Human: embed in a deploy+boot job<br/>with the services block, submit
Proxy->>Ser2net: connect to the console (read-only)
Proxy->>MCP: dial out ssh -R over wss (websocat, loopback reverse port)
Note over Proxy: console-ready sentinel → enable writes
Human->>MCP: attach_console(session_id) → ssh -W command
Human->>MCP: ssh -W (wss via websocat, human key)
MCP->>Proxy: tunnel to the relay
loop live serial console
Human->>Ser2net: keystrokes (via gateway → proxy)
Ser2net-->>Human: boot/kernel logs + shell output
end
Human->>MCP: close_console_session(session_id)Console handoff wrinkle: ser2net must allow the proxy's concurrent connection. The
console-ready action open_console_session returns holds the job open with an
interactive shell (LAVA tolerates a silent console — a test-shell expect timeout just
loops — so no keepalive is needed); the user ends the session by exiting the shell.
Configuration
Env var | CLI flag | Meaning |
|
| LAVA base URL (stdio fallback; HTTP clients send |
|
| API token (stdio fallback; HTTP clients send |
|
| REST version, default |
|
| Hide write tools (submit/cancel/resubmit) |
|
|
|
|
| HTTP bind (hosted mode) |
|
| Enable interactive SSH board-session gateway |
|
| Internal loopback asyncssh port the WS bridge relays to (default 2222) |
|
| Host containers dial back to |
|
| Advertised |
|
| Source IPs/CIDRs allowed to reach the SSH gateway (empty = all) |
|
| LAVA users allowed the interactive 'use' tools (empty = all) |
|
| LAVA users allowed the 'attach' (SSH/console) tools (empty = all) |
|
| Device tag required to host remote-access sessions (empty = no gate) |
| — | Lifetime (seconds) of an ephemeral human access key from |
Tools (v1)
Read/observe: whoami, version, list_devices, get_device,
get_device_dictionary, list_device_types, list_workers, list_jobs,
get_job, get_job_definition, get_job_logs, get_job_results, get_queue,
get_running, get_lab_health, validate_job.
Write (omitted with --read-only): submit_job, cancel_job, resubmit_job.
Interactive board sessions (hosted gateway mode): open_board_session,
run_in_session, attach_shell, close_board_session, list_board_sessions.
Serial console (hosted gateway mode): check_serial_console_support,
open_console_session, attach_console, close_console_session.
Test
pytestRoadmap
The interactive board sessions gateway is implemented here, along with the container image + test definition the in-job container runs (
interactive/, published toghcr.io/mattface/lava-mcp/interactive).Human shell proxy + interactive PTY through the gateway (design above).
Direct serial console for humans via ser2net, gated on a
console-readyjob signal (design above).
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceProvides an interface to the CloudNet Service REST API v3, enabling AI assistants to observe and manage CloudNet nodes, services, and players.Last updated9MIT
- Flicense-qualityDmaintenanceExposes a Jenkins controller's REST API to MCP-compatible clients, enabling job management, build triggering, and log retrieval through natural language.Last updated4
- AlicenseAqualityAmaintenanceEnables management of Proxmox VE infrastructure via its REST API and execution of commands inside LXC containers via SSH, with built-in trust layers for planning, audit, undo, and diagnosis.Last updated210017Apache 2.0
- Alicense-qualityBmaintenanceEnables an agent to run shell commands through a JupyterLab/JupyterHub-hosted terminal, using REST API and WebSocket via Selenium for session handling, when SSH is not available.Last updatedGPL 3.0
Related MCP Connectors
Discover, search, invoke, and rate A2A (Agent-to-Agent) protocol agents.
Operate smplkit from your agent: feature flags, config, logging, audit, and scheduled jobs.
Discover Wiplash and manage owned agents with human OAuth.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/lava-mcp/lava-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server