dnd5e-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # dnd5e mcp
2
+
3
+ MCP server for the **D&D 5e SRD** (dnd5eapi.co), monsters, spells, classes, equipment.
4
+
5
+ ## Tools
6
+
7
+ | Tool | What it does |
8
+ |---|---|
9
+ | `get_monster` | Full stat block by index slug (`adult-red-dragon`, `goblin`…) |
10
+ | `get_spell` | Spell by index slug (`fireball`, `wish`…), full text + components |
11
+ | `list_monsters` | A, Z monster list to find valid slugs |
12
+ | `get_class` | Class overview, hit die, skill choices, starting equipment |
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ npm run build && node dist/index.js
18
+ ```
19
+
20
+ Example:
21
+
22
+ ```
23
+ get_monster { index: "adult-red-dragon" }
24
+ get_spell { index: "fireball" }
25
+ ```
26
+
27
+ Keyless (community hosted SRD endpoint). Stat blocks include AC/HP/speed, all six ability scores, actions, and special abilities.
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ npm install
33
+ npm run build
34
+ node dist/index.js
35
+ ```
36
+
37
+ The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
38
+
39
+ ## Tools at a glance
40
+
41
+ - `get_monster`: Get a D&D 5e monster by index slug (e.g.
42
+ - `get_spell`: Get a D&D 5e spell by index slug (e.g.
43
+ - `list_monsters`: List D&D 5e SRD monsters by name (a-z), useful for finding valid slugs.
44
+ - `get_class`: Get a D&D 5e class overview - hit die, proficiencies, starting equipment.
45
+
46
+ ## Limits and privacy
47
+
48
+ This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required unless the project explicitly says otherwise.
49
+
50
+ ## Try it
51
+
52
+ After building, connect the server through your MCP client. The repository root also contains `smoke-test.mjs` for projects covered by the shared harness. A typical tool call starts with `get_monster`.
package/dist/api.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ export declare class DndError extends Error {
2
+ }
3
+ export interface Monster {
4
+ index: string;
5
+ name: string;
6
+ size?: string;
7
+ type?: string;
8
+ alignment?: string;
9
+ armor_class?: {
10
+ value?: number;
11
+ type?: string;
12
+ }[];
13
+ hit_points?: number;
14
+ hit_dice?: string;
15
+ speed?: Record<string, string>;
16
+ strength?: number;
17
+ dexterity?: number;
18
+ constitution?: number;
19
+ intelligence?: number;
20
+ wisdom?: number;
21
+ charisma?: number;
22
+ challenge_rating?: number;
23
+ languages?: string;
24
+ xp?: number;
25
+ actions?: {
26
+ name?: string;
27
+ desc?: string;
28
+ }[];
29
+ special_abilities?: {
30
+ name?: string;
31
+ desc?: string;
32
+ }[];
33
+ image?: string;
34
+ }
35
+ export interface Spell {
36
+ index: string;
37
+ name: string;
38
+ level?: number;
39
+ school?: {
40
+ name?: string;
41
+ };
42
+ casting_time?: string;
43
+ range?: string;
44
+ components?: string[];
45
+ duration?: string;
46
+ ritual?: boolean;
47
+ concentration?: boolean;
48
+ material?: string;
49
+ desc?: string[];
50
+ higher_level?: string[];
51
+ }
52
+ export interface ClassInfo {
53
+ index: string;
54
+ name: string;
55
+ hit_die?: number;
56
+ proficiency_choices?: {
57
+ from?: {
58
+ options?: {
59
+ item?: {
60
+ name?: string;
61
+ };
62
+ }[];
63
+ };
64
+ }[];
65
+ starting_equipment?: {
66
+ equipment?: {
67
+ name?: string;
68
+ };
69
+ quantity?: number;
70
+ }[];
71
+ }
72
+ export declare function listMonsters(limit?: number): Promise<{
73
+ index: string;
74
+ name: string;
75
+ }[]>;
76
+ export declare function getMonster(index: string): Promise<Monster | null>;
77
+ export declare function getSpell(index: string): Promise<Spell | null>;
78
+ export declare function getClassInfo(index: string): Promise<ClassInfo | null>;
79
+ export declare function fmtSpeed(speed?: Record<string, string>): string;
80
+ export declare function formatMonster(m: Monster): string;
81
+ export declare function formatSpell(s: Spell): string;
package/dist/api.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * D&D 5e SRD client — dnd5eapi.co, keyless.
3
+ * Docs: https://www.dnd5eapi.co/docs — free community-hosted SRD data.
4
+ * (The official API moved; we pin the stable community endpoint.)
5
+ */
6
+ const BASE = "https://www.dnd5eapi.co/api";
7
+ export class DndError extends Error {
8
+ }
9
+ async function getJson(path) {
10
+ const res = await fetch(`${BASE}${path}`, {
11
+ headers: { Accept: "application/json", "User-Agent": "dnd5e-mcp/1.0" },
12
+ redirect: "follow",
13
+ });
14
+ if (!res.ok)
15
+ throw new DndError(`dnd5eapi error ${res.status}: ${res.statusText}`);
16
+ return (await res.json());
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Endpoints
20
+ // ---------------------------------------------------------------------------
21
+ export async function listMonsters(limit = 30) {
22
+ const d = await getJson(`/monsters?limit=${limit}`);
23
+ return (d.results ?? []).map((r) => ({ index: r.index ?? "", name: r.name ?? "?" }));
24
+ }
25
+ export async function getMonster(index) {
26
+ try {
27
+ return (await getJson(`/monsters/${encodeURIComponent(index)}`));
28
+ }
29
+ catch (e) {
30
+ if (e instanceof DndError && String(e).includes("404"))
31
+ return null;
32
+ throw e;
33
+ }
34
+ }
35
+ export async function getSpell(index) {
36
+ try {
37
+ return (await getJson(`/spells/${encodeURIComponent(index)}`));
38
+ }
39
+ catch (e) {
40
+ if (e instanceof DndError && String(e).includes("404"))
41
+ return null;
42
+ throw e;
43
+ }
44
+ }
45
+ export async function getClassInfo(index) {
46
+ try {
47
+ return (await getJson(`/classes/${encodeURIComponent(index)}`));
48
+ }
49
+ catch (e) {
50
+ if (e instanceof DndError && String(e).includes("404"))
51
+ return null;
52
+ throw e;
53
+ }
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Formatting
57
+ // ---------------------------------------------------------------------------
58
+ export function fmtSpeed(speed) {
59
+ if (!speed)
60
+ return "";
61
+ return Object.entries(speed)
62
+ .map(([k, v]) => `${k} ${v}`)
63
+ .join(", ");
64
+ }
65
+ export function formatMonster(m) {
66
+ const ac = m.armor_class?.map((a) => `${a.value ?? "?"}${a.type ? ` (${a.type})` : ""}`).join(" / ");
67
+ const stats = ["STR", "DEX", "CON", "INT", "WIS", "CHA"]
68
+ .map((s, i) => `${s} ${[m.strength, m.dexterity, m.constitution, m.intelligence, m.wisdom, m.charisma][i] ?? "?"}`)
69
+ .join(" ");
70
+ const lines = [
71
+ `${m.name} — ${m.size ?? ""} ${m.type ?? ""}${m.alignment ? `, ${m.alignment}` : ""}`,
72
+ `AC ${ac ?? "?"} · HP ${m.hit_points ?? "?"} (${m.hit_dice ?? ""}) · Speed ${fmtSpeed(m.speed) || "?"}`,
73
+ `CR ${m.challenge_rating ?? "?"}${m.xp ? ` (${m.xp} XP)` : ""} · ${m.languages ?? "no languages"}`,
74
+ stats,
75
+ m.special_abilities?.length
76
+ ? `\nAbilities:\n` +
77
+ m.special_abilities.map((a) => `• ${a.name}: ${(a.desc ?? "").slice(0, 220)}`).join("\n")
78
+ : "",
79
+ m.actions?.length
80
+ ? `\nActions:\n` + m.actions.map((a) => `• ${a.name}: ${(a.desc ?? "").slice(0, 220)}`).join("\n")
81
+ : "",
82
+ m.image ? `\nhttps://www.dnd5eapi.co${m.image}` : "",
83
+ ].filter(Boolean);
84
+ return lines.join("\n");
85
+ }
86
+ export function formatSpell(s) {
87
+ const lines = [
88
+ `${s.name} — level ${s.level ?? 0} ${s.school?.name ?? ""}${s.ritual ? " (ritual)" : ""}`,
89
+ `Casting: ${s.casting_time ?? "?"} · Range: ${s.range ?? "?"} · Components: ${(s.components ?? []).join(", ") || "?"}${s.material ? ` (${s.material})` : ""}`,
90
+ `Duration: ${s.duration ?? "?"}${s.concentration ? " (concentration)" : ""}`,
91
+ "",
92
+ ...(s.desc ?? []).map((d) => d),
93
+ s.higher_level?.length ? `\nAt higher levels: ${s.higher_level.join(" ")}` : "",
94
+ `\nhttps://www.dnd5eapi.co/api/spells/${s.index}`,
95
+ ];
96
+ return lines.join("\n");
97
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,103 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { DndError, formatMonster, formatSpell, getClassInfo, getMonster, getSpell, listMonsters, } from "./api.js";
4
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ export function createServer() {
8
+ const server = new McpServer({
9
+ name: "dnd5e-mcp",
10
+ version: "1.0.0",
11
+ });
12
+ server.registerTool("get_monster", {
13
+ title: "Get monster",
14
+ description: "Get a D&D 5e monster by index slug (e.g. 'aboleth', 'adult-red-dragon', 'goblin').",
15
+ inputSchema: z.object({ index: z.string().describe("Monster index slug") }),
16
+ annotations: READ_ONLY,
17
+ }, async ({ index }) => {
18
+ try {
19
+ const m = await getMonster(index);
20
+ if (!m)
21
+ return text(`No monster "${index}". Try list_monsters for valid slugs.`);
22
+ return text(formatMonster(m));
23
+ }
24
+ catch (e) {
25
+ return textError(errorMessage(e));
26
+ }
27
+ });
28
+ server.registerTool("get_spell", {
29
+ title: "Get spell",
30
+ description: "Get a D&D 5e spell by index slug (e.g. 'fireball', 'magic-missile', 'wish').",
31
+ inputSchema: z.object({ index: z.string().describe("Spell index slug") }),
32
+ annotations: READ_ONLY,
33
+ }, async ({ index }) => {
34
+ try {
35
+ const s = await getSpell(index);
36
+ if (!s)
37
+ return text(`No spell "${index}".`);
38
+ return text(formatSpell(s));
39
+ }
40
+ catch (e) {
41
+ return textError(errorMessage(e));
42
+ }
43
+ });
44
+ server.registerTool("list_monsters", {
45
+ title: "List monsters",
46
+ description: "List D&D 5e SRD monsters by name (a-z), useful for finding valid slugs.",
47
+ inputSchema: z.object({ limit: z.number().int().min(5).max(100).default(30) }),
48
+ annotations: READ_ONLY,
49
+ }, async ({ limit }) => {
50
+ try {
51
+ const monsters = await listMonsters(limit);
52
+ return text(`SRD monsters (${monsters.length}):\n` +
53
+ monsters.map((m, i) => `${i + 1}. ${m.name} [${m.index}]`).join("\n"));
54
+ }
55
+ catch (e) {
56
+ return textError(errorMessage(e));
57
+ }
58
+ });
59
+ server.registerTool("get_class", {
60
+ title: "Get class",
61
+ description: "Get a D&D 5e class overview — hit die, proficiencies, starting equipment.",
62
+ inputSchema: z.object({ index: z.string().describe("Class index slug, e.g. 'barbarian', 'wizard'") }),
63
+ annotations: READ_ONLY,
64
+ }, async ({ index }) => {
65
+ try {
66
+ const c = await getClassInfo(index);
67
+ if (!c)
68
+ return text(`No class "${index}".`);
69
+ const lines = [
70
+ `${c.name} — hit die d${c.hit_die ?? "?"}`,
71
+ c.proficiency_choices?.length
72
+ ? `Skill choices:\n` +
73
+ c.proficiency_choices
74
+ .map((pc, i) => ` ${i + 1}. ` +
75
+ (pc.from?.options ?? [])
76
+ .map((o) => o.item?.name ?? "?")
77
+ .slice(0, 8)
78
+ .join(", "))
79
+ .join("\n")
80
+ : "",
81
+ c.starting_equipment?.length
82
+ ? `Starting equipment:\n` +
83
+ c.starting_equipment
84
+ .map((e) => ` • ${e.quantity ?? 1}× ${e.equipment?.name ?? "?"}`)
85
+ .join("\n")
86
+ : "",
87
+ `https://www.dnd5eapi.co/api/classes/${c.index}`,
88
+ ].filter(Boolean);
89
+ return text(lines.join("\n"));
90
+ }
91
+ catch (e) {
92
+ return textError(errorMessage(e));
93
+ }
94
+ });
95
+ return server;
96
+ }
97
+ function errorMessage(e) {
98
+ if (e instanceof DndError)
99
+ return `Error: ${e.message}`;
100
+ if (e instanceof Error)
101
+ return `Error: ${e.message}`;
102
+ return `Error: ${String(e)}`;
103
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dnd5e-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Use this MCP server to the D&D 5e SRD, monsters, spells, classes, and equipment. Tools include get monster, get spell, list monsters",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/dnd5e-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/dnd5e-mcp.git"
10
+ },
11
+ "bin": {
12
+ "dnd5e-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "start": "node dist/index.js",
21
+ "dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
22
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "dnd",
27
+ "dungeons",
28
+ "dragons",
29
+ "ttrpg"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.4",
34
+ "zod": "^3.23.8"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "typescript": "^5.6.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ }
43
+ }