Skip to main content
Glama
xfwil

open-artifacts

by xfwil

Open Artifacts

A self-hosted take on Claude Artifacts, open to any agent. Your AI agent writes an HTML page, publishes it over MCP, and hands you a shareable URL. The page is not a screenshot — it can store data, recognise who is viewing it, and save new versions of itself.

Built because Artifacts is a good idea that shouldn't require using one particular assistant. Works with opencode, Claude Code, Cursor, or anything else that speaks MCP.

you ──"make me a poll"──> agent ──MCP──> server ──> https://poll.art.example.com

What you get

  • Publish from your agent. Seven MCP tools: publish, update, list, read, delete, plus read/write of a page's stored data. No upload button anywhere — the agent does it.

  • Pages that hold state. window.artifact.db gives a page a small shared database, so a poll, a checklist or a sign-up sheet actually remembers what people did.

  • Pages that know who's looking. window.artifact.user() returns the viewer's Discord identity, once they consent. Per-viewer data stays private to that viewer.

  • Pages that rewrite themselves. A page can save a new version of itself, and every version is kept and restorable.

  • Real isolation. Each artifact is served from its own subdomain under a CSP that blocks its own network access, so one page can never read another's data.

  • Discord sign-in, API tokens per agent, and private / unlisted / public links.

Stack: Node + Express, SQLite via node:sqlite (no database dependency), React + Vite dashboard. Three runtime dependencies in total.


New here? Start with docs/USAGE.md — how to connect an agent and what to ask it.

AGENTS.md is what the agent itself reads: the rules a page has to follow and a worked example. The UI brief is docs/FRONTEND_BRIEF.md.

Running it

npm install
npm run build     # builds the dashboard into dist/
npm start         # http://localhost:8787

Open http://localhost:8787, sign in, then go to Connections to create a token and copy the MCP snippet into opencode, Claude Code or Cursor.

With no Discord app configured, /auth/discord signs you into a local account so the server is usable out of the box. Configure Discord for real accounts:

DISCORD_CLIENT_ID=…      # OAuth2 app credentials
DISCORD_CLIENT_SECRET=…  # redirect URI: <DASHBOARD_ORIGIN>/auth/callback
SESSION_SECRET=…         # otherwise generated once into data/.session-secret
ARTIFACT_HOST=art.example.com
PROTOCOL=https
NODE_ENV=production

ARTIFACT_HOST needs a wildcard DNS record and certificate (*.art.example.com), because every artifact is served from its own subdomain. Locally *.localhost resolves without any setup.

Tests

npm test

An integration suite on Node's built-in runner — no framework, no fixtures. It boots the real server against a temporary database and drives it over HTTP, covering the things that would otherwise break quietly: MCP publishing and token auth, version history and restore, subdomain isolation and the CSP, visibility enforcement, and every authorisation rule in the runtime broker (capability gating, per-viewer data privacy, anonymous read-but-not-write).

Working on the frontend

DASHBOARD_ORIGIN=http://localhost:5199 npm run server  # terminal 1 — API on :8787
npm run dev                                            # terminal 2 — Vite on :5199, proxies /api /auth /mcp

DASHBOARD_ORIGIN has to match the origin the dashboard is actually served from, since it is both the frame-ancestors value and the origin the runtime bridge posts to.

Related MCP server: webserver-mcp

Deploying

Files are in deploy/. Order matters: get it working over HTTP first, then add TLS.

git clone … /srv/artifacts && cd /srv/artifacts
npm ci && npm run build
cp .env.example .env      # fill in Discord credentials and SESSION_SECRET

pm2 start deploy/ecosystem.config.cjs   # edit ARTIFACT_HOST in it first
pm2 save && pm2 startup

sudo cp deploy/nginx.conf /etc/nginx/sites-available/artifacts
sudo ln -s /etc/nginx/sites-available/artifacts /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

DNS: two records, both to the server — art.example.com and *.art.example.com.

TLS: deploy/nginx.conf contains no ssl_* directives on purpose; certbot writes those. But certbot --nginx alone will not work here — a wildcard certificate cannot be issued over HTTP-01, and artifacts need one. Use a DNS-01 challenge:

sudo certbot certonly --preferred-challenges dns --manual \
  -d art.example.com -d '*.art.example.com'

Then point nginx at the certificate (re-run with --nginx, or add the ssl_certificate lines yourself) and set PROTOCOL=https so cookies are marked Secure.

Discord: add https://art.example.com/auth/callback to the app's OAuth2 redirect URIs.

Two nginx settings are load-bearing and easy to lose in a rewrite: proxy_set_header Host $host (without it every subdomain collapses into one artifact) and client_max_body_size 12m (the default 1 MB rejects publishes with a bare 413).

How it fits together

agent --MCP--> server --stores--> SQLite
                  |
                  +--> art.example.com          dashboard (session cookie, REST API)
                  +--> <id>.art.example.com     the artifact page, its own origin

Two origins, deliberately. If every artifact shared one origin, one page could read another's localStorage. The artifact origin serves the page and nothing else — no API, no session, and a CSP that blocks its own network access.

Because the page therefore cannot call the server, window.artifact.* posts messages to the dashboard window, which replays them against the API using the viewer's own cookie. A page opened directly, with no dashboard parent, forwards itself to the viewer route.

Runtime API

Available to a published page when the artifact declares the matching capability.

Call

Capability

Notes

artifact.db.get(collection, id)

db

artifact.db.set(collection, id, data)

db

Replaces the document

artifact.db.update(collection, id, data)

db

Merges into it

artifact.db.list(collection, limit?)

db

artifact.db.delete(collection, id)

db

artifact.user()

user

The viewer, or null when signed out

artifact.publish(html, label?)

self_publish

Saves a new version; owner only

Collections under data/users/me/… resolve to the caller's own id and are private to them. Everything else is shared by every viewer. Reading shared data works signed out; writing needs an account.

Viewers are asked for consent once per artifact before any of this runs.

Limits

There is no asset store. Pages inline images as data: URIs and load fonts and libraries from an allowlist (cdnjs, jsDelivr, the Tailwind play CDN, code.jquery.com, Google Fonts). A page may be up to 10 MB — override with MAX_HTML_BYTES.

Layout

server/
  index.js    express wiring and Discord OAuth routes
  config.js   hosts, origins, subdomain parsing
  db.js       node:sqlite schema and queries
  auth.js     sessions, API tokens, Discord exchange
  routes.js   dashboard REST API and the runtime broker
  serve.js    artifact subdomain serving, CSP, bridge injection
  mcp.js      MCP tools for agents
src/
  App.tsx     the whole dashboard
  app.css     design tokens and styles
  lib/        API client, contract types, formatters
test/         integration suite, run with `npm test`
deploy/       nginx site and PM2 process file

Data lives in data/artifacts.db (SQLite, WAL). Back it up by copying that directory.

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

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

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Publishes HTML pages straight from your AI assistant to a shareable URL, then lets you manage them - update, list, search, fetch, and delete pages in a public or private workspace. Turns "share what I just made" into a single tool call from Claude, Cursor, or any MCP client.
    MIT

Latest Blog Posts

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/xfwil/open-artifacts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server