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 +52 -0
- package/dist/api.d.ts +81 -0
- package/dist/api.js +97 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +103 -0
- package/package.json +43 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/server.d.ts
ADDED
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
|
+
}
|