Skip to main content
Glama

pages-mcp

A tiny static-site host with an MCP upload tool. Claude (or any MCP client) uploads static sites/artifacts; they're served over HTTPS at an internal hostname.

  • Serve: https://pages.internal.white.fm/<site>/ (open on the tailnet)

  • Upload (MCP): https://pages-mcp.internal.white.fm/mcp — Authorization: Bearer <MCP_TOKEN>

  • Storage: files persist under SITES_DIR (default /data/sites), one dir per site.

A <base href="/<site>/"> is injected into served HTML (when absent) so relative-path multi-file sites work under the path prefix, not just self-contained single files.

MCP tools

Tool

Purpose

deploy_site(name, files, replace=True)

Upload a site. files = [{path, content, encoding}] (encoding: text|base64). replace replaces vs. merges. Returns {url, files, bytes}.

list_sites()

All sites with URL, file count, size, last-modified.

get_site(name)

One site's stats + the list of file paths.

delete_site(name)

Remove a site and all its files.

Related MCP server: Torreable

Writable data files

A site can let its own pages save JSON back to the server, e.g. a gallery that keeps user-curated items. Opt in by deploying a pages-writable.json at the site root:

{"files": {"collages/saved.json": {"max_bytes": 262144}}}

Then the page can PUT that path with Content-Type: application/json:

  • Only on WRITE_HOSTS (default: the canonical SERVE_HOST, i.e. the internal zone), so a site also exposed on a public zone stays read-only there.

  • Only listed .json paths; the body must parse as JSON and fit max_bytes (capped by MAX_WRITABLE_BYTES). Cross-origin Origin headers are refused.

  • GET returns an ETag; send it back as If-Match and a write that raced another one gets 412 with the current ETag, so the page can re-read, merge and retry instead of silently dropping the other write.

  • Each write is committed to the git mirror (update <site>/<path>).

  • The live file outlives redeploys: deploy_site seeds a writable file the first time but never overwrites or deletes it afterwards (returned as preserved).

/healthz is open (used by k8s probes). Site names are lowercase DNS labels (a-z, 0-9, -), 1–63 chars.

Env

Var

Default

Notes

MCP_TOKEN

(empty)

Bearer token for the control plane. Empty = auth disabled (don't run that way).

PORT

8080

SITES_DIR

/data/sites

Persistent storage root.

SERVE_HOST

pages.internal.white.fm

Content host (Host header → serving plane).

MCP_HOST

pages-mcp.internal.white.fm

Control host (Host header → /mcp).

PUBLIC_BASE_URL

https://$SERVE_HOST

Base used in returned URLs.

MAX_TOTAL_BYTES

67108864 (64 MiB)

Per-site upload cap.

WRITE_HOSTS

first SERVE_HOST

Serve zones that accept writable-file PUTs (comma-separated).

MAX_WRITABLE_BYTES

1048576 (1 MiB)

Upper bound for any writable file.

Build

GitHub Actions (.github/workflows/build.yml) builds ghcr.io/robertdwhite/pages-mcp (linux/amd64) on push to main / v* tags. Pin the resulting digest in the cluster repo at apps/ai/pages/kustomization.yaml.

Register with Claude Code

# Token from the k8s secret:
sops -d apps/ai/pages/11-secret.sops.yaml | grep MCP_TOKEN

claude mcp add --transport http pages \
  https://pages-mcp.internal.white.fm/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"

Related MCP Connectors

Related MCP Servers