Skip to main content
Glama

mcp-relay

The Model Context Protocol front end of the VM relay, packaged as a Claude Code plugin and as a pi package. pi-vm-relay, which gave pi a native relay tool directly, is retired; mcp-relay replaces it for both Claude Code and pi, the latter loaded through pi-mcp-adapter. It is an MCP server: fourteen relay_* tools, each with its own schema, title and annotations, and the server's instructions. The user command, status, ships as one small command file per host (see "Commands"). There is no skill and no hook. Together they offer recorded, snapshot-evidenced scripts, commands included as shell scripts, in a fresh, dedicated VM or on a durable machine, a real Mac leased whole, for any work the agent chooses to run there; a script drives the desktop or a browser through the software the image lists, such as cua-driver or Playwright. Nothing here executes local computer-use on the relay's own host, spawns subagents, records video, or attaches to existing browsers.

The project was named mcp-vm-relay before v0.12.0. The npm package is now @wezzard/mcp-relay, the plugin and its marketplace are mcp-relay, and every MCP_VM_RELAY_* variable is now MCP_RELAY_*; the old names are not read. The default state directory moved with the name, so finish or release a lease the old relay owns before upgrading.

From v0.16.0 the relay's backend is machine-service, vm-service under its new name. The relay asks for /machines paths, which vm-service never served, and refuses a service whose /health does not answer machine-service. A selected environment profile must be at schemaVersion 2 with machctlPath; a profile at version 1 or with vmctlPath is refused with its fix in the error (DC-27). The relay's own names do not change: MCP_RELAY_*, VM_ENVIRONMENT_FILE, VM_RELAY_* and the state directories. Release every lease before switching, as machine-service's installer requires.

How it is built

The relay implementation lives in this repository: the manager (src/manager.ts), the machine-service client, the registry with its OS-level lock, the guest transfer and transport, the evidence package and its state merge, the selected-environment profile, the console and saved-image contracts, the strict tool contract (src/schema.ts), the action dispatch and bounded result rendering (src/surface.ts), and the guest program, the receiver (src/guest/receiver.ts). It was inherited from pi-vm-relay at commit 8990123, synchronized with its implementation through commit 0d69fc7 before that project's retirement, and is maintained here as mcp-relay's own implementation from then on. src/server.ts binds the core to MCP over standard input and output.

The committed dist/ holds server.mjs (the MCP server with the core bundled in), the guest bundle the manager stages (receiver.mjs) and the doctor. Compiled relay-driver code, the recorded-execution and evidence substrate, is bundled with hashes and provenance in dist/build-info.json. Consumers load dist/; no build, SDK checkout or network fetch happens at install time.

The Claude Code plugin and pi both reach this same server as plain MCP: pi through pi-mcp-adapter (see "Use with pi" below), Claude Code by starting it directly. Every tool's identity, schema and contract are shared; the plugin adds nothing Claude-Code-specific beyond the marketplace packaging and the vm-relay-operator agent definition.

In pi-vm-relay (retired, native pi extension)

Here (MCP server)

a registered relay tool with thirteen actions

fourteen relay_* MCP tools from the server relay, each with its own schema, title and annotations

doctrine injected before each agent turn

the server's MCP instructions, plus a compact version in the relay_probe and relay_acquire descriptions for clients that do not surface instructions

/relay-status command

/mcp-relay-status in pi, /mcp-relay:status in Claude Code; it asks the assistant to call relay_status

prompt-composed guest operator

the vm-relay-operator agent definition (Claude Code only), limited to the relay's tools and read-only file tools

typed image blocks in a run result, with pi's tool_result hook keeping the error flag

MCP image content blocks in the tool result, with the MCP isError flag set beside them

session shutdown pauses lease renewal

the same when the server's stdio closes or it is signalled: renewal pauses after one final renewal for the rest of the lease's TTL, the recording detaches, the VM is retained until that TTL for an explicit relay_finish or relay_release, and the machine-service TTL is the backstop

the settled agent pauses renewal

none: MCP has no such hook, so the instructions and each run tool's description say "finish or release before you return"

Related MCP server: claw-server

Install

Use with Claude Code

Try it from a checkout:

claude --plugin-dir /path/to/mcp-relay

Or add this repository as a marketplace and install the plugin from it:

/plugin marketplace add WeZZard/mcp-relay
/plugin install mcp-relay@mcp-relay

Once loaded, the model sees the fourteen tools as mcp__plugin_mcp-relay_relay__relay_search, mcp__plugin_mcp-relay_relay__relay_probe, and so on through mcp__plugin_mcp-relay_relay__relay_status (see "The tools" below for the full table). The vm-relay-operator agent definition allows them by those names. The server can also be configured directly in a project's .mcp.json with node /path/to/mcp-relay/dist/server.mjs, in which case the tools are mcp__relay__relay_search and so on.

Use with pi

pi install npm:@wezzard/mcp-relay

This needs pi-mcp-adapter installed. The tools appear as relay_search, relay_probe, and so on through relay_status, with no host-specific prefix (pi-mcp.json sets toolPrefix: "none"). The package also ships a pi prompt template for the command, /mcp-relay-status.

Use with npx

npx -y @wezzard/mcp-relay

This runs the MCP server directly on stdio, for a client that speaks MCP without a Claude Code plugin or pi package wrapper.

Prerequisites

Node 22+, Python 3 with fcntl for the registry lock, and for VM execution an Apple-silicon Mac with Tart, a running machine-service (formerly vm-service) and prepared guest images. Check readiness with node dist/doctor.mjs. It does not acquire a VM or establish guest UI readiness. Staging and runs assume no software in the guest but Node.js, which runs the relay's own guest code; relay_stage refuses an image without it. A run takes before and after snapshots only when the image lists capture software, cua-driver (catalog ids bundle:com.trycua.driver and dpkg:cua-driver); otherwise its record has no snapshots, and that is not a gap. Snapshots need cua-driver with capture permission; on Linux, native X11 and serve --no-overlay. relay_script runs JavaScript with the guest's Node.js, TypeScript with tsx and Python with python3, each from the guest's PATH; the relay installs none of them, nor the software a script drives, such as cua-driver, Playwright or a browser. Console viewing needs the machine-service guest-sharing backend and its guest preparation; see docs/console.md here.

The tools

Each tool's inputSchema is a plain object with no root anyOf/oneOf/allOf and no action field, derived directly from the strict per-action contract in src/schema.ts (nested unions inside a property, such as relay_image's target, are unaffected). A call is mapped back to that contract's shape and dispatched exactly as the retired single relay tool was, so behaviour, results, image blocks and isError semantics are unchanged from before this split. One tool runs anything in a guest: relay_script runs a shell, JavaScript, TypeScript or Python script, given inline or as a project file. A command is a shell script. It takes the optional reason, step, snapshots, timeoutMs and, for a shell script, diagnostic, and its evidence is automatic: see "Runs" below.

Tool

Title

Replaces (action)

Purpose

relay_search

Search installed applications

search

Find installed applications from image inventories by name and optional OS.

relay_probe

Probe host and guest readiness

probe

Default scope: "host" reports host facts, service availability and every guest the session holds, with its state. scope: "guest" with guest checks Node.js, which staging requires, and reports as facts whether each relay_script language's runtime runs (/bin/sh, node, tsx, python3), the software the image lists in the catalog and whether runs take snapshots, on that guest, without installing, starting or claiming capture readiness.

relay_acquisition_capabilities

Read acquisition capabilities

acquisition-capabilities

Read versioned acquisition options (VNC backends) without allocation or ownership recovery.

relay_acquire

Acquire a VM

acquire

Register the task, acquire a fresh VM, a guest, on the network MCP_RELAY_GUEST_NETWORK names and start its heartbeat; declare outputs before work. Answers guest, the guest's name in the session, which every later call on it names. Optional vnc prepares sharing without opening a viewer.

relay_stage

Stage the guest runtime

stage

Push and hash-check the runtime, support files and an opt-in workspace, all from the project folder. Needs only Node.js in the guest, and reports whether runs take snapshots (only when the image lists cua-driver). Failed staging can be retried; a staged runtime accepts corrected executable paths; resetRecording: true archives the recording and starts a new one on the same VM.

relay_script

Run a guest script

run

One recorded guest script in language (shell, javascript, typescript or python); a command is a shell script. inline: true takes the script as code, up to 1 MiB; inline: false takes localPath, a file in the project folder. diagnostic: true, for a shell script only, records diagnosis or repair without screenshot evidence, including before staging.

relay_image

Retrieve a saved image

image

Retrieve one saved display image, declared application image or immutable image reference without input, capture, directory export or acquisition.

relay_extract

Extract declared outputs

extract

Pull only declared files or directories with source and host checksum verification.

relay_finish

Finish and deliver evidence

finish

Extract the guest's declared outputs, deliver and verify its snapshot package, destroy its VM and unregister it; the session's other guests keep running.

relay_release

Release the VM

release

Retain the guest's available evidence and abandon or destroy its VM; the other guests keep running. When this session holds no guest, the result is an error that says nothing was released.

relay_console_resolve

Resolve console status

console-resolve

Resolve non-secret console status for the guest's lease and environment.

relay_console_open

Open console viewing

console-open

Open explicitly user-requested viewing on the service host, with console_id, attempt_id, userRequested: true, reason and expected.

relay_console_cancel

Cancel console viewing

console-cancel

Cancel the identified viewing attempt without releasing the VM.

relay_status

Show relay status

(unchanged)

Every guest this session holds, by name, each with its lease: backend binding, guest state, renewal state, console observation, last error, staging state and evidence path; plus maxGuests, the project directory, the machine-service origin and the selected environment. active is false when no guest's lease is being renewed.

readOnlyHint/idempotentHint are true for relay_search, relay_probe, relay_acquisition_capabilities, relay_console_resolve, relay_image and relay_status; destructiveHint is true only for relay_finish and relay_release, which destroy a guest's VM; openWorldHint is true only for relay_script, whose guest code may reach the network.

relay_command, relay_exec, relay_code, relay_run, relay_gate and relay_tools are gone (DC-23, DC-26, docs/guest-code-runs.md §10). A command is a shell script: {"argv":["ls","-al"]} becomes {"language":"shell","inline":true,"code":"ls -al"}. relay_code is relay_script with inline: true. A run carries no kind. In place of relay_run's forwarded MCP tool calls, a script drives cua-driver, Playwright or Chrome DevTools through their own libraries or command lines, as the image lists them; the relay no longer stages MCP servers in the guest.

A run's result carries the execution identity and outcome, the derived step record, and, when the snapshot plan captured the after phase and delivery succeeded, the saved after-image as an MCP image content block. A run's result also carries the guest's bounded standard output and error. The full receipt stays in the evidence package.

Commands

The command asks the assistant to call one relay tool and report the answer. It changes nothing itself.

Command in pi

Command in Claude Code

Arguments

Purpose

/mcp-relay-status

/mcp-relay:status

none

Calls relay_status and reports this session's guests and their leases as they are. Read-only.

It is not an MCP prompt. pi's MCP adapter can only name a prompt /mcp__<package>__<server>__<prompt>, so each host gets its own command file instead: pi prompt templates in pi-prompts/ (listed in package.json under pi.prompts) and Claude Code plugin commands in commands/. Both are generated by npm run build from scripts/host-commands.mjs; do not edit them by hand. CI and the release workflow check the packed tarball with scripts/verify-package.mjs: the generated files must match their source, pi's glob must select exactly the one template, and the packed server must start without the prompts capability. The release publishes and attaches the same tarball it checked.

Ownership, failure and recovery

  • One server session holds several guests at once, up to MCP_RELAY_MAX_GUESTS (4 by default). relay_acquire names each guest, such as login-test-1, and every tool that acts on a guest takes that name as guest. Each guest keeps its own lease and heartbeat, its own evidence folder under relay-evidence/, its own background downloads, owner lock and registry row, and its own finish or release; finishing or releasing one leaves the others running. Calls on different guests run at the same time; calls on one guest keep their order. An acquisition beyond the limit is refused with the names of the guests held; the backend's own limits, such as two macOS VMs per host, still hold. An acquisition the host refuses for capacity waits for it, up to MCP_RELAY_CAPACITY_WAIT, then refuses with the backend's reason and the time waited; a waiting guest counts against the limit, and the backend's other refusals pass through at once.

  • An operation failure retains the VM so the agent can inspect, repair and submit a new operation. Failed and uncertain operations are never automatically replayed.

  • Use finish to deliver evidence and release, or release to abandon explicitly. A declared output that was never produced is recorded as incomplete (incompleteExtractions) rather than failing finish. A failed delivery retains the VM; a release failure retains ownership until destruction is verified.

  • When the session ends (the client closes the server's standard input, a write to its standard output fails, it is signalled with SIGTERM, SIGINT or SIGHUP, or the process that started it goes away), MCP_RELAY_ON_EXIT says what happens to the guests. With release, the relay releases every guest the session holds through its own release path, keeping the evidence the host can still pull, then exits; an operation already in flight completes first, all within the shutdown grace period, after which a guest still held is released directly in one bounded attempt. A guest whose release fails keeps its record and stops renewing, so its lease ends within its last 15-minute window. With pause, the default, the relay renews each guest's lease once for the rest of its TTL, then renewal pauses and the recording detaches; no VM is destroyed and is retained until its TTL for an explicit finish or release. A finish or release already in flight completes first, for up to the shutdown grace period; after that the operation is cancelled and renewal pauses without it. The backend TTL and grace period handle abandoned leases. Status reports the last confirmed expiration time.

  • While the session is live, the relay tracks the lease's own deadline (its start plus ttlHours) and renews machine-service only 15 minutes ahead at a time, never past that deadline, and stops renewing at it. A client that crashes without ending its session therefore leaves its VM leased for at most about 15 minutes, plus the backend's grace period, rather than its whole TTL.

  • A restarted server with the same MCP_RELAY_SESSION reconciles each guest's durable ownership and reattaches its recording session without replaying prior work; every guest keeps its name. A session state written by a relay before several guests is restored as one guest named from its task. Context compaction does not reset VM state; probe reports the owned state without relying on earlier messages.

  • A run tool's timeoutMs defaults to 120,000 ms and accepts integers up to 3,600,000 ms. It bounds command execution, not the snapshot delay or the lease lifetime. A timeout reports confirmed termination or uncertainty and keeps the VM available.

  • A shell relay_script with diagnostic: true records diagnosis or repair without screenshots, for explicitly requested diagnosis when capture is unavailable. It runs through machine-service as /bin/sh -c <script>, a script file read on the host first: before staging in the guest's default directory, after staging in the recording workspace. Only a shell script may be diagnostic, because no receiver or Node.js is involved. It cannot join a snapshot group and is not visual verification.

  • After diagnosing damaged recording state, relay_stage with resetRecording: true archives the old recording and starts a new recording session in the same VM. An existing receiver lock refuses the reset until its operation is reconciled. Prior evidence paths remain in the owned status.

Runs

relay_script runs one script as one recorded operation; a command is a shell script:

{"guest":"login-test-1","language":"shell","inline":true,"code":"/usr/bin/xdg-open https://example.com"}
{"guest":"login-test-1","language":"python","inline":true,"code":"import subprocess\nprint(subprocess.run(['cua-driver','call','list_apps'],capture_output=True,text=True).stdout)"}
{"guest":"login-test-1","language":"typescript","inline":false,"localPath":"checks/login.ts"}
  • Evidence is automatic. Every run is admitted by the guest receiver, journaled, and, when the image lists cua-driver, bracketed by a before and an after display snapshot. The step record is derived from the call: the title is a shell script's first line (shell <line>), code (<language>) or script <file>, and the expected result is "Exits with status 0". reason, step and snapshots are optional overrides; the default wait before the after-snapshot is 500 ms.

  • Scripts. A relay_script with inline: true streams its code to the guest with its hash; with inline: false its localPath, a file in the project folder, is pushed hash-checked. The script runs in the staged workspace: shell with /bin/sh, JavaScript with the guest's Node.js, TypeScript with tsx or Node.js type stripping, Python with python3. The script's exit status says whether it succeeded: a script that calls cua-driver reads the driver's answer and exits nonzero on an error. A call that gives code and localPath other than as inline names is refused before anything reaches the guest.

  • One interaction per run. A run is one recorded operation; consecutive keystrokes may form an explicit text group with snapshots.group. A script that drives several interactions records them as one step.

  • Outcomes. A refused, uncertain or nonzero run is an error result and keeps the VM. Nothing is ever replayed.

Saved images

A run tool names its saved after-image whenever the snapshot plan captures that phase: a standalone event or a text group's last event. The result does not wait for the image. Its imageDelivery is pending with the image identity (imageId, originalPath, hash and size). In the background, one download at a time, the relay brings each step's evidence to the host as one archive: its before- and after-images and its receiver and action records. relay_finish and relay_release wait only for the download in flight. They put the steps still queued, and every other file the host lacks, into one archive of their own, and copy the files the step downloads already brought instead of pulling them again. relay_status counts the steps whose download a lifecycle operation took over as coalesced. A first or intermediate group event and a diagnostic script do not invent an image. Execution and image delivery are independent outcomes: a pending image does not turn a failed run into a successful one. The delivery identity leads the result text so it survives truncation. The design is docs/screenshot-delivery.md §10.

relay_image shows a saved image. Asked for an image whose download is still queued or running, it waits for that download within its own deadline. It retrieves the same saved image through one closed selector; it never repeats input, creates a capture, exports the consumer directory or acquires a VM:

{"target":{"source":"display","sessionId":"<recording-session>","executionId":"<saved-execution>","phase":"after"}}
{"target":{"source":"application","name":"screens","path":"login/step-1.png"}}
{"target":{"source":"reference","imageId":"image-<64 lowercase hex digits>"}}
  • Display selection needs before or after; a phase the plan did not request returns not-requested. Application selection needs an acquisition-time declaration: a directory declaration takes one relative file path, a file declaration omits path. A reference resolves to the same original bytes or an explicit failure.

  • Originals are limited to 64 MiB, 40,000,000 decoded pixels and 32,768 pixels per dimension; PNG, JPEG and WebP are accepted. The preview is at most 2,000 × 2,000 pixels and 4 MiB of base64. Presentation differs from pi here: pi resizes through its own image helper, while this core has no codec dependency. A PNG original is decoded and, when it exceeds the preview bounds, resampled in-process (area averaging, node:zlib); a JPEG or WebP original has its container validated and passes through unchanged when within bounds, and is reported presentation-unavailable otherwise. No EXIF orientation is applied; dimensions are those stored. The presentation receipt records the policy, the original and preview dimensions, the MIME type, hash, size and whether bytes were transformed, beside the untouched original.

  • Each delivery has a 90-second deadline and a shared budget of three byte-transfer attempts. Only transient transfer failures are retried; authorization, unsafe-path, capture, format and integrity failures are not. Recommend at most two explicit recovery calls for the same reference; if a required image still cannot be inspected, stop exploratory input and finish or release.

  • Immutable catalog entries bind each reference to the owner, enclosure, backend and original identity. A closed or sealed guest returns stale-reference; delivered originals remain readable with the host's file tools. Finalization materializes verified originals at canonical snapshot paths and preserves prior state when merging guest evidence.

  • Attachment means the block was included in the result; it does not prove the model inspected it or that a human reviewed it.

Live console viewing

Acquisition never opens a viewer. relay_acquisition_capabilities reads the backend's VNC options; relay_acquire with vnc: true checks OS availability and requires a ready console and lease identity in the response. relay_console_open is permitted only following an explicit user request, requires userRequested: true, console_id, attempt_id, reason and expected, and opens on the declared service host, not on a remote client. relay_console_resolve refreshes the non-secret observation; relay_console_cancel closes managed viewing resources and never releases the VM. Console status ready is guest preflight, not launch success; transport connection, authentication, displayed pixels and human confirmation are separate observations that console actions never establish. On macOS the human must choose Standard sharing of the existing console, not a new Log In session or a High Performance display; the relay never confirms that selection, and server_enforced_view_only: false with session_binding: viewer-selection-unverified remain limitations after transport connects. Failed or uncertain launches retain ownership; resolve or cancel the attempt rather than replaying it. Console tests use mocked backends; no live viewer, installation or platform acceptance is claimed here.

Durable machines

relay_acquire takes machine in place of image to lease a durable machine, a real Mac that machine-service exposes, such as Mac-A. Every other tool works on it as on a VM (docs/durable-machines.md).

  • The relay finds the instance that owns the machine as machctl list does: the loopback instance, then every instance that advertises _machine-service._tcp with Bonjour on the network, so a Mac needs nothing else installed. With MACHINE_SERVICE_DISCOVERY=consul it asks the Consul agent at CONSUL_HTTP_ADDR (default http://127.0.0.1:8500) instead. Each instance found is used only once its /health confirms its ID.

  • Every call carries the lease's lease_id, files go as bytes, and the relay stages under the lease's workspace, which machine-service removes at release.

  • When machine-service reports machine_id, the relay saves it and can resume the same lease after the Mac is renamed or its address changes. Requests keep using the name the lease was granted under. Older services keep the existing name and origin checks.

  • A durable 404 never proves release. The relay finds the owner again and verifies that its machine record no longer names the lease. An unreachable owner leaves the lease retained with guest state unknown.

  • Runs take snapshots only while the Mac's cua-driver daemon (CuaDriver.app, com.trycua.driver) holds Screen Recording, read with cua-driver permissions status --json in the lease. machine-service's own permissions are not read, and it needs no Screen Recording grant.

  • A durable machine is on the home network and not isolated from it, a CI job may share its desktop, and it has no console.

Selected environments

Set VM_ENVIRONMENT_FILE to a profile, at schemaVersion 2 with machctlPath, that binds a loopback machine-service endpoint, an image repository, a Tart store and separate service, image and relay state directories together. The profile is validated before use, is authoritative over the individual MCP_RELAY_* variables, and an invalid selection fails rather than falling back. Leases are bound to their backend and store: an owned lease restored under a different environment fails before any backend operation, and destruction is verified with the selected Tart binary and TART_HOME. docs/selected-environments.md documents the schema. The bundle the profile exports to subprocesses is the canonical one machine-service defines, with MACHCTL and the MACHINE_SERVICE_* names, so its relay entries keep the VM_RELAY_STATE_DIR and VM_RELAY_URL names and the three runtimes agree; this server reads its own settings from the profile.

Evidence and cleanup

Default output is relay-evidence/<unique-task>/ under the project, with state/ (the guest journal, action records, receipts and snapshot PNGs), host/ (reasons, submissions, transfer facts, receipts, diagnostics, image deliveries and lifecycle events), extractions/ (declared files, including the page captures), manifest.json, which checksums everything, and OPENING.txt. The package is the raw evidence only; pi-secretary's computer-use extension shows its steps and verdicts beside the agent's session. The steps and verdicts are derived from the evidence when you review it, so an earlier package is reviewed with today's rules. A program that reads relay packages gets the same derivation from @wezzard/mcp-relay/review-data: await reviewData(<directory>) returns the steps (with each snapshot's package path), the verdicts and their reasons. No network, VM or external assets are needed. Diagnostic commands appear in the review data as command-only steps without screenshots. finish verifies the package before destroying the VM; a failed delivery removes only the derived files it created and retains the VM for a corrected attempt. Cleanup checks the read-only host Tart inventory (the selected one, when an environment is selected) before claiming destruction. Leases default to 4 hours, renewed 15 minutes at a time up to that TTL; the machine-service reaper is the final backstop for process death.

Relay evidence has no size limit (owner decision PS-D12): screenshots are delivered as captured, never scaled, compressed, deduplicated or budgeted, and the relay state is delivered whatever its total size or file count. Only what a client stages into the guest is bounded (512 MiB and 10,000 files per staging request), and a single image original must be at most 64 MiB to be valid. Before finish pulls the relay state it checks that the evidence volume has room for it; when it does not, finish fails before pulling, names the shortfall and keeps the VM for a retry after space is freed.

Configuration

  • MCP_RELAY_PROJECT: the project directory (the plugin passes Claude Code's). Evidence lands under relay-evidence/<task>/ there. Every host file the relay reads, relay_stage's workspace and files[].local and relay_script's localPath, lies in this folder: a relative path resolves against it, an absolute path must already lie in it, and a path through a symlink is refused.

  • MCP_RELAY_SESSION: an explicit session identity. By default each server process takes a fresh random one; a stable identity lets a restarted server reconcile and reattach the guests of the same identity.

  • MCP_RELAY_URL: the loopback machine-service origin, default http://localhost:6240. Non-loopback servers and redirects are refused; a durable machine's instance on another Mac is reached only when discovery finds it.

  • MACHINE_SERVICE_DISCOVERY: how durable machines' instances are found, bonjour (the default) or consul, as for machine-service and machctl.

  • CONSUL_HTTP_ADDR: the Consul agent that lists machine-service instances, in Consul mode; default http://127.0.0.1:8500.

  • MCP_RELAY_GUEST_NETWORK: the network every guest is acquired on, nat (the default) or internet. With internet, machine-service gives a guest that reaches the internet and reaches neither this host nor any private network; an application that hosts agents sets it. The agent cannot choose it: relay_acquire takes no network. Any other value stops the server at startup and refuses every acquisition, and a machine-service that cannot give internet refuses the acquisition with its own reason, never falling back to nat.

  • MCP_RELAY_ON_EXIT: what the end of a session does to its guests, pause (the default: renewal pauses and the VMs are kept for an explicit finish or release) or release (every guest is released, then the server exits). An application that owns its tasks' machines, such as The Desk, sets release. Any other value stops the server at startup.

  • MCP_RELAY_CAPACITY_WAIT: how many seconds relay_acquire waits when the host is at capacity, such as at machine-service's limit of two running macOS VMs, asking again every 15 s; default 1800, and 0 for not at all. Any other value stops the server at startup.

  • MCP_RELAY_MAX_GUESTS: the most guests one session holds at once, a positive integer, default 4. Any other value stops the server at startup.

  • MCP_RELAY_STATE_DIR: host state, otherwise $XDG_STATE_HOME/mcp-relay or ~/.local/state/mcp-relay, one subdirectory per session, with one guests/<guest>/ folder per guest holding its ownership record and owner lock.

  • MCP_RELAY_REGISTRY: an explicit task registry file; otherwise an existing compatible ~/AGENTS.md VM table, or a private managed registry.md in the state directory.

  • MCP_RELAY_SHUTDOWN_GRACE_MS: how long a session end waits for an in-flight operation, and with MCP_RELAY_ON_EXIT=release for the guests' releases, before cancelling it; default 120000.

  • MCP_RELAY_PYTHON: the Python 3 used for the registry lock; default python3 on PATH.

  • VM_ENVIRONMENT_FILE: a selected environment profile; it overrides the URL, state directory and registry above and binds the Tart store.

  • Guest files go to /var/tmp/<UTC-timestamp>-mcp-relay-<task>/ on a VM and to <lease workspace>/work/mcp-relay-<task>/ on a durable machine, with the runtime in receiver.mjs, support in support/ and work in workspace/.

Development

Build relay-driver using its own instructions, then in this checkout:

npm ci
npm run setup:dev -- /absolute/path/to/built/relay-driver
npm run check                                            # build, typecheck, tests

Three tests need something outside this repository and skip with a message when it is absent: the console fixture regeneration needs a historical ../vm-service checkout with the Python backend (or VM_SERVICE_PYTHON_BACKEND); the environment parity test needs machine-service's machctl (MACHCTL, PATH, or a release build in ../machine-service/rust) and ../pilot-images; and the shared catalog text corpus needs ../pilot-images. The JPEG and WebP presentation fixtures in tests/fixtures/ were encoded once from the test's spatial PNG with a codec outside this repository and are committed, because the core carries none.

End-to-end with headless Claude Code

npm run test:e2e

This runs claude --print with the plugin loaded from this checkout (--plugin-dir), the model limited to the relay's tools, and the server pointed at a loopback stand-in for machine-service (tests/e2e/fixture-service.ts), which keeps leases as records, runs the relay's own Node and transfer commands locally, and fakes the desktop capture driver. Four cases run, each a separate headless session: the status call (which also proves the server is pointed at the stand-in before anything is acquired), an invalid call refused by the contract, a full acquire, stage, exec, finish lifecycle with a verified package, and a lease abandoned when the session ends, which is retained with its renewal paused rather than released. What is checked is what happened on disk and at the service, not what the model said. The suite needs claude on PATH with an account and spends a few model turns per case, so it is not part of npm test.

The relay-driver links are development-only. The tests run the manager, the guest programs and the shipped server against fixture HTTP services, fixture MCP servers built with the MCP SDK and a real MCP client over stdio; no VM, desktop or user registry is touched. Commit the regenerated dist/ with source changes.

Releasing

Cut a release with:

node scripts/bump.mjs X.Y.Z && git push --follow-tags

scripts/bump.mjs sets X.Y.Z as the version in package.json, .claude-plugin/plugin.json and the pinned pi-mcp.json arg, refreshes package-lock.json, rebuilds dist/, commits Release vX.Y.Z and creates the annotated tag vX.Y.Z. It refuses to run against a dirty working tree or a malformed version, and it never pushes — git push --follow-tags is a separate, explicit step.

Pushing the tag triggers .github/workflows/release.yml, which rebuilds machctl, checks the working build against a fresh one, and publishes to npm using trusted publishing (OIDC — no NPM_TOKEN secret involved), then creates the matching GitHub release.

Trusted publishing has to be configured on npmjs.com, and npm requires the package to already exist before you can do that. The first publish must therefore be done by hand (npm publish --access public from a maintainer's machine) before its trusted publisher can be configured for this workflow.

License

MIT — see LICENSE.

Available Tools

14 tools
relay_acquireAcquire a machineA

Acquire one machine, a guest, and start its ownership heartbeat: a fresh VM from image (a key from relay_probe or relay_search), or a durable machine, a real Mac leased whole, by machine (its name as machctl list shows it, such as Build-Mac-1). Give exactly one of them. A durable machine is on the home network and not isolated from it, a CI job may share its desktop, it has no console (vnc is refused), and its runs take snapshots only while its cua-driver daemon holds Screen Recording; every other call works on it as on a VM. The result's guest is the guest's name in this session, such as login-test-1; every later call on this guest names it with guest. Requires task (a short slug), image or machine, and extractions: every output you intend to bring home, declared before any work ([] is allowed; nothing is extracted automatically). Optional ttlHours (default 4), env (a credential pack, never baked into images), fullWorkspace (opt in to deliver the whole workspace) and vnc (default false; only prepares console-sharing capacity, never opens a viewer). Acquire one guest for each machine the work needs: several guests may run at once, each with its own lease, evidence and finish, up to the session's limit (relay_status reports maxGuests, MCP_RELAY_MAX_GUESTS, default 4); an acquisition beyond it is refused with the names of the guests held. When the host is at capacity, such as at its limit on running macOS VMs, or the durable machine is leased by someone else, the acquisition waits for it, asking again every 15 s for up to MCP_RELAY_CAPACITY_WAIT (30 minutes by default), and the result's waitedForCapacityMs says how long it waited; once the wait runs out it is refused with the backend's reason and the time waited. The backend's other refusals pass through at once. Continue with relay_stage, relay_script, relay_image/relay_extract, then relay_finish or relay_release explicitly before you return; a failed step keeps the VM for repair rather than replaying uncertain input.

Inputs: task (required), extractions (required), env, vnc, +4 more.

relay_acquisition_capabilitiesRead acquisition capabilitiesA
Read-onlyIdempotent

Read the backend's versioned VNC console options for the images it serves. Read-only: it neither acquires a VM nor recovers ownership of one.

Inputs: no named parameters.

relay_console_cancelCancel console viewingA

Cancel an open or uncertain console-viewing attempt by console_id and attempt_id. Closes managed viewing resources only; it never releases or destroys the VM.

Inputs: guest (required), attempt_id (required), console_id (required).

relay_console_openOpen console viewingA

Open console viewing for the lease of the guest that guest names, only following an explicit user request to watch. Requires console_id and attempt_id from relay_console_resolve, userRequested: true, reason (intent) and expected (what the human should expect to see); it opens on the declared service host, never a remote client, and is never triggered automatically by relay_acquire's vnc option. See relay_console_resolve for what a ready status does and does not prove. A failed or uncertain open keeps the attempt: resolve or cancel it rather than opening again with a new attempt.

Inputs: guest (required), reason (required), expected (required), attempt_id (required), +2 more.

relay_console_resolveResolve console statusA
Read-onlyIdempotent

Refresh this session's non-secret console-viewing status for the lease of the guest that guest names. Read-only, and it never recovers ownership. A ready status means guest preflight only, not that a viewer launched: console operations carry no screenshots and never establish authentication, displayed pixels or a human's confirmation. On macOS, viewing requires the human to choose Standard sharing of the existing console, not a new Log In session or a High Performance display, and the relay never auto-confirms that choice; server_enforced_view_only: false and an unverified viewer selection remain limitations even once transport connects. Never replay an uncertain attempt: resolve it here, or close it with relay_console_cancel.

Inputs: guest (required).

relay_extractExtract declared outputsA

Pull one or more already-declared extraction outputs home early, by names. Only outputs declared on relay_acquire may be pulled; each pull verifies source and host hashes and rejects path traversal, symlinks, or a source that changed underneath it.

Inputs: guest (required), names (required).

relay_finishFinish and deliver evidenceA
Destructive

Complete one guest's work: extract every declared output, deliver and verify a portable evidence package, then destroy the guest's VM and unregister it; the session's other guests keep running. The result reports delivery, snapshot completeness, execution outcome and human review as separate facts; a verified package is not a passing test, and a delivered package is not itself human approval. Call this, or relay_release, explicitly for every guest before you return, since ending the session does not do it for you. A declared output that was never produced is listed in incompleteExtractions and does not stop the delivery; any other failed extraction or delivery keeps the VM for a corrected attempt.

Inputs: guest (required).

relay_imageRetrieve a saved imageA
Read-onlyIdempotent

Retrieve one already-saved image; never a new capture, input or directory export, and it never acquires a VM. target selects a display phase (sessionId/executionId/phase: before/after), a declared application file (name, plus a relative path for a directory declaration), or an immutable reference (imageId). PNG, JPEG and WebP only; originals up to 64 MiB and 40,000,000 decoded pixels; the delivered preview is at most 2000x2000 px and 4 MiB of base64 (PNG originals are resampled in-process; JPEG/WebP pass through only within bounds). Each delivery has a 90-second deadline and up to three transfer attempts. A run's after-snapshot is pending in its result; retrieve it here with its imageId or display selector, which waits for its background download within the same deadline. If an image did not arrive, recover it here the same way, never by repeating the input or capturing again, and stop after at most two such recovery calls if it still cannot be inspected. A closed guest returns stale-reference for a display or reference target; its delivered originals stay readable with host file tools. An attached image proves only that the block was included, not that anyone inspected or reviewed it.

Inputs: guest (required), target (required).

relay_probeProbe host and guest readinessA
Read-onlyIdempotent

Read-only host and service facts, used to pick an image; it reports current owned state on its own, so call it again after context compaction rather than trusting earlier messages. Default scope: "host" reports host permissions and activity, VM service availability, and every guest this session holds with its state, not guest readiness. scope: "guest" with guest (an already-acquired guest) reports whether Node.js, which staging requires, is available, and, as facts, whether each relay_script language's runtime runs (/bin/sh, node, tsx, python3), the software the image lists in the catalog, and whether this lease's runs take snapshots, without installing or starting anything, and never claims capture readiness. Acquire one guest for each machine the work needs; several may run at once. Continue each guest in order: relay_acquire, relay_stage, relay_script, relay_image/relay_extract, then relay_finish or relay_release, called explicitly for every guest before you return.

Inputs: guest, scope.

relay_releaseRelease the VMA
Destructive

Abandon one guest's work: destroy the VM of the guest that guest names and unregister it, retaining whatever evidence already exists, without claiming the task succeeded; the session's other guests keep running. Use this instead of relay_finish when the task is not being completed. Safe to retry if a previous release attempt failed; a failed release keeps ownership until destruction is verified. When this session holds no guest, the result is an error saying that nothing was released; a lease acquired under another session identity is never guessed at.

Inputs: guest (required).

relay_scriptRun a guest scriptA

Run one guest script as one recorded operation; never hide several interactions in one call. A command is a shell script. The script drives whatever the image lists, such as cua-driver, Playwright or Chrome DevTools, through their own libraries or command lines, and its exit status says whether it succeeded, so a script that calls cua-driver checks the driver's answer itself. Requires language (shell, javascript, typescript or python) and inline: with inline: true give the script itself as code (up to 1 MiB); with inline: false give localPath, a host file in the project folder (relative to it or absolute inside it, with no symlinks). Give exactly the one inline names; any other call is refused. It runs in the staged workspace: shell with /bin/sh, JavaScript with the guest's Node.js, TypeScript with tsx from the guest's PATH or else the guest Node.js's own type stripping (22.6 or later), and Python with python3 from the guest's PATH (relay_probe scope=guest reports which run); a missing runtime refuses the run before it starts. Evidence is automatic: the relay records a step derived from the call and, when the image lists cua-driver, snapshots the display before and after. Optional reason (intent, never authorization), step (id, title, expected, inputMode) and snapshots.afterIntervalMs (default 500 ms) enrich or tune the record; consecutive keystrokes may form an explicit text group with snapshots.group (first/member/last). timeoutMs bounds execution (default 120000, up to 3600000). The result carries the outcome and bounded stdout/stderr; it names the after-snapshot as pending with its imageId while the image downloads in the background, so call relay_image with that imageId to see it. A refused, uncertain or nonzero result keeps the VM; never replay input whose effect is uncertain. diagnostic: true, for shell scripts only, records a diagnosis or repair without snapshots, even before relay_stage, running the script as /bin/sh -c; it is never visual verification, and its result has one output field with stdout and stderr combined.

Inputs: guest (required), inline (required), language (required), code, +6 more.

relay_stageStage the guest runtimeA

Push and hash-check the guest runtime, plus an optional workspace and support files (files, landing under support/), onto the acquired guest that guest names. workspace and each files[].local lie in the project folder, relative to it or absolute inside it, with no symlinks. Staging needs only Node.js in the guest (nodePath, default node); an image without it is refused, because the relay pushes no runtime. Runs take before and after snapshots only when the image lists capture software (cua-driver); otherwise their records have none, and the result's snapshots says which. When the image lists cua-driver, staging waits up to 120 s for it to take a screenshot, since the image starts its daemon at login, and refuses if it never does. cuaDriver names the guest cua-driver the snapshots use. A failed stage can be retried; once staged, only corrected executable paths may be resubmitted, and staging does not prove capture readiness. resetRecording: true archives the current recording's evidence and starts a fresh recording on the same VM; it refuses while a receiver lock is held.

Inputs: guest (required), files, nodePath, cuaDriver, +2 more.

relay_statusShow relay statusA
Read-onlyIdempotent

List every guest this session holds, by the name relay_acquire answered, each with its VM lease (backend binding, guest state, renewal, console observation, last error), staging state and evidence path, plus the most guests the session may hold (maxGuests), the project directory, the VM service origin and the selected environment in use; active:false when no guest's lease is being renewed. Read-only; it does not touch the VMs.

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 14 tools

    Disambiguation4/5

    Most tools have clear resource/action boundaries across the guest lifecycle, such as acquire, stage, script, image, finish, and release. However, relay_probe and relay_status both report session/guest state, and relay_extract overlaps somewhat with relay_finish's extraction phase, so an agent may hesitate between these pairs despite the detailed descriptions.

    Naming Consistency5/5

    All tools use a consistent relay_ prefix and snake_case naming. The console tools form a predictable relay_console_resolve/open/cancel group, and the few noun-only names like relay_status and relay_image remain readable and consistent with the overall convention.

    Tool Count5/5

    Fourteen tools is well within the appropriate range for a VM relay server with discovery, lifecycle, evidence, and console-sharing concerns. Each tool maps to a distinct phase or safety boundary, and the console trio is justified by the need to resolve, open, and cancel viewing attempts separately.

    Completeness4/5

    The surface covers the full guest lifecycle: discovery, acquisition, staging, scripted execution, image and extraction evidence, completion/release, status, and console viewing. A minor gap is the lack of an explicit tool to list all available images or durable machines independently of app-name search, leaving some discovery dependent on relay_search and relay_probe output.

    Maintenance

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables autonomous desktop automation by delegating tasks to vision-based agents operating within cloud-based virtual machine sandboxes. It allows users to manage VMs, execute complex computer tasks, and receive text-based screen summaries across Linux, Windows, and macOS environments.
      2
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables a Copilot Studio agent to operate a dedicated, disposable Windows VM by running commands, PowerShell scripts, file operations, and background jobs over authenticated HTTPS.
      MIT
    • A
      license
      B
      quality
      C
      maintenance
      Enables AI coding agents to work inside a disposable Windows guest desktop, where they can run shell commands, launch applications, inspect and operate native GUI controls, perform browser DOM actions, and transfer files. Fresh observations, PNG screenshots, and hashed artifacts let agents verify results without touching the host desktop.
      18
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables computer-use agents to record normalized screen observations, connect verified transitions into a reusable map, and replay learned browser or desktop routes in one guarded call without a model in the loop.
      17
      MIT