Host and share HTML pages from LLMs and coding agents in one line.
Project description
HTMLShip
Host and share HTML pages from LLMs and coding agents in one line.
HTMLShip has four surfaces:
- a public Python library and CLI (
htmlshipon PyPI) - a Node.js CLI and MCP server (
htmlshipon npm — no install required, runs vianpx) - a FastAPI service for creating, updating, deleting, and viewing HTML pages
- a stdio MCP server (the
mcpsubcommand of both CLIs) for agent clients
# Node — runs immediately, no install
npx htmlship publish report.html
npx htmlship publish report.html --password "demo-pass"
# Build a frontend project (React/Vite/etc.) and ship the compiled app
npx htmlship deploy ./my-app
# Python
pip install htmlship
import htmlship
page = htmlship.publish(
"<h1>Hello</h1>",
title="Demo",
password="demo-pass",
expires_in=60, # minutes
)
print(page.url)
print(page.owner_key) # save this to update or delete the page later
curl -X POST https://api.htmlship.com/api/v1/pages \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello</h1>","title":"Demo","password":"demo-pass"}'
See htmlship-implementation-spec.md for the product spec and DEPLOY.md for the production runbook.
Python Library
The module-level helpers use https://api.htmlship.com by default. Override with HTMLSHIP_API_URL or htmlship.configure(base_url=...).
import htmlship
page = htmlship.publish(
"<h1>Hello</h1>",
title="Demo",
password="optional-password",
expires_in=1440, # minutes (24 hours)
)
fresh = htmlship.get(page.slug)
updated = htmlship.update(page.slug, "<h1>Updated</h1>", owner_key=page.owner_key)
htmlship.delete(updated.slug, owner_key=page.owner_key)
owner_key is returned only when a page is created. It is the publisher-only secret required for updates and deletes, and the API does not return it again from metadata calls. password is only a view-time gate for readers; it does not authorize mutations.
CLI
The CLI is shipped both as a Python package (pip install htmlship) and an npm package (npx htmlship or npm i -g htmlship). The two share the same on-disk owner-key store, so a page created in one is editable from the other.
htmlship publish report.html
cat report.html | htmlship publish -
htmlship publish report.html --password "demo-pass"
htmlship publish --file report.html --title "Q4 Report" --expires-in 60
htmlship get <slug>
htmlship update <slug> updated.html
htmlship delete <slug>
htmlship list-mine
Equivalent npx form (no install):
npx htmlship publish report.html
npx htmlship publish report.html --password "demo-pass"
npx htmlship list-mine
The CLI stores owner keys in ~/.htmlship/keys.json so update, delete, and list-mine can work with pages you created locally. When a page's key isn't saved there and --owner-key isn't passed, update and delete prompt for it interactively — delete asks for the key right after the [y/N] confirmation — so the public slug alone is never enough to mutate a page. Set HTMLSHIP_KEYS_DIR to use another key-store directory, and set HTMLSHIP_API_URL to point the CLI at a local or staging API.
Deploy built apps
deploy builds a modern frontend project (React, Vite, Svelte, Next.js, …) locally and publishes the compiled output. Single-page apps are inlined into one self-contained page; multi-file sites (Next.js static export — auto-detected — Astro, Docusaurus, VitePress, …) are hosted as a file tree at view.htmlship.com/{slug}/. No separate asset hosting required either way.
htmlship deploy ./my-app # SPA -> one inlined page
htmlship deploy ./my-next-app # Next.js -> multi-file site (auto-detected)
htmlship deploy ./my-app --site --out dist # force multi-file hosting (Astro, etc.)
htmlship deploy ./my-app --single-file # force single-file inlining
htmlship deploy ./my-app --password "demo-pass" # password-protect the deploy
htmlship deploy --dry-run # build, but don't publish (inspect first)
htmlship deploy --build-cmd "vite build" --out dist
npx htmlship deploy ./my-app # same, no install
How it works:
- The build runs locally, on your machine — never on the server.
deploydetects your package manager (npm/pnpm/yarn/bun) and runs thebuildscript (falling back tobuild:prod, or--build-cmdto override). This is what keeps the service from ever executing untrusted build steps. - Single-file (default for SPAs): the output (
dist/,build/, or--out) is inlined into one HTML file — scripts and stylesheets embedded, images/icons/fonts as data URIs — and published withsandbox_mode: "relaxed". - Multi-file (Next.js auto-detected, or
--site): the whole output tree is uploaded as a base64 file manifest toPOST /api/v1/sitesand served atview.htmlship.com/{slug}/, each response carrying a path-scoped per-slug sandbox CSP.
Both kinds run with relaxed sandboxing (see Rendering); a single inlined page is capped at 10 MB, a multi-file site at 50 MB. --single-file and --site force a mode when you don't want the auto-detected one.
The same flow is available programmatically (Python library) and via the deploy_project MCP tool:
import htmlship
# build ./my-app locally and publish (auto-detects single-file vs. multi-file site)
page = htmlship.HTMLShipClient().deploy_project("./my-app", password="demo-pass", expires_in=1440)
print(page.url)
Relaxed-mode limits (by design): a deployed app runs in an isolated, opaque origin and cannot make network requests (connect-src 'none'), read cookies, access other pages, or use eval. For a multi-file Next.js site that means client-side data fetches fail closed and Next falls back to full-page navigation — links still work, runtime API calls don't. It's intended for self-contained client-side apps, demos, and static content — not for apps that call external APIs at runtime.
What deploy supports:
- Single-page apps that build to one
index.htmlplus assets — Vite, Create React App, SvelteKit (adapter-static), Vue CLI, plain static-site generators. Auto-detectsdist/,build/, andout/with no flags. - Next.js (auto-detected): the CLI transparently builds a static export based at the slug path — you don't edit
next.config(JS,.mjs, or.tsconfigs are all wrapped automatically and restored afterward). The app must be statically exportable (no middleware, SSR, or server features). - Other multi-file frameworks via
--site(Astro, Docusaurus, VitePress, multi-page builds): build with your framework's base path set to/__htmlship_base__(e.g. Astro'sbase, orvite build --base=/__htmlship_base__/) so root-absolute asset URLs resolve under/{slug}/; the server rewrites that placeholder to the real slug at upload. Automatic base injection is currently Next.js-only.
API
Base URL: https://api.htmlship.com.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check with service version. |
GET |
/version |
Service version. |
POST |
/api/v1/pages |
Create a page. |
POST |
/api/v1/sites |
Upload a multi-file static site (base64 file manifest); served at view.htmlship.com/{slug}/. |
GET |
/api/v1/pages/{slug} |
Fetch page metadata. |
PATCH |
/api/v1/pages/{slug} |
Replace HTML or title. Requires X-Owner-Key. |
DELETE |
/api/v1/pages/{slug} |
Soft-delete a page. Requires X-Owner-Key. |
POST |
/api/v1/pages/{slug}/version |
Create a new page linked to an existing parent slug. |
Create payload:
{
"html": "<h1>Hello</h1>",
"title": "Optional title",
"password": "optional password",
"expires_in": 60,
"parent_slug": "optional-parent",
"sandbox_mode": "strict"
}
Notes:
htmlis stored and served verbatim. Scripts are blocked by the view CSP, not by sanitizing the body.- Payloads are limited to 10 MiB by default.
expires_inis in minutes and must be between 1 and 10080 (7 days). Requests above the cap are rejected with422.sandbox_modeacceptsstrict(default) orrelaxed.strictblocks all scripts;relaxedlets the page's own inline scripts run inside an isolated, opaque origin (used bydeployfor compiled apps). See Rendering.- Password-protected views set a signed, HTTP-only session cookie after the correct password is submitted.
There is no server-side build endpoint, by design: builds always run on the client (see Deploy built apps). The API only ever receives already-built output — single-file deploys are a POST /api/v1/pages with "sandbox_mode": "relaxed"; multi-file sites are a POST /api/v1/sites with a base64 file manifest, served per-slug at view.htmlship.com/{slug}/.
Rendering
Rendered HTML is served from view.htmlship.com/{slug}. Strict pages (the default) get a CSP that blocks all scripts:
Content-Security-Policy: default-src 'none'- images from
data:andhttps: - inline styles plus HTTPS stylesheets
- HTTPS/data fonts and HTTPS media
X-Content-Type-Options: nosniffReferrer-Policy: no-referrer- restrictive
Permissions-Policy
Relaxed pages (sandbox_mode: "relaxed", used by deploy) let the page's own inline scripts run but isolate them with Content-Security-Policy: sandbox allow-scripts — deliberately without allow-same-origin. That forces an opaque origin, so a deployed app cannot read cookies, touch localStorage, or fetch other pages same-origin. connect-src 'none' additionally blocks network egress. The body is still served verbatim; isolation is enforced entirely by response headers, with no change to the single-file storage model.
Multi-file sites (deploy of a Next.js export or --site) are served at view.htmlship.com/{slug}/ from a per-slug file tree. Each response carries the same opaque-origin sandbox CSP, additionally path-scoped to /{slug}/ (script-src/style-src/img-src/font-src are pinned to that prefix). Per-slug isolation therefore comes from the CSP, not from the URL path — one slug's scripts can't read another's, and connect-src 'none' blocks egress just as for single-file pages.
The app routes by Host header:
htmlship.comserves the landing pageapi.htmlship.comserves the API and landing assetsview.htmlship.com/{slug}serves a sandboxed single page;view.htmlship.com/{slug}/…serves a multi-file site's tree
For local development without DNS, append ?_host=view.htmlship.com (or your configured view host) to spoof the host header, for example:
curl "http://localhost:8000/<slug>?_host=view.htmlship.com"
MCP Server
HTMLShip ships a stdio MCP server with four tools:
publish_html(accepts optionalpassword)deploy_project— build a local frontend project and publish the compiled app (runs the build on the machine hosting the MCP server)fetch_htmlupdate_html
There are two equivalent ways to run it. The npx form is the easiest — it requires no install on the user's machine and works with every MCP client.
Option A — npx (recommended)
{
"mcpServers": {
"htmlship": {
"command": "npx",
"args": ["-y", "htmlship", "mcp"],
"env": {
"HTMLSHIP_API_URL": "https://api.htmlship.com"
}
}
}
}
Option B — Python install
If you already have a Python install with pip install htmlship:
{
"mcpServers": {
"htmlship": {
"command": "htmlship-mcp",
"env": {
"HTMLSHIP_API_URL": "https://api.htmlship.com"
}
}
}
}
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows. Paste either config block above into the mcpServers map and restart Claude Desktop. Then ask: publish this HTML: <h1>test</h1>.
Claude Code
Edit ~/.claude.json or use claude mcp add with the same config (set "type": "stdio" if you're editing the file by hand).
Local Development
Requirements:
- Python 3.12+
- uv
- Docker
# 1. Install dependencies (creates .venv)
uv sync --extra server --extra cli --extra mcp --extra dev
# 2. Start Postgres on localhost:5433
docker compose up -d postgres
# 3. Copy env and edit if needed
cp .env.example .env
# 4. Run migrations
uv run alembic upgrade head
# 5. Start the server
uv run uvicorn htmlship_server.main:app --reload
# 6. Health check
curl http://localhost:8000/health
Run tests and linting:
uv run pytest
uv run ruff check .
The test suite uses SQLite and a temporary local blob store. Local development uses Postgres metadata plus ./tmp/blobs/ for HTML blobs unless SPACES_BUCKET is configured.
Configuration
The server reads .env via Pydantic settings.
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://htmlship:htmlship@localhost:5433/htmlship |
Async SQLAlchemy database URL. |
PUBLIC_BASE_DOMAIN |
htmlship.com |
Base domain used to derive host routing. |
API_BASE_URL |
https://api.htmlship.com |
Public API URL setting. |
VIEW_BASE_URL |
https://view.htmlship.com |
Public view URL used in page responses. |
LANDING_BASE_URL |
https://htmlship.com |
Public landing URL. |
SPACES_BUCKET |
empty | If empty, use local blob storage; otherwise use DigitalOcean Spaces/S3. |
SPACES_REGION |
nyc3 |
Spaces/S3 region. |
SPACES_ENDPOINT_URL |
https://nyc3.digitaloceanspaces.com |
Spaces/S3 endpoint. |
SPACES_ACCESS_KEY / SPACES_SECRET_KEY |
empty | Spaces/S3 credentials. |
SECRET_KEY |
development placeholder | Signs password-view session cookies. Use a strong value in production. |
ENVIRONMENT |
development |
Enables API docs outside production and secure cookies in production. |
LOG_LEVEL |
info |
Application log level. |
MAX_PAYLOAD_BYTES |
10485760 |
Server-side single-file HTML size limit (10 MB). |
MAX_SITE_BYTES |
52428800 |
Total size limit for a multi-file site upload (50 MB). |
MAX_SITE_FILES |
2000 |
Max number of files in a multi-file site upload. |
DEFAULT_EXPIRES_IN_MINUTES |
empty | Optional default TTL (minutes) for new pages. |
Architecture
One FastAPI process hosts the landing page, JSON API, and view renderer. HostRoutingMiddleware classifies requests by host and prevents API routes from being served on the view host.
Postgres stores page metadata, owner-key/password hashes, expiry, view counts, and parent-version links. HTML bodies are stored as blobs, either in LocalBlobStore for development/tests or DigitalOcean Spaces in production.
Project Layout
src/htmlship/ Public Python library + CLI
src/htmlship_server/ FastAPI app, API routers, storage, database models
src/htmlship_mcp/ MCP server (stdio) — Python
npm/ Node CLI + MCP server (publishes as the `htmlship` npm package)
web/ Static landing page
tests/ API, client, CLI, MCP, landing, and view tests (Python)
alembic/ Database migrations
deploy/ Production configs (nginx, systemd)
scripts/ Deploy and smoke-test scripts
The npm package mirrors the Python CLI surface and reads/writes the same ~/.htmlship/keys.json file format, so users can mix and match. The Node MCP server lives at htmlship mcp (subcommand) rather than a separate htmlship-mcp bin.
License
Source code in this repository is licensed under the MIT License — see LICENSE.
The "HTMLShip" name and the brand assets in web/assets/ (logo, favicons) are not covered by MIT and are reserved — see web/assets/LICENSE. If you fork to run your own instance, please replace those files with your own branding.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file htmlship-0.3.3.tar.gz.
File metadata
- Download URL: htmlship-0.3.3.tar.gz
- Upload date:
- Size: 37.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a57e59241dc1d8e82a5e8ee9b1eaa3b9c96aa60fd9cb02e763202d1959434de7
|
|
| MD5 |
cf79c0408d051d35921943c428bff2a3
|
|
| BLAKE2b-256 |
de206ca14c60cefb84e6087465380b4dd838d52fd7f4e6570c04114392242e40
|
File details
Details for the file htmlship-0.3.3-py3-none-any.whl.
File metadata
- Download URL: htmlship-0.3.3-py3-none-any.whl
- Upload date:
- Size: 45.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db582f0ca526590eeaab0ca493fe6569d732a5e227eda3e02e7192b8087214ee
|
|
| MD5 |
e8c325fb91c3c05b0d45212200d31d26
|
|
| BLAKE2b-256 |
0f82231b89c19cdeda9acb90f0c8733c5abcf9a6d86912737eaeb6503c82adbb
|