agent-sidecar

Documentation

Everything about agent-sidecar: how the canvas works, the MCP tools your agent calls, the HTTP API underneath, and how to write artifacts that talk back.

version 0.8.0 requires Bun ≥ 1.2 license MIT localhost only

Installation

agent-sidecar is a Claude Code plugin (and a standard MCP server usable from other agents). The repo doubles as its own plugin marketplace, and the server itself ships as a self-contained bundle on npm. The only prerequisite is Bun on your PATH.

# in any Claude Code session
 /plugin marketplace add smarchetti/agent-sidecar
 /plugin install agent-sidecar@agent-sidecar

Restart Claude Code (or /reload-plugins). The plugin launches bunx agent-sidecar@0.8.0 — the first run fetches the package from npm and caches it; after that startup is instant. agent-sidecar is then available in every project.

Verify with /mcp: you should see agent-sidecar listed as connected.

Other agents

agent-sidecar is a standard MCP stdio server — the plugin is just Claude Code packaging. Any MCP client can run it with this one entry (Bun on PATH is still the only prerequisite):

{ "mcpServers": { "agent-sidecar": { "command": "bunx", "args": ["agent-sidecar"] } } }
AgentWhere to put it
Cursor.cursor/mcp.json in the project, or ~/.cursor/mcp.json globally
VS Code (Copilot).vscode/mcp.json — same entry, but under a "servers" key instead of "mcpServers"
Codex CLI~/.codex/config.toml:
[mcp_servers.agent-sidecar] · command = "bunx" · args = ["agent-sidecar"]
Gemini CLI~/.gemini/settings.json under "mcpServers"
Windsurf~/.codeium/windsurf/mcp_config.json under "mcpServers"

Everything transfers: the tools, the canvas, token auth, the .sidecar/ session files. Two Claude-flavored details to be aware of:

Pin the version outside Claude Code too if you want reproducible behavior: "args": ["agent-sidecar@0.8.0"].

Your first artifact

You don't call agent-sidecar directly — your agent does. Ask for something visual:

 show me three layout options for the settings screen on the canvas

The agent creates an artifact (a complete HTML document), your browser opens the canvas, and the artifact renders live. When the artifact contains buttons wired to claude.send(...), clicking one delivers the payload back into the conversation, and the agent continues with your choice.

That's the whole product: the agent shows, you click, the agent continues.

Architecture

agent-sidecar is one process with two faces: an MCP server on stdio (facing your agent) and an HTTP server on localhost (facing your browser).

Your agent spawns the server, calls MCP tools
agent-sidecar artifact store · interaction queue · session token
Browser canvas renders artifacts (SSE live), forwards clicks

The interesting part is the return path. A click in the browser POSTs to the webhook and lands in an in-memory queue. The agent consumes the queue in one of two ways (below) — in both cases the payload arrives as ordinary tool/command output, which is why agent-sidecar works without any push mechanism (like Claude Code channels) being available.

Artifacts render inside a sandboxed iframe with an opaque origin. Artifact code can't reach the canvas shell, the session token, or the server; its only output channel is claude.send(), which crosses a postMessage bridge that the canvas shell validates and forwards with the token.

Receiving input: two patterns

1. Blocking tool — quick decisions

The agent calls await_interaction right after showing choices. The tool call parks until you click (or times out after up to 120s, in which case the agent simply calls it again). Best when a response is expected within seconds — the answer lands in the same turn.

2. Background watcher — long waits

The agent starts a background shell task against the long-poll endpoint and keeps working. When you eventually click, the watcher exits and the agent is re-invoked with the payload:

curl -s "http://127.0.0.1:<port>/api/wait?token=<token>&artifact_id=<id>"

agent-sidecar's server instructions teach the agent both patterns each session — with the real port and token baked in — plus the rule of thumb: block for quick picks, watch in the background when the user may take minutes.

Why artifact_id matters: both consumers accept an artifact filter. Passing it means a stale click on some older artifact can't be mistaken for the answer to the current question — non-matching interactions stay queued.

Sessions & files

Each agent session runs its own agent-sidecar. The server prefers port 8765 (override with the SIDECAR_PORT env var); if another session already holds it, it falls back to an ephemeral port instead of crashing.

Per-project state lives in .sidecar/ under the directory your agent runs in (add it to .gitignore):

FileContents
session.jsonThis session's coordinates: { pid, port, url, token, startedAt }. How external callers discover the port and auth token. Removed on clean exit.
artifacts.jsonCanvas contents, restored on restart so a session bounce doesn't empty the canvas.
interactions.jsonlAppend-only log of every interaction (one JSON object per line, with seq, receivedAt, kind, artifactId, payload). Rotates to .old at 5 MB. tail -f it to watch the loop live.

MCP tools

Six tools, prefixed mcp__plugin_agent-sidecar_agent-sidecar__ when installed as a plugin.

create_artifact

ParamTypeDescription
titlestring, requiredShown in the canvas sidebar
htmlstring, requiredComplete, self-contained HTML document
openbooleanOpen the browser if no canvas tab is connected (default true)

Returns the artifact id and canvas URL. New artifacts take focus on connected tabs automatically.

update_artifact

ParamTypeDescription
idstring, requiredId from create_artifact
htmlstring, requiredReplacement HTML document
titlestringNew title (optional)

Connected tabs hot-reload the artifact. Prefer updating over creating a new artifact when iterating on feedback — it keeps the sidebar clean and the user's focus in place.

await_interaction

ParamTypeDescription
artifact_idstringOnly accept interactions from this artifact; others stay queued
timeout_secondsnumberHow long to block (default 25, max 120)

Returns the oldest matching queued interaction immediately if one exists, otherwise blocks. On success:

status=received
[2026-07-05T00:04:22.597Z] from artifact a1-xq5zj ("Plugin install check"):
{"install":"confirmed"}

On timeout it returns status=no_response — the caller just calls again to keep waiting.

get_interactions

No parameters. Drains all queued interactions without blocking — useful for catching clicks that happened while the agent was doing other work.

list_artifacts / remove_artifact

list_artifacts lists id, title, and timestamps for everything on the canvas. remove_artifact takes an id and removes it; connected tabs move to the newest remaining artifact.

HTTP API

Bound to 127.0.0.1 only. The two /api/* endpoints require the session token — via the X-Sidecar-Token header, a ?token= query param, or Authorization: Bearer.

EndpointAuthDescription
GET /The canvas UI
GET /eventsSSE stream: a snapshot event on connect, then created / updated / removed
GET /artifact/:idArtifact HTML with the claude.send() helper injected
POST /api/webhooktokenQueue a payload for the agent. JSON body { artifactId, payload } is recorded as an artifact interaction; anything else is recorded as an external webhook event, body forwarded as-is
GET /api/waittokenLong-poll: returns the next interaction as JSON. ?artifact_id= filters; ?timeout=SECS caps the wait (then 408); no timeout means wait indefinitely
GET /health{ ok, artifacts, canvasTabs, queuedInteractions }

claude.send()

Every artifact gets a claude global injected before its own scripts run. One method:

claude.send(payload)  // → Promise<boolean>

Under the hood the sandboxed artifact posts a message to the canvas shell, which attaches the session token and forwards to /api/webhook. Artifact code never handles the token.

Writing artifacts

Artifacts are complete HTML documents rendered in a sandboxed iframe (allow-scripts allow-forms allow-popups, opaque origin). Practical rules:

Pattern: choice cards

<button onclick="claude.send({ choice: 'option-a' })">Choose A</button>
<button onclick="claude.send({ choice: 'option-b' })">Choose B</button>

Pattern: form submission

<form onsubmit="event.preventDefault();
  claude.send(Object.fromEntries(new FormData(event.target)))">
  <input name="title" /> <textarea name="notes"></textarea>
  <button>Send</button>
</form>

Pattern: approve / iterate bar

claude.send({ verdict: 'approve' })
claude.send({ verdict: 'iterate', notes: field.value })

Give the user visual confirmation on top of the toast — flip the clicked button's label, highlight the selected card — and structure payloads as small objects ({ choice, notes }) rather than bare strings, so the receiving side stays unambiguous.

External webhooks

Anything that can POST can push events into the session — CI, scripts, other tools. Read the session coordinates from .sidecar/session.json:

url=$(jq -r .url .sidecar/session.json)
token=$(jq -r .token .sidecar/session.json)
curl -X POST -H "X-Sidecar-Token: $token" \
  -d "build failed on main: https://ci.example.com/run/1234" \
  "$url/api/webhook"

Non-artifact payloads are tagged kind: "webhook" and reach the agent through the same queue — a waiting await_interaction (without an artifact filter) or /api/wait picks them up.

Security model

Development

git clone https://github.com/smarchetti/agent-sidecar
cd agent-sidecar
bun install
bun test          # 24 end-to-end tests over real MCP stdio

Source lives in src/ (sidecar.ts is the whole server; canvas.html is the browser shell, inlined into the bundle at build time). To run your working copy in a live session:

claude --mcp-config dev.mcp.json
Two sidecars warning: if the plugin is installed globally, a dev session loads both the plugin server and your working copy. Ports won't collide (ephemeral fallback), but you'll have duplicate tool sets — disable the plugin while developing.

Releasing: bump version in package.json and the agent-sidecar@<version> pin in .claude-plugin/plugin.json, then npm publish (the prepublishOnly hook rebuilds dist/sidecar.js). Publish before pushing the manifest, so it never points at a version that doesn't exist.

Troubleshooting

SymptomCheck
Plugin doesn't load/mcp shows agent-sidecar? Is bun on PATH? First launch fetches from npm — needs network once.
Canvas didn't openThe URL is in every create_artifact result and in .sidecar/session.json. Auto-open only fires when no tab is connected.
Clicks not arrivingtail -f .sidecar/interactions.jsonl — if clicks appear there, the agent just hasn't consumed them yet (get_interactions drains). If not, check GET /health and the browser console.
Wrong portAnother session holds 8765; this one fell back to an ephemeral port. session.json always has the live URL.
403 from the APIMissing/stale token — re-read session.json; the token rotates every session.
Old version runningPlugin pins agent-sidecar@<version>. Update the plugin, restart Claude Code.