Skip to main content
Glama
guan-tends

dice-mcp-server

by guan-tends

@guan-tends/dice-mcp-server

Stateless dice engine MCP tool server. Pure dice math — no character sheets, no sessions, no persistence.

Three tools via the Model Context Protocol:

Tool

System

Character

dice_roll

Generic d20-style notation

expression in, number out

l5r4_roll

Legend of the Five Rings 4e Roll & Keep (XkY+Z)

structured in, number + raises out

l5r5_roll

Legend of the Five Rings 5e (FFG) ring/skill symbol dice

structured in, symbols out

Built on @guan-tends/mcp-ai SimpleServer (HTTP transport, raw zod-shape schemas). Same composition-root pattern as @guan-tends/matrix-mcp-server.

Quick Start

Zero-config (stdio — most MCP clients, same as passgen):

{
  "mcpServers": {
    "dice": {
      "command": "npx",
      "args": ["-y", "@guan-tends/dice-mcp-server"]
    }
  }
}

HTTP (daemon style):

npm install
npm test    # 168 tests
npm start   # serves MCP HTTP on 127.0.0.1:3777

Point any MCP client at http://localhost:3777/ (Streamable HTTP transport).

Example: a d20 attack roll with advantage

{
  "name": "dice_roll",
  "arguments": { "expression": "2d20kl1+5", "dc": 15 }
}
{
  "success": true,
  "expression": "2d20kl1+5",
  "terms": [
    {
      "notation": "2d20kl1",
      "rolls": [
        { "face": 17, "chain": [], "final": 17, "rerolled": false },
        { "face": 8, "chain": [], "final": 8, "rerolled": false }
      ],
      "kept": [{ "face": 8, "chain": [], "final": 8, "rerolled": false }],
      "dropped": [{ "face": 17, "chain": [], "final": 17, "rerolled": false }],
      "subtotal": 13
    }
  ],
  "total": 18,
  "dc": 15,
  "success": true
}

Example: a 4e katana strike (Agility 3, Kenjutsu 4, emphasis, Void spent)

{
  "name": "l5r4_roll",
  "arguments": {
    "trait": 3,
    "skill": 4,
    "tn": 25,
    "emphasis": true,
    "rollBonus": 1,
    "keepBonus": 1,
    "label": "Katana strike"
  }
}

Returns 7d10k4 — pool, kept/dropped dice with explosion chains, totals, TN verdict, and a notes[] array explaining every rule that fired. A failed roll with raises carries wouldSucceedWithoutRaises: true — the GM-narration hook ("he'd have made it, but the flourish cost him").

Example: a 5e Fire check (Fire 3, Tactics 2, advantage)

{
  "name": "l5r5_roll",
  "arguments": { "ring": 3, "skill": 2, "tn": 3, "advantage": true }
}

Returns the full pool with per-die symbols, kept indices, conversions, tallies, and the derived totals (⚑ + 🔥 = totalSuccesses):

{
  "tallies": { "successes": 3, "opportunities": 1, "strife": 1, "explosive": 2 },
  "totalSuccesses": 5,
  "bonusSuccesses": 2,
  "success": true,
  "composureExceeded": false
}

Related MCP server: MCP Dice Roller

Tool Reference

dice_roll — generic notation

2d6+3        sum of 2d6 plus 3
d20          one d20
4d6kh3       keep highest 3 of 4d6 (ability scores)
2d20kl1      keep lowest 1 (disadvantage)
1d10!        exploding: reroll-and-sum while max faces appear
1d12!11,12   exploding on specific faces (5e skill die: 11 and 12)
3d6r1        reroll initial 1s once (emphasis shape)
2d6+1d4+2    multi-term

Param

Type

Notes

expression

string

required; whitespace-tolerant, case-insensitive

dc

int, optional

adds dc + success (total >= dc) to the result

Parse errors are position-aware (... at position 4) so callers can fix their own syntax. Doubled signs (2d6--3) and signless term concatenation (2d61d4) are rejected with named errors.

l5r4_roll — Roll & Keep (4e)

Pools (Trait + Skill) d10s, keeps the highest Trait. D10s explode on 10s (trained rolls only). Every pool passes through the Ten Dice Rule: no more than 10 rolled / 10 kept dice — excess kept dice add +2 each, excess rolled dice convert 2:1 into kept dice (while kept < 10), and any leftover rolled dice add +2 each. preCapPool and overflowBonus are reported for audit.

Build a pool two ways:

  • Trait + Skill (skill rolls): (Trait + Skill) k Trait

  • Direct rolled/kept (everything else): initiative 1k4 (Insight k Reflexes — kept > rolled is legal), melee damage 6k2, Honor rolls 6k6, spell casting 3k2, unarmed 3k1

Param

Range

Default

Notes

trait

1–10

also the keep count; omit for direct pools

skill

0–10

0 (with no rollType/untrained) = unskilled

rolled / kept

1–50

direct pool input; overrides trait/skill

rollType

enum

skill

skill · trait · ring (explode + raises legal) · unskilled (neither) · custom

untrained

bool

explicit flag; false with skill 0 = Trait roll

tn

5 trivial · 10 easy · 15 average · 20 difficult · 25 very hard · 30 extreme · 40 near-impossible

raises

0–10

0

+5 effective TN each; capped by voidRing

freeRaises

0–10

0

effect only, no TN, never counts vs cap

voidRing

1–10

caps declared raises

voidPoint

bool

false

spend a Void Point: +1k1 to the pool

emphasis

bool

false

reroll initial 1s once, BEFORE explosions; trained rolls only

penalty

int

0

wound/stance penalty — raises the effective TN, never the total (Nicked +3 … Down +40)

rollBonus / keepBonus

−10–10

0

dice bonuses/penalties; penalties clamp kept ≤ rolled

totalBonus

int

0

flat bonus to the kept sum (Honor Rank on Fear rolls)

keepMode

highest/lowest

highest

lowest = deliberate failure

explodeOn

string

"10"

9 (weapon mastery) · "9,10" · none (thrown weapons)

Unskilled rolls (no skill ranks): trait dice only, ALL kept, no explosions, no raises, no emphasis (raises/emphasis on unskilled rolls are rejected or ignored with an explanatory note). Trait rolls are a separate thing — rollType: "trait" explodes and allows raises.

l5r5_roll — Ring & Skill dice (5e)

Implements the corebook check pipeline (pp. 20–26, spec of record): assemble the pool (Step 3) → modify rolled dice (Step 4) → choose kept dice (Step 5) → resolve symbols on KEPT dice (Step 6).

The law, as implemented:

  • Total successes = ⚑ + 🔥 (p. 24: "the sum total of success and explosive success symbols"). A kept skill-12 (pure explosive face) counts as one success; a kept ring-6 counts as two.

  • Keep 1..ring dice (p. 24: "at least one... up to the value of the ring"; +1 per assisting character, p. 26). Under-keeping is legal and reported in notes.

  • Explosions resolve post-keep, from kept dice only (Step 6.1): each 🔥 on a kept die rolls one bonus die of the same type; a kept bonus die's own 🔥 chains.

  • Advantage + disadvantage cancel (p. 24 consolidate rule) — both flags together have no effect, per the book.

  • Assistance (p. 26): +1 skill die per skilled helper, +1 ring die per unskilled helper.

Param

Range

Default

Notes

ring

1–5

required

also the keep maximum (before assistance)

skill

0–5

0

0 = untrained (ring dice only)

tn

1–10

successes needed: 1 easy · 2 average · 3 difficult · 4 very hard · 5 extremely hard · 6 extraordinary · 7+ heroic

assistants

object

{skilled, unskilled} (p. 26); keep max +1 per assistant

keepCount

1..keepMax

keepMax

under-keep via policy (book-legal); values above max clamp with a note

kept

string

"1,3" explicit keep (1-based, 1..keepMax — under-keeping legal)

bonusDice

enum

auto_keep

auto_keep (tallied) · auto_drop (shown, not tallied) · manual (pending — caller decides from the audit array)

conversions

string

explicit "2:skill,4:ring" (1-based, book-accurate surface; overrides adv/disadv)

advantage

bool

false

house simplification of named categories; cancels against disadvantage (p. 24)

disadvantage

bool

false

house simplification; cancels against advantage

policy

enum

success_first

min_strife, max_opportunity (select keepCount-many dice)

composure

int

advisory composureExceeded flag (kept strife ≥ value)

label

string

echoed in the result

Deprecated: includeExplosionBonuses (bool) maps to bonusDice (true → auto_keep, false → auto_drop); cannot be combined with it.

Result contract (the result is a full transcript — every automated decision is visible): base pool with per-die symbols · kept / dropped · bonusDice[] audit (sourceDieIndex, type, face, symbols, chainDepth, disposition kept/dropped/pending) · conversions[] · tallies (raw ⚑/⧫/⏳/🔥 counts; bonusPending appears in manual mode only) · totalSuccesses (⚑+🔥) · vs tn: success, bonusSuccesses, shortfall · keepMax, requestedKeepCount, keptIndices · explosiveTriggers, untrained · notes[] narrating every automated decision.

House safety valve (disclosed, not book law): MAX_BONUS_CHAIN = 10 caps per-chain bonus-die depth — the book is naturally finite because a player may always drop, but auto_keep automation needs a guard; the cap firing is reported in notes.

Verified symbol charts (cross-checked against two independent 5e references):

Ring d6

1

2

3

4

5

6

symbols

opp+strife

opp

succ+strife

succ

succ+strife+expl

Skill d12

1–2

3–5

6–7

8–9

10

11

12

symbols

opp

succ+strife

succ

succ+opp

succ+strife+expl

expl (no strife)

Explosive faces on KEPT dice add one bonus die of the same type after keep selection (book Step 6.1) — chained, capped, fully audited in the bonusDice[] array. Kept bonus dice are tallied (their symbols count toward the TN, per the Sakura worked example, p. 23).

Architecture

Functional core, imperative shell. All randomness is constructor-injected (rng(sides) => int in [1, sides]): production uses crypto.randomInt (unbiased CSPRNG); tests inject sequence RNGs, making every engine test deterministic. Symbol tables are data — adding Genesys/Star Wars dice later is a table entry, not a rewrite.

src/
├── index.js        bootstrap only (lifecycle, graceful shutdown)
├── config.js       config layers (defaults ← JSON5 ← env) + validation
├── mcp-server.js   tool definitions + SimpleServer wiring
└── engine/
    ├── rng.js      createCryptoRng / createSequenceRng
    ├── core.js     rollDice: explode-on-faces, reroll, caps
    ├── notation.js expression parser (position-aware errors)
    ├── d20.js      expression evaluator
    ├── symbols.js  5e symbol tables (data)
    ├── l5r4.js     Roll & Keep engine
    └── l5r5.js     ring/skill symbol engine

Error rejection is two-layer: zod-schema violations (out-of-range params) are rejected at the MCP protocol layer before the tool body runs; cross-field rule violations (e.g. raises > Void Ring) surface as isError JSON results with the engine's human-readable message.

Design Decisions

  • Stateless. No sheets, no sessions, no persistence. The caller is the GM brain; the server is dice math. Restart-safe by construction.

  • Teaching descriptions. Every tool description embeds its system's TN scale and worked examples — an LLM caller learns the system from the tool itself.

  • Deterministic tests. Sequence RNG injection makes explosion chains and keep decisions exactly reproducible; distribution tests with derived constants cover the real CSPRNG.

  • Dice-as-data. 5e symbol tables are frozen data; new symbol systems are new tables, not new engines.

  • No array-typed tool params. Kept indices, conversions, and explode faces are comma-strings — an MCP-gateway compatibility choice.

  • Interpretation notes (parameterized, documented, adjustable if a table ruling differs): 5e untrained = ring dice only; advantage converts pre-keep and never sacrifices an explosive ring die; composure is an explicit input with an advisory flag (no hardcoded formula); 4e wound penalties apply once to the total. dc is permissive (any integer) — permissive inputs, strict dice math.

Transports

Transport

How

Default for

stdio

JSON-RPC over stdin/stdout pipes

the dice-mcp-server bin (npx)

http

Streamable HTTP

the library entry (src/index.js, npm start)

sse

Server-Sent Events

opt-in via env

Selection precedence (highest wins): explicit transport option → DICE_MCP_TRANSPORT env (stdio|http|sse) → JSON5 config file transport key → entry default (bin: stdio, library: http).

DICE_MCP_TRANSPORT=stdio node bin/dice-mcp-server.mjs   # stdio
DICE_MCP_TRANSPORT=http  node bin/dice-mcp-server.mjs   # http on :3777

stdio mode writes diagnostics to stderr only — stdout carries exclusively the MCP protocol. An explicit DICE_MCP_TRANSPORT (or config-file transport) always beats the entry default.

Deployment

Systemd unit (example)

Adjust paths/user for your host. If you use nvm, point PATH and ExecStart at your node install (or source nvm in an ExecStartPre).

[Unit]
Description=Dice MCP Server
After=network.target

[Service]
Type=simple
User=YOURUSER
WorkingDirectory=/opt/dice-mcp-server
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
RestartSec=5
# Loopback-only enforcement (see below)
IPAddressDeny=any
IPAddressAllow=localhost
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Loopback enforcement

mcp-ai SimpleServer's express listen() binds all interfaces and ignores the host config field. Loopback-only is therefore enforced at the deployment layer — systemd IPAddressDeny=any + IPAddressAllow=localhost (cgroup packet filter) with UFW default-deny. Until mcp-ai supports a server-side bind host, deploy with both controls. (The port config IS honored.)

Gateway wiring (mcp-ai aggregator)

Add to the aggregator's mcps array (back up config.json first):

{ "id": "dice", "connection": { "type": "http", "url": "http://localhost:3777" } }

Then restart the gateway.

Testing

npm test              # 154 tests: engines, tools, e2e, distribution
npm run test:coverage # with coverage
npm run lint          # eslint
npm run format:check  # prettier

Test layers:

  1. Engine units — deterministic via sequence RNG; every rule path (explosions, emphasis ordering, untrained, raises/void, policies, conversions, overrides) asserted exactly.

  2. Tool units — schema shape contract (raw zod shapes), happy paths, engine-error surfacing.

  3. E2E — real MCP client over Streamable HTTP: initialize → listTools → callTool for every tool, both error layers.

  4. Distribution sanity — real CSPRNG with derived constants (geometric-series explosion means, binomial success expectations).

Support

If this server powers your table or your agents, tips are appreciated — they fund compute, inference, and the rest of the tool fleet:

  • GitHub Sponsors: github.com/sponsors/guan-tends

  • Solana: Eu8wQcW68TKMs1a6eqzZu8znzU52QLqQugAMG8uCD6y6

  • EVM: 0x2733ff7c865C56d565a99BE1DC11B81cc76850A5

  • XRP: r4X6e7McAQj7e8vBCeued1RYu4mCJrREDG

Roadmap

  • Genesys / Star Wars dice — new symbol tables (data), same engine.

  • mcp-ai upstream: server-side bind-host support for express listen() — DONE upstream in @guan-tends/mcp-ai 1.6.7-guan.0 (consumed here via the dependency bump); systemd loopback enforcement retained as defense-in-depth.

  • Fate / other systems — candidate engines behind the same tool interface.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables dice rolling mechanics for tabletop RPGs including standard dice notation, coin flips, advantage/disadvantage rolls, exploding dice, and maintains a history of recent rolls. Supports D\&D 5e mechanics and various gaming dice types (d4, d6, d8, d10, d12, d20, d100).
    -
  • A
    license
    A
    quality
    C
    maintenance
    An MCP server that provides tools for rolling dice using standard notation, flipping coins, and selecting random items from lists. It supports advanced tabletop gaming features such as character stat generation and keep-highest/lowest mechanics.
    6
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provides comprehensive TRPG dice rolling functionality including standard notation, advantage/disadvantage mechanics, and success-counting dice pools. It enables users to perform complex dice logic and track roll history through an MCP-compliant interface.
    2
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables dice rolling for RPG games using standard dice notation, supporting complex expressions like keep/drop, reroll, exploding dice, and batch rolls.
    4
    3
    MIT