Metadata-Version: 2.4
Name: htmlship
Version: 0.1.5
Summary: Host and share HTML pages from LLMs and coding agents in one line.
Project-URL: Homepage, https://htmlship.com
Project-URL: Repository, https://github.com/htmlship/htmlship
Author: HTMLShip
License: MIT
License-File: LICENSE
Keywords: agent,hosting,html,llm,mcp,paste
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.12
Requires-Dist: click<9,>=8.1
Requires-Dist: httpx<0.28,>=0.27
Provides-Extra: cli
Requires-Dist: pyperclip<2,>=1.9; extra == 'cli'
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.22.1; extra == 'dev'
Requires-Dist: greenlet>=3.5.0; extra == 'dev'
Requires-Dist: pytest-asyncio<0.25,>=0.24; extra == 'dev'
Requires-Dist: pytest-cov<6,>=5; extra == 'dev'
Requires-Dist: pytest<9,>=8; extra == 'dev'
Requires-Dist: ruff<0.8,>=0.7; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.0; extra == 'mcp'
Provides-Extra: server
Requires-Dist: alembic<2,>=1.13; extra == 'server'
Requires-Dist: argon2-cffi<24,>=23; extra == 'server'
Requires-Dist: asyncpg<0.31,>=0.30; extra == 'server'
Requires-Dist: boto3<2,>=1.35; extra == 'server'
Requires-Dist: fastapi<0.116,>=0.115; extra == 'server'
Requires-Dist: itsdangerous<3,>=2.2; extra == 'server'
Requires-Dist: nanoid<3,>=2; extra == 'server'
Requires-Dist: pydantic-settings<3,>=2.5; extra == 'server'
Requires-Dist: pydantic<3,>=2; extra == 'server'
Requires-Dist: python-multipart<0.1,>=0.0.12; extra == 'server'
Requires-Dist: slowapi<0.2,>=0.1.9; extra == 'server'
Requires-Dist: sqlalchemy<3,>=2.0; extra == 'server'
Requires-Dist: uvicorn[standard]<0.33,>=0.32; extra == 'server'
Description-Content-Type: text/markdown

# HTMLShip

Host and share HTML pages from LLMs and coding agents in one line.

HTMLShip has four surfaces:

- a public Python library and CLI (`htmlship` on PyPI)
- a Node.js CLI and MCP server (`htmlship` on npm — no install required, runs via `npx`)
- a FastAPI service for creating, updating, deleting, and viewing HTML pages
- a stdio MCP server (the `mcp` subcommand of both CLIs) for agent clients

```bash
# Node — runs immediately, no install
npx htmlship publish report.html
npx htmlship publish report.html --password "demo-pass"

# Python
pip install htmlship
```

```python
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
```

```bash
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`](./htmlship-implementation-spec.md) for the product spec and [`DEPLOY.md`](./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=...)`.

```python
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.

```bash
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):

```bash
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.

## 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. |
| `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:

```json
{
  "html": "<h1>Hello</h1>",
  "title": "Optional title",
  "password": "optional password",
  "expires_in": 60,
  "parent_slug": "optional-parent",
  "sandbox_mode": "strict"
}
```

Notes:

- `html` is 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_in` is in **minutes** and must be between 1 and 10080 (7 days). Requests above the cap are rejected with `422`.
- `sandbox_mode` accepts `strict` or `relaxed`; the current view headers use the strict CSP.
- Password-protected views set a signed, HTTP-only session cookie after the correct password is submitted.

## Rendering

Rendered HTML is served from `view.htmlship.com/{slug}` with strict security headers:

- `Content-Security-Policy: default-src 'none'`
- images from `data:` and `https:`
- inline styles plus HTTPS stylesheets
- HTTPS/data fonts and HTTPS media
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: no-referrer`
- restrictive `Permissions-Policy`

The app routes by `Host` header:

- `htmlship.com` serves the landing page
- `api.htmlship.com` serves the API and landing assets
- `view.htmlship.com/{slug}` serves sandboxed HTML

For local development without DNS, append `?_host=view.htmlship.com` (or your configured view host) to spoof the host header, for example:

```bash
curl "http://localhost:8000/<slug>?_host=view.htmlship.com"
```

## MCP Server

HTMLShip ships a stdio MCP server with three tools:

- `publish_html` (accepts optional `password`)
- `fetch_html`
- `update_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)

```json
{
  "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`:

```json
{
  "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](https://docs.astral.sh/uv/)
- Docker

```bash
# 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:

```bash
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 HTML size limit. |
| `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

```text
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`](./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`](./web/assets/LICENSE). If you fork to run your own instance, please replace those files with your own branding.
