npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guan-tends/dice-mcp-server

v1.4.1

Published

Stateless dice engine MCP tool server - generic d20-style notation, L5R 4e Roll & Keep, and L5R 5e ring/skill symbol dice, via the Model Context Protocol.

Readme

@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
}

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.