Skip to main content
Glama

mcp-agent-relay

Durable MCP dispatch for explicit CLI executors.

mcp-agent-relay lets one MCP client hand a job to a locally installed CLI agent and retrieve a durable result. It keeps the queue, state, leases, cancellation, and review policy in one MCP-native relay while delegating each turn to a small, allowlisted executor adapter.

It is designed for a practical local workflow: Codex can request an independent read-only review from Codex, Claude Opus, or Claude Fable without turning the relay into a remote shell.

Status: research preview / single-machine relay. The durable queue and executor adapters are production-shaped; wake-up integration with Claude Code depends on Claude Code preview capabilities.

Why use it?

The hard part of agent-to-agent work is not starting a command. It is retaining a correct answer when processes restart, jobs collide, a worker loses its lease, or a caller gives up waiting.

The relay provides:

  • Durable dispatch. File-backed jobs survive server and worker restarts. A request_id makes dispatch idempotent.

  • Explicit routing. The to field selects one allowlisted worker id; it cannot select a binary, arguments, Claude agent, or environment.

  • Safe execution defaults. Jobs are read-only by default. Codex writes require an explicit worker opt-in; Claude writes are rejected in this release.

  • Correct worker coordination. Workers claim jobs using leases, fencing tokens, heartbeats, cancellation, timeout handling, and recovery states.

  • MCP-first integration. Any MCP client can dispatch, poll, dispatch_wait, inspect audit, read a bounded stream or hash-verified changelog, create/dispatch an approved handoff, or diagnose with trace/health. A running Claude Code session can also be notified when a job changes state.

  • Operational visibility. Every turn has a bounded deadline, redacted lifecycle events, worker heartbeat evidence, and a passive monitor for diagnosing Codex/Claude jobs without redispatching them.

  • Auditable executor profiles. Requested model policy, effective-model evidence, CLI version, timestamps, and exit status are recorded without accepting model selection from the payload.

Related MCP server: Parley

At a glance

Need

Use

Quick read-only review with Codex

to: "codex"

Deep Claude review with the local deep-reasoner agent

to: "claude-opus"

Independent Claude review with the local fable-reasoner agent

to: "claude-fable"

Verified three-family review from Codex or Claude Code

/tri-r after installing the bundled skill

Two-lens cross-harness review with Sol/xhigh and Opus/max

/dual-r after installing the bundled skill

Wait for one result in the tool call

dispatch_wait

Submit now and inspect later

dispatch then poll

Inspect executor/model evidence after result retention

audit

Inspect one job's redacted lifecycle

trace

Inspect project provenance and handoff history

changelog

Inspect relay and worker health

health or bin/relay-monitor.mjs

Make a Codex change in an isolated worktree

write: true, worktree: true and a Codex worker started with --allow-writes

Architecture

MCP client                         durable relay                         worker
──────────                         ─────────────                         ──────
dispatch(to, task, request_id) ──▶ queue + request-id dedup
                                  claim + lease + fencing ───────────▶ adapter.runTurn()
poll(job_id)                 ◀─── result + timing evidence ◀───────── Codex or Claude CLI
trace(job_id)                ◀─── redacted process/state timeline
health()                     ◀─── server + worker heartbeat evidence
audit(job_id)                ◀─── compact terminal ledger

Layer

Responsibility

lib/relay-jobs.mjs

Durable queue, locking, deduplication, leases, fencing, turn deadlines, expiry, cancellation, recovery, review state, trace events, and compact audit ledger

server.mjs

MCP stdio facade, inbox resources, synchronous wait, and optional Claude channel

lib/relay-worker.mjs

Claim loop, heartbeat, timeout/deadline fencing, cancellation, process tracing, worktree orchestration, and durable completion

lib/worker-lifecycle.mjs

Worker autospawn/reuse, build identity, PID/heartbeat health, and safe replacement of stale daemons

bin/relay-monitor.mjs

Passive JSON/text monitor for jobs and Codex/Claude worker health

bin/relay-debug.mjs

One-shot, read-only correlation of MCP registration, workspace store, workers, and queued jobs

bin/relay-gc.mjs

Operator-triggered bounded retention for Context Store streams, summaries, handoffs, and locks

bin/relay-cross-host-test.mjs

Cross-host contract check for CLIs, MCP, skills, profiles, monitor/debug, and the suite

bin/relay-studio-self-test.mjs

Deterministic A → approved handoff → B contract test without starting a CLI

bin/relay-studio-mvp0-smoke.mjs

Real read-only MVP-0 smoke: workspace A → approved handoff → workspace B

lib/executor-registry.mjs

Central allowlist from worker id to an adapter and its fixed configuration

lib/codex-executor.mjs / lib/claude-executor.mjs

One isolated CLI turn with final-result compatibility, normalized stream hooks, and process-group shutdown

lib/relay-streams.mjs

Bounded, redacted per-attempt stream spool and passive tail reader; never queue state

lib/relay-errors.mjs

Redaction and 8 KiB cap for executor errors crossing into durable state or MCP responses

lib/relay-context.mjs / lib/relay-changelog.mjs

Deterministic run summaries and redacted, hash-verified project changelog entries

lib/relay-handoffs.mjs

Hash-verified, approval-gated context manifests between allowlisted workspaces/executors

The executor boundary is deliberately narrow: the relay is executor-agnostic, not command-agnostic.

Host-aware /tri-r

The bundled tri-r skill always reviews with three distinct model families: Opus, Fable, and GPT-5.6 Sol. The logical panel is the same in both harnesses, while the native and relayed routes are inverted:

The complete cross-harness capability matrix is maintained in docs/review-capability-matrix.md.

Orchestrator host

Opus lens

Fable lens

Sol lens

Claude Code

Native deep-reasoner

Native fable-reasoner

AgentRelay to: "codex"

Codex

AgentRelay to: "claude-opus"

AgentRelay to: "claude-fable"

Fresh native pipeline_deep_reviewer; fallback AgentRelay to: "codex"

The Codex route never treats deep-reasoner.toml or fable-reasoner.toml as Claude models. Agent names are prompts and routing labels, not model evidence. Every relayed lens records requestedModelPolicy, effectiveModel, and modelEvidence when the executor supplies them.

Install or refresh the same canonical skill in the personal discovery directories of both harnesses:

node bin/relay-install-tri-r.mjs

This writes ~/.agents/skills/tri-r/SKILL.md for Codex and ~/.claude/skills/tri-r/SKILL.md for Claude Code. The operation is idempotent. Use --codex-only, --claude-only, or --home /alternate/home when needed. Restart open Codex and Claude Code sessions after updating a skill because skill catalogs are loaded when a session starts.

/tri-r is intentionally expensive and produces a consolidated, verified panel. For a single-family review, ask the MCP client to use claude-opus, claude-fable, or codex directly instead of running a partial tri-r.

A dispatch_wait timeout is not a failed review. It only ends the MCP caller's bounded wait; the durable job continues. /tri-r records that lens as PENDING, preserves its job_id, and does not consolidate the panel. After a terminal notification it performs one targeted poll and resumes the same job without redispatching or changing the request_id. Only terminal failed, cancelled, or expired states degrade a lens.

Host-aware /dual-r

dual-r is a smaller two-lens panel with inverted routing:

Orchestrator

Native lens

AgentRelay lens

Codex

dual_sol_reviewer — GPT-5.6 Sol, xhigh, read-only

claude-opus-maxdeep-reasoner, Opus alias, max

Claude Code

dual-opus-reasoner — Opus alias, max, read-only

codex-sol-xhigh — GPT-5.6 Sol, xhigh, read-only

Install the same skill and the native profiles in both personal harnesses:

node bin/relay-install-dual-r.mjs

The installer is idempotent. It writes ~/.agents/skills/dual-r/SKILL.md, ~/.codex/agents/dual_sol_reviewer.toml, ~/.claude/skills/dual-r/SKILL.md, and ~/.claude/agents/dual-opus-reasoner.md. It does not edit ~/.codex/config.toml: register the Codex profile once if needed:

[agents.dual_sol_reviewer]
description = "Independent read-only Sol xhigh reviewer for dual-r"
config_file = "./agents/dual_sol_reviewer.toml"

Add codex-sol-xhigh and claude-opus-max to RELAY_WORKER_AGENTS in the MCP registration. Restart Codex and Claude Code sessions after installing skills; their catalogs are loaded at session start. The durable wait rules are the same as tri-r: dispatch once, optionally one bounded dispatch_wait, preserve PENDENTE on caller timeout, and make one poll only after terminal notification.

The relay uses Claude's opus latest-model alias, not an invented numbered model id. Use the returned effectiveModel/modelEvidence (or report the gap) before calling it “Opus 5”. Both lenses are read-only; write: true is rejected before a process starts.

Available executors

Worker id / to

Adapter

Fixed model policy

Turn ceiling

Policy kind

Write policy

codex

Codex CLI

gpt-5.6-sol / high

10 min

Pinned profile

Denied unless that worker has --allow-writes

codex-sol-xhigh

Codex CLI

gpt-5.6-sol / xhigh

30 min

Pinned dual-r profile

Always rejected

claude-opus

Claude Code

deep-reasoner + --model opus

10 min

Latest Opus alias

Always rejected

claude-opus-max

Claude Code

deep-reasoner + --model opus + --effort max

10 min

Latest Opus alias

Always rejected

claude-fable

Claude Code

fable-reasoner + --model claude-fable-5

10 min

Pinned model id

Always rejected

For the Claude routes, the local Claude Code installation must already be authenticated and must have the corresponding agent definitions:

~/.claude/agents/deep-reasoner
~/.claude/agents/dual-opus-reasoner
~/.claude/agents/fable-reasoner

Claude runs in non-interactive stream-json mode. The initial structured event is the evidence source for the effective model, and the terminal structured event carries the final response and session id. The -- delimiter ensures a prompt cannot be parsed as a Claude CLI option.

Relay Claude turns are isolated from ambient Claude configuration: the adapter loads only the local settings source (an empty --setting-sources would mean the defaults), runs from a private temporary cwd and grants the target workspace only through --add-dir, passes a fixed empty --mcp-config with --strict-mcp-config, and supplies the allowlisted agent definition inline. It disables Chrome, slash skills and session persistence, and allows only the built-in Read, Grep and Glob tools. This prevents a review from recursively starting AgentRelay, Backlog or browser MCP servers inherited from the user's project. OAuth/keychain authentication remains available through the isolated process's HOME; credential environment variables are deliberately not inherited. The relay never installs credentials or accepts environment/configuration from a payload.

claude-opus deliberately means the CLI's opus latest-model alias; it is not a claim that a specific numbered Opus release ran. claude-fable is currently pinned to the full claude-fable-5 model id. Payload fields such as command, args, agent, model, and environment settings cannot alter either profile.

Quick start: connect Codex

1. Clone the relay and verify Node

git clone https://github.com/brenoperucchi/mcp-agent-relay.git
cd mcp-agent-relay
node --version  # Node 18.18 or newer

No package installation or build step is required.

2. Register the MCP server

Register a global Codex MCP server, or put the equivalent configuration in a trusted project if it should be project-scoped:

codex mcp add agentrelay \
  --env RELAY_WORKER_AUTOSPAWN=1 \
  --env RELAY_WORKER_AGENTS=codex,codex-sol-xhigh,claude-opus,claude-opus-max,claude-fable \
  -- node /absolute/path/to/mcp-agent-relay/server.mjs

If a CLI is installed outside the inherited environment, provide a minimal explicit PATH for the relay process:

codex mcp add agentrelay \
  --env PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin \
  --env RELAY_WORKER_AUTOSPAWN=1 \
  --env RELAY_WORKER_AGENTS=codex,codex-sol-xhigh,claude-opus,claude-opus-max,claude-fable \
  -- node /absolute/path/to/mcp-agent-relay/server.mjs

Confirm the registration:

codex mcp list

When the server receives a job, autospawn starts one short-lived worker per configured executor id as needed. You can instead run workers yourself; see Running workers.

3. Dispatch a read-only review

In a Codex session, call dispatch_wait with an explicit executor id:

{
  "to": "claude-opus",
  "task": {
    "prompt": "Review the current diff for correctness, regressions, and missing tests. Report only actionable findings."
  },
  "request_id": "review-current-diff-opus-1",
  "timeout_ms": 120000
}

For the Fable-backed Claude agent:

{
  "to": "claude-fable",
  "task": {
    "prompt": "Independently review the current diff. Focus on security and reliability risks."
  },
  "request_id": "review-current-diff-fable-1",
  "timeout_ms": 120000
}

For the Opus/max lens used by /dual-r from Codex:

{
  "to": "claude-opus-max",
  "task": {
    "prompt": "Independently review the current diff. Read-only; report actionable findings with file/line evidence."
  },
  "request_id": "review-current-diff-opus-max-1",
  "timeout_ms": 120000
}

From Claude Code, the relayed Sol/xhigh lens uses the inverse route:

{
  "to": "codex-sol-xhigh",
  "task": {
    "prompt": "Independently review the current diff. Read-only; focus on implementation edges and missing tests."
  },
  "request_id": "review-current-diff-sol-xhigh-1",
  "timeout_ms": 120000
}

If claude is missing, not on PATH, unauthenticated, or its named local agent is unavailable, the job reaches a clear failed state with the CLI error. The relay never installs Claude, changes global settings, or creates credentials.

MCP tools and job lifecycle

Submit now, retrieve later

dispatch creates (or deduplicates) a durable job and returns immediately:

// dispatch
{
  "to": "codex",
  "task": { "prompt": "Review the current diff for correctness." },
  "request_id": "review-current-diff-codex-1"
}

// response
{ "job_id": "relay-…", "deduped": false, "state": "queued" }

Call poll until it reaches a terminal state:

// poll
{ "job_id": "relay-…" }

// response
{
  "found": true,
  "state": "completed",
  "result": { "output": "…" },
  "attempts": 1,
  "execution": {
    "executorId": "claude-opus",
    "requestedModelPolicy": "opus",
    "modelPolicyKind": "latest_alias",
    "effectiveModel": "claude-opus-…",
    "modelEvidence": "claude-stream-json:init",
    "cliVersion": "…",
    "exitCode": 0
  }
}

The same request_id returns the same job rather than scheduling the work twice. If a later request reuses that ID with a different target executor or payload, the relay returns request_id_conflict instead of silently routing the new request to the old job.

Submit and wait

dispatch_wait follows the same idempotent path but waits for a terminal result, up to timeout_ms:

{
  "to": "codex",
  "task": { "prompt": "Review the current diff for correctness." },
  "request_id": "review-current-diff-codex-2",
  "timeout_ms": 120000
}

If the caller timeout expires first, the result says timed_out: true and reports the current queued or running state. The job continues server-side; retrieve it later with poll.

Audit executor and model evidence

poll and dispatch_wait expose execution while the job is retained. The audit tool keeps a compact terminal history independently of normal job/result retention:

// audit
{ "job_id": "relay-…" }

// response
{
  "found": true,
  "job_id": "relay-…",
  "records": [
    {
      "state": "completed",
      "to": "claude-opus",
      "execution": {
        "requestedModelPolicy": "opus",
        "modelPolicyKind": "latest_alias",
        "effectiveModel": "claude-opus-…",
        "modelEvidence": "claude-stream-json:init"
      },
      "resultSha256": "…"
    }
  ]
}

The audit ledger stores no prompt, raw result, raw error, working-directory path, or request id. It stores hashes for correlation plus allowlisted execution metadata. It is bounded to the latest 5,000 terminal records per workspace. Existing version-1 stores migrate automatically with an empty audit history.

Executor failures are redacted and capped at 8 KiB before entering the durable job state; the full stderr remains available only through the separately redacted, quota-bound stream spool.

requestedModelPolicy says what the registry requested. effectiveModel is populated only when the CLI provides structured evidence; otherwise it remains null. In particular, passing -m gpt-5.6-sol proves the Codex policy requested by the relay, not a private backend rollout identifier.

Debugging and monitoring a live relay

The relay has two non-mutating diagnostics for investigating a request from either Codex or Claude Code:

// trace
{ "job_id": "relay-…", "limit": 200 }

// health
{ "agents": ["codex", "codex-sol-xhigh", "claude-opus", "claude-opus-max", "claude-fable"] }

trace returns the bounded lifecycle (claimed, running, process spawn/close, abort, and terminal transition) plus timing and worker metadata. health returns the MCP server instance/build id, workspace store id, worker PIDs, heartbeat age, and build mismatch status. Neither tool returns a prompt, result, claim token, or environment variable.

Reading a workspace stream

stream reads only the bounded tail emitted by the executor adapter:

{ "job_id": "relay-…", "limit": 100 }

Events carry sequence, timestamp, channel (stdout, stderr or provider_event), adapter, attempt and redaction metadata. They are stored in per-attempt *.stream.jsonl files outside relay-state.json. This is a structured process stream, not a scrape of terminal pixels or another window's screen buffer. PTY capture remains a later capability for explicitly interactive adapters. A completed turn also gets a deterministic run_summary in the separate Context Store: it contains hashes, counts, model evidence, truncation and provenance, never the full prompt or response.

Changelog e proveniência

O Context Store também deriva uma entrada de changelog por execução terminal e por handoff. A entrada contém apenas estado, timestamps, hashes, referências de artefatos e a cadeia sourceJobId → handoffId → targetJobId; prompts, respostas, transcript e variáveis de ambiente não entram no arquivo. A operação MCP é somente leitura:

{ "limit": 100 }

O comando changelog devolve essas entradas com entryHash, e o monitor inclui o mesmo resumo no snapshot JSON. Assim, a passagem A → B pode ser correlacionada sem copiar o buffer de uma tela ou confiar em texto não verificado. Se a entrada for adulterada, ela é omitida até ser regenerada a partir do run_summary/manifesto.

Handoff entre workspaces

Um handoff nunca copia uma tela inteira nem aceita um comando do texto de uma resposta. O workspace de origem cria um manifesto em rascunho, que só se torna aprovado após uma operação explícita, com resumo, referências de artefatos/faixas de eventos, IDs de origem/destino e hash de contexto:

{
  "source_job_id": "relay-source…",
  "target_workspace_id": "<store-id-do-destino>",
  "target_agent_id": "claude-opus",
  "summary": "Contexto sugerido para a próxima revisão.",
  "created_by": "llm"
}

Depois que um operador local registrar a aprovação humana, no workspace de destino:

node bin/relay-handoff-approve.mjs handoff-… --by "reviewer"
{ "handoff_id": "handoff-…", "request_id": "handoff-destination-1" }

Isso chama handoff_dispatch. O executor é lido do manifesto hash-verificado e continua sujeito à allowlist, às políticas de escrita e ao lease normal. Um handoff de job que não terminou em completed, com agente desconhecido, manifesto adulterado ou caminho absoluto de artefato é recusado. O worker reconstrói o contexto usando o run_summary hash-verificado e registra handoff_id, job de origem e workspace de origem nos eventos e no resultado do destino.

O endpoint MCP handoff_approve permanece fail-closed por compatibilidade, mas nunca aceita uma identidade declarada pelo próprio modelo. target_workspace_id, artifactRefs e eventRanges são referências opacas limitadas e não substituem um transporte ou catálogo de artefatos entre hosts. Essa autoridade externa fica para a fase de multi-host.

For a longer observation window, use the passive monitor in a separate terminal:

node bin/relay-monitor.mjs --workspace . \
  --agents codex,codex-sol-xhigh,claude-opus,claude-opus-max,claude-fable --follow --interval 1000

# Machine-readable snapshots for a log collector:
node bin/relay-monitor.mjs --workspace . --follow --json

The monitor reports queued, running, completed, failed, needs_recovery, and stale. It never dispatches, retries, cancels, or changes a payload. A stale job is a diagnostic signal; the durable store's deadline fence is what transitions an expired read-only turn to failed (or a write turn to needs_recovery). The JSON snapshot also exposes bounded changelog metadata and handoff provenance; the text mode appends a handoff=<id> marker for destination jobs.

Derived files are pruned opportunistically after terminal summaries and can also be collected explicitly without touching queue state:

node bin/relay-gc.mjs --workspace . --max-files 5000 --max-bytes 536870912

Active jobs and draft/approved handoffs are retained. Workspace-local GC only considers manifests owned by that workspace; unknown/foreign manifests and all handoff locks are retained because an mtime is not proof that a lock is idle. Dispatched/old derived files are eligible once the count or byte budget is exceeded. This command is safe to run from a scheduled maintenance job on each host.

When a smoke test remains queued with attempts: 0, run the one-shot correlator before dispatching again:

node bin/relay-debug.mjs \
  --workspace ~/Devs/mcp-agent-relay \
  --agents codex-sol-xhigh,claude-opus-max \
  --job-id relay-…

The command reports RELAY_WORKER_AGENTS/RELAY_WORKER_AUTOSPAWN from the shell and the Codex registration (~/.codex/config.toml by default) as separate sources; it does not pretend that a standalone shell has the exact environment of an already-running MCP process. It reports worker heartbeat/PID state and scans the current RELAY_DATA_DIR (or the default state root) for the job id. It prints only metadata—never task prompts, results, or raw error text—and exits with status 2 when a P0/P1 operational finding is present. Typical findings are:

  • mcp_config_unavailable: the default Codex registration is missing or unreadable and no explicit relay worker environment was supplied;

  • worker_not_configured: the MCP server can enqueue that to, but autospawn has no worker configured to claim it;

  • queued_without_worker: the job is still queued with zero attempts and no live worker;

  • configuration_divergence: the shell environment and persisted MCP registration disagree; inspect both before changing either one;

  • store_mismatch: the job exists in another workspace store, so the monitor is watching the wrong --workspace under the same state root. If the server uses another RELAY_DATA_DIR, run the diagnostic with that same variable explicitly.

The diagnostic is intentionally passive. After correcting the registration, restart the Codex/Claude session so its MCP environment is reloaded, then use relay-monitor with the same workspace and RELAY_DATA_DIR to follow the new job.

Bounded logging with journald/systemd

relay-monitor is useful interactively, but it is not a daemon. For a bounded observation window, use relay-observe. It samples the selected workspace and writes redacted JSONL only when a job/worker is active or changes. The record includes job state transitions as seen by the store, worker liveness, executor/model evidence, elapsed/deadline fields, and a coarse error_code (timeout, cancelled, lease_expired, write_policy, or executor_error). It never writes prompts, results, raw errors, environment variables, or worker log paths. Historical terminal records remain available through audit/trace and are not replayed on collector startup.

One snapshot:

node bin/relay-observe.mjs \
  --workspace "$(git rev-parse --show-toplevel)" \
  --agents codex,codex-sol-xhigh,claude-opus,claude-opus-max,claude-fable \
  --once

To avoid choosing the wrong project store, use --all-stores. This scans every workspace under the active RELAY_DATA_DIR (or ~/.mcp-agent-relay/state) and is the recommended mode for the systemd service:

node bin/relay-observe.mjs --all-stores --duration-ms 7200000 --interval 1000

Observe for two hours to stdout (which can be piped to rsyslog or jq):

node bin/relay-observe.mjs \
  --workspace "$(git rev-parse --show-toplevel)" \
  --duration-ms 7200000 \
  --interval 1000 \
  --output /tmp/mcp-agent-relay-observations.jsonl

For a user-level systemd collector, install the unit on each host where a Claude or Codex session dispatches jobs:

mkdir -p ~/.config/systemd/user ~/.config/mcp-agent-relay
cp systemd/mcp-agent-relay-observer.service ~/.config/systemd/user/
cp systemd/observer.env.example ~/.config/mcp-agent-relay/observer.env
# The example uses RELAY_OBSERVER_ALL_STORES=1, so no project path is required.
# Set it to 0 and edit RELAY_OBSERVER_WORKSPACE for one project only.
systemctl --user daemon-reload
systemctl --user enable --now mcp-agent-relay-observer.service
journalctl --user -u mcp-agent-relay-observer.service -f -o cat

The unit is deliberately bounded (RuntimeMaxSec=2h) and sends JSONL to journald. Stop it early with systemctl --user stop mcp-agent-relay-observer.service; it never redispatches or mutates jobs. Repeat the same installation in SSHWSL, using the path of the repository and project on that machine. If that WSL instance does not run systemd, use the CLI directly with --output and keep the terminal/process alive for the requested window.

Every claim now carries a server-side turn deadline in addition to its lease. A worker heartbeat can keep a healthy silent turn leased, but it cannot keep an expired executor running forever. An old daemon is identified by its relay build id. A new MCP server replaces an outdated daemon only while it has no active claim; if a turn is in flight, the update is deferred so a build change cannot abort→release→reclaim that job mid-review.

Job states

State

Meaning

queued

Waiting for a worker

running

Claimed by one worker with an active lease

completed

Durable final result available

failed

The adapter or policy rejected the job

cancelled

Cancellation was accepted

needs_recovery

A write-capable run lost its lease; it is never silently re-executed

needs_review

A human decision is required before or after execution

Security model

The relay assumes task prompts are untrusted data. It does not treat them as a shell request.

  • The central registry owns the executable, fixed CLI arguments, Claude agent/model policy, and the Codex gpt-5.6-sol / high plus dual-r gpt-5.6-sol / xhigh profiles.

  • A Codex task may omit model and effort; if it supplies either, it must exactly match that fixed profile. A downgrade or arbitrary model selection fails before the CLI starts.

  • Claude tasks may omit model; any conflicting payload model is rejected before the CLI starts. Agent, binary, arguments, and environment always come from the registry.

  • The dual-r profiles also reject payload effort overrides; codex-sol-xhigh and claude-opus-max are fixed allowlist entries, not arbitrary command runners.

  • A job can choose only an exact, known to id. Unknown ids fail safely.

  • Claude adapters run with a reduced environment, an isolated local settings source, and an empty MCP configuration; they do not load user/project hooks, plugins, MCP aliases, or payload environment values. The registry still supplies the explicit agent/model.

  • All Claude jobs are read-only in this version. write: true for either Claude worker is an explicit failure before a CLI process starts.

  • Codex writes are deny-by-default and require both a write: true job and a worker launched with --allow-writes.

  • Write jobs with an expired lease go to needs_recovery instead of being replayed.

  • Worktree execution is available for eligible Codex writes, so the main worktree stays untouched.

  • Wake-up notifications contain only a minimal job envelope, never untrusted prompt text or model output.

This protects the relay’s command-selection boundary. It does not make a prompt harmless to the model receiving it; write careful task prompts and inspect all results.

Running workers

Autospawn is convenient for local MCP use, but explicit workers work the same queue and are useful for long-lived or supervised setups.

# Claim at most one queued job, execute it, then exit.
node worker.mjs --agent codex --once

# Keep a read-only Claude worker running.
node worker.mjs --agent claude-opus --interval 1000
node worker.mjs --agent claude-opus-max --interval 1000
node worker.mjs --agent codex-sol-xhigh --interval 1000
node worker.mjs --agent claude-fable --interval 1000

# Permit Codex write jobs (still requires task.write: true).
node worker.mjs --agent codex --allow-writes --interval 1000

# Stop after five minutes with no jobs processed.
node worker.mjs --agent codex --idle-timeout 300000

# Fail a read-only turn that still runs after ten minutes. A timed-out write
# job is preserved in needs_recovery instead of being replayed.
node worker.mjs --agent claude-opus --timeout 600000

Worker selection is always based on --agent and the registry. It is never taken from a job payload. dispatch_wait.timeout_ms limits only the MCP caller's wait. The worker has a separate per-turn budget. An explicit --timeout or RELAY_WORKER_TIMEOUT_MS override wins; otherwise the allowlisted profile owns the budget (600000 ms for normal profiles and 1800000 ms for codex-sol-xhigh). The selected value is persisted as a fenced deadline in the claim; a read-only turn that exhausts it fails terminally rather than being re-run, while a write turn enters needs_recovery. Increasing a job's ttl_ms or the MCP caller's timeout_ms does not silently change the worker profile's execution ceiling.

Store location

By default, state is stored beneath:

~/.mcp-agent-relay/state

Set RELAY_DATA_DIR to choose another durable local location. Every process that participates in the same relay—the MCP server, workers, and optional hooks—must use the same store location.

The version-2 store embeds a bounded audit ledger alongside the active/retained job collection so terminal transition and audit evidence are committed by the same atomic store write.

Codex writes in isolated worktrees

Codex is the only executor that can write in this first release. To opt in, start the Codex worker with --allow-writes and request an isolated worktree:

{
  "to": "codex",
  "task": {
    "prompt": "Implement TASK-192 and add focused tests.",
    "write": true,
    "worktree": true
  },
  "request_id": "implement-task-192-1"
}

The relay creates a branch and git worktree based on the caller’s current HEAD. The result includes worktree.path, worktree.branch, and worktree.baseSha for manual review and merge. Nothing merges automatically.

If a write turn makes no change, its temporary worktree and branch are removed. If it fails after making changes, the worktree is preserved and its path is included in the error for manual recovery.

A worktree starts from the last commit, not from uncommitted edits in the caller’s main worktree.

Human review gate

Jobs can require a human decision rather than running or completing autonomously.

  • Add a non-empty requireReview reason to a task to put it in needs_review before execution.

  • An executor can self-flag an ambiguous or sensitive task with RELAY_NEEDS_REVIEW: <reason> in the final response; its partial result is retained for inspection.

  • Resolve gates only from the local review CLI, never through MCP tools:

node bin/relay-review.mjs list
node bin/relay-review.mjs approve <jobId> --by "reviewer" --note "approved after inspection"
node bin/relay-review.mjs reject <jobId> --by "reviewer" --note "not safe to run"

Predeclared approval returns a job to queued so it can run. Approval of a self-flagged result accepts that captured result. Rejection marks the job failed.

The CLI gate prevents a normal MCP client from approving its own job through the tools it already holds. It is not a complete process-isolation boundary: a local process with shell access can invoke the review CLI. Stronger approval authority requires a separate credential or isolation boundary.

Claude Code integration

The relay can be used from Claude Code either as a plugin or as a plain MCP server.

Plugin install

claude plugin marketplace add <your-org>/mcp-agent-relay
claude plugin install mcp-agent-relay

The plugin declares agentrelay in .mcp.json, includes the host-aware tri-r and dual-r skills, and includes relay slash commands. The supplied commands dispatch to codex:

  • /mcp-agent-relay:review <path> [focus] requests a read-only Codex review.

  • /mcp-agent-relay:implement <task> requests an isolated Codex worktree run; it needs a Codex worker allowed to write.

Plain Claude MCP install

claude mcp add --scope user agentrelay \
  node /absolute/path/to/mcp-agent-relay/server.mjs \
  -e RELAY_AGENT=claude-main \
  -e RELAY_WORKER_AUTOSPAWN=1 \
  -e RELAY_WORKER_AGENTS=codex,codex-sol-xhigh,claude-opus,claude-opus-max,claude-fable

Plugin and plain-server installations expose different MCP names:

Installation

Tool prefix

Channel source

Plugin

mcp__plugin_mcp-agent-relay_agentrelay__

plugin:mcp-agent-relay@mcp-agent-relay

Plain claude mcp add

mcp__agentrelay__

server:agentrelay

Optional wake-up channel

Set a logical RELAY_AGENT identity on the Claude session and launch it with its corresponding development channel:

# Plugin installation
RELAY_AGENT=claude-main claude \
  --dangerously-load-development-channels plugin:mcp-agent-relay@mcp-agent-relay

# Plain MCP installation
RELAY_AGENT=claude-main claude \
  --dangerously-load-development-channels server:agentrelay

The channel sends only a small job_id and state notification. Claude then uses poll to obtain the normal structured result. When CLAUDE_CODE_SESSION_ID is available, notifications are narrowed to the specific session that dispatched the job.

Stop hook alternative

The channel is optional. A Stop hook checks the store as Claude is about to end a turn and gives it one more turn to poll a newly completed job:

# Add project settings. Use --global for ~/.claude/settings.json.
node bin/relay-install-hook.mjs

The helper is idempotent. Use --print to preview or --remove to undo it. The hook needs the same RELAY_AGENT and RELAY_DATA_DIR configuration as the MCP server. It fails open, so an internal hook error never blocks a Claude session from ending.

Requirements and limitations

  • Node.js 18.18 or newer. The runtime has no npm dependencies.

  • The codex worker needs the Codex CLI available on its PATH.

  • Claude workers need an existing local, authenticated Claude Code CLI plus the allowlisted agent definitions. The relay does not provision either.

  • Claude jobs are read-only only. There is no Claude write mode in this release.

  • claude-opus tracks the Claude CLI opus alias. Use execution.effectiveModel or audit, not the route name, when you need to know what the CLI reported for one completed run.

  • claude-opus-max uses that same latest alias with a fixed --effort max; it still does not prove a numbered Opus release without effectiveModel evidence.

  • effectiveModel may be null when an executor does not provide structured evidence. The relay never turns a requested alias into a fabricated effective version.

  • Handoffs are explicit and hash-verified, but human identity/authentication and a global workspace/artifact registry are outside this local-file MVP.

  • The store is a local file-backed queue coordinated by an interprocess lock. It is designed for one machine, not a multi-host queue.

  • Stream fencing is amortized in bounded batches (up to 64 KiB or 16 events) under the queue lock; this preserves lease correctness while limiting the time a slow filesystem can delay another claim or heartbeat.

  • Handoff lock files are never unlinked by workspace-local GC. The lock protocol itself reclaims stale JSON locks only through an atomic rename after checking process identity; legacy or malformed locks fail closed and remain until an operator removes them because an old mtime is not proof that no process still holds them.

  • After updating relay source, restart the MCP server/session that owns the relay. health and dispatch diagnostics now expose source_build_id/build_mismatch; the server captures a startup fingerprint for worker fencing, so a mismatch is an explicit restart signal rather than a silent stale session.

  • The development channel is a Claude Code preview feature and may require the explicit channel flag. The queue, polling, and Stop hook remain usable without it.

Development

node --test

The suite covers the store and MCP facade, worker lifecycle, review and worktree protections, Codex compatibility, executor-registry resolution, and mocked Claude CLI success, failure, cancellation, and payload-isolation behavior. It also verifies the host-aware tri-r and dual-r routing contracts and the idempotent cross-harness skill installers.

For an end-to-end environment check without executing a real review turn, run:

node bin/relay-cross-host-test.mjs

The harness checks the five allowlisted executor profiles, codex/claude CLI availability (--version only), MCP registrations, installed tri-r/dual-r skills and native profiles, passive monitor/debug startup, and npm test locally and on ryzen5wsl. Use --json for CI/log collection, --local-only to skip SSH, and --skip-suite for a fast configuration check. A missing CLI or skill is reported as a failure; the optional Codex native-agent registration is reported as a warning unless --strict is used. The remote check is read-only and does not dispatch work.

For a minimal real smoke test, which does dispatch a fixed read-only prompt through the actual MCP server and confirms the job in relay-monitor, run:

node bin/relay-real-smoke.mjs --json

By default it exercises codex-sol-xhigh, claude-opus, and claude-fable locally and on ryzen5wsl. It accepts no prompt, command, model, agent definition, or environment from the payload; the prompt is fixed to RELAY_SMOKE_OK with write:false. Use --agents codex,claude-opus to reduce the matrix further or --local-only to test one host. This test requires the real CLIs and local Claude authentication; it may consume a small amount of model quota.

To validate the first Relay Studio business slice — transferring approved context from workspace A to workspace B — run:

node bin/relay-studio-mvp0-smoke.mjs \
  --source-agent codex \
  --target-agent claude-opus \
  --timeout-ms 180000 \
  --json

The harness creates two temporary workspaces, starts the real MCP server and allowlisted workers, runs a fixed read-only turn in A, creates a draft handoff, approves it through the local operator CLI, dispatches only the handoff_id to B, and checks both relay-monitor and the bounded changelog. It never copies a raw buffer or accepts a command/model/agent from the payload. This is a same-host MVP-0 check; SSHWSL transport, external PTY capture and proactive workflow transitions remain later slices. Real CLI availability and local authentication are required.

For a fast deterministic self-test of the relay itself, without Claude/Codex installation, authentication or model quota, run:

npm run relay:studio:self-test

This uses injected turns but the real queue, leases, worker processing, bounded stream spool, Context Store, approval-gated handoff, destination provenance, changelog and passive monitor. It verifies that the destination receives bounded approved context instead of the source's private result, that write:true is denied for Claude before the executor runs, and that unknown profiles are rejected. Use node bin/relay-studio-self-test.mjs --keep-state --json when the temporary state must be inspected after the run.

Relay Studio context map

For the complete architecture/document map used to discuss Relay Studio with another LLM, start at docs/relay-studio-chatgpt-context.md. It points to the requirements, RFC, ADR, backlog, visual artifacts, implementation boundaries and executable tests without duplicating those specifications.

License

MIT

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    Not graded
    quality
    A
    maintenance
    A transport-agnostic MCP seam for messages, context sharing, and task hand-off between humans, chat bots, and coding agents. Supports multiple backends like SQLite, Redis, Matrix, NATS, and XMPP to bridge Claude chat with Claude Code.
    40,616
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    MCP server for inter-agent communication. Gives multiple Claude Code sessions a shared message board, agent registry, and orchestration layer — backed by a cloud relay so agents can coordinate across machines, repos, and teams.
    8
    40
    MIT

View all related MCP servers

Related MCP Connectors

  • Durable agent-to-agent handoffs and shared scratchpad for multi-agent workflows.

  • Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.

  • Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.

View all MCP Connectors

Latest Blog Posts

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/brenoperucchi/mcp-agent-relay'

If you have feedback or need assistance with the MCP directory API, please join our Discord server