mcp-page-host
Provides a GitHub mirroring integration: every deployed or deleted page is committed and pushed to a private GitHub repository via an SSH deploy key, enabling backup, version history, and recovery of pages.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-page-hostDeploy this HTML as 'periodic-table' with title 'Periodic Table'."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-page-host
Host single-file HTML pages, deploy them over MCP.
An LLM generates an interactive page — an animation, a simulation, a visual explanation, a small practice tool — and a teacher needs a stable URL to put in a wiki, a PDF, Moodle or a presentation. This closes the gap between "the page exists" and "here is the link":
deploy_page(slug: "physics-pendulum", html: "<!DOCTYPE html>…")
→ https://pages.example.org/physics-pendulum/One page is one self-contained HTML file with its CSS and JavaScript inline. No build step, no bundler, no multi-file deployments, no database. State lives in the filesystem and is mirrored to a private git repository.
Setup
Needs Node.js 22+ and Docker. Everything is configured through environment
variables; see .env.example for the full list.
git clone <this repository>
cd mcp-page-host
cp .env.example .env
# One token per teacher. The name lands in meta.json and the audit log.
echo "PAGE_HOST_TOKENS=jane:$(openssl rand -hex 32)" >> .env
npm install && npm test
docker compose up -d --build
curl -s localhost:8080/healthz # {"status":"ok"}Deploy a page and read it back:
TOKEN=… # the token from .env
curl -X PUT localhost:8080/api/pages/physics-pendulum \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"html":"<!DOCTYPE html><title>Pendulum</title><h1>Hello</h1>","title":"Pendulum"}'
curl -s localhost:8080/physics-pendulum/For a public deployment, put a reverse proxy in front of the loopback port —
deploy/ has an Apache vhost pair as an example — and point
./deploy.sh at your server:
HOST=example.org DEPLOY_USER=pages ./deploy.shRelated MCP server: Aigist MCP
HTTP API
All endpoints take Authorization: Bearer <token>.
Method | Path | Purpose |
|
| Create or replace a page |
|
| List all pages ( |
|
| Metadata ( |
|
| Delete a page |
PUT refuses with 409 when the slug is taken, and names who created it — so
one teacher cannot quietly overwrite another's lesson. Pass overwrite: true
to replace it anyway.
MCP
The MCP endpoint is POST /mcp (Streamable HTTP, stateless), authenticated
with the same tokens. It exposes four tools: deploy_page, list_pages,
get_page and delete_page.
{
"mcpServers": {
"pages": {
"type": "http",
"url": "https://pages.example.org/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}Handing out tokens
There is no user management, no login and no self-service. A token is a random string an administrator generates and gives to one teacher:
openssl rand -hex 32Add it to PAGE_HOST_TOKENS as name:token, comma separated, and restart the
container. The name appears in meta.json as createdBy, in the audit log,
and in the 409 message another teacher sees — so use something recognisable,
not user1.
To revoke access, delete the entry and restart. Pages created with that token stay where they are; only the ability to deploy is withdrawn. Two names must never share a token, or the audit log will name the wrong person; the service refuses to start in that case.
Tokens belong in .env on the server, chmod 600, never in the repository.
The CSP trade-off
Pages are served with a Content-Security-Policy that includes
script-src 'unsafe-inline'. This is a deliberate decision and the weakest
point of the design, so it is worth stating plainly.
The format of this service is one self-contained file: the JavaScript that
makes a page interactive sits in a <script> tag inside the document. There is
no build step that could hash or nonce those scripts, because introducing one
would mean introducing a bundler, a second file and a toolchain — everything
this service exists to avoid. So inline script has to be allowed.
What that means in practice: if a generated page contains hostile JavaScript, the CSP will not stop it from running. The mitigations are elsewhere:
A separate origin. Pages are served from their own hostname, which shares no cookie with any other system. A page cannot read a session from a wiki, a timetable or a learning platform, because none of those cookies are valid for this domain. This is the mitigation that actually matters — keep it.
A short CDN allowlist.
PAGE_HOST_CDN_HOSTSdecides which other hosts may execute code. Every entry is a host trusted with a student's browser, so the default list is two well-known CDNs and it should stay short.object-src,base-uriandform-actionare'none'. Each of these removes a way to turn a generated page into a redirector or a credential collector, and none of them costs anything for a self-contained page.frame-ancestors 'self'by default, so a page cannot be embedded elsewhere. Widen it viaPAGE_HOST_FRAME_ANCESTORSif pages should appear inside a wiki or Moodle.X-Robots-Tag: noindex, nofollow, because generated lesson pages do not belong in search results.
The application is the single source of truth for these headers. The example
vhost adds Header setifempty for the three simple ones as a fallback, but
deliberately not for the CSP: Apache's config parser treats single quotes as
quoting characters, so 'self' arrives as bare self — invalid CSP syntax —
and two CSP headers are enforced as the intersection of both policies.
Note the missing always on those setifempty lines. It looks like the safer
spelling, but always writes to a different header table than the one a proxied
response lands in, so setifempty never sees what the backend sent and every
response goes out with the header twice.
The GitHub mirror
With PAGE_HOST_MIRROR_ENABLED=true, every deploy and delete is committed and
pushed to a private git repository. The data volume is a cache; the mirror is
the source of truth. Losing the volume is not an incident — the next start
clones it back.
Access uses an SSH deploy key, not a personal access token: a deploy key is scoped to one repository and cannot be used against the account.
ssh-keygen -t ed25519 -f ./mirror_deploy_key -N "" -C "page host mirror"
# Add mirror_deploy_key.pub to the repo under
# Settings → Deploy keys → Add deploy key, with "Allow write access" ticked.Put the private key on the server and mount it read-only into the container —
the mount is commented out in docker-compose.yml — then
point PAGE_HOST_GIT_SSH_KEY at it.
A bind mount keeps the host's ownership, and the container runs as uid 10001,
so the key file has to belong to 10001, not to the account you deploy with.
ssh refuses a key it cannot read, and the mirror then disables itself with
Load key …: Permission denied in the log:
sudo install -d -o 10001 -g 10001 -m 700 /opt/<app>/secrets
sudo install -o 10001 -g 10001 -m 400 mirror_deploy_key /opt/<app>/secrets/Keep server-specific mounts in an untracked docker-compose.override.yml next
to the compose file; deploy.sh excludes it from the rsync so it survives a
redeploy.
Make the mirror repository private. A public one would undo the noindex
header, because GitHub lets its contents be indexed. And note that pages leave
your infrastructure when they are mirrored: generated pages must not contain
personal data.
What happens at start-up
Volume | Mirror | Result |
has | — |
|
empty | has commits |
|
has pages | empty |
|
has pages | has commits | refuses, mirroring off, pages still served |
The last row is the ambiguous one, and nothing is guessed: no reset --hard,
no --force, in either direction. Reconcile the two by hand once and restart.
The commit is synchronous and local — that is what protects the previous
revision. The push waits up to PAGE_HOST_PUSH_WAIT_MS and then retries in the
background, so GitHub being slow never turns into a failed deploy of a page
that is already live. A response with mirrored: false means "live and
committed locally, not yet at GitHub", never "lost".
Restoring
The mirror is an ordinary git repository, so recovery needs no tooling:
# The whole service: delete the volume and restart. Start-up clones it back.
docker compose down && docker volume rm <project>_pages-data && docker compose up -d
# One page, from before it was changed or deleted:
git clone git@github.com:your-org/your-pages-mirror.git
git -C your-pages-mirror log --oneline -- pages/physics-pendulum
git -C your-pages-mirror show <commit>:pages/physics-pendulum/index.htmlThen redeploy that HTML through deploy_page or PUT, rather than writing into
the volume by hand.
Security notes
The container runs as an unprivileged user with a read-only root filesystem, all capabilities dropped,
no-new-privileges, and memory, PID and CPU limits.It publishes only on the host's loopback interface. TLS belongs to the reverse proxy.
Slugs match
^[a-z0-9][a-z0-9-]{2,59}$, and the resolved path is checked against the pages root a second time. Onlyindex.htmlis ever served, always astext/html; charset=utf-8;meta.jsonreturns 404, because it carries the teacher's name. Symlinks are refused, so a symlink committed to the mirror cannot be used to read the volume.Pages are limited to 2 MiB and writes to 30 per token and hour.
Every deploy and delete is logged with timestamp, token name and slug. Page content is never logged.
Development
npm install
npm test # unit tests, HTTP API, static serving, MCP, mirror
npm run dev # tsx watch, needs PAGE_HOST_TOKENS and PAGE_HOST_DATA_DIR
npm run typecheckThe mirror tests use a local bare repository as a stand-in for GitHub, so the suite needs neither network access nor a deploy key.
Not in scope
No build step, no multi-file deployments, no web UI, no database, no user management, no analytics, no server-side rendering. If a change starts to need one of these, it probably belongs in a different project.
License
MIT — see LICENSE.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Publish and manage existing HTML presentations from an MCP-capable Agent.
Deploy HTML from any agent: POST markup, get a live URL. Static hosting API with MCP tools.
Publish HTML, files, or a URL to a permanent public URL, then update it — from any MCP agent.
Publish HTML, Markdown, and multi-file sites as shareable URLs instantly via MCP.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables publishing, updating, and sharing HTML artifacts with strict security isolation (origin separation, CSP, API keys) via MCP tools.1-
- AlicenseNot gradedqualityCmaintenancePublish and manage shareable HTML/Markdown pages with access control and comments via MCP clients.MIT
- FlicenseNot gradedqualityBmaintenanceEnables sharing self-contained HTML files via public or access-key-protected private links. Provides MCP tools to create shares, retrieve public share metadata, and describe the service.3-
- AlicenseAqualityBmaintenanceMCP server for publishing HTML or Markdown to a live hosted URL via htmldrop. Provides tools to publish, list, and delete hosted sites, with remote OAuth and API token authentication.3136MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/BZZ-Public/mcp-page-host'
If you have feedback or need assistance with the MCP directory API, please join our Discord server