Skip to content

feat(agent-bus): MCP server on the local bus - #63

Open
AUDOSt0ck1ng wants to merge 6 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feature/mcp-server-61
Open

feat(agent-bus): MCP server on the local bus#63
AUDOSt0ck1ng wants to merge 6 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feature/mcp-server-61

Conversation

@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator

The local agent bus now answers Streamable HTTP MCP at /mcp, so an agent in an editor can drive the stage without hand-rolling HTTP calls and token headers.

A peer of /v1/command and /v1/socket, not a wrapper: the tools call the same dispatch, resolve against the same catalog the window reports, and come back with the same refusals. In the Electron main process rather than a sidecar, which keeps the client registration a URL and makes the endpoint exist exactly while AVATAR does.

The reasoning behind each choice — packaging, the Origin measurements, why the tool names are not a 1:1 mirror of the HTTP paths — is in this comment on #61, along with corrections to the issue body. What follows is what to look at here.

Worth reviewing

  • play_animation defaults to playing once. The bus defaults to select, which persists to config.yaml. That default is right for the menus and wrong for an agent, and this layer only serves agents; persist: true is the opt-in.
  • audio.source is not exposed. It is the user's lip-sync setting. One fewer tool is one fewer thing for a model to reach for by mistake.
  • animation stays an open string, validated against the live catalog at call time — a custom folder derives ids from file paths, so the set changes while the app runs and a client with a stale tool list still has to work.
  • No new settings. enabled and requireToken govern /mcp and /v1/* together. The panel gained the endpoint and Copy MCP URL beside the existing Copy token, in one block, rather than a claude mcp add line that would presume which client you use.
  • @modelcontextprotocol/sdk is pinned exactly. It decides how /mcp behaves on the wire; a minor release should not change that after the endpoint has been tested against real clients.

Tools: list_stage, get_status, play_animation, stop_animation, set_avatar, set_environment. A refused command is a tool error carrying the way out — an unknown id says to call list_stage — rather than a protocol error, because the model is the one who has to fix it.

Testing

  • 15 new tests driving a real SDK client against a real server, not a mock: the tool surface, the once default, label resolution, each refusal path, the token gate, the 405 for GET/DELETE, the Origin refusal, the parse error. Full suite 146 passing; lint and build clean.
  • Smoke-tested against a live client: claude mcp add --transport http with a Bearer header — connected, tools listed.
  • Security-reviewed: /mcp reuses the bus's existing refuse() gate — Host, Origin, constant-time token — before routing, rather than introducing a parallel one. No new sink.

Known limit

Clients that send an Origin header are refused, like every other route here. Command-line clients send none; a webview-based desktop client may, and is not supported in this version. Documented in Local agent bus, which also gained the MCP section.

Refs #61, #6

🤖 Generated with Claude Code

AUDOSt0ck1ng and others added 6 commits August 21, 2026 19:25
The loopback bus now answers Streamable HTTP MCP at /mcp, so an agent in an
editor can drive the stage without hand-rolling HTTP calls.

A peer of /v1/command and /v1/socket rather than a wrapper: the tools call the
same dispatch, resolve against the same catalog the window reports, and come
back with the same refusals. In the Electron main process rather than a spawned
sidecar, which is what keeps the registration a URL - no absolute path into an
install directory the user can change, no unpacking out of the asar, no
assumption that Node is installed - and what makes the endpoint exist exactly
while AVATAR does.

No new settings: Enable local bus and Require token govern /mcp and /v1/*
together, and Copy MCP setup hands over the whole registration line, since only
the app knows the port it bound and the token.

The tool surface is not a mirror of the HTTP API. It is read by a model, so it
is smaller - the audio source stays out as a user setting - and play_animation
defaults to playing once instead of the bus default of select, with persist as
the deliberate opt-in that writes to config.yaml. animation stays an open string
validated against the live catalog at call time, because a custom folder changes
its ids while the app runs and a client holding a stale tool list still has to
work. A refused command is a tool error carrying the way out rather than a
protocol error, because the model is the one who has to fix it.

Stateless: one server per request, nothing pushed, nothing left behind by a
client that is killed. GET and DELETE are answered 405 for clients written
against revisions before 2026-07-28.

The Origin refusal is unchanged and now documented as a limit. Measured before
deciding: Claude Code, the official TypeScript SDK, and the Codex CLI all send
no Origin header; a webview-based desktop client may, and is not supported here.

Refs ARPAHLS#61, #6

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 500 fallback matched `request.url` with startsWith, while routing matched
`url.pathname` exactly, so a throw while handling /mcpfoo would have answered in
JSON-RPC shape for a request that never reached the MCP route. Both now ask the
same predicate.

Refs ARPAHLS#61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy MCP setup handed over a `claude mcp add` line, which only helps people
using that client. Every MCP client spells registration differently, so the
panel now shows the endpoint itself with Copy MCP URL, beside the Copy token
that was already there and already hidden when Require token is off - the two
values every client needs, and nothing that presumes which one you use.

Refs ARPAHLS#61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MCP section had its own heading and had to point at "the token above",
which is the layout admitting the split was wrong: one server, one switch, one
token, and two ways to reach it. The endpoint now sits with the token, and the
two copy actions share a row.

Stacked rather than side by side - "Copy example curl" wraps inside a
half-width button in a drawer this narrow, and the label is worth more than the
row.

Refs ARPAHLS#61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs ARPAHLS#61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SDK decides how /mcp behaves on the wire, so a caret range would let a
minor release change the endpoint's observable behaviour without anyone
choosing it - after it had been tested against real clients. Upgrading should
be a deliberate edit. zod keeps its range; it only shapes tool inputs.

Refs ARPAHLS#61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AUDOSt0ck1ng

AUDOSt0ck1ng commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Demo:
https://www.youtube.com/watch?v=52-srVCIh7M

voice issue is not checked yet...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant