lava-mcp

lava-mcp

Enables agents to query LAVA board farms, submit and manage test jobs, and open interactive board sessions via the LAVA REST API.

Category
访问服务器

README

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]

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_URL on the server to the LAVA instance it serves. Each connecting client then sends only its own X-Lava-Token and acts as its own LAVA user; no X-Lava-Url is needed. The server stores no per-user token.
  • Hosted (HTTP), multi-tenant — leave LAVA_URL unset; each client sends both X-Lava-Url and X-Lava-Token, so one server can front many LAVA instances.
  • Local (stdio) mode — falls back to LAVA_URL / LAVA_TOKEN in 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-only

Launch 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 -d
  • Agents 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 /mcp route serves it). Set LAVA_MCP_GATEWAY_WS_URL to that URL; clients use websocat.

Or run the HTTP transport directly:

lava-mcp --transport streamable-http --host 0.0.0.0 --port 8000 --gateway

Interactive 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 (via whoami) 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:

  1. open_board_session with device_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 the session_id and connected: true.
  2. run_in_session with that session_id and a command (qdl, fastboot, adb, any shell) — returns the exit status, stdout and stderr.
  3. close_board_session with the session_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:

  1. open_console_session() mints a session and returns a job_environment block. Add it to your deploy-and-boot job's top-level environment:, include the interactive/ser2net-proxy services block, and set the SER2NET_* vars for your lab. Submit the job.
  2. 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.
  3. attach_console(session_id) returns an ssh -W command that tunnels to the gateway over wss://.../mcp/gateway-ssh (via websocat; wrap with socat for a raw PTY) — you get the live UART, bridged through the gateway on a loopback-only port.
  4. 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_URL --url LAVA base URL (stdio fallback; HTTP clients send X-Lava-Url)
LAVA_TOKEN --token API token (stdio fallback; HTTP clients send X-Lava-Token)
LAVA_API_VERSION --api-version REST version, default v0.3
LAVA_MCP_READ_ONLY --read-only Hide write tools (submit/cancel/resubmit)
LAVA_MCP_TRANSPORT --transport stdio (default) or streamable-http
LAVA_MCP_HOST / LAVA_MCP_PORT --host / --port HTTP bind (hosted mode)
LAVA_MCP_GATEWAY_ENABLED --gateway Enable interactive SSH board-session gateway
LAVA_MCP_GATEWAY_PORT --gateway-port Internal loopback asyncssh port the WS bridge relays to (default 2222)
LAVA_MCP_GATEWAY_ADVERTISE_HOST --gateway-advertise-host Host containers dial back to
LAVA_MCP_GATEWAY_WS_URL --gateway-ws-url Advertised wss://host/mcp/gateway-ssh URL; the SSH gateway is served as a WebSocket route on the MCP app (same port as /mcp). Required for interactive sessions; clients need websocat
LAVA_MCP_GATEWAY_ALLOW_IPS --gateway-allow-ip Source IPs/CIDRs allowed to reach the SSH gateway (empty = all)
LAVA_MCP_HTTP_ALLOW_USERS --http-allow-user LAVA users allowed the interactive 'use' tools (empty = all)
LAVA_MCP_SSH_ALLOW_USERS --ssh-allow-user LAVA users allowed the 'attach' (SSH/console) tools (empty = all)
LAVA_MCP_REMOTE_ACCESS_TAG --remote-access-tag Device tag required to host remote-access sessions (empty = no gate)
LAVA_MCP_GATEWAY_HUMAN_KEY_TTL — Lifetime (seconds) of an ephemeral human access key from attach_* (default 3600)

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

pytest

Roadmap

  • The interactive board sessions gateway is implemented here, along with the container image + test definition the in-job container runs (interactive/, published to ghcr.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-ready job signal (design above).

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选