MCP server: drive Nexus as a tool #140
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Publish Nexus's control plane as an MCP server, so any MCP client — Claude on
the operator's phone or desktop, or a session running inside another Nexus
worker — can create workspaces and sessions, message sessions, tear them down,
and ask about fleet state.
Blocked on:
docs/superpowers/specs/2026-09-11-vault-envelope-encryption-design.md.Only two of the tools below need that work; the rest could ship first if we
choose to split. See Vault dependency.
This issue records the decisions and the measurements behind them so a spec can
be derived without redoing the research. Everything under
Verified by probe was measured against claude-code
2.1.268 inside a Nexus worker, not read off the docs.
The framing
The distinction that matters is not where the client runs, it is who starts
the conversation:
sessions, start session 2 on the auth bug." An ordinary MCP server. This
issue.
operator approved your preview." A different mechanism
(below), not a prerequisite.
"Fleet coordination" dissolves into the first: a session inside a worker is
just another MCP client holding a credential. There is one server, not two.
Tool surface
list_workspaces/get_workspacelistWorkerscreate_workspacespawnWorkerstart_workspacestartWorkerstop_workspacestopWorkerdelete_workspaceremoveWorkerlist_sessions/get_sessioncreate_session(workspace, name, initial_message?)createSessiondelete_sessiondeleteSessionsend_message(session, text)get_nexus_info/api/state+ quota + attention roll-upget_nexus_infoshould include the fleet-wide attention roll-up. Thederivation in
nexus/src/lib/attention.tsis client-side today and only everpaints the web UI; exposing it is what lets a remote client answer "is anything
waiting on me?" without opening Nexus. Note its three inputs — roster row,
health probe, and whether the workspace is running at all (AGENTS.md fact
#26); a server-side roll-up must not drop the third.
Decisions already taken
initial_messageis a launch argument, not a nudgeclaudeaccepts the prompt as a positional argument. Socreate_session(..., initial_message)appends it inbuildLaunchCommandand itbecomes the session's first turn — no
tmux send-keys, no window resolution, norace against startup.
Trap: several claude flags are variadic (
--channels <servers...>,--dangerously-load-development-channels <servers...>) and will silently eat atrailing positional prompt. This was hit during the probe: the session died with
Input must be provided either through stdin or as a prompt argument. Auditargument order in
buildLaunchCommandand pin it with a test.Second trap found the same way: piping claude's stdout (e.g.
| tee) flips itinto
--printmode.send_messageuses the inbox socket, notsendNudgeEvery session exports
CLAUDE_CODE_MESSAGING_SOCKETandCLAUDE_CODE_MESSAGING_TOKEN. Measured inside one worker:A message delivered there arrives as a cross-session message, which per the
docs "never counts as your consent, so it can't answer a pending permission
prompt on your behalf."
sendNudgetypes raw text into the PTY and has no suchguarantee — the hazard its own comment in
nexus/src/lib/server/artifacts/tmux-nudge.tsalready describes.Today that gap is tolerable because the only sender is the operator approving
their own artifact feedback. If multi-user lands it becomes a hole: user B's
send_messagecould answer a permission dialog in user A's session. The socketis the option that survives the planned auth change.
Integration details for the spec:
docker exec, so it is not an "own-child"sender; the message goes through the receiving session's
crossSessionInboundcontrols.
defaultMode: auto, which counts as prompting, so messagesare delivered. A session running
bypassPermissionswould hold themfor approval instead. Pin this in a test rather than discovering it later.
{"type":"auth","token":"<CLAUDE_CODE_MESSAGING_TOKEN>"}as thefirst line (optional on Linux, required on Windows). Open the connection only
when the payload is ready — claude closes a connection idle for 30s.
sendNudgestays where it is; artifact feedback submission is not in scopehere. Whether it should also migrate is a follow-up.
Do NOT build a mailbox or session-to-session messaging
Claude Code already does it, and Nexus made its own fleet addressable without
meaning to.
ListAgentsfrom inside a worker returned 28 peers — siblingsessions in the same container over a Unix socket, plus sessions in other
Nexus workers via Remote Control, listed under Nexus's own
--remote-control "<workspace> · <name>"naming:A Nexus mailbox would be a worse copy: the upstream one has inbound controls,
loop throttling, burst caps, a 100-message hold queue, and the no-consent
guarantee above. Note the container boundary — same-container peers reach each
other by socket, cross-container only via Remote Control.
Rapid succession needs a serialization guard
createSessionshells out togit -C /workspace worktree addwith noserialization. Concurrent adds contend on
index.lock. There is a retry-oncerecovery path and it is safely scoped to each session's own name (it will not
delete another session's checkout), so the failure mode is flakiness, not data
loss — but two simultaneous creates can both land in it. Serialize creates per
workspace.
The optimistic
status: 'creating'row is inserted before the slow worktree andtmux work, so a tool call can return promptly and let the client poll.
Vault dependency
spawnWorker(db, docker, config, masterKey, input)decrypts the provider tokento build the authenticated clone URL;
startWorkerre-injects secrets. Bothneed the vault key, which by design exists only in the operator's unlocked web
session, and a bearer token deliberately never sets
locals.masterKey(AGENTS.md fact #24).
Rejected resolutions, and why:
is more than one user — whose key, with whose permissions? Throwaway work.
unlocktool taking the passphrase. Same ambiguity, and it drags thecredential through an MCP client's tool-call path and logs.
Chosen: envelope encryption first. One DEK seals the data; each principal
holds a wrapped copy. A machine principal is then just another wrapped DEK, and
multi-user is a row insert rather than a redesign. That is the linked spec.
A vault-bearing credential must be a separate credential class from the
existing GET-only
nxs_tokens, which must not silently acquire it.Auth and transport notes
/api/mcp.event.route.id, neverevent.url.pathname— AGENTS.mdfact #23, and a security requirement rather than a style rule. Whatever is
added must follow it.
tokenMay's GET-only rule does not cover it. That ruleis load-bearing today (
/api/workers/[id]/sessionsPOST has nomasterKeyguard of its own). A write-capable MCP surface needs its own scope model, and
must not be reached by widening
TOKEN_ROUTES.terminal/upgrade.tsandnever sees
handle. Anything added to the gate does not apply there.Out of scope: channels
Separately proven to work, and worth its own issue later. A channel is an MCP
server that pushes events into a running session — the inverse direction of this
issue. Measured end to end on 2.1.268 with a 91-line dependency-free Node stdio
server (no SDK, no Bun):
← nexusprobe: NEXUSPROBE-EVENT-42….server received
{request_id:"tdpor", tool_name:"Bash", description, input_preview}→ server replied
behavior:"allow"→ dialog closed and the command ran.instruction."
Why it is not this issue:
--channelsaccepts onlyplugin:name@marketplace; a bare.mcp.jsonentryis refused with
server: entries need --dangerously-load-development-channels.convergeSessionsrelaunches sessions unattended, so every restore would have to answer it —
most likely by
tmux send-keys Enter, the exact hack channels would beadopted to retire.
CLAUDE_CODE_AUTO_UPDATE=1, so a rename breaks the whole fleet at once.allowedChannelPlugins) is honoured on apersonal Max account, which is the only route to a modal-free
--channels.Testing it means writing
/etc/claude-code/managed-settings.json, which ismachine-wide and affects every session in a shared worker.
Verified by probe
Measured on claude-code 2.1.268, not taken from documentation:
--channelsand--dangerously-load-development-channelsexist but areabsent from
--help; a bogus flag reportsunknown option, these reportargument missing.Negotiated protocol revision
2025-11-25./tmp/cc-socks/.ListAgentsfrom a worker sees sibling sessions locally and other Nexusworkers' sessions via Remote Control.
spawnWorkerandstartWorkertakemasterKey;createSession,deleteSession,stopWorkerandremoveWorkerdo not.MCP server: drive Nexus as a tool (blocked on vault envelope encryption)to MCP server: drive Nexus as a tool