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.
.html file an agent
generated — exactly the kind of artifact protocontent exists to host.01 The problem
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
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.
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
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
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
// 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 })
{
"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
D1 Push the bytes — never tunnel Store HTML server-side; do not expose a local server. ▶
- ✓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. ▶
*.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.*.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. ▶
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. ▶
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. ▶
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.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. ▶
publish writes a file (or a set) into it; relative links between them just work..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. ▶
<meta> tag — meta-only CSP inside an iframe is escapable. Full
mapping in §11.07 Tool API
| Tool | Args | Returns | Purpose |
|---|---|---|---|
| publish_html | path|content, name?, ttl? |
url, space_url, markdown, edit_token, expires_at | Write a file (bridge reads the path). The 95% tool. |
| publish_folder | dir, entry?, ttl? | url, space_url, markdown | Write a multi-file artifact (relative links). |
| list | — | [{url, name, expires_at}] | What's live in this thread's space. |
| history | name | [{version, at, url}] | Past versions of an artifact. |
| unpublish | name|url, edit_token | ok | Kill an artifact now. |
| keep | name, edit_token | expires_at:null | Promote 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:
08 Security model
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.
09 Prior art
| Reference | URL / share model | Ephemeral | Borrow ✓ / Avoid ✗ |
|---|---|---|---|
| Claude Artifacts | Publish → public link, no account; separate claudeusercontent.com | Until unpublished | ✓ Separate-origin + sandbox + CSP blueprint · ✗ can't re-publish after unpublish |
| EdgeOne Pages MCP | deploy_html → public URL; edge + KV | No | ✓ Clean tool surface, edge/KV storage · ✗ transport tool-parity gap |
| Netlify Drop | Random *.netlify.app, no account | 24h unless claimed | ✓ No-auth + auto-TTL + claim-to-keep |
| ChatGPT Canvas | chatgpt.com/share/…, tied to a chat | No | ✗ Artifact coupled to transcript — we want standalone pages |
| ngrok / tunnels | Tunnel to a live local server | URL is, process isn't | ✗ Breaks cloud + mobile; dies on sleep (see D1) |
| DropBin MCP | Temp 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
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
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 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
| Need | Primitive | Why 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 access | npx stdio bridge → same Worker | Covers agents that only speak stdio; one published npm package. |
| Store HTML + assets | R2 | S3-compatible object store, zero egress fees, cheap. Blobs keyed space/name. |
| Metadata, sessions, tokens, TTL | D1 (serverless SQLite) | Relational — the clean "list every artifact in a space" query the index needs. |
| Wildcard subdomain serving | Workers + wildcard DNS *.protocontent.app |
Native per-subdomain edge routing; CSP + noindex headers set in the Worker. |
| Live session view | Durable Objects (one per space) + WebSocket/SSE | Holds viewer connections; publish notifies it; pushes "new artifact." Purpose-built for live. |
| Expiry / garbage collection | Cron Triggers (scheduled Worker) | Periodic sweep deletes expired R2 objects + D1 rows. R2 lifecycle rules as backstop. |
| Publisher auth + secrets | D1 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
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 → returnsurl,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 filelist,unpublish,keep; default 7d TTL; CSP + noindex; bearer auth;npx protocontentstdio bridge
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
12 Decisions
1h…30d; keep makes it durable.*.protocontent.app; legacy .com links 301→.app. Remaining: submit protocontent.app to the Public Suffix List for inter-artifact isolation. Detail in §08.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.