feat(agent-bus): MCP server on the local bus - #63
Open
AUDOSt0ck1ng wants to merge 6 commits into
Open
Conversation
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>
Collaborator
Author
|
Demo: voice issue is not checked yet... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/commandand/v1/socket, not a wrapper: the tools call the samedispatch, 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
Originmeasurements, 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_animationdefaults to playing once. The bus defaults toselect, which persists toconfig.yaml. That default is right for the menus and wrong for an agent, and this layer only serves agents;persist: trueis the opt-in.audio.sourceis 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.animationstays 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.enabledandrequireTokengovern/mcpand/v1/*together. The panel gained the endpoint and Copy MCP URL beside the existing Copy token, in one block, rather than aclaude mcp addline that would presume which client you use.@modelcontextprotocol/sdkis pinned exactly. It decides how/mcpbehaves 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 calllist_stage— rather than a protocol error, because the model is the one who has to fix it.Testing
oncedefault, label resolution, each refusal path, the token gate, the405forGET/DELETE, theOriginrefusal, the parse error. Full suite 146 passing; lint and build clean.claude mcp add --transport httpwith a Bearer header — connected, tools listed./mcpreuses the bus's existingrefuse()gate —Host,Origin, constant-time token — before routing, rather than introducing a parallel one. No new sink.Known limit
Clients that send an
Originheader 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