Skip to main content
Glama
IsacGama

publish-artifacts-mcp

by IsacGama
README.md
# publish-artifacts-mcp

Publish self-contained **HTML artifacts safely**, controlled by **API key**, and
manage them over **MCP** (stdio + Streamable HTTP). Minimal admin UI included.

The hard part here is not "serve HTML" — it's hosting *untrusted* HTML without
letting it steal credentials or exfiltrate data. The design centers on **origin
isolation + strict CSP + scoped API keys**.

## Stack

TypeScript · Node 22 · [Hono](https://hono.dev) · `@modelcontextprotocol/sdk`
· SQLite (`better-sqlite3`, WAL) · zod. Deploy via Docker.

## How it works

Two planes share one process and one SQLite file, but are **routed by Host header**:

| Plane | Origin (example) | Serves | Auth |
|---|---|---|---|
| Control | `api.example.com` | REST API, admin UI (`/admin`), MCP-over-HTTP (`/mcp`) | API key (Bearer) |
| Content | `content.example.com` | published artifacts at `/a/:slug` | public, no cookies |

They **must be different origins** — the server refuses to boot otherwise (set
`ALLOW_SAME_ORIGIN=1` only for local dev). This guarantees untrusted artifact
HTML can never reach the API/admin surface.

### Safety layers

- **Origin isolation** (above) + host-based routing.
- **Strict CSP** on every served artifact: `default-src 'none'`, `connect-src 'none'`,
  `form-action 'none'`, `frame-ancestors 'none'`, `base-uri 'none'` — blocks
  network/form exfiltration and framing while still allowing the artifact's own
  inline script/style to render. `relaxed` mode (opt-in per artifact) permits
  https network access.
- **Hardening headers**: `X-Content-Type-Options`, `Referrer-Policy`,
  `X-Frame-Options`, `Permissions-Policy`, `Cross-Origin-Resource-Policy`. No
  cookies are ever set on the content origin.
- **Unguessable slugs** (~132 bits), optional **expiry**, and **rotate** to
  invalidate a shared link.
- **API keys**: high-entropy, **SHA-256-hashed at rest**, prefix-indexed,
  revocable, and **owner-scoped** (a key sees/mutates only its own artifacts).
- **Limits**: max HTML size + per-key publish rate limiting. Optional `sanitize`
  tier strips scripts via DOMPurify.

> **Accepted trade-off (raw-path model):** all artifacts share one content
> origin, so they can read each other's `localStorage`. That's why no credentials
> live on the content origin. Upgrading to per-artifact subdomains
> (`<slug>.content-domain`) later requires only routing + wildcard DNS changes.

## Quick start (local dev)

```bash
npm install
cp .env.example .env          # defaults use content.localhost for isolation
npm run build

# Bootstrap an API key (printed once):
npm run create-key -- "my laptop"

# Run the server:
npm start
# admin UI: http://localhost:8787/admin  (paste the key when prompted)
```

`content.localhost` resolves to 127.0.0.1 on most systems. If yours differs, add
`127.0.0.1 content.localhost` to `/etc/hosts`, or set `ALLOW_SAME_ORIGIN=1` for
dev only.

## MCP tools

`publish_artifact`, `update_artifact`, `delete_artifact`, `get_artifact`,
`list_artifacts`, `get_share_link`. All owner-scoped by the calling key.

### stdio (Claude Code / Desktop) — `.mcp.json`

```json
{
  "mcpServers": {
    "artifacts": {
      "command": "node",
      "args": ["/abs/path/dist/mcp/stdio.js"],
      "env": {
        "ARTIFACTS_API_KEY": "ak_...",
        "CONTENT_BASE_URL": "https://content.example.com",
        "CONTROL_BASE_URL": "https://api.example.com",
        "DB_PATH": "/data/artifacts.db"
      }
    }
  }
}
```

### Remote (Streamable HTTP)

`POST https://api.example.com/mcp` with header `Authorization: Bearer ak_...`.

## Deploy (Docker)

Point two hostnames you control at the container (reverse proxy / DNS), set them
as `CONTROL_BASE_URL` / `CONTENT_BASE_URL` in `docker-compose.yml`, then:

```bash
docker compose up -d --build
docker compose exec artifacts node dist/cli/createKey.js "prod key"
```

SQLite persists in the `artifacts-data` volume (`/data`).

## Tests

```bash
npm test        # unit + MCP in-memory + HTTP host-isolation integration
```

> The only `npm audit` findings are in the dev-only `vitest`/`vite`/`esbuild`
> chain (not shipped to production). Left unpinned to avoid a breaking vitest v4
> bump; revisit when upgrading the test tooling.