Charactercheck
FreeNot checkedEnables deriving and auditing D&D Beyond character sheets with full provenance, including combat stats, stance, and QA checks, surfacing unhandled data instead
About
Enables deriving and auditing D&D Beyond character sheets with full provenance, including combat stats, stance, and QA checks, surfacing unhandled data instead of guessing.
README
Deterministic D&D Beyond character-sheet derivation with provenance — every stat computed and traceable, every unhandled case named, never guessed.
The D&D Beyond API returns build data — there is no computed AC, attack bonus, or save anywhere in the payload. Everyone who consumes it re-derives the math, usually inside a host app (a VTT module, a browser extension) or, worse, by letting a language model guess. charactercheck is that derivation as a standalone, dependency-free library and CLI: the character accountant for agents.
Cold-boot probe (2026-07-24): a fresh agent session given only this repo URL derived a live character correctly in 2 commands, zero failures (install → derive), ~seconds end-to-end.
Status: 0.1 — young but real. The derivation surface is the 100-question QA pass below, run in CI on synthetic fixtures. Unrecognized data is reported, never silently defaulted — that honesty contract is the product.
Agent? → AGENTS.md is written for you: the six things that matter, in one page. Everything below is the same story with more prose.
Start here (agent or human): 2 commands
$ pip install charactercheck # stdlib only, no dependencies
$ charactercheck derive https://www.dndbeyond.com/characters/<id>
pip installblocked by an "externally-managed-environment" error? That is Homebrew/Debian Python protecting the system site-packages, not a problem with this package. Use a venv —python3 -m venv .venv && .venv/bin/pip install charactercheck— or skip installing entirely and use the clone path below. There are no dependencies either way.
Want a real character to try right now? This one is public and kept that way on purpose:
$ charactercheck derive https://www.dndbeyond.com/characters/150991647 --brief
Shalia — Cleric 3
AC 12 (confirm) · HP 21/21 · init -1
trusted: hp, initiative, saves, skills, attacks, weapons, speeds, proficiency_bonus, spellcasting, spell_save_dc, spell_attack_bonus, inventory
ASK: ac, prepared_spells, spell_slots
UNSUPPORTED: spell_output (bonus:spell-group-healing)
? Which armour are you actually wearing right now?
? Your sheet shows no spell slots, but a caster of your level should have 4×L1, 2×L2. How many do you have?
? Which leveled spells do you have prepared today?
She is a deliberately useful example rather than a tidy one: a real sheet from a running campaign that exercises all three trust lanes at once — armour not flagged equipped, spell slots missing from the payload, no prepared list, and one unsupported healing-group modifier. A clean character would show you nothing.
The example deliberately includes a negative case. Shalia carries one homebrew feat (
Dark Bargain) that is not in the SRD 5.2.1 feat table, and that is the point — it is what produces theunsupportedlane in the output above. A tidy all-SRD character would demonstrate the happy path and nothing else, whereas real sheets are full of homebrew, legacy options and manual overrides. This one shows the honesty machinery doing its actual job: naming content it does not model, scoping the blast radius tospell_output, and leaving the other twelve families trustworthy.charactercheck categorises it as "outside SRD 5.2.1 feat table"; the sibling srdcheck independently refuses it as out of jurisdiction. Two tools agreeing from different directions.
First, prove it works — offline, no account, no character of your own:
$ charactercheck selftest
[PASS] name: 'Torvald Brightmantle'
[PASS] level: 3
[PASS] abilities derived
...
offline derivation works — no network, no D&D Beyond account, no character of your own required.
That one command separates "the tool is broken" from "I cannot reach that
character" — two failures that look identical from outside and are the two you
will actually hit. There is a sample character in the box
(examples/sample-character.json) you can derive directly too.
Cloned the repo instead of installing? That works too, with no install step — there are no dependencies to install:
$ git clone https://github.com/chaoz23/charactercheck && cd charactercheck
$ python3 -m charactercheck derive https://www.dndbeyond.com/characters/<id>
That is the whole happy path. Works on any public D&D Beyond character — URL, bare id, or a saved character-service JSON file. No login, no cookies, no API key, ever.
If it does not work, read the exit code
Every failure prints structured JSON with an action field — the one next
thing to do. Never a traceback.
| exit | meaning | what to do |
|---|---|---|
| 0 | derived clean | use the output |
| 1 | lint findings — the sheet disagrees with itself | usable output; resolve lint[] with the player |
| 2 | unhandled content present | not a failure. Output is complete and usable; resolve the named unhandled items with a human. Don't retry |
| 3 | could not retrieve the sheet | read action in the JSON — private sheet, bad id, or no network |
$ charactercheck derive https://www.dndbeyond.com/characters/1
{
"ok": false,
"error": "not_public",
"message": "D&D Beyond returned 403 Forbidden for this character — it is private or not shared.",
"action": "Open the character on D&D Beyond, set Character Privacy to Public, and retry. If you cannot change it, save the character-service JSON and pass that file path instead — charactercheck reads a saved file with no permissions at all, and never asks for credentials.",
"exit_code": 3
}
Error kinds are stable and matchable: not_public · not_found · bad_ref ·
network · rate_limited · bad_json · upstream.
Stuck? One command diagnoses it
$ charactercheck doctor <ref> # add --json for machine output
[PASS] python: 3.12.1
[PASS] dns: character-service.dndbeyond.com resolves
[PASS] network: outbound HTTPS works
[FAIL] character: not_public: D&D Beyond returned 403 Forbidden ...
-> Open the character on D&D Beyond, set Character Privacy to Public ...
Private character? Two supported answers
charactercheck never asks for credentials — that is a deliberate boundary, not a gap. So:
- Make it public. Character sheet → Character Privacy → Public. This is the usual answer and takes ten seconds.
- Or work from a file. Save the character-service JSON anywhere you can
read it and pass the path:
charactercheck derive ./shalia.json. No permissions involved at all — useful for private campaigns, air-gapped hosts, and CI.
The trust map — read this before you state a number
derive returns a trust block that routes every family into exactly one
lane. It is a re-shaping of verified_clean, lint and unhandled into the
one question a caller actually has: may I say this out loud?
$ charactercheck derive <ref> --brief
Shalia — Cleric 3
AC 12 · HP 21/21 · init -1
trusted: hp, initiative, saves, skills, attacks, weapons, speeds, proficiency_bonus, spellcasting, spell_save_dc, spell_attack_bonus, inventory
ASK: ac, prepared_spells, spell_slots
UNSUPPORTED: spell_output (bonus:spell-group-healing)
? Which armour are you actually wearing right now?
? Your sheet shows no spell slots, but a caster of your level should have 4×L1, 2×L2. How many do you have?
? Which leveled spells do you have prepared today?
| lane | meaning |
|---|---|
| trusted | nothing outstanding touches it — safe to state |
| ask_player | derived, but a lint puts it in doubt; the resolving question is right there |
| unsupported | the engine saw content it does not model targeting this family — say what is missing rather than stating a value |
Unsupported content is never applied to any derived value, so the rest of the arithmetic is unaffected by it. Trust the computed fields; do not improvise around the named unsupported feature.
Every lint finding carries the question that resolves it — the difference between a tool reporting a caveat and an agent resolving one at the table.
$ charactercheck intake <ref> # one pre-session packet: settled, unresolved, player-authority
Breaking in 0.6.0:
lintentries are now objects ({code, message, ask, affects}) rather than plain strings. Joinmessagefor the old rendering.
What a derived character looks like
$ charactercheck derive https://www.dndbeyond.com/characters/<id>
{
"combat": {
"ac": {"value": 16, "provenance": "Breastplate 14 + DEX +1 + +1 [manual adjustment]"},
"initiative": {"bonus": 6, "provenance": "DEX +1 + 5 [bonus:initiative]"},
"hp": {"current": 51, "max": 51, "provenance": "base 30 + CON +2×7 + 7 [per-level bonuses]"},
"stance": {"main_hand": "Night Rapier", "off_hand": "Boot Knife (off hand)",
"ac_states": {"current": 16,
"shield raised (+2)": {"ac": 18, "cost": "requires the off hand"}}},
...
},
"unhandled": {"modifier_patterns": ["munch:cookies"]},
"lint": []
}
For agents
tool.jsonat the repo root andcharactercheck --schemadescribe the full I/O contract.--pipereads refs from stdin for batch runs.- MCP server:
charactercheck-mcp(stdio) exposesderive,stance,qa,report. - Every derived number carries a provenance string — the arithmetic that produced it — so a downstream agent (or a suspicious player) can audit any value without re-deriving it.
$ charactercheck stance <ref> # what's in each hand, AC states with costs
$ charactercheck seatpack <ref> # everything a seat needs before a session
$ charactercheck report <ref> # ONLY the honesty lanes — resolve these before play
$ charactercheck qa <ref> # the 100-question pass, per-question OK/PARTIAL/NO
$ charactercheck diff <ref> --baseline intake.json # what the player changed mid-session
Why this matters, from a real table: an agent was handed a D&D Beyond link and reported "D&D Beyond is showing me the signed-out shell, not the sheet stats" — then played a whole session rolling flat d20s with no modifiers, while the GM hand-derived her numbers and got them wrong twice (a throwaway script matched
wis-score; D&D Beyond calls itwisdom-score, silently dropping a feat's +2 WIS).charactercheck derivegets that sheet right in one command. Hand-derivation is the bug this product exists to delete.
Why provenance and refusal, not just numbers
A character sheet is a mix of derivable core-rules content and everything else — homebrew, legacy-edition options, manual overrides someone typed in three campaigns ago. Tools that guess produce confident wrong numbers; at a real table those become wrong rulings. charactercheck's contract:
- Derived values carry their arithmetic (
AC 17 = Breastplate 14 [equipped] + DEX +1 [medium cap] + 2 [manual adjustment]). - Unhandled data is surfaced by name (an unknown modifier pattern, an unrecognized characterValue type) and flips the exit code — your cue to ask the player, not to guess.
- Lint catches sheets that disagree with themselves — including slot
gaps caught by an independent anchor: the SRD multiclass table says how
many slots a caster level must have, so a Cleric 3 reporting none is a
data gap, and a sheet reporting
available 0whileused 3is a contradiction (those slots were spent, so they existed). Also: nothing flagged equipped, stale damage, a caster with slots and zero prepared spells, gear stashed in a container that was left somewhere else entirely (yes, the container graph is modeled — a chest labeled "stashed @ the docks" stops contributing weight and armor candidates).
The QA pass
tests/ ships a 100-question QA suite covering the surface a table actually uses — vitals, saves, all 18 skills, passives, weapons and masteries, spell slots (pact included), resources with used-counts, encumbrance, attunement. CI runs it on synthetic fixtures on every push; the scorecard is generated, never hand-edited. Current: 92 OK / 7 PARTIAL / 1 NO per fixture-class, with every PARTIAL/NO carrying a named reason in the output.
What it does NOT do (on purpose)
- No rules adjudication — "is this action legal" is a different product (srdcheck, this project's sibling: srdcheck judges actions, charactercheck derives the actor).
- No private sheets — public share links only; this tool will never ask for credentials.
- No VTT output, no homebrew content database, no character building.
- No guessing — the whole point.
As a library
from charactercheck import derive, stance, fetch
r = derive("https://www.dndbeyond.com/characters/<id>")
r["combat"]["ac"] # {'value': 17, 'provenance': 'Breastplate 14 + ...'}
r["unhandled"] # what you must resolve by asking a human
stance(fetch("<id>")) # hands / AC states / attack lines
Credits
- Schema semantics for the D&D Beyond v5 payload were partly informed by reading the source of MrPrimate/ddb-importer (MIT) — the most complete derivation math in the ecosystem, coupled to FoundryVTT. No code was copied; see
NOTICE. - The 100-question QA schema was authored for this project.
- D&D Beyond is a trademark of Wizards of the Coast. charactercheck is unofficial, unaffiliated, and reads only what a character's owner has made public.
mcp-name: io.github.chaoz23/charactercheck
The settlement quiz (v0.3)
charactercheck quiz <ref>
Questions the GM asks out loud at a ledger-flush boundary, each with the
silently-held expected answer where derivation has authority (AC, HP max,
total slots, attunement — with provenance strings). Live state only the
player tracks (current HP, expended slots) is expect: null, authority: "player" — the engine never estimates. Grade privately, remind
diplomatically; diff against the intake snapshot is the reality check.
Unhandled patterns propagate as a caveat naming what the answer key cannot
verify. Unhandled items now also carry the payload's own verbatim text
so intake interviews read the source's words, never a paraphrase.
The seat pack (v0.4)
charactercheck seatpack <ref> [--for-dm]
Everything a seat needs at session start, in one call: abilities, saves,
skills with proficiency flags, passives, DCs, combat block, resources,
inventory, vision (species darkvision plus Devil's Sight-class features,
with provenance — born from a live table where a DM narrated a warlock blind),
and a persona section carrying the sheet's own trait/ideal/bond/flaw text
verbatim with an explicit not_derivable list: charactercheck never invents
personality. --for-dm redacts player-authority live state per the
settlement contract.
Install Charactercheck in Claude Desktop, Claude Code & Cursor
unyly install charactercheck-mcpInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add charactercheck-mcp -- uvx charactercheckStep-by-step: how to install Charactercheck
FAQ
Is Charactercheck MCP free?
Yes, Charactercheck MCP is free — one-click install via Unyly at no cost.
Does Charactercheck need an API key?
No, Charactercheck runs without API keys or environment variables.
Is Charactercheck hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Charactercheck in Claude Desktop, Claude Code or Cursor?
Open Charactercheck on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectCompare Charactercheck with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
