open-artifacts
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.comWhat 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.dbgives 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:8787Open 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=productionARTIFACT_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 testAn 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 /mcpDASHBOARD_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.
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 nginxDNS: 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 originTwo 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 |
|
| |
|
| Replaces the document |
|
| Merges into it |
|
| |
|
| |
|
| The viewer, or |
|
| 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 fileData lives in data/artifacts.db (SQLite, WAL). Back it up by copying that directory.
License
MIT — see LICENSE.
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/xfwil/open-artifacts'
If you have feedback or need assistance with the MCP directory API, please join our Discord server