Platform architecture · meta-artifact

protocontent.com

A cross-agent MCP server that turns the HTML (and other artifacts) any agent generates into instantly shareable, sandboxed links — plus one live page per session you can watch from your phone.

Status · draft for build Date · Jun 5, 2026 Stack · MCP + Cloudflare edge Domains · protocontent.com control · protocontent.app content
This document is itself the product: a self-contained .html file an agent generated — exactly the kind of artifact protocontent exists to host.

01 The problem

The agent makes great HTML. You can't open it.

Agents now emit single-file HTML constantly — plans, dashboards, diffs, reports. The format is right (see §02). The distribution is broken. The artifact lands as a local file:// path, and a path is not a link. Three concrete failure modes:

☁️ Cloud agents

A remote/cloud session writes the file onto a machine you'll never touch. There is no localhost to open and nothing to scp. The artifact is stranded.

🌳 Worktrees

Locally it sometimes opens — but the HTML is an ephemeral plan, not source. You don't want to git add a throwaway, so it lives awkwardly outside version control.

📱 Mobile / remote

Driving an agent from your phone, the result won't open: a desktop file path means nothing on mobile. You need a URL you can tap.

Every path leads to the same primitive: a real URL, hosted off your machine — and, importantly, on an origin that can't reach back into your stuff.

02 Why HTML

HTML is the unreasonably effective agent output format

The premise comes from Thariq Shihipar's "The Unreasonable Effectiveness of HTML". The argument, compressed:

Information density

Tables, CSS, SVG, scripts, interactions — "there is almost no set of information that Claude can read that you cannot efficiently represent with HTML."

Humans actually read it

Long Markdown goes unread; HTML's nav, tabs and illustrations make a large document legible. Two-way controls (sliders, toggles) beat static text.

Single file, no build

Every artifact is one self-contained .html, purpose-built and disposable — generated by simply asking for "an HTML file."

Sharing is the point

"As long as you upload the HTML file, you can share the link easily." That "as long as you upload it" is the unsolved step.

"Markdown files are fairly hard to share… As long as you upload the HTML file, you can share the link easily. Your colleagues can open it wherever they wish." — Thariq Shihipar, The Unreasonable Effectiveness of HTML

The post nails the generate half and leaves distribute as a hand-wave: "as long as you upload it…" — but never says how. protocontent is that missing verb.

03 The concept

write_file, but the file lands at a URL

Your agent already writes files. protocontent is the same primitive — write a file — except it lands at a shareable URL instead of in your repo. It's the home for everything an agent produces that isn't meant to be committed: plans, prototypes, dashboards, screenshots, generated images, scratch notes. We start with HTML because it's the richest format, but the model is content-agnostic — a remote scratch filesystem for the agent's non-repo output. Three properties define it:

🔗 Always a URL

Server-side hosting, not a tunnel. Works identically from a laptop, a cloud agent, or your phone, because the bytes are pushed to the host.

⏳ Ephemeral by default

Pages auto-expire (TTL). They're plans and scratch artifacts, not products. Opt-in to "claim" one that earns permanence.

🛡️ Walled off

Served from an isolated content origin so published HTML can never touch your session, cookies, or repo. "Off the URL," by design.

Mental model: publish(file, name) is a remote write_file — cheap, idempotent, throwaway. Call it again with the same name to overwrite; many files accumulate in the session's one live directory page (§10).

04 Architecture

Two planes, one push — all on Cloudflare
ANY AGENT (local · cloud · mobile) Agent + protocontent holds the .html calls publish() CONTROL · mcp.protocontent.com MCP server · Workers authz · name · TTL · token stdio + HTTP transport R2 + D1 projects · spaces · files CONTENT · *.protocontent.app Workers + Durable Obj random subdomain/name CSP · noindex · live push Viewer (browser) phone · teammate · you ① push html ② store ③ read ④ GET + live ⑤ returns link → shared anywhere

The load-bearing idea is : the agent pushes the HTML to the control plane. Nothing depends on the agent's machine being reachable — which is precisely why cloud and mobile work. The control plane never serves the artifact itself; it writes to storage, and a separate content origin renders it and pushes live updates to viewers (§08, §10).

05 Publish flow

What one publish() call looks like

tool call · from the agent

// Agent calls the MCP tool with the artifact it just generated
publish_html({
  path:  "./plan.html",                     // bridge reads the file (or a folder) — no blob in context
  name:  "plan",                            // optional, readable; becomes the path
  ttl:   "7d"                              // optional, default 7d
})

response · back to the agent (and the human)

{
  "url":        "https://amber-canyon-7f3.protocontent.app/plan",
  "space_url":  "https://amber-canyon-7f3.protocontent.app/",   // the live session page
  "markdown":   "[plan ↗](https://amber-canyon-7f3.protocontent.app/plan)",
  "edit_token": "et_9f3a…",   // re-publish same name to update in place
  "expires_at": "2026-06-12T20:00:00Z"
}

Re-running publish_html with the same name overwrites the page at the same URL — the "it just gets updated" behavior. The agent surfaces the pre-formatted markdown link inline (a tappable title where the UI renders it, a clean …/plan URL where it doesn't), and drops space_url into its closing summary.

06 Key decisions

The choices that define the system — tap to expand rationale
D1 Push the bytes — never tunnel Store HTML server-side; do not expose a local server.
WhyThis is the decision that makes cloud + mobile work at all. Tunnels (ngrok, localtunnel, Cloudflare Tunnel) expose a live local process: they fail from cloud agents, die when the laptop sleeps, and leak the local machine.
Options
  • Push HTML string/file to a hosted store chosen
  • Tunnel localhost (ngrok-style) — breaks the core use cases
  • Commit to a gh-pages branch — defeats "ephemeral, uncommitted"
D2 Isolated content origin Artifacts render on a content origin, never on the control-plane origin.
WhyThe "off the URL so you can't access your shit" instinct is exactly the same-origin boundary. Untrusted agent HTML sharing an origin with anything that holds your identity = stored XSS with full session access.
ShippedArtifacts serve on random subdomains of *.protocontent.app — a separate registrable domain from the control plane (api./mcp.protocontent.com), which sets no cookies and uses bearer-token auth. Different site → no cookie/storage crossover. Sandboxed serving (CSP + noindex) on top.
RemainingLegacy *.protocontent.com artifact links 301→.app. Last step for full isolation: submit protocontent.app to the Public Suffix List so each artifact subdomain is its own site — the claudeusercontent.com / githubusercontent.com pattern.
D3 Ephemeral by default, claim to keep Default TTL ~7 days; opt-in persistence.
WhyThese are plans and scratch artifacts. Auto-expiry keeps the namespace clean, bounds storage cost, and matches the throwaway spirit of the format. Borrowed from Netlify Drop (24h unless claimed) and tmpfiles.
HowTTL per call (1h…30d) stored in D1; a Cron Trigger Worker sweeps expired blobs (R2) + rows (D1). A keep clears the expiry. Expired pages 410, then GC.
D4 Capability URLs, no viewer account Unguessable link = access. Publisher authenticates; viewer doesn't.
WhyFrictionless sharing is the whole value — a teammate or your phone opens the link with zero signup. The random high-entropy subdomain is the capability. Publisher side is keyed per MCP install.
DetailThe session page exposes every artifact, so it's private-by-default (token) even while individual artifacts ride on capability URLs. noindex everywhere so nothing hits search.
D5 Co-located bridge primary, remote MCP as fallback The npx bridge runs wherever the agent runs; remote HTTP is the degraded path.
WhyResearch settled this: only a process co-located with the agent can (a) read the files to publish and (b) mint a stable per-thread space id — a remote server can do neither (no MCP primitive reads the client's disk; Claude Code doesn't echo the session id). The npx protocontent bridge runs in the agent's environment, local or cloud VM alike, so the cloud case works as long as the agent can spawn an stdio MCP server.
HowBridge = the stdio MCP server the agent talks to; it reads paths and calls the Worker's HTTP API. A hosted remote McpAgent endpoint stays available for agents that only accept a URL — but inline-content only, weaker session scoping.
D6 A session is a directory of files Files are the unit; an artifact can be one file or several with relative links.
WhyAgents rarely collapse everything into one blob — a page pulls in a stylesheet, a screenshot, a data file, a second page. Forcing single-file inlining fights how agents actually write. So the data model is a directory per session, and publish writes a file (or a set) into it; relative links between them just work.
MVPServe individual files first (a single .html, standalone images) for speed — but store them as files-in-a-space from day one, so multi-file and publish_folder are a small extension, not a re-architecture.
D7 All-Cloudflare backend Workers + R2 + D1 + Durable Objects + Cron — one vendor, great DX.
WhyThis problem's exact shape — wildcard-subdomain edge serving, blob storage, live push, scheduled GC — is what Cloudflare's primitives are built for. One bill, one Wrangler workflow, generous free tiers, header-level CSP control.
vs VercelVercel's DX is excellent for deploying apps, but its functions are short-lived — persistent WebSocket "live" push needs an add-on, and per-subdomain untrusted-content serving cuts against its grain. Durable Objects + wildcard Workers fit the two hardest parts natively. Both are agent-buildable; Cloudflare ships official remote-MCP-on-Workers templates that match this almost exactly.
NoteCSP must be set via HTTP headers from the Worker, not a <meta> tag — meta-only CSP inside an iframe is escapable. Full mapping in §11.

07 Tool API

A surface small enough to fit in your head
ToolArgsReturnsPurpose
publish_htmlpath|content, name?, ttl? url, space_url, markdown, edit_token, expires_atWrite a file (bridge reads the path). The 95% tool.
publish_folderdir, entry?, ttl?url, space_url, markdownWrite a multi-file artifact (relative links).
list[{url, name, expires_at}]What's live in this thread's space.
historyname[{version, at, url}]Past versions of an artifact.
unpublishname|url, edit_tokenokKill an artifact now.
keepname, edit_tokenexpires_at:nullPromote to durable.

URL scheme — try it

A random subdomain is the session (and the capability); the readable name is just the path. Drag the controls:

https://amber-canyon-7f3.protocontent.app/plan

08 Security model

"Off the URL" is an origin boundary

The original instinct — "off the URL so that you aren't able to access your shit" — describes the single most important rule in this space: untrusted, agent-generated HTML must not share an origin with anything that holds your identity. Defense in depth, outermost first:

① Isolated content origin

Artifacts on *.protocontent.app — a separate registrable domain from the .com control plane (api./mcp., no cookies). Inter-artifact cookie isolation is achieved PSL-free (see ②), so the slow PSL petition is no longer needed.

② CSP sandbox = opaque origin ✓ shipped

Every untrusted artifact is served with CSP: sandbox allow-scripts… (no allow-same-origin) → an opaque origin with no cookie/storage access. The PSL-free substitute for inter-artifact isolation — and stronger (per-document, not per-site). The first-party index isn't sandboxed.

③ CSP via HTTP headers

The Worker sets connect-src/script-src to curb exfiltration — as a header, never a <meta> tag (meta-only CSP in an iframe is escapable, per Willison).

④ noindex + capability URL

X-Robots-Tag: noindex everywhere; artifact access gated by the unguessable subdomain; the session page is token-private by default.

Anthropic chose browser-native iframe + origin isolation for Artifacts over a custom sandbox precisely because "browsers already enforce strong isolation between origins… hardened over years." We inherit that boundary instead of inventing one.

09 Prior art

What we borrow, what we avoid
ReferenceURL / share modelEphemeralBorrow ✓ / Avoid ✗
Claude ArtifactsPublish → public link, no account; separate claudeusercontent.comUntil unpublished Separate-origin + sandbox + CSP blueprint · can't re-publish after unpublish
EdgeOne Pages MCPdeploy_html → public URL; edge + KVNo Clean tool surface, edge/KV storage · transport tool-parity gap
Netlify DropRandom *.netlify.app, no account24h unless claimed No-auth + auto-TTL + claim-to-keep
ChatGPT Canvaschatgpt.com/share/…, tied to a chatNo Artifact coupled to transcript — we want standalone pages
ngrok / tunnelsTunnel to a live local serverURL is, process isn't Breaks cloud + mobile; dies on sleep (see D1)
DropBin MCPTemp URLs, no auth (SSE MCP)Yes Closest existing positioning to ours

Every mature system converges on the same intersection: push to a store → random subdomain on a dedicated origin → sandbox + CSP → no viewer account → optional TTL. protocontent is that consensus — packaged cross-agent, with a live session page.

10 Beyond HTML

One session, one URL — a portable artifacts list

The primitive — push bytes → sandboxed capability URL + TTL — doesn't care that it's HTML. HTML is the wedge; the category is an ephemeral host for everything an agent makes and never commits: images, markdown plans, data, screenshots, several linked HTML files.

Comes (nearly) free

Images, PDFs, raw HTML render natively in the browser — same cloud/mobile pain, and images need no sandbox since they don't execute. Strongest second type.

The one real build

Markdown is the lone type that needs a server-side renderer (raw .md shows as plain text). Data viewers (CSV/JSON) are a nice-to-have after.

The session space = the unbundled artifacts panel

The unlock is grouping. Give each agent session a space, and everything it publishes shows up as a live, auto-generated index at one URL — the Claude.ai / ChatGPT artifacts panel, but hosted, portable, and openable on your phone. The index is itself an unreasonably-effective HTML page — meta again.

// one space per session — every publish lands in it
https://amber-canyon-7f3.protocontent.app/        ← live index of the whole task
  ├── /plan            updated in place
  ├── /schema.png      native render, no sandbox
  ├── /before-after
  └── /notes           rendered server-side
The killer unlock: open the space URL on your phone and watch a cloud agent's artifacts appear as it works — not just the final link, a live window into the session. The cleanest answer to the original remote / mobile pain.

Stance: build the general primitive, expose a narrow surface. Storage and serving are MIME-agnostic from day one; a new type only ships when it clears the bar "is viewing this painful today?" — images clear it instantly, arbitrary files don't. The per-session space is what turns a pastebin into something worth the "Dropbox for agents" comparison — without having to claim it on day one.

11 MVP & stack

The thinnest slice that delivers "watch it on my phone"

The whole MVP exists to prove one 20-second moment: kick off a cloud agent, open the session URL on your phone, and watch artifacts appear as it works. Everything below is in service of that. Every component is a first-party Cloudflare primitive — one vendor, one wrangler deploy.

Stack — every need → a Cloudflare primitive

NeedPrimitiveWhy this one
Remote MCP server (HTTP/SSE)Workers + Agents SDK (McpAgent) First-class remote MCP on Workers; built-in OAuth provider for later. Wrangler DX.
Local / stdio accessnpx stdio bridge → same Worker Covers agents that only speak stdio; one published npm package.
Store HTML + assetsR2 S3-compatible object store, zero egress fees, cheap. Blobs keyed space/name.
Metadata, sessions, tokens, TTLD1 (serverless SQLite) Relational — the clean "list every artifact in a space" query the index needs.
Wildcard subdomain servingWorkers + wildcard DNS *.protocontent.app Native per-subdomain edge routing; CSP + noindex headers set in the Worker.
Live session viewDurable Objects (one per space) + WebSocket/SSE Holds viewer connections; publish notifies it; pushes "new artifact." Purpose-built for live.
Expiry / garbage collectionCron Triggers (scheduled Worker) Periodic sweep deletes expired R2 objects + D1 rows. R2 lifecycle rules as backstop.
Publisher auth + secretsD1 API keys · Wrangler secrets Per-install bearer token, no cookies (bounds same-domain risk per D2).

All comfortably inside Cloudflare's free / low tiers at MVP scale. No niche SaaS, no bare metal — one account, one dashboard, one bill.

Build phases

Build nowthe v1 push

Ship the whole loop — live, multi-file, project-aware.

  • publish({path, name?, ttl?}) — the local bridge reads a file or folder and uploads it → R2 + D1 → returns url, space_url, markdown
  • Space = one agent thread (isolated); spaces roll up into a durable project (account-scoped) that aggregates threads
  • Durable Objects push for live updates from the start — the session page is simple: shows everything, no curation
  • publish_folder / project folders (multi-file, relative links) + version history per file
  • list, unpublish, keep; default 7d TTL; CSP + noindex; bearer auth; npx protocontent stdio bridge
Laterdeferred on purpose

Hardening & breadth, once the loop proves out.

  • Dedicated content domain + PSL (the §08 origin hardening) — before any logged-in dashboard or sensitive outside content
  • Images (native) → markdown rendering; size caps + abuse limits
  • Token-private session pages; team sharing; Workers OAuth; diff-view UI; one-command installers
The one metric that answers "is it a thing": publishes from more than one distinct agent (Claude Code and Cursor and Codex…). All-Claude usage means a vendor absorbs it; cross-agent usage means the neutral layer is real.

12 Decisions

Locked for the build — two items to confirm while building
Q1Hosted vs self-host ✓ hosted — managed on Cloudflare. Self-host is a maybe-later, not now.
Q2Default TTL ✓ 7 days — per call 1h…30d; keep makes it durable.
Q3Content domain ✓ shipped — artifacts now serve on the separate registrable domain *.protocontent.app; legacy .com links 301→.app. Remaining: submit protocontent.app to the Public Suffix List for inter-artifact isolation. Detail in §08.
Q4Input model ✓ path-first — research confirms no MCP primitive lets a server read the client's disk, so the co-located bridge (runs wherever the agent runs — laptop or cloud VM) reads the path/folder and uploads bytes. Pure-remote-with-no-bridge falls back to inline content, single small files only.
Q5What is a "session" ✓ per agent thread — a space = one isolated thread; spaces roll up into a durable project (account-scoped). Mechanism (from research): the bridge mints a per-process space id (seeded from CLAUDE_SESSION_ID when present), routed to a named Durable Object via idFromName(spaceId). Not the HTTP Mcp-Session-Id — Claude Code doesn't echo it today.
Q6Session page ✓ show everything — simple auto-include, no curation or pinning for now.
Q7Live transport ✓ Durable Objects — real push from the start; skip the polling interim.