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.11.0 requires Node ≥ 20 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 Node 20+ 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 node ${CLAUDE_PLUGIN_ROOT}/dist/sidecar.js — the bundle ships inside the plugin, so there is no registry fetch and installing works offline. 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:

{ "mcpServers": { "agent-sidecar": { "command": "npx", "args": ["-y", "agent-sidecar"] } } }

On a locked-down network, npm i -g agent-sidecar once and use { "command": "agent-sidecar" } to skip the registry on every launch.

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 = "npx" · args = ["-y", "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.11.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

Two halves: a thin MCP server on stdio per agent session (facing your agent) and one canvas server per machine on localhost (facing your browser). The first session starts the canvas server as a detached process; every session after that attaches to the same one.

Your agent sessions one MCP client each, any project
agent-sidecar server sessions · artifact store · interaction queues · token
Browser canvas one tab, all sessions (SSE live)

The interesting part is the return path. A click in the browser POSTs to the webhook and lands in the interaction queue of the session that owns that artifact. The agent consumes its own 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 auth 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>&session=<id>&artifact_id=<id>"

agent-sidecar's server instructions teach the agent both patterns each session — with the real port, token, and session id 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

Every agent session — in any project, from any MCP client — appears as its own session on one canvas. The sidebar is a tree — repo, then worktree, then session (labelled with its git branch), then that session's artifacts nested beneath it — so the whole canvas is visible at once and any artifact is one click away; collapse any level you don't need. Artifacts and interaction queues are per session, so parallel sessions never read each other's answers.

The canvas follows your system light/dark preference, and the sun/moon control in the top bar pins a choice that's remembered per browser. Artifacts stay light in both themes — an artifact is the agent's own document, not part of the shell.

The stage has two modes, toggled in the top bar or with t: single shows one artifact at a time, timeline stacks every artifact of the session in one scroll for reviewing a whole session at once. Timeline cards size themselves to their content — the injected helper reports its own height, since a sandboxed frame can't be measured from the outside — and buttons work in every card, each attributed to its own artifact.

Grouping follows your checkouts rather than directory names: sessions in two git worktree checkouts of one repo nest under a single repo, keyed on the main worktree's git dir and named from the origin remote when there is one. The worktree level appears only when a repo has more than one checkout; a non-repo directory gets its own group, marked dir.

A session that ends stays on the canvas, dimmed, so you can still read what it produced — dismiss it with the × on its row. When an artifact arrives in a session you aren't looking at, that session is badged and a clickable toast appears; the view never jumps out from under an interaction you're in the middle of.

The server prefers port 8765 (override with SIDECAR_PORT) and scans upward if a foreign process holds it. It outlives your agent sessions and exits by itself after 30 minutes with no sessions and no canvas tab (SIDECAR_IDLE_EXIT_MS, 0 disables). Manage it directly:

npx agent-sidecar --status   # server, sessions, artifact counts
npx agent-sidecar --stop     # shut it down
npx agent-sidecar --serve    # run in the foreground (debugging)

Machine-wide state lives in ~/.agent-sidecar/ (override with SIDECAR_HOME):

FileContents
server.jsonServer coordinates: { pid, port, url, token, startedAt }, mode 0600. Kept (marked stoppedAt) when the server exits, so the next one reuses the token and old watcher URLs stay valid.
state.jsonEvery session's canvas contents, restored on restart so a server bounce — or a reboot — doesn't lose your artifacts.
server.logServer output. The server runs detached, so this is where its errors go.

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

FileContents
session.jsonHow external callers reach this project's session: { pid, serverPid, port, url, token, sessionId, startedAt }. Removed on clean exit.
interactions.jsonlAppend-only log of every interaction for this project (one JSON object per line, with seq, receivedAt, kind, sessionId, 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. Every /api/* endpoint requires the server token — via the X-Sidecar-Token header, a ?token= query param, or Authorization: Bearer.

EndpointAuthDescription
GET /The canvas UI (all sessions)
GET /eventsSSE stream: a snapshot of every session on connect, then session / session_removed and created / updated / removed (each carrying a sessionId)
GET /artifact/:idArtifact HTML with the claude.send() helper injected
POST /api/webhooktokenQueue a payload for an agent. JSON body { artifactId, payload } is recorded as an artifact interaction and routed to that artifact's session; anything else is recorded as an external webhook event, body forwarded as-is. Routing without an artifact: ?session=, else the session the canvas is showing, else the most recently active one
GET /api/waittokenLong-poll: returns the next interaction as JSON. ?session= picks the session (an unknown id is a 404, never another session's queue); ?artifact_id= filters; ?timeout=SECS caps the wait (then 408); no timeout means wait indefinitely
GET /api/draintokenReturns and clears everything queued for ?session= without blocking
GET /api/sessionstokenEvery session with its label, liveness, and artifacts
POST /api/shutdowntokenStop the server (what --stop calls)
POST /api/restarttokenHand the canvas to the newest version any session has registered from, reusing the same port. 409 when nothing newer is attached. This is what the canvas's update bar calls
GET /health{ ok, server, version, protocol, pid, port, idle, sessions, 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 a session — CI, scripts, other tools. Read the coordinates from .sidecar/session.json (or ~/.agent-sidecar/server.json):

url=$(jq -r .url .sidecar/session.json)
token=$(jq -r .token .sidecar/session.json)
sid=$(jq -r .sessionId .sidecar/session.json)  # omit to land on the session you're viewing
curl -X POST -H "X-Sidecar-Token: $token" \
  -d "build failed on main: https://ci.example.com/run/1234" \
  "$url/api/webhook?session=$sid"

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          # 55 end-to-end tests over real MCP stdio
bun run test:node # the same suite against the built bundle under node

Source lives in src/: sidecar.ts (CLI entry), server.ts (the singleton canvas server), client.ts (server discovery and the session link), mcp.ts (the tools), shared.ts (paths and types), and canvas.html (the browser shell, inlined into the bundle at build time). Tests run against an isolated server via SIDECAR_HOME and a random port, so they never touch the one you're using. 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 — duplicate tool sets, and whichever starts first owns the canvas server for both. Disable the plugin while developing, and --stop the server after switching versions.

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 — one tab covers every session.
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 --status and the browser console.
Can't see your sessionnpx agent-sidecar --status lists every session. Sessions are labelled by git branch, grouped by project.
Wrong portA foreign process holds 8765, so the server scanned upward. server.json and session.json always have the live URL.
403 from the APIMissing/stale token — re-read server.json. The token survives server restarts but not a deleted ~/.agent-sidecar/.
Server won't dienpx agent-sidecar --stop. It also self-exits after 30 idle minutes; ~/.agent-sidecar/server.log has its output.
Old version runningPlugin pins agent-sidecar@<version>. Update the plugin, restart Claude Code.