@iamjameslennon/ddb-mcp 2.7.0 → 2.8.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 CHANGED
@@ -222,25 +222,37 @@ Can a character use the Help action to assist with a skill check?
222
222
 
223
223
  ## Installation
224
224
 
225
- ### Option A Install from npm (recommended)
225
+ > 🆕 **New to all this?** If you're on a Mac starting from scratch (no Homebrew, no Node, no Claude Desktop), follow the [step-by-step beginner tutorial](INSTALL-MACOS.md) instead — it covers everything below plus how to install the prerequisites.
226
226
 
227
- ```bash
228
- npm install -g @iamjameslennon/ddb-mcp
227
+ Add this to your MCP client's config — no separate install step needed.
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "dndbeyond": {
233
+ "command": "npx",
234
+ "args": ["-y", "@iamjameslennon/ddb-mcp"]
235
+ }
236
+ }
237
+ }
229
238
  ```
230
239
 
231
- Playwright Chromium is installed automatically as part of the global install.
240
+ On first launch, `npx` fetches the package itself (small — under 200 kB unpacked of JS). Playwright Chromium (~140 MB) is downloaded **on demand the first time you run `ddb_login`**, with progress printed to the server log; subsequent logins reuse the cached browser. This keeps server startup fast and the heavy download happens at a moment you expect to wait.
232
241
 
233
- ---
242
+ Configure the path to your client's config file in the [Connecting to your MCP client](#connecting-to-your-mcp-client) section below.
234
243
 
235
- ### Option B Clone and build manually
244
+ To pin a version (recommended for production setups), change the args to `["-y", "@iamjameslennon/ddb-mcp@2.7.0"]`.
245
+
246
+ ### Alternative: install globally
247
+
248
+ If you'd rather have a persistent binary on `PATH` (offline use, air-gapped networks, faster startup):
236
249
 
237
250
  ```bash
238
- git clone https://github.com/iamjameslennon/ddb-mcp.git
239
- cd ddb-mcp
240
- npm ci
241
- npx playwright install chromium
251
+ npm install -g @iamjameslennon/ddb-mcp
242
252
  ```
243
253
 
254
+ Then use `"command": "ddb-mcp"` (no args) in your client config. Chromium is still fetched on first `ddb_login` rather than during install.
255
+
244
256
  ---
245
257
 
246
258
  ## Security & Privacy
@@ -258,7 +270,7 @@ npx playwright install chromium
258
270
  - Screenshots (opt-in): `~/Downloads` only
259
271
  - **Transport**: stdio only — the server opens no HTTP listeners and no ports.
260
272
  - **Untrusted content**: tools that return D&D Beyond page text (`ddb_navigate`, `ddb_get_page`) wrap the scraped output in `<untrusted_dndbeyond_content>` tags. Character notes, campaign descriptions, party-member backstories, and book content can be authored by other DDB users (DMs, party members, forum posters) and may contain prompt-injection attempts — treat them as untrusted input, never as instructions. The `confirm_click` / `confirm_fill` gates on `ddb_interact` exist for exactly this reason.
261
- - **Recommendation**: pin the version when installing `npm install -g @iamjameslennon/ddb-mcp@2.6.1`.
273
+ - **Recommendation**: pin the version in your MCP client config `"@iamjameslennon/ddb-mcp@2.7.0"` — rather than letting `npx` auto-update on every launch.
262
274
 
263
275
  ---
264
276
 
@@ -266,85 +278,44 @@ npx playwright install chromium
266
278
 
267
279
  This server was built and tested with Claude — it will work with any MCP-compatible client, but response quality for D&D-specific reasoning will vary depending on the model used.
268
280
 
269
- First, find your global npm path:
270
-
271
- ```bash
272
- npm root -g
273
- # macOS/Linux example: /usr/local/lib/node_modules
274
- # Windows example: C:\Users\<you>\AppData\Roaming\npm\node_modules
275
- ```
276
-
277
- > In all config examples below, replace `/usr/local/lib/node_modules` with the output of `npm root -g` on your system.
278
-
279
- > **Windows note**: JSON treats `\` as an escape character. Either use forward slashes (Node accepts them on Windows) — `"C:/Users/<you>/AppData/Roaming/npm/node_modules/@iamjameslennon/ddb-mcp/dist/index.js"` — or double every backslash: `"C:\\Users\\<you>\\AppData\\Roaming\\npm\\node_modules\\@iamjameslennon\\ddb-mcp\\dist\\index.js"`. Forward slashes are simpler and harder to get wrong.
281
+ All clients below use the same JSON config from the [Installation](#installation) section. Drop it into your client's config file (paths below), then restart the client.
280
282
 
281
283
  ### Claude Desktop (recommended)
282
284
 
283
- Edit your Claude Desktop config file:
284
-
285
285
  - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
286
286
  - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
287
- - **Linux**: `~/.config/Claude/claude_desktop_config.json`
287
+ - **Linux**: `~/.config/Claude/claude_desktop_config.json` (community builds only — Claude Desktop has no official Linux release; use [Claude Code](#claude-code) or [Cursor](#cursor) instead)
288
288
 
289
- Linux note: Claude Desktop has no official Linux release. If you don't run a community build, use [Claude Code](#claude-code), [Cursor](#cursor), or any other MCP client below.
289
+ ### Claude Code
290
290
 
291
- ```json
292
- {
293
- "mcpServers": {
294
- "dndbeyond": {
295
- "command": "node",
296
- "args": ["/usr/local/lib/node_modules/@iamjameslennon/ddb-mcp/dist/index.js"]
297
- }
298
- }
299
- }
291
+ One-liner — no manual JSON editing:
292
+
293
+ ```bash
294
+ claude mcp add dndbeyond -- npx -y @iamjameslennon/ddb-mcp
300
295
  ```
301
296
 
302
- ### Claude Code
297
+ Or if you installed globally:
303
298
 
304
299
  ```bash
305
- claude mcp add dndbeyond node $(npm root -g)/@iamjameslennon/ddb-mcp/dist/index.js
300
+ claude mcp add dndbeyond ddb-mcp
306
301
  ```
307
302
 
308
303
  ### Cursor
309
304
 
310
- Edit `~/.cursor/mcp.json`:
311
-
312
- ```json
313
- {
314
- "mcpServers": {
315
- "dndbeyond": {
316
- "command": "node",
317
- "args": ["/usr/local/lib/node_modules/@iamjameslennon/ddb-mcp/dist/index.js"]
318
- }
319
- }
320
- }
321
- ```
305
+ `~/.cursor/mcp.json`
322
306
 
323
307
  ### Windsurf
324
308
 
325
- Edit `~/.codeium/windsurf/mcp_config.json`:
326
-
327
- ```json
328
- {
329
- "mcpServers": {
330
- "dndbeyond": {
331
- "command": "node",
332
- "args": ["/usr/local/lib/node_modules/@iamjameslennon/ddb-mcp/dist/index.js"]
333
- }
334
- }
335
- }
336
- ```
309
+ `~/.codeium/windsurf/mcp_config.json`
337
310
 
338
311
  ### LM Studio
339
312
 
340
- MCP support was added in LM Studio 0.3.x. Configure through the UI under **Settings → MCP Servers** using the same JSON shape as Claude Desktop above. Steps may vary between versions — see [lmstudio.ai/docs](https://lmstudio.ai/docs) for current instructions.
313
+ MCP support was added in LM Studio 0.3.x. Configure through the UI under **Settings → MCP Servers** using the same JSON shape as above. Steps may vary between versions — see [lmstudio.ai/docs](https://lmstudio.ai/docs) for current instructions.
341
314
 
342
315
  ### Open WebUI
343
316
 
344
317
  MCP servers are configured through the admin panel under **Settings → Tools**. See [docs.openwebui.com](https://docs.openwebui.com) for current instructions — the UI changes frequently between releases.
345
318
 
346
- > LM Studio and Open WebUI use the same underlying `node` command and path as the other clients above.
347
-
348
319
  ---
349
320
 
350
321
  ## First-time login
@@ -547,15 +518,17 @@ Remove-Item "$env:APPDATA\ddb-mcp\session.json"
547
518
  Your session has expired. Run `ddb_login` to re-authenticate.
548
519
 
549
520
  **Chromium not found / browser won't launch**
521
+ Chromium is installed lazily on first `ddb_login`. If the download failed (network or sandbox issue), run `ddb_login` again — the server retries on each call. If it keeps failing, fetch the browser manually:
550
522
  ```bash
551
523
  npx playwright install chromium
552
524
  ```
525
+ The same global Playwright cache is shared by every install path (npx, global, local clone) — one successful install is reused everywhere.
553
526
 
554
527
  **Character returns 403 or "private"**
555
528
  The character is set to private on D&D Beyond. You must be logged in as the owner, or the owner must make it public.
556
529
 
557
530
  **MCP server not appearing in Claude Code**
558
- Run `/mcp` in Claude Code to reconnect. If it still doesn't appear, verify the path in `claude mcp list` points to the correct `dist/index.js`.
531
+ Run `/mcp` in Claude Code to reconnect. If it still doesn't appear, run `claude mcp list` to confirm the `dndbeyond` entry exists.
559
532
 
560
533
  **Server crashes on startup**
561
534
  Make sure you're running Node.js 20 or later: `node --version`.
@@ -565,10 +538,12 @@ Make sure you're running Node.js 20 or later: `node --version`.
565
538
  ## Development
566
539
 
567
540
  ```bash
568
- # Install dependencies (always use npm ci not npm install)
541
+ # Install dependencies prefer npm ci to respect the lockfile
569
542
  npm ci
570
543
 
571
- # Run in development mode (no build step needed)
544
+ # Run in development mode (no build step needed). Chromium is fetched lazily
545
+ # on first `ddb_login`; if you want to pre-warm the cache:
546
+ # npx playwright install chromium
572
547
  npm run dev
573
548
 
574
549
  # Build
@@ -577,7 +552,7 @@ npm run build
577
552
  # Watch mode
578
553
  npm run build:watch
579
554
 
580
- # Run tests
555
+ # Run tests (browser-free — they mock the Playwright surface)
581
556
  npm test
582
557
  ```
583
558
 
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKrE,OAAO,EAA0B,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AAMrC,wBAAsB,UAAU,CAAC,QAAQ,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAclE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAmB1E;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCxE;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQrE,OAAO,EAA0B,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AA0DrC,wBAAsB,UAAU,CAAC,QAAQ,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBlE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAmB1E;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCxE;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
package/dist/browser.js CHANGED
@@ -1,10 +1,65 @@
1
1
  import { chromium } from "playwright";
2
2
  import { existsSync, mkdirSync, openSync, closeSync, writeFileSync, fchmodSync, renameSync, constants, } from "fs";
3
+ import { spawn } from "child_process";
4
+ import { createRequire } from "module";
5
+ import { dirname, join } from "path";
3
6
  import { invalidateSessionCache, SESSION_DIR, SESSION_PATH } from "./session-fetch.js";
4
7
  export { SESSION_DIR, SESSION_PATH };
5
8
  let browserInstance = null;
6
9
  let browserHeadless = null;
7
10
  let contextInstance = null;
11
+ // Singleton Promise so concurrent getBrowser() callers share one install run.
12
+ // Cleared on failure so subsequent calls can retry.
13
+ let chromiumInstallPromise = null;
14
+ async function ensureChromiumInstalled() {
15
+ if (chromiumInstallPromise)
16
+ return chromiumInstallPromise;
17
+ // chromium.executablePath() returns the *expected* path; if the binary is
18
+ // not there, we need to fetch it. Wrapped in try because some Playwright
19
+ // versions throw on this call when no browsers are registered yet.
20
+ let executablePath = "";
21
+ try {
22
+ executablePath = chromium.executablePath();
23
+ }
24
+ catch { /* fall through to install */ }
25
+ if (executablePath && existsSync(executablePath))
26
+ return;
27
+ chromiumInstallPromise = (async () => {
28
+ process.stderr.write("[ddb-mcp] Chromium not found — downloading (~140 MB, one-time)…\n");
29
+ // Resolve Playwright's CLI relative to *this* module (not CWD), so the
30
+ // install works whether the server was launched via npx, a global install,
31
+ // or a local clone. Note: Playwright's `exports` map doesn't expose
32
+ // `cli.js`, so we resolve `package.json` (which IS exported) and walk
33
+ // sideways. process.execPath ensures we use the same Node binary.
34
+ const require = createRequire(import.meta.url);
35
+ const cliPath = join(dirname(require.resolve("playwright/package.json")), "cli.js");
36
+ await new Promise((resolve, reject) => {
37
+ // stdout → parent stderr: critical because the MCP server uses stdout
38
+ // for JSON-RPC frames; leaking install progress into stdout would break
39
+ // the client connection.
40
+ const child = spawn(process.execPath, [cliPath, "install", "chromium"], {
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ child.stdout?.pipe(process.stderr);
44
+ child.stderr?.pipe(process.stderr);
45
+ child.on("error", reject);
46
+ child.on("exit", code => {
47
+ if (code === 0) {
48
+ process.stderr.write("[ddb-mcp] Chromium installed.\n");
49
+ resolve();
50
+ }
51
+ else {
52
+ reject(new Error(`Chromium install failed (exit ${code}). ` +
53
+ `Network or sandbox issue? Run \`npx playwright install chromium\` manually to retry.`));
54
+ }
55
+ });
56
+ });
57
+ })();
58
+ // Allow retry on failure — clear the cached promise so the next caller
59
+ // doesn't immediately re-fail against the same rejected Promise.
60
+ chromiumInstallPromise.catch(() => { chromiumInstallPromise = null; });
61
+ return chromiumInstallPromise;
62
+ }
8
63
  export async function getBrowser(headless = true) {
9
64
  // If a browser is already running with a different headless setting, close it
10
65
  // first so ddb_login always gets a visible window even if headless tools ran before.
@@ -13,6 +68,10 @@ export async function getBrowser(headless = true) {
13
68
  }
14
69
  if (browserInstance)
15
70
  return browserInstance;
71
+ // Lazy Chromium provisioning: fetched on first use (during ddb_login) so
72
+ // package install stays fast and the ~140 MB download happens at a moment
73
+ // the user expects work to happen and we can surface progress.
74
+ await ensureChromiumInstalled();
16
75
  const args = ["--disable-blink-features=AutomationControlled"];
17
76
  // Sandbox should stay enabled. Only disable it in constrained container
18
77
  // environments (e.g. CI/Docker) where the kernel doesn't support it.
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ #!/usr/bin/env node
1
2
  export {};
2
3
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
+ #!/usr/bin/env node
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { z } from "zod";
4
5
  import { getBrowser, getContext, closeBrowser } from "./browser.js";
5
6
  import { login } from "./auth.js";
6
- import { getCharacter, downloadCharacter, listCharacters, parseCharacter, findCharacterByName, getDefinition } from "./tools/character.js";
7
- import { getCampaign, listMyCampaigns } from "./tools/campaign.js";
7
+ import { getCharacter, downloadCharacter, listCharacters, parseCharacter, findCharacterByName, getDefinition, clearCharacterCache } from "./tools/character.js";
8
+ import { getCampaign, listMyCampaigns, invalidateCampaignCache } from "./tools/campaign.js";
8
9
  import { getParty } from "./tools/party.js";
9
10
  import { navigate, interact, getCurrentPageContent } from "./tools/navigate.js";
10
11
  import { search } from "./tools/search.js";
11
12
  import { listLibrary, readBook } from "./tools/library.js";
12
- import { searchMonsters, getMonster } from "./tools/monster.js";
13
+ import { searchMonsters, getMonster, clearMonsterCache } from "./tools/monster.js";
13
14
  import { rateEncounter, targetEncounterCr } from "./tools/encounter.js";
14
15
  import { generateTreasure } from "./tools/treasure.js";
15
- import { getCondition, searchSpells, getSpell, searchItems, getItem, searchRaces, searchClasses, searchBackgrounds, searchFeats, searchClassFeatures, searchRacialTraits, searchRules, getRule } from "./tools/reference.js";
16
+ import { getCondition, searchSpells, getSpell, searchItems, getItem, searchRaces, searchClasses, searchBackgrounds, searchFeats, searchClassFeatures, searchRacialTraits, searchRules, getRule, clearReferenceCache } from "./tools/reference.js";
17
+ import { clearOpen5eCache } from "./open5e.js";
16
18
  const server = new McpServer({
17
19
  name: "dndbeyond",
18
20
  version: "1.0.0",
@@ -59,8 +61,35 @@ server.tool("ddb_close_browser", "Close the background browser window if one is
59
61
  return { content: [{ type: "text", text: `Failed to close browser: ${msg}` }], isError: true };
60
62
  }
61
63
  });
64
+ // ─── ddb_clear_cache ─────────────────────────────────────────────────────────
65
+ server.tool("ddb_clear_cache", "Wipe in-process caches so the next call re-fetches from D&D Beyond. Use this when search results look like the SRD/Open5e fallback (e.g. 2024 cantrips missing from spell results) after you've logged in — the partial compendium build is cached for up to 5 minutes and this forces an immediate retry. cache='spells' (default) clears just the spell/reference compendium; 'all' wipes every cache.", {
66
+ cache: z
67
+ .enum(["spells", "characters", "monsters", "all"])
68
+ .default("spells")
69
+ .describe("Which cache to clear. 'spells' (default) — the spell / equipment / races / classes / feats compendium. 'characters' — character JSON cache. 'monsters' — monster stat-block cache. 'all' — everything, including campaigns and Open5e responses."),
70
+ }, async ({ cache }) => {
71
+ const cleared = [];
72
+ if (cache === "spells" || cache === "all") {
73
+ clearReferenceCache();
74
+ cleared.push("spells/reference compendium");
75
+ }
76
+ if (cache === "characters" || cache === "all") {
77
+ clearCharacterCache();
78
+ cleared.push("character JSON");
79
+ }
80
+ if (cache === "monsters" || cache === "all") {
81
+ clearMonsterCache();
82
+ cleared.push("monsters");
83
+ }
84
+ if (cache === "all") {
85
+ invalidateCampaignCache();
86
+ clearOpen5eCache();
87
+ cleared.push("campaigns", "Open5e responses");
88
+ }
89
+ return { content: [{ type: "text", text: `Cleared: ${cleared.join(", ")}. The next relevant tool call will re-fetch from D&D Beyond.` }] };
90
+ });
62
91
  // ─── ddb_list_characters ──────────────────────────────────────────────────────
63
- server.tool("ddb_list_characters", "List all characters in your D&D Beyond account, including their ID, level, race, and class.", {}, async () => {
92
+ server.tool("ddb_list_characters", "List all characters in your D&D Beyond account, including their ID, level, race, and class. Requires login — run ddb_login first if you haven't already.", {}, async () => {
64
93
  try {
65
94
  const result = await listCharacters();
66
95
  return { content: [{ type: "text", text: result }] };
@@ -72,7 +101,7 @@ server.tool("ddb_list_characters", "List all characters in your D&D Beyond accou
72
101
  }
73
102
  });
74
103
  // ─── ddb_get_character_raw ────────────────────────────────────────────────────
75
- server.tool("ddb_get_character_raw", "Returns raw 300–500 KB character JSON. Requires confirm_large_response: true. Use ddb_get_character instead for all normal use.", {
104
+ server.tool("ddb_get_character_raw", "Returns raw 300–500 KB character JSON. Requires confirm_large_response: true. Use ddb_get_character instead for all normal use. Requires login — run ddb_login first if you haven't already.", {
76
105
  character_id: z.string().min(1).optional().describe("The D&D Beyond character ID (e.g. '12345678')"),
77
106
  character_name: z.string().min(1).optional().describe("Character name to look up (fuzzy matched against your account)"),
78
107
  confirm_large_response: z.literal(true).describe("Must be true to proceed — acknowledges this call returns 300–500 KB."),
@@ -99,7 +128,7 @@ server.tool("ddb_get_character_raw", "Returns raw 300–500 KB character JSON. R
99
128
  }
100
129
  });
101
130
  // ─── ddb_download_character ───────────────────────────────────────────────────
102
- server.tool("ddb_download_character", "Download a character's full JSON data to a local file.", {
131
+ server.tool("ddb_download_character", "Download a character's full JSON data to a local file. Requires login — run ddb_login first if you haven't already.", {
103
132
  character_id: z.string().min(1).describe("The D&D Beyond character ID"),
104
133
  output_path: z
105
134
  .string()
@@ -118,7 +147,7 @@ server.tool("ddb_download_character", "Download a character's full JSON data to
118
147
  }
119
148
  });
120
149
  // ─── ddb_get_character ───────────────────────────────────────────────────────
121
- server.tool("ddb_get_character", "Parse and display a character sheet. Use sections to reduce output: summary (vitals+stats), combat (adds actions/weapons), spells (spellcasting only), inventory, features, concentration, notes (backstory, traits, bonds), or full (default).", {
150
+ server.tool("ddb_get_character", "Parse and display a character sheet. Use sections to reduce output: summary (vitals+stats), combat (adds actions/weapons), spells (spellcasting only), inventory, features, concentration, notes (backstory, traits, bonds), or full (default). Requires login — run ddb_login first if you haven't already.", {
122
151
  character_id: z.string().min(1).optional().describe("The D&D Beyond character ID (e.g. '12345678')"),
123
152
  character_name: z.string().min(1).optional().describe("Character name to look up (fuzzy matched — e.g. 'Throin' finds 'Thorin Ironforge')"),
124
153
  sections: z.enum(["summary", "combat", "spells", "inventory", "features", "concentration", "notes", "full"])
@@ -147,7 +176,7 @@ server.tool("ddb_get_character", "Parse and display a character sheet. Use secti
147
176
  }
148
177
  });
149
178
  // ─── ddb_character_lookup ────────────────────────────────────────────────────
150
- server.tool("ddb_character_lookup", "Look up the full description of a spell, feat, class feature, subclass feature, racial trait, background feature, or equipped item by name. Supports partial and fuzzy name matching (e.g. 'cutting' finds Cutting Words, 'sheild' finds Shield). Accepts either a numeric character_id or a character_name.", {
179
+ server.tool("ddb_character_lookup", "Look up the full description of a spell, feat, class feature, subclass feature, racial trait, background feature, or equipped item by name. Supports partial and fuzzy name matching (e.g. 'cutting' finds Cutting Words, 'sheild' finds Shield). Accepts either a numeric character_id or a character_name. Requires login — run ddb_login first if you haven't already.", {
151
180
  character_id: z.string().min(1).optional().describe("The D&D Beyond character ID"),
152
181
  character_name: z.string().min(1).optional().describe("Character name (fuzzy matched against your account)"),
153
182
  name: z.string().min(1).describe("Name to search for — partial match, e.g. 'hunter' finds Hunter's Mark"),
@@ -205,7 +234,7 @@ server.tool("ddb_get_monster", "Get the full stat block for a specific monster f
205
234
  }
206
235
  });
207
236
  // ─── ddb_get_campaign ─────────────────────────────────────────────────────────
208
- server.tool("ddb_get_campaign", "Fetch campaign information including player characters from a D&D Beyond campaign.", {
237
+ server.tool("ddb_get_campaign", "Fetch campaign information including player characters from a D&D Beyond campaign. Requires login — run ddb_login first if you haven't already.", {
209
238
  campaign_id: z.string().min(1).describe("The D&D Beyond campaign ID (found in the campaign URL)"),
210
239
  }, async ({ campaign_id }) => {
211
240
  try {
@@ -219,7 +248,7 @@ server.tool("ddb_get_campaign", "Fetch campaign information including player cha
219
248
  }
220
249
  });
221
250
  // ─── ddb_list_campaigns ───────────────────────────────────────────────────────
222
- server.tool("ddb_list_campaigns", "List all D&D Beyond campaigns you are part of (as DM or player).", {}, async () => {
251
+ server.tool("ddb_list_campaigns", "List all D&D Beyond campaigns you are part of (as DM or player). Requires login — run ddb_login first if you haven't already.", {}, async () => {
223
252
  try {
224
253
  const data = await listMyCampaigns();
225
254
  return { content: [{ type: "text", text: data }] };
@@ -231,7 +260,7 @@ server.tool("ddb_list_campaigns", "List all D&D Beyond campaigns you are part of
231
260
  }
232
261
  });
233
262
  // ─── ddb_get_party ────────────────────────────────────────────────────────────
234
- server.tool("ddb_get_party", "Fetch a compact summary of every character in a campaign. Returns HP, AC, initiative, passive scores, ability scores, and skills for the whole party in one call.", {
263
+ server.tool("ddb_get_party", "Fetch a compact summary of every character in a campaign. Returns HP, AC, initiative, passive scores, ability scores, and skills for the whole party in one call. Requires login — run ddb_login first if you haven't already.", {
235
264
  campaign_id: z.string().min(1).describe("The D&D Beyond campaign ID (found in the campaign URL)"),
236
265
  }, async ({ campaign_id }) => {
237
266
  try {
@@ -339,7 +368,7 @@ server.tool("ddb_search_site", "Search D&D Beyond for spells, monsters, magic it
339
368
  }
340
369
  });
341
370
  // ─── ddb_list_library ─────────────────────────────────────────────────────────
342
- server.tool("ddb_list_library", "List all books and sourcebooks you own in your D&D Beyond library.", {}, async () => {
371
+ server.tool("ddb_list_library", "List all books and sourcebooks you own in your D&D Beyond library. Requires login — run ddb_login first if you haven't already.", {}, async () => {
343
372
  try {
344
373
  const context = await getSharedContext();
345
374
  const books = await listLibrary(context);
@@ -354,7 +383,7 @@ server.tool("ddb_list_library", "List all books and sourcebooks you own in your
354
383
  }
355
384
  });
356
385
  // ─── ddb_read_book ────────────────────────────────────────────────────────────
357
- server.tool("ddb_read_book", "Read a D&D Beyond book. Accepts either a slug (e.g. dnd/phb-2024) or a plain title (e.g. \"Player's Handbook\" or \"monster manual\") — the server resolves titles against your library automatically. Specify chapter_slug for a chapter, query to jump to a heading, and max_chars to control response size.", {
386
+ server.tool("ddb_read_book", "Read a D&D Beyond book. Accepts either a slug (e.g. dnd/phb-2024) or a plain title (e.g. \"Player's Handbook\" or \"monster manual\") — the server resolves titles against your library automatically. Specify chapter_slug for a chapter, query to jump to a heading, and max_chars to control response size. Requires login — run ddb_login first if you haven't already.", {
358
387
  book_slug: z
359
388
  .string()
360
389
  .min(1)
@@ -451,7 +480,7 @@ server.tool("ddb_get_equipment", "Get the full stats and description of any item
451
480
  return { content: [{ type: "text", text: `Item lookup failed: ${msg}` }], isError: true };
452
481
  }
453
482
  });
454
- server.tool("ddb_search_races", "Search all D&D Beyond races and subraces (including homebrew). Not character-specific.", {
483
+ server.tool("ddb_search_races", "Search all D&D Beyond races and subraces (including homebrew). Not character-specific. Requires login — run ddb_login first if you haven't already.", {
455
484
  name: z.string().optional().describe("Partial race name (e.g. 'elf', 'tiefling')"),
456
485
  limit: z.number().int().min(1).max(100).default(30).describe("Max results (default 30)"),
457
486
  offset: z.number().int().min(0).default(0).describe("Skip N results for pagination"),
@@ -466,7 +495,7 @@ server.tool("ddb_search_races", "Search all D&D Beyond races and subraces (inclu
466
495
  return { content: [{ type: "text", text: `Race search failed: ${msg}` }], isError: true };
467
496
  }
468
497
  });
469
- server.tool("ddb_search_classes", "Search all D&D Beyond classes with hit die, spellcasting, and subclasses.", {
498
+ server.tool("ddb_search_classes", "Search all D&D Beyond classes with hit die, spellcasting, and subclasses. Requires login — run ddb_login first if you haven't already.", {
470
499
  name: z.string().optional().describe("Partial class name (e.g. 'fighter', 'wizard')"),
471
500
  limit: z.number().int().min(1).max(100).default(30).describe("Max results (default 30)"),
472
501
  offset: z.number().int().min(0).default(0).describe("Skip N results for pagination"),
@@ -481,7 +510,7 @@ server.tool("ddb_search_classes", "Search all D&D Beyond classes with hit die, s
481
510
  return { content: [{ type: "text", text: `Class search failed: ${msg}` }], isError: true };
482
511
  }
483
512
  });
484
- server.tool("ddb_search_backgrounds", "Search all D&D Beyond backgrounds (including homebrew).", {
513
+ server.tool("ddb_search_backgrounds", "Search all D&D Beyond backgrounds (including homebrew). Requires login — run ddb_login first if you haven't already.", {
485
514
  name: z.string().optional().describe("Partial background name (e.g. 'sage', 'criminal')"),
486
515
  limit: z.number().int().min(1).max(100).default(30).describe("Max results (default 30)"),
487
516
  offset: z.number().int().min(0).default(0).describe("Skip N results for pagination"),
@@ -496,7 +525,7 @@ server.tool("ddb_search_backgrounds", "Search all D&D Beyond backgrounds (includ
496
525
  return { content: [{ type: "text", text: `Background search failed: ${msg}` }], isError: true };
497
526
  }
498
527
  });
499
- server.tool("ddb_search_feats", "Search feats by name or prerequisite text.", {
528
+ server.tool("ddb_search_feats", "Search feats by name or prerequisite text. Requires login — run ddb_login first if you haven't already.", {
500
529
  name: z.string().optional().describe("Partial feat name (e.g. 'sharpshooter', 'magic')"),
501
530
  prerequisite: z.string().optional().describe("Filter by prerequisite text (e.g. 'spellcaster', 'level 4')"),
502
531
  limit: z.number().int().min(1).max(100).default(30).describe("Max results (default 30)"),
@@ -512,7 +541,7 @@ server.tool("ddb_search_feats", "Search feats by name or prerequisite text.", {
512
541
  return { content: [{ type: "text", text: `Feat search failed: ${msg}` }], isError: true };
513
542
  }
514
543
  });
515
- server.tool("ddb_search_class_features", "Search class features by name, class, or level gained.", {
544
+ server.tool("ddb_search_class_features", "Search class features by name, class, or level gained. Requires login — run ddb_login first if you haven't already.", {
516
545
  name: z.string().optional().describe("Partial feature name (e.g. 'action surge', 'sneak attack')"),
517
546
  class_name: z.string().optional().describe("Class name filter (e.g. 'fighter', 'rogue')"),
518
547
  level: z.number().int().min(1).max(20).optional().describe("Level at which the feature is gained"),
@@ -529,7 +558,7 @@ server.tool("ddb_search_class_features", "Search class features by name, class,
529
558
  return { content: [{ type: "text", text: `Class feature search failed: ${msg}` }], isError: true };
530
559
  }
531
560
  });
532
- server.tool("ddb_search_racial_traits", "Search racial traits by name or race.", {
561
+ server.tool("ddb_search_racial_traits", "Search racial traits by name or race. Requires login — run ddb_login first if you haven't already.", {
533
562
  name: z.string().optional().describe("Partial trait name (e.g. 'darkvision', 'breath weapon')"),
534
563
  race_name: z.string().optional().describe("Race name filter (e.g. 'elf', 'dragonborn')"),
535
564
  limit: z.number().int().min(1).max(100).default(30).describe("Max results (default 30)"),