fvtt-mcp-molten5e
# fvtt-mcp-molten5e
A **D&D 5e–only**, **[Molten Hosting](https://moltenhosting.com)–optimized** [Model Context
Protocol](https://modelcontextprotocol.io) server for [Foundry VTT](https://foundryvtt.com),
driven by **Claude Code**. It lets an AI GM assistant read and edit a live Foundry world (actors,
items, journals, scenes, compendia, roll tables, cards…) and manage a Molten-hosted server's
static files.
Paired with its bundled **skills**, it goes well beyond CRUD: it can author a **complete, table-ready
adventure end to end** — scene, monsters, NPCs, a pregen PC or party, treasure, linked journals, and
roll tables — scaled to however much the DM provides. Hand it **only a map image** and Claude reads the
map and builds the whole module; hand it **your own finished module** and it faithfully recreates every
stat block, item, and handout in the VTT.
**An importer worth calling out:**
- **Adventure map packs → your world.** Import a battlemap *module* (a distributed Foundry scene-pack
with its own compendiums) faithfully: every scene with its walls, lights, day/night mood, and
navigation thumbnails, plus the journal of map keys — with all assets re-pointed into your world, and
cross-version (older and newer Foundry formats) handled. **[Tom Cartos](https://www.tomcartos.com)**
packs are the first supported format; more to come.
---
> 📐 **Design north star — [`design.md`](design.md).** The mission, scope, the *skills decide, tools
> do* contract, and the NPC authoring doctrine all live there; it's the document every skill, tool,
> and refactor traces back to. **🚧 Still under construction** — actively evolving alongside the
> project, so expect it (and the tool surface) to change.
## Why this shape
Managed Foundry hosts (like Molten) don't expose a general control API and you can't run a process
next to the game server. The only supported way in is Foundry's own authenticated client.
So the MCP server drives a **headless Chromium** client (via [Playwright](https://playwright.dev)):
it wakes the (sleeping) Molten box with the **Magic URL**, joins the world as a dedicated Foundry user, waits for `game.ready`, and injects a page-side library that exposes the
world's own client APIs. Claude Code talks to the MCP server over stdio; the server turns each tool
call into a call inside that live page.
```
Claude Code ──stdio──> MCP server (dist/index.js, on your PC)
│ Playwright → headless Chromium (src/foundry.ts)
▼
Headless Foundry client
(wakes the box, joins the live world as a dedicated GM user)
│ the world's own client APIs (window.__fvtt)
▼
Foundry VTT world (Molten-hosted)
```
The headless client connects **lazily**: `tools/list` answers without touching Foundry, and the
first actual tool call is what wakes the box and joins the world. The whole tool tree depends on one
seam — `foundry.call(name, args)` — and only `src/foundry.ts` ever imports Playwright.
### Two-plane model
- **Plane A — the live bridge.** World documents (actors, items, journals, scenes, compendia, roll
tables, cards, ownership). Goes through the headless Foundry client while the server is awake — the
**only** safe way to read/write live world data.
- **Plane B — Molten files.** Talks to Molten's own file endpoints directly (no bridge):
upload/serve static assets over WebDAV and map `Data/`-relative paths to public URLs.
**Safety rule baked in:** a running world's database (LevelDB stores under `Data/worlds/<world>/data/`)
must **never** be written over the file channel — that corrupts it. Plane-B file ops are restricted
to static assets and refuse world-DB paths; bulk DB edits are an offline-only flow (stop → Create
Backup → `fvtt unpack` → edit → `fvtt pack` → start, via
[foundryvtt-cli](https://github.com/foundryvtt/foundryvtt-cli)). The Molten **management panel is
never scripted** (their ToU forbids it); only the Magic-URL wake, WebDAV, and the Foundry server are
automated.
## Scope
**In scope:** actors — both **NPCs** and full leveled **PCs** — items, journals, scenes (the scene **document** _and_ its **placeables** —
walls, lights, tokens, regions/teleporters, ambient sounds, tiles, drawings, map notes), playlists,
roll tables, cards, macros, combat-tracker config, compendium manipulation — especially **pulling**
content out ("make an actor from the MM owlbear") — and asset upload. With the bundled skills these
compose into **end-to-end adventures** — from **reading a provided map image** to drive a scene and
everything in it, to **importing a distributed battlemap module** (e.g. Tom Cartos scene-packs)
faithfully into your world. Authoring prefers the **2024** dnd5e data model, sourced from
**PHB / DMG / MM**; if the requested content isn't in those packs the tool says so rather than
inventing it.
**Out of scope (for now):** non-5e game systems; **live session assistance** — monitoring a running
game and interjecting during play (live chat, running the monsters' combat turns) is the next phase
(see [`design.md`](design.md) §8), not built yet; AI **map-image** generation (Claude reads a
*provided* map, it does not draw one); scripting the Molten management panel. (Scene placeables —
walls, lights, tokens, regions — _are_ authored and edited as scene contents; what's out of scope is
driving them live on the canvas during a running session.)
**Removed deliberately: D&D Beyond import.** DDB character exports strip the embedded effect
automation the premium compendium items carry, so an imported PC looks right and silently fails at
the table. Ask for the character instead and it's built **natively from the premium books**, using
the DDB sheet only as a reading reference.
---
## Repository layout
```
src/
index.ts MCP server entry (stdio) — serves the registry's tools over JSON-RPC
registry.ts single source of truth: tool name → handler (advertised list derived from it)
foundry.ts THE Playwright seam: launch headless Chromium → wake → join → inject → call()
config.ts env/config loader (reads .env from the repo root)
tools/ MCP tool classes — Plane A world tools + molten/ (Plane B WebDAV file tools)
page/ page-side domain library, bundled into dist/page.bundle.js and injected
scripts/ dev/maintenance scripts (verify-*.mjs live acceptance, spike-headless)
tests/ gated live integration suites (offline unit tests live beside the code in src/**)
```
## Requirements
- **Node.js 22+** (developed/tested on Node 24; see `.nvmrc`; CI runs 22 + 24). On Windows, if Node
isn't on `PATH`, use the full path to `node.exe` (see wiring below).
- A **Chromium for Playwright** — `npx playwright install chromium` (Playwright is a devDependency;
the headless bridge drives this browser).
- **Foundry VTT 14.x** with the **D&D 5e** system, hosted on **Molten**, plus a dedicated
**passwordless Foundry user** for the MCP to join as.
## Build
```bash
npm install
npx playwright install chromium # one-time: the headless browser the bridge drives
npm run build # tsc → dist/, then esbuild bundles the in-page library
```
`npm run build` runs `tsc && node esbuild.page.mjs`: TypeScript compiles `src/**` to `dist/`, then
esbuild bundles the page-side library (`src/page/**`) into `dist/page.bundle.js` for injection.
Tests: `npm test` (offline unit suite on vitest). Live integration suites are gated — see
[`vitest.integration.config.ts`](vitest.integration.config.ts) and `npm run test:integration`.
> **Dev watch:** `npm run dev` rebuilds the page bundle once, then runs `tsc --watch` for `src/**`.
> Because the page library is a **separate** esbuild artifact, editing anything under `src/page/**`
> while developing needs `npm run dev:page` (esbuild `--watch`) alongside it — otherwise the running
> server keeps injecting the stale `dist/page.bundle.js`.
## Wire into Claude Code
Register the built MCP server in your Claude Code config. Copy
[`.mcp.json.example`](.mcp.json.example) to a `.mcp.json` Claude Code reads (project-scoped, or your
`~/.claude.json` `mcpServers`) and set **absolute** paths:
```json
{
"mcpServers": {
"foundry-molten5e": {
"command": "C:/Program Files/nodejs/node.exe",
"args": ["C:/path/to/fvtt-mcp-molten5e/dist/index.js"]
}
}
}
```
- Use an **absolute** path to the root `dist/index.js` (Claude Code may launch the server from any
directory).
- On Windows, point `command` at the full `node.exe` path if Node isn't on `PATH`.
- The server loads its `.env` from the repo root regardless of working directory.
- The headless client connects lazily — the first tool call wakes the Molten box and joins the
world, so the initial call after a cold box can take a while.
## Configuration
Copy [`.env.example`](.env.example) to `.env` (gitignored) and fill in your instance:
- **Non-secret, per-instance:** `MOLTEN_SERVER_URL`, `MOLTEN_WORLD_ID`, `MOLTEN_WEBDAV_URL`,
`MOLTEN_FILEBROWSER_URL`, `FOUNDRY_USER` (the dedicated passwordless user to join as; defaults to
`MCP-Claude`). The committed defaults are neutral `your-server`/`your-world` placeholders.
- **Wake (optional but recommended):** `MOLTEN_MAGIC_URL` — Molten's "Server Startup / Magic URL"
(`…?s=token`), GET to wake a sleeping box before joining.
- **Secrets (never commit — env only):** `MOLTEN_WEBDAV_PASSWORD` (upload-asset / asset file ops),
`MOLTEN_ADMIN_KEY`. Read them from your Molten panel → Server Details. Each tool reports which
variable to set if its secret is missing.
## Tools
**149 tools total: 139 over the headless bridge (Plane A) + 10 Molten WebDAV file tools (Plane B).**
Plane A (bridge) covers world introspection and editing — actors, items, compendium search,
journals & quests, scenes **and their placeables** (walls, lights, tokens, regions/teleporters,
ambient sounds, tiles, drawings, notes), **who-sees-what routing** (the one active scene, pulling
connected users to a side scene, and per-user landing scenes for where players come up at login —
core Foundry has no such thing, so `set-landing-scene` writes a flag the companion
[`fvtt-mod-openserver`](https://github.com/Txpple/fvtt-mod-openserver) module acts on, and warns
when that module is absent rather than claiming success),
roll tables, cards, playlists, **per-scene atmospheric sound sets** (`configure-soundscape`, for the
companion [`fvtt-mod-soundscape`](https://github.com/Txpple/fvtt-mod-soundscape) module — randomized
one-shots with silence between them, or crossfaded ambient beds, which neither AmbientSound
placeables nor Playlists can express), ownership,
folders/organization, macros, combat-tracker config, and 5e-specific helpers (NPC creation,
**PC building & leveling**, feature/spell granting, structured inventory/loot
authoring), **full-fidelity actor JSON export** (`export-actor`), and **per-combat session
analytics** (`get-combat-stats`, folded from the companion
[`fvtt-mod-battleflow`](https://github.com/Txpple/fvtt-mod-battleflow) module's stat stamps),
**plus the asset-composition + reference-integrity tools**. Plane B (Molten WebDAV) is the
asset file library.
**Plane B — Molten file tools (WebDAV):**
| Tool | What it does |
| --------------------- | -------------------------------------------------------------------------------- |
| `list-assets` | List a directory under `Data/` (folders + files, with size/type/public URL) |
| `asset-info` | Existence + size/type/mtime/public URL for one path under `Data/` |
| `download-asset` | Download a file from under `Data/` to a local path |
| `upload-asset` | Upload a local file under `Data/` (auto-creates parents; refuses world-DB paths) |
| `upload-asset-tree` | Recursively upload a local directory tree under `Data/` (preserves layout) |
| `create-asset-folder` | Create a folder (and missing parents) under `Data/` (idempotent) |
| `delete-asset` | Delete a file (reference-aware; refuses if still used unless `force`) |
| `move-asset` | Move/rename a file (refuses or relinks references; `relink`/`force`) |
| `copy-asset` | Copy a file under `Data/` |
| `asset-url` | Map a `Data/`-relative path to its public HTTPS URL (pure, no network) |
**Plane A — asset composition + reference integrity (bridge):**
| Tool | What it does |
| ----------------------- | ---------------------------------------------------------------------------- |
| `find-asset-references` | Find every scene/actor/journal/playlist/… that references an asset path |
| `relink-asset` | Rewrite all references from one asset path to another (`dryRun` supported) |
| `create-playlist` | Create a Playlist from sound paths (the flagship "upload → playlist" wiring) |
| `create-scene` | Create a Scene from a background image path |
| `update-scene` | Update a scene's fields, including swapping its background image |
| `set-actor-art` | Set an actor's portrait (+ prototype token) from an image path |
| `add-journal-image` | Append an image page to a journal entry |
The remaining Plane A tools cover world CRUD (`create-actor-from-compendium`/`author-npc`, `add-feature` (features / compendium
features / spells), `import-item` (copy a real PHB/DMG item — art + stats — onto an actor or the
sidebar), `add-item` (author structured weapons/armor/consumables/loot/containers), `create-item`,
`create-journal`/`create-quest-journal`, `create-rolltable`, `create-cards`, …), listing/search
(`list-actors`, `search-compendium`, `list-journals`, …), and organization (`create-folder`,
`move-documents`, `bulk-delete`). See the `handlers` map in [`src/registry.ts`](src/registry.ts) for the full dispatch table.
> Plane B file ops run over WebDAV (need `MOLTEN_WEBDAV_PASSWORD`, work whenever the VM is awake).
> Plane A tools run over the headless bridge (need the world joined). Write tools refuse live
> world-DB paths; destructive file ops consult `find-asset-references` first.
## Security
- **Outbound-only, nothing public.** The server and the headless browser run on your machine and make
only outbound connections (to Foundry on Molten, and to Anthropic); nothing listens for inbound
traffic, and the headless client authenticates to Foundry exactly as a normal user would.
- **Secrets stay in `.env`** (gitignored), with tight file perms — never commit `MOLTEN_WEBDAV_PASSWORD`,
`MOLTEN_ADMIN_KEY`, or your Claude token. Errors name the missing variable, never its value.
- **Treat all agent inputs as untrusted** (chat, transcripts, web) — prompt-injection can ride in.
Plane-A writes are inherently safe because they go through Foundry's own client APIs; Plane-B
destructive file ops are reference-aware, refuse live world-DB paths (canonicalized, `..`-rejecting),
and deletes resolve strictly (exact id/name, no fuzzy match).
- **Anything under `Data/` is served publicly over HTTPS with no auth** — don't upload anything
sensitive.
## Contributing
The project is one package: a Node-side MCP server (`src/`) that drives a headless Foundry page
through the `foundry.call(name, args)` seam, plus a page-side library (`src/page/**`, bundled into
`dist/page.bundle.js` and injected as `window.__fvtt`). Adding a tool touches both halves:
1. **MCP tool class** (`src/tools/<category>.ts`) — declare the input contract **once** as a hoisted
zod schema; `getToolDefinitions()` returns `{ name, description, inputSchema: toInputSchema(schema) }`
(the advertised JSON Schema is **generated** from that zod via `src/utils/schema.ts` — never
hand-written), plus a `handleX(args)` that `schema.parse`es and calls `foundry.call('<op>', data)`.
2. **Register it** (`src/registry.ts`) — instantiate the class, add its `getToolDefinitions()` to the
collected definitions, and add a `'<tool-name>': args => tool.handleX(args)` entry to the `handlers`
map. The advertised tool list is **derived** from `handlers`, so a handler with no matching
definition fails loudly at startup (`src/tools/registry.test.ts` guards the surface).
3. **Page-side op** (`src/page/<domain>.ts`) — implement `<op>(args)` and register it in
`src/page/index.ts`. This runs **inside** the live Foundry page (the actual `Document.create` /
`update` / `delete`): import only browser + Foundry globals here, never Node/Playwright.
4. **Build + verify** — `npm run build`, then `npm test`, `npm run typecheck`, `npm run knip`, and
biome (`npm run check`). For live changes, `npm run test:integration` against a real world.
---
## Support
Issues: [GitHub Issues](https://github.com/Txpple/fvtt-mcp-molten5e/issues)
## Acknowledgments
Used as a reference:
[adambdooley/foundry-vtt-mcp](https://github.com/adambdooley/foundry-vtt-mcp) by Adam Dooley.
## License
MIT License — see [LICENSE](LICENSE) for details.
TDQS
Scored across 130 tools
Most CRUD families are clearly separated, and many descriptions explicitly steer agents (e.g. prefer import-item over add-item). However, there are several near-overlapping families—add-item/create-item/import-item, get-actor/get-actor-entity/search-actor-contents, asset-info/asset-url, and the multiple PC/NPC creation tools—that create real selection ambiguity despite helpful descriptions.
The set overwhelmingly follows a consistent verb_noun pattern (create/list/update/delete/get/search/set) with no camelCase/snake_case mixing. Minor singular/plural inconsistencies like update-token vs delete-tokens and list-notes vs update-note, plus the overloaded use of 'item' for both world Items and actor gear, prevent a perfect score.
At 130 tools, this far exceeds the 50+ extreme threshold and is the dominant coherence problem. Even though Foundry VTT is a broad domain, an agent cannot reasonably hold this many tools in context, and many per-placeable CRUD families could be consolidated without losing capability.
The surface covers nearly the full authoring lifecycle: actors, items, journals, scenes, placeables, assets, playlists, rolltables, cards, chat, folders, ownership, and compendium import. Minor gaps such as no playlist-track management, no update/draw/shuffle for cards, and no direct editing of region behaviors keep it from a 5.