Run campaigns as game master in CozyVTT, a self-hosted virtual tabletop: dice, chat narration, tokens and maps, initiative, character sheets and rulebooks.

ماذا يمكنك أن تفعل مع Cozyvtt MCP؟

  • Session/campaign: campaign_get (role and owner reported separately; new capability keys may be unknown), session_manage, session_list, session_notes_update, campaign_transfer_dm (owner reclaim uses the same tool), map_list, map_switch
  • Narration: chat_send (DM / PLAYER), chat_read
  • Dice: dice_roll (server-authoritative results; is_secret=true (wire field: secret) delivers to the roller and DMs on v1.4.0), events_poll (recent buffered dice events, not durable history; DICE_ROLL events are absent from chat history)
  • Tokens/maps: token_add (integer sizes 1..10), token_move, token_hp_update, token_place_creature, token_delete, map_create, map_delete, creature_search (SRD + custom library).
  • Combat: initiative_manage (add / remove / roll / set / reorder / start / next / end), initiative_read (note: CoC7e initiative is DEX-ordered, no roll — this is upstream rules behavior, and roll is gated accordingly)
  • Characters: character_list, character_get, character_create, character_delete, character_validate, character_update (rules math is done by the agent; the bridge just writes values)
  • Documents: document_upload, document_create, document_list, campaign_document_list, document_read, document_update, document_share, document_unshare, document_delete
  • Saved Rolls: saved_roll_list, saved_roll_create, saved_roll_update, saved_roll_delete (private to the current user and campaign; 50 macros per user/campaign, server-validated expressions). saved_roll_list returns complete macros — there is no ...

التثبيت

خطوات التثبيت موجودة في التوثيق أدناه. التوثيق

التوثيق

مأخوذ من ملف README للمشروع على GitHub ويُحدَّث أسبوعيًا. تفتح الروابط على GitHub.

المحتويات
  1. cozyvtt-mcp
  2. Upgrading
  3. Tool sets
  4. Compatibility
  5. Features
  6. System gating
  7. Architecture
  8. Result and update contracts
  9. Requirements
  10. Install
  11. Configuration
  12. Testing
  13. Troubleshooting
  14. License
  15. Links
  16. Ecosystem

cozyvtt-mcp

English | 中文

MCP (Model Context Protocol) bridge for CozyVTT — the self-hosted, open-source virtual tabletop. It lets an AI agent join a campaign as DM/KP: narrate over chat, roll server-authoritative dice on the server, move tokens, switch maps, manage initiative, and settle character sheets.

Built for and tested with Hermes Agent, but works with any MCP client (stdio transport).

Upgrading

This README describes how to install and use the current release; the change history lives in CHANGELOG.md. Two migration points are worth knowing before you wire up a client:

  • Three tools were renamed in 0.3.0: campaign_status → campaign_get, initiative_state → initiative_read, token_hp → token_hp_update. No aliases are registered — update client tool selections. Old-to-new mapping.
  • chat_read paginates with limit/cursor (the offset argument was removed in 0.2.0).

The default tool set is every tool; optional presets are described under Tool sets.

Tool sets

The default is every tool. Optional registration-time filtering drops excluded tools from tools/list entirely; it changes nothing about how the remaining tools behave. Choose comma-separated presets (union); duplicates are accepted and all wins. The flag overrides COZYVTT_MCP_TOOLSETS. Empty values or unknown names fail with valid preset names and counts. --list-toolsets prints all memberships and the default, then exits successfully without connecting to CozyVTT.

PresetToolsSelection
play20campaign_get, chat_read, chat_send, creature_search, dice_roll, events_poll, initiative_manage, initiative_read, map_create, map_delete, map_list, map_switch, session_list, session_manage, session_notes_update, token_add, token_delete, token_hp_update, token_move, token_place_creature
docs9All document_* tools plus campaign_document_list
roster7All character_* tools
macros4All saved_roll_* tools
admin1campaign_transfer_dm
all (default)41Every tool
.venv/bin/python server.py --toolsets play,macros
COZYVTT_MCP_TOOLSETS=docs,roster .venv/bin/python server.py
.venv/bin/python server.py --list-toolsets

For an MCP client, append "--toolsets", "play" to its server args, or set COZYVTT_MCP_TOOLSETS in its server env.

Measured with .venv/bin/python scripts/measure_tool_budget.py (offline stdio). Bytes sum UTF-8 JSON tool definitions, excluding JSON-RPC envelope/list separators; tokens are estimates (bytes // 4), not tokenizer measurements.

PresetJSON bytesEstimated tokensSavings vs all
play30,9257,73148%
docs13,4183,35477%
roster9,1522,28885%
macros4,6641,16692%
admin1,19929998%
all59,35814,8390%

The surface also includes map_create, map_delete, token_delete, and character_delete. Create maps from existing image assets; use map_switch before deleting the current map. token_delete removes a map token; token_hp_update changes sheet HP using a character ID. character_delete permanently deletes an owned sheet. Add roster or docs when a session needs those tools.

Compatibility

cozyvtt-mcpCozyVTTNotes
0.6.1v1.5.3 (1dd6bcc)Tooling-only patch: configurable smoke pins and offline deployment-evidence validation. 503 offline tests on Python 3.12.13, including the v0.6.0 contract coverage; 41 runtime tools and parameter schemas unchanged. No new live campaign run performed for this patch.
0.6.0v1.5.3 (1dd6bcc)Offline review of v1.5.0→v1.5.3 (REST/WS docs + implementation): scope-aware document edits, Keeper-note filtering, newer input limits, conservative dice throttling, campaign.access.revoked. 456 offline tests on Python 3.12.13; 41 tools and parameter schemas unchanged. No live v1.5.3 campaign run; v1.5.0 remains the last live-smoked baseline.
0.5.0v1.5.0 (b03e287)Exact pinned baseline. 379 offline tests on Python 3.12.13 (89 adaptation + 62 helper + 228 historical); recorded 2026-10-04 15:44 CST DM read-only REST/passive WS smoke: 16/16 checks, post-cleanup teardown and observed exit 0. Deployment identity is operator-attested.
0.4.0v1.2.2 / v1.4.0Dual baseline. Tool-set presets plus map_create/map_delete/token_delete/character_delete. Offline contract suite 228/228; the Dockerfile was replayed inside the real python:3.12-slim rootfs (hash-checked layers) and boots with no environment set, listing 41 tools. No live campaign run, and no OCI image was built (no Docker daemon on the build host).
0.3.0v1.2.2 / v1.4.0Dual baseline. Parameter descriptions, MCP annotations, container image. Offline suite 176/176; image built and booted (37 tools, no environment). No live campaign run.
0.2.0v1.2.2 / v1.4.0Dual baseline: retain original tools; new REST routes degrade explicitly on old instances. Offline suite 176/176; live v1.4.0 smoke (read/write + Documents/Saved Rolls round-trips) passed 2026-09-17.
0.1.1v1.2.2Previous 20-tool release

The recorded DM run used CALL_OF_CTHULHU_7E in PREPARATION: 22 requests (1 login + 21 GETs), one passive WS connection, and only authenticate / initiative.request_state emits. No live campaign writes, conflict-write roundtrip, multi-user visibility or secret-recipient isolation were tested. Optional character/document checks were skipped (zero character sheets/readable documents); session, Saved Roll and custom creature lists were empty. This is an explicit version pin, not a v1.5.x/future-compatibility promise; older compatibility rows remain historical evidence only. See CHANGELOG for the final verification and limits.

The compatibility table describes supported contracts, not an inferred server version. New feature availability is unknown until established; an empty list or a business 404 is not evidence that the route is missing. See SPEC v2 for the complete 41-tool contract.

The current default baseline is CozyVTT v1.5.3. Its selected offline contract coverage does not certify full compatibility or live behavior; see the versioned evidence in CHANGELOG.md.

Features

41 tools returning {ok, data?, error?} for tool-body results. MCP argument validation remains handled by FastMCP:

  • Session/campaign: campaign_get (role and owner reported separately; new capability keys may be unknown), session_manage, session_list, session_notes_update, campaign_transfer_dm (owner reclaim uses the same tool), map_list, map_switch
  • Narration: chat_send (DM / PLAYER), chat_read
  • Dice: dice_roll (server-authoritative results; is_secret=true (wire field: secret) delivers to the roller and DMs on v1.4.0), events_poll (recent buffered dice events, not durable history; DICE_ROLL events are absent from chat history)
  • Tokens/maps: token_add (integer sizes 1..10), token_move, token_hp_update, token_place_creature, token_delete, map_create, map_delete, creature_search (SRD + custom library).
  • Combat: initiative_manage (add / remove / roll / set / reorder / start / next / end), initiative_read (note: CoC7e initiative is DEX-ordered, no roll — this is upstream rules behavior, and roll is gated accordingly)
  • Characters: character_list, character_get, character_create, character_delete, character_validate, character_update (rules math is done by the agent; the bridge just writes values)
  • Documents: document_upload, document_create, document_list, campaign_document_list, document_read, document_update, document_share, document_unshare, document_delete
  • Saved Rolls: saved_roll_list, saved_roll_create, saved_roll_update, saved_roll_delete (private to the current user and campaign; 50 macros per user/campaign, server-validated expressions). saved_roll_list returns complete macros — there is no saved_roll_get.
  • Hit Dice: character_hitdice_spend (DND_5E only; dispatches one spend without rolling dice or healing)

System gating

The bridge stays game-system agnostic, but a few capabilities only make sense under a specific rule system. Those are gated against the campaign's gameSystem (fetched once, cached; enum: DND_5E / PATHFINDER_2E / SHADOWRUN_6E / CALL_OF_CTHULHU_7E):

CapabilityAllowed systemsWhy
creature_search source=srdDND_5EThe SRD library is seeded from Open5e — a D&D 5e data source
initiative_manage action=rollDND_5E, PATHFINDER_2E, SHADOWRUN_6EThe server derives the initiative dice per system; CoC7e doesn't roll at all (DEX order)
character_hitdice_spendDND_5EOnly the system gate is enforced locally. No reliable WS capability probe exists; dispatch always remains pending.

Gated calls return a clear {ok: false, error} explaining which systems are allowed, instead of emitting an event the server would ignore or misinterpret. Campaigns with no gameSystem set (flexible) fail closed. With no source specified, non-5e campaigns search only custom; 5e campaigns may search both sources. campaign_get().features reports the current campaign's available gated capabilities.

Architecture

MCP client (stdio)
  └─ server.py (FastMCP, lazy init, synchronous first-call self-check)
      ├─ auth.py        — rememberMe login, 10-min keepalive, 3-min re-login spacing, 429 backoff
      ├─ client.py      — REST wrapper: one 401→re-login→retry, 429 exponential backoff (1/2/4s, ≤3)
      ├─ ws_listener.py — socket.io listener, 500-event ring buffer, one reconnect worker
      └─ tools/         — 41 MCP tools (read/write, documents, campaign additions)

Design notes:

  • Dice discipline: rolls are generated and persisted by the server. Public results go to the table; reviewed v1.4.0 sends secret results to the roller and DMs. The bridge provides recent buffered events, not a durable roll-history query.
  • Rules live outside the bridge: skill checks, SAN loss, damage — computed by the agent/GM, the bridge only performs authoritative rolls and writes results. The bridge is game-system agnostic.
  • token_move uses REST PUT; the server checks DM/controlledBy permissions, and the bridge additionally rejects spectators. REST persistence does not imply a map.changed broadcast. map_switch saves through REST then explicitly dispatches map.change through WS; a WS failure preserves the successful REST result and never replays it. On both REST and WS token.move, PLAYER must use the current campaign map while DM may use any map of this campaign.

Result and update contracts

  • Resource IDs identify one URL segment. Empty IDs and ./.. are rejected; separators in other IDs are encoded so a malformed ID cannot select a different resource through URL normalization.
  • Transient login failures wait at least 180 seconds, including any longer server Retry-After. A login 401 stops password retries until you correct the configured credentials and restart the bridge; repeatedly invoking tools cannot bypass this block.
  • Keepalive does not extend an absolute sign-in lifetime. Session-expiry 401 gets one bounded recovery attempt; policy 403 retains its upstream code. The bridge has no MFA login/enrolment workflow.
  • For newer server validation, send whole-square token coordinates, MAP image assets and maps at most 500 squares wide/high. Use grid sizes 10..500; the server may substitute 50 outside that range. Dice labels should fit 200 characters for character_name and 300 for purpose. The bridge preserves server refusals and returned coordinates without silently coercing inputs.
  • Malformed business REST responses return INVALID_RESPONSE with the actual HTTP status. A write may already have happened: inspect state before retrying. In particular, an invalid map-switch response prevents the later WS broadcast. Raw document content, conditional reads and /health reachability retain their separate handling.
  • dice_roll, chat_send, token_hp_update, initiative_manage, and character_hitdice_spend return sent: true, confirmed: false, status: "pending". Read business broadcasts and system.error with events_poll; do not blindly replay writes. Neither baseline provides correlated business ACKs. Dice may carry purpose and character_name (wire: characterName) for Custom Roll display. Hit Dice spend, rolling, and healing are separate operations, not a transaction; an old server may silently ignore the spend event.
  • events_poll returns the oldest unread events first. Save next_seq for the next since; latest_seq is its compatibility alias. high_water_seq is the buffer high-water mark, not a pagination cursor. Check gap, cursor_reset, has_more, connected, and authenticated.
  • character_update(character_id, data={"data": {"hp": {"current": 5}}}) recursively merges sheet fields before PUT. Unspecified fields survive; arrays/scalars replace values and null is explicit. Top-level fields are name, data, and tokenImageUrl. Every accepted update reads under the process lock and sends the exact updatedAt value as a guard when present. A 409 is returned without retry; if the timestamp is absent, the legacy unguarded path remains. This protects only edits after the tool GET, not stale intent from an earlier read. The upstream parsed validator may discard undeclared fixed-system fields even when preserved in the outbound merge.
  • character_create creates the card, checks the roster, then assigns it only if not confirmed as assigned. If assignment fails, the error includes the created character ID: assign that card in the UI instead of creating another. CoC conditions/Mythos/spells/appearance/notes and DND legacy/new hit-dice fields survive character merges; The server may withhold Keeper notes; do not fill in omitted private fields.
  • session_manage uses REST. Pause/end resolve campaign.activeSession.id; start creates a session. End accepts notes (≤2000 characters, shared with the campaign) and save_state=true. Empty end notes do not clear old notes; use session_notes_update(session_id,notes="") to clear. session_list includes active sessions among the most recent 50.
  • Documents use scope USER (personal), CAMPAIGN, or GLOBAL. document_list filters the asset library; campaign_document_list discovers shared private documents too. Typed txt/md create/update is limited to 900 KiB UTF-8; file uploads use the instance limit (default 50 MiB). PDF content cannot be edited. Consult canEdit/canChange when supplied; edit rights depend on asset scope and current membership. Unsharing removes only one link and cannot revoke native/global access; shared:false indicates a native campaign document. Deleting removes the asset and all its links.
  • WS reconnects use fresh, URL-scoped Cookies and request current initiative state after campaign authentication. Use HTTPS for remote deployments.
  • chat_read paginates with limit + cursor: read the newest page first, then pass the returned pagination.nextCursor back as cursor, and stop when it is null. Instances without cursor metadata serve only the latest page and say so (This instance does not support reliable cursor pagination for history; only the latest page can be read.) instead of repeating it.
  • Raw document reads preserve MIME type and ETag. Text returns {mime_type,etag,content}; PDFs are written to the project's downloads/<document_id>.pdf and return {mime_type,etag,file_path,file_size}. Pass etag as If-None-Match; a 304 returns {not_modified:true} so the caller can reuse existing content. Downloads are Git-ignored and local copies survive upstream deletion.
  • REST 401 invalidates existing WS authentication and campaign caches; losing campaign membership cancels WS authentication and clears cached authority; the reconnect worker retains its existing bounded backoff. campaign_transfer_dm clears role and system caches, and the bridge buffers character.updated, campaign.dm.transferred, roster.updated, dice.historyCleared, campaign.role.changed, map:settings:updated, token:appeared, token:disappeared, and campaign.access.revoked for events_poll. Revocation also produces system.error; only sign-out requests a credential refresh, subject to cooldown and the login-rejection block.
  • character_validate always includes validation_reliable:false: CozyVTT v1.5.0 fixes the v1.4.0 validation defect; the bridge retains the false flag because a running server may be unverified.
  • initiative_read(refresh=true) requests fresh state and reports unknown on timeout; use the returned server token coordinates because the server may clamp positions to the map.
  • Route-missing 404 — upstream message exactly The requested resource does not exist — returns This CozyVTT instance does not provide this feature; upgrade to a supported version and retry. Other 404s return Resource not found or inaccessible to the current account (HTTP 404): <upstream message>. Error data preserves the status and upstream detail. Uploading a DOCUMENT to an old upload route may return 400; that error is preserved without retrying with a different type or scope.

Requirements

  • Python ≥ 3.11
  • A CozyVTT v1.5.3 instance (the current default baseline; see compatibility evidence and limits) and a campaign where your account has the permissions required by the tools
  • uv (recommended) or pip

Install

git clone https://github.com/yanjingzhaisun/cozyvtt-mcp.git
cd cozyvtt-mcp
uv sync   # or: python -m venv .venv && .venv/bin/pip install fastmcp requests "python-socketio[client]" websocket-client
Docker (stdio)
docker build -t cozyvtt-mcp:0.6.1 .
docker run --rm -i --env-file /path/to/cozyvtt.env cozyvtt-mcp:0.6.1

Use -i to keep stdin open; MCP uses stdin/stdout, without a network port or TTY. The image installs only compatible, hash-checked runtime wheels from uv.lock, including fastmcp, requests, python-socketio[client], and websocket-client. No editable package install or dependency re-resolution is performed. Credentials are supplied at runtime. Without any COZYVTT variables, initialize and tools/list still work; only business tool calls initialize authentication. Mount upload files inside the container and pass those container paths to document_upload. Mount /app/downloads if binary downloads must survive container removal.

Wheel URLs come from the lock as direct links with sha256 hashes, so only the host serving those blobs is configurable. The default is the canonical CDN; builders in mainland China can use a mirror, which serves byte-identical files (the hashes still verify):

docker build --build-arg WHEEL_BASE=https://mirrors.aliyun.com/pypi/packages -t cozyvtt-mcp:0.6.1 .

scripts/docker_requirements.py --wheel-base "" keeps the lock file's own URLs.

Configuration

Environment variables (no secrets in the repo):

VarExampleNotes
COZYVTT_URLhttp://localhost:8899Your instance URL
COZYVTT_EMAILdm@example.localDM account
COZYVTT_PASSWORD—DM password
COZYVTT_CAMPAIGN_IDuuidTarget campaign
Hermes Agent (config.yaml)
mcp_servers:
  cozyvtt:
    command: /path/to/cozyvtt-mcp/.venv/bin/python
    args: [/path/to/cozyvtt-mcp/server.py]
    env:
      COZYVTT_URL: "http://localhost:8899"
      COZYVTT_EMAIL: "dm@example.local"
      COZYVTT_PASSWORD: "<secret>"
      COZYVTT_CAMPAIGN_ID: "<campaign-uuid>"

Restart Hermes after registering (MCP servers are not hot-reloaded).

Generic MCP client

Any stdio-capable client: command = the venv python, args = server.py, env as above.

Testing

.venv/bin/python -m pytest -q

The guarded smoke helper retains its original scripts/smoke_v150.py filename and accepts any explicit upstream pin. --deployed-version defaults to v1.5.3; --deployed-revision defaults to 1dd6bccd536afe1defc48dfc42378653e86d6343 and requires a full 40-character hexadecimal revision. The nonempty version and revision must exactly match cozyvtt_version and source_revision in the JSON file supplied with --deployment-evidence. Both values are emitted in the report. To smoke an older or different revision, supply both flags and its matching evidence.

Create an evidence file outside the repository using this structure, replacing evidence with the actual operator verification record (never credentials):

{
  "cozyvtt_version": "v1.5.3",
  "source_revision": "1dd6bccd536afe1defc48dfc42378653e86d6343",
  "evidence": "Replace with the operator's deployment verification record"
}

Check the pin offline before a live run:

COZYVTT_SMOKE=1 .venv/bin/python scripts/smoke_v150.py \
  --dry-run --allow-login \
  --deployed-version v1.5.3 \
  --deployed-revision 1dd6bccd536afe1defc48dfc42378653e86d6343 \
  --deployment-evidence /path/to/deployment-evidence.json \
  --report /path/to/smoke-dry-run.json

--dry-run checks evidence without reading credentials, initializing the bridge, or contacting the network. Its report is identified as a dry run and does not prove live compatibility. It still requires COZYVTT_SMOKE=1, an explicit --cookie-file or --allow-login choice, evidence, and a report path.

For an authorized live read-only smoke, set COZYVTT_URL, COZYVTT_CAMPAIGN_ID, COZYVTT_EMAIL and COZYVTT_PASSWORD in the environment as described under Configuration, then run:

COZYVTT_SMOKE=1 .venv/bin/python scripts/smoke_v150.py \
  --allow-login \
  --deployed-version v1.5.3 \
  --deployed-revision 1dd6bccd536afe1defc48dfc42378653e86d6343 \
  --deployment-evidence /path/to/deployment-evidence.json \
  --report /path/to/smoke-report.json

Alternatively, replace --allow-login with --cookie-file /path/to/session-cookie.txt: the file must be outside the repository with mode 0600; no default credential path exists. Add --ws only when passive WS observation is authorized. The helper retains its HTTP/WS allowlists, single-login/connection limits, read-only state comparisons and teardown checks. Deployment identity is operator-attested; /health does not discover the server version.

(scripts/smoke_write.py writes chat, a public roll, and a secret roll — run it manually and only on a throwaway campaign. It checks sender and unique purpose; proving that players do not receive secret rolls additionally requires an independent player connection.)

Offline tests block TCP connections and include real FastMCP in-memory and stdio checks; they do not require campaign credentials. Live integration against an attested v1.5.0 instance was verified 2026-10-04 with authenticated read-only REST and passive WS state reads; no campaign writes or multi-user isolation were tested. Historical v1.4.0 read/write evidence remains historical. The local venv used for verification runs Python 3.12.13; Python 3.13 remains unverified.

Troubleshooting

  • Logs: logs/cozyvtt-mcp.log (auth events, WS state, tool calls; never contains passwords)
  • Repeated 401: reviewed v1.4.0 limits failed credential attempts to 5 per 15 minutes/IP. Successful credential requests do not count there; older accounting is unverified. The bridge enforces a re-login interval of at least 3 minutes, including failed attempts; Retry-After may extend it.
  • events_poll empty: may mean no new events. Inspect connected, authenticated, last_error, and connection_error; the single WS worker retries disconnected or rejected connections. Initialization failures can be retried after a 180-second cooldown without restarting.
  • CoC7e initiative doesn't roll dice: upstream behavior — CoC7e initiative is DEX-ordered, no roll is produced

License

MIT (see LICENSE). CozyVTT itself is AGPLv3 — this project is an independent API client and contains no CozyVTT code.

Ecosystem

  • dnd5e-rules — deterministic D&D 5e rules calculations (pure functions, SRD 5.1 data under CC-BY-4.0). The rules layer we pair with this bridge: the server rolls the dice, the bridge carries them, this library does the math, the agent narrates.

الأسئلة الشائعة

ما هو Cozyvtt MCP؟

Run campaigns as game master in CozyVTT, a self-hosted virtual tabletop: dice, chat narration, tokens and maps, initiative, character sheets and rulebooks.

كيف أثبّت Cozyvtt MCP؟

اتبع خطوات التثبيت في توثيقه المعروض في هذه الصفحة وفي مستودعه.

هل Cozyvtt MCP مفتوح المصدر؟

نعم. شيفرته متاحة على GitHub بترخيص MIT.

من يصون Cozyvtt MCP؟

ينشره yanjingzhaisun. يدرجه AI.tn لكنه لا يصونه ولا يوصي به.

من القوائم المجتمعية awesome-mcp-servers ومستودع المشروع على GitHub. الإدراج ليس توصية: راجع شيفرة الخادم وصلاحياته قبل منحه الوصول إلى بياناتك.