Architecture

Architecture: the helloscope Cloudflare + MCP design

A standalone reference for the Worker, its Durable Objects, the R2 archive, and the MCP surface — and why this is a genuinely unusual application of the Model Context Protocol.

Thesis

The Model Context Protocol almost always fronts a SaaS or productivity API: a ticket tracker, a calendar, a document store, a database. The agent calls a tool, the tool mutates or reads a business record, the agent moves on. helloscope uses MCP for something with no resemblance to that world. The same MCP endpoint does two things that do not exist elsewhere:

  1. It serves prepared binary knowledge, not raw bytes. Behind the archive_* tools is a content-addressed store of a real Linux glibc: per-function GNU-objdump disassembly, DWARF-mapped source lines, and classified call edges, keyed by GNU build-id and served byte-verbatim. An agent asking "what does puts actually execute, and which glibc source line is that?" gets a provenance-stamped, reproducible answer — not a raw ELF to parse itself.
  1. It lets a visiting agent drive a live visualization as a docent. The join_session / apply_state / play_step tools bind an MCP session to a browser tab showing an ELF Atlas room, and let the agent narrate an interactive scene in real time — applying scene states, writing captions into a transcript the human is watching, and answering the questions the human asks in their own agent chat.

Prepared binary artifacts over MCP, and an agent that narrates an interactive room as it happens: both are far from MCP's usual home, and both fall out of one small, authless, edge-served Cloudflare Worker.

Topology

One Worker (live/src/index.ts, deployed at helloscope-live.iunknown.workers.dev) is the entire backend. It is authless for readers, edge-served from every Cloudflare PoP, and sized to stay inside the Workers free tier — a deliberate constraint that shapes every decision below (most sharply the 10 ms/request CPU budget). Four route families share the single fetch handler, dispatched in this order:

HTTP / WS request Worker fetch() live/src/index.ts GET /ws?session=CODE SessionRoom DO per room code holds the page WS ⇅ the open room tab POST /mcp room tools archive_* · stateless McpSession DO per MCP session id drives the room rpc GET /archive/* R2 · ARCHIVE build-id keyed immutable objects everything else Static assets env.ASSETS.fetch live/public/
Request routing in the Worker — four route families share one fetch handler; hover a route.

Why these live where they do: the WebSocket and the room-driving tools need addressable, persistent per-session identity, so they get Durable Objects. The archive is immutable content, so it needs no session state at all and is served straight from R2 (and, for the tools, a per-isolate content cache) — putting it through a DO would only burn quota and latency.

The two Durable Objects

SessionRoom — one instance per room session code. It owns the browser tab's WebSocket and is the correlation point between the human's screen and the agent's tool calls. Three things make it the right tool:

McpSession — one instance per MCP session id (the UUID minted at initialize and echoed back in the Mcp-Session-Id header). Its only durable state is a single value: which room code this MCP session joined (ctx.storage.put("code", …)). On each room tool call it looks up its code and forwards to the matching SessionRoom via RPC. DELETE /mcp calls terminate(), which clears that binding.

Why Durable Objects and not KV or a global variable: the binding is live, per-session, and bidirectional. A global Map in the isolate would not survive across the many isolates Cloudflare spreads requests over, and could not hold an open WebSocket. KV is eventually consistent and write-latent — wrong for a socket handle and a request/response correlation table. A Durable Object is the one primitive that gives a single-threaded, addressable, strongly-consistent home for "the tab named ABC234" and "the MCP session named <uuid>". The DO's addressability by name is exactly what lets two unrelated HTTP connections (a browser's /ws and an agent's /mcp) rendezvous on the same session.

The R2 archive layer

The archive (contract in archive/schema.md) is a content-addressed store keyed by GNU build-id. The object layout mirrors 1:1 into R2 keys:

<build-id>/meta.json                  full manifest        (archive.meta/1)
<build-id>/lookup.json                compact symbol index (archive.lookup/1)
<build-id>/fn/<addr-hex>-<size>.json  one per function body (archive.fn/1)
<build-id>/src/<path>                 verbatim glibc source bytes
index.json                            catalogue of binaries (archive.index/1)

Two properties make this safe and cheap:

The economics close the loop: R2 has zero egress fees. An authless public tool that streams disassembly and source to any agent that asks has no per-download cost — the thing that would normally make a public binary archive financially dangerous simply is not billed.

The MCP surface

tools/list returns 10 tools[...TOOLS, ...ARCHIVE_TOOLS] — in two groups that share one endpoint:

6 room-driving tools (stateful, routed through the McpSession DO):

ToolRole
join_sessionBind this MCP session to the room code on the page. Required first.
get_agent_guideReturn the bundled wall text (state grammar, tour format). Needs no session.
get_stateThe page's current normalized scene state.
apply_stateApply a scene state (no beat). Normalizes illegal input, echoing the repaired state.
play_stepApply a state and append a narrated caption beat to the live transcript. Rejects illegal state verbatim, records nothing.
export_tourThe beats recorded this session, as a replayable tour JSON.

4 archive tools (stateless, answered in the fetch path, no join_session): archive_lookup (catalogue / one binary's summary), archive_function (windowed disassembly listing, default 200 rows / cap 500), archive_source (verbatim numbered glibc source, cap 400 lines/call), archive_callees (outgoing control-transfer edges).

Two design commitments run through the surface:

Why this is a novel use of MCP

Strip away the domain and the pattern is: an agent shows up as a museum visitor, brings its own tokens, and is handed a room. The room is four things at once, and MCP is what makes all four reachable through one interface:

That combination does not appear in the SaaS-productivity MCP canon. The usual MCP tool does work for the agent; here the agent does work for a human watching a screen, and the tool's job is to keep it honest against physical evidence. Reproducibility is first-class: content-addressing by build-id means the same question yields the same provenance-stamped answer forever, and the free-tier hard cap plus R2's zero egress make an authless public agent tool actually safe to run unattended.

The interface is model-agnostic by evidence, not assertion: the room and archive tools have been driven end-to-end by both Claude and Codex, each producing truthful tours from the same guide and the same tool contracts — a cross-model check that the honesty scaffolding lives in the tools and their descriptions, not in one model's habits.

What this enables next


Notes where behavior differs from live/README.md