OpenTree DashboardSign in

$ claude mcp add

Publish from a Claude Code session

Claude Code writes the HTML, then publishes it itself — to an unguessable private URL, without a deploy step, a repository or a hosting account for whoever opens it.

Install

Register the server once, in the project you are already working in:

claude mcp add --transport http opentree https://read.botook.ai/mcp \
  --header "Authorization: Bearer otr_live_YOUR_KEY"

The key comes from the dashboard and starts with otr_live_. Prefer a local process to an HTTP header? The same server runs over stdio:

npx -y opentree-mcp

Check it took:

claude mcp list

Publish

Ask for the page, then ask for the link. The tool call the model makes is this one:

publish_html({
  content: "<!doctype html><title>Q3 review</title>…",
  expires_in_hours: 168,
  password: "optional",
  agentation: true
})

What comes back is the site id and a URL carrying a 22-character random token. That is the whole deploy: no build, no DNS, no CI. The page title is read out of the document, so you do not have to name anything.

Replace in place, every revision

This is the part that matters in a coding session. The second version of a page is not a new link — update_site writes the new bytes behind the same token, so the address you already pasted into a chat keeps resolving to the current build:

curl -X PUT https://read.botook.ai/sites/SITE_ID \
  -H "Authorization: Bearer otr_live_YOUR_KEY" \
  -F file=@dist/index.html

Wire that into the loop and the output of every run lands at one stable address — see agent-loop hosting.

A build directory, not one file

Zip the build output and publish the archive; the whole tree is served, with index.html as the entry:

cd dist && zip -r ../site.zip . && cd ..
curl -X POST https://read.botook.ai/sites \
  -H "Authorization: Bearer otr_live_YOUR_KEY" \
  -F file=@site.zip

Markdown straight out of the session

A design doc or a postmortem does not need to be HTML first. Publish the markdown; it is rendered to a self-contained document when it is stored, not when it is read:

curl -X POST https://read.botook.ai/sites \
  -H "Authorization: Bearer otr_live_YOUR_KEY" \
  -H "content-type: text/markdown" \
  --data-binary @NOTES.md

Gate it before you send it

Access is a publish-time argument, not a second workflow:

set_password({ site_id: "…", password: "hunter2" })
set_email_gate({ site_id: "…", allowed_email_domain: "client.com" })
set_expiry({ site_id: "…", expires_in_hours: 48 })

Or in one call at publish time — password, allowed_email_domain, burn_after_read and expires_in_hours are all fields on the publish.

Read the reaction without leaving the terminal

Turn the on-page toolbar on and viewers can leave a comment anchored to the paragraph they are objecting to. The session reads them back:

list_feedback({ site_id: "…", unresolved_only: true })
resolve_feedback({ feedback_id: "…" })

So a review round is: publish, send the link, keep working, and pick the feedback up as structured input on the next turn — how that loop works.

A slash command that ships the current file

Drop this in .claude/commands/publish.md and the whole thing is one keystroke:

---
description: Publish the current build output to a private link
---
Publish $ARGUMENTS (default: dist/index.html) with publish_html,
expires_in_hours 168, agentation true. Print only the URL.

What this is not

It is not Vercel and it is not Netlify. There is no framework detection, no serverless function, no preview environment per branch — it hosts a document. If the project needs a build pipeline, deploy the project normally and use this for the artifacts around it: the status page, the report, the one-off tool, the thing you would otherwise paste into Notion and watch render badly.

The link works in any browser with no login, so it drops cleanly into an email or a Slack thread. If it is going to a client, put your own hostname in front of it — custom domains take a CNAME and a TXT record.

One primitive, several jobs

There is exactly one publish primitive here — bytes in, unlisted URL out. Everything else in the session is that primitive plus an argument: a password makes it a client deliverable, an expiry makes it a temporary handoff, burn_after_read makes it a one-time credential drop, and update_site makes it a build address. Learning one call is enough to get all of them, which is the reason the tool surface is twelve verbs and not fifty.

What the person on the other end sees

A page, in a browser, with no login and no download. If agentation is on they also get a small toolbar for leaving a note anchored to a paragraph; if reactions is on they get the emoji row. When the link is pasted into a chat app, the unfurl uses a real screenshot of the page — unless the page is gated, in which case it is deliberately a placeholder, because a preview image that leaks a password-protected page would defeat the password.

What comes back to the session

After the link has been out for a day, the same key that published it can read what happened:

curl -s https://read.botook.ai/sites/SITE_ID/engagement \
  -H "Authorization: Bearer otr_live_YOUR_KEY" | jq .

Opens, unique viewers, median dwell, scroll depth in ten buckets, and whether anyone reached the end. Aggregate counters only — there is no session replay and no per-visitor trail.

Cleaning up after a session

A working session produces drafts nobody should still be able to open next month. List what the key owns, then delete what is finished:

curl -s https://read.botook.ai/sites -H "Authorization: Bearer otr_live_YOUR_KEY" | jq '.sites[] | {id,title,url}'
curl -X DELETE https://read.botook.ai/sites/SITE_ID -H "Authorization: Bearer otr_live_YOUR_KEY"

Deletion removes the row and the stored object together. Anything you forget expires on its own after seven days, which is the point of having a default expiry at all.

Questions

Does the model see my key?

It sees a configured server, the same way it sees any other tool. The key sits in the client configuration; rotate or revoke it in the dashboard at any time.

Can I publish without a key at all?

Yes — the anonymous route. Short-lived, rate-limited by address, claimable afterwards: curl -X POST https://read.botook.ai/v1/publish -F file=@index.html.

Does the page get indexed?

No. Every viewer response carries noai, noimageai, noindex and no referrer.

What happens when it expires?

The row and the stored object are both deleted by a scheduled purge. Expiry here is deletion, not hiding.

Keep reading