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.
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"] } } }
| Agent | Where 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:
- The helper injected into artifacts is named
claude.send()regardless of which agent is driving. - The background-watcher pattern needs an agent that can run shell commands in the background and surface their output. The blocking
await_interactiontool works in every client.
"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).
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.
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):
| File | Contents |
|---|---|
session.json | This session's coordinates: { pid, port, url, token, startedAt }. How external callers discover the port and auth token. Removed on clean exit. |
artifacts.json | Canvas contents, restored on restart so a session bounce doesn't empty the canvas. |
interactions.jsonl | Append-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
| Param | Type | Description |
|---|---|---|
title | string, required | Shown in the canvas sidebar |
html | string, required | Complete, self-contained HTML document |
open | boolean | Open 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
| Param | Type | Description |
|---|---|---|
id | string, required | Id from create_artifact |
html | string, required | Replacement HTML document |
title | string | New 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
| Param | Type | Description |
|---|---|---|
artifact_id | string | Only accept interactions from this artifact; others stay queued |
timeout_seconds | number | How 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.
| Endpoint | Auth | Description |
|---|---|---|
GET / | — | The canvas UI |
GET /events | — | SSE stream: a snapshot event on connect, then created / updated / removed |
GET /artifact/:id | — | Artifact HTML with the claude.send() helper injected |
POST /api/webhook | token | Queue 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/wait | token | Long-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>
payloadis any JSON-serializable value; it arrives in the agent's context verbatim.- Resolves
trueonce the canvas shell confirms delivery; the shell also shows a "✓ Sent" toast. - Resolves
falseif delivery fails or times out (5s), or if the identical payload was sent within the last 1.5s — the built-in double-click debounce.
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:
- Inline everything. The opaque origin means same-origin API calls are blocked and cross-origin requests fail CORS. No CDNs, no external fonts — inline CSS and JS only.
- No storage.
localStorage/cookiesare unavailable. Keep state in JS variables; if it must survive, send it to the agent. claude.send()is the only output. Design every artifact around the payload you want back.
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
- Localhost only. The HTTP server binds
127.0.0.1; nothing off the machine can reach it. Don't port-forward or tunnel it — anything that reaches the webhook is eventually placed in front of the agent. - Per-session token. Without auth, any webpage you visit could fire cross-site POSTs at localhost and inject text into your session. Both
/api/*endpoints require a random 128-bit token generated at startup. Web pages can't read it (it lives insession.jsonand the canvas shell); local processes can, and are trusted anyway. - Sandboxed artifacts. Artifact HTML is authored by the agent, which makes it semi-trusted — a prompt-injected artifact shouldn't get free rein. The opaque-origin sandbox denies it the token, the canvas DOM, storage, and same-origin network; the canvas shell only accepts messages from the artifact iframe it created.
- Interactions are user input. agent-sidecar instructs the agent to treat payloads as coming from you. Keep that in mind if you point external systems at the webhook: whatever they post is read by the agent with that framing.
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
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
| Symptom | Check |
|---|---|
| Plugin doesn't load | /mcp shows agent-sidecar? Is bun on PATH? First launch fetches from npm — needs network once. |
| Canvas didn't open | The URL is in every create_artifact result and in .sidecar/session.json. Auto-open only fires when no tab is connected. |
| Clicks not arriving | tail -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 port | Another session holds 8765; this one fell back to an ephemeral port. session.json always has the live URL. |
| 403 from the API | Missing/stale token — re-read session.json; the token rotates every session. |
| Old version running | Plugin pins agent-sidecar@<version>. Update the plugin, restart Claude Code. |