build_encounter
Calculate XP budgets and suggest monster combinations for balanced D&D 5e encounters. Input party size and level to get encounter difficulty options.
Instructions
Build balanced D&D 5e encounters. Given party size and level, calculates XP budgets for each difficulty and suggests monster combinations.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| party_size | Yes | Number of party members (1-10) | |
| party_level | Yes | Average party level (1-20) | |
| difficulty | No | Target difficulty. If omitted, shows budgets and suggestions for all difficulties. | |
| monster_cr_min | No | Minimum monster CR to consider (numeric, e.g. 0.25 for 1/4) | |
| monster_cr_max | No | Maximum monster CR to consider (numeric) |
Implementation Reference
- src/tools/build-encounter.ts:164-251 (handler)The `registerBuildEncounter` function registers the 'build_encounter' tool on the MCP server. The handler (async callback starting at line 190) calculates XP budgets, queries monsters by CR range, and generates encounter suggestions using helper functions for difficulty calculation, encounter multipliers, and monster group composition.
export function registerBuildEncounter( server: McpServer, db: Database.Database, ): void { server.registerTool( 'build_encounter', { description: 'Build balanced D&D 5e encounters. Given party size and level, calculates XP budgets for each difficulty and suggests monster combinations.', inputSchema: { party_size: z.number().min(1).max(10).describe('Number of party members (1-10)'), party_level: z.number().min(1).max(20).describe('Average party level (1-20)'), difficulty: z .enum(['easy', 'medium', 'hard', 'deadly']) .optional() .describe('Target difficulty. If omitted, shows budgets and suggestions for all difficulties.'), monster_cr_min: z .number() .optional() .describe('Minimum monster CR to consider (numeric, e.g. 0.25 for 1/4)'), monster_cr_max: z .number() .optional() .describe('Maximum monster CR to consider (numeric)'), }, }, async ({ party_size, party_level, difficulty, monster_cr_min, monster_cr_max }) => { const budgets = calculatePartyBudget(party_size, party_level); const crMin = monster_cr_min ?? 0; const crMax = monster_cr_max ?? party_level + 3; const monsters = getMonstersByCrRange(db, crMin, crMax); const lines: string[] = []; lines.push(`# Encounter Builder`); lines.push(''); lines.push(`**Party:** ${party_size} characters at level ${party_level}`); lines.push(`**Monster CR range:** ${crMin}–${crMax}`); lines.push(''); // XP budget table lines.push('## XP Budgets'); lines.push(''); lines.push('| Difficulty | XP Budget |'); lines.push('|------------|-----------|'); for (const d of DIFFICULTIES) { const marker = difficulty === d ? ' ←' : ''; lines.push(`| ${d.charAt(0).toUpperCase() + d.slice(1)} | ${budgets[d].toLocaleString()} XP${marker} |`); } lines.push(''); if (monsters.length === 0) { lines.push('*No SRD monsters found in the specified CR range.*'); return { content: [{ type: 'text' as const, text: lines.join('\n') }], }; } // Suggest groups for target difficulties const targetDifficulties: Difficulty[] = difficulty ? [difficulty] : DIFFICULTIES; for (const d of targetDifficulties) { const budget = budgets[d]; lines.push(`## ${d.charAt(0).toUpperCase() + d.slice(1)} Encounters (${budget.toLocaleString()} XP)`); lines.push(''); const groups = findMonsterGroups(monsters, budget, 5); if (groups.length === 0) { lines.push('*No suitable monster combinations found for this budget and CR range. Try widening the CR range.*'); } else { groups.forEach((group, i) => { lines.push(`**Option ${i + 1}:**`); lines.push(formatGroup(group)); lines.push(''); }); } lines.push(''); } lines.push('---'); lines.push('*Adjusted XP uses DMG encounter multipliers based on monster count. The actual difficulty may vary based on terrain, tactics, and party composition.*'); return { content: [{ type: 'text' as const, text: lines.join('\n') }], }; }, ); } - src/server.ts:54-57 (registration)Registration call: `registerBuildEncounter(server, db)` is invoked in the `createServer` function to wire the tool into the MCP server instance.
registerBuildEncounter(server, db); registerPlanSpells(server, db); registerCompareMonsters(server, db); registerAnalyzeLoadout(server, db); - src/tools/build-encounter.ts:171-188 (schema)Input schema using Zod: defines `party_size` (1-10), `party_level` (1-20), optional `difficulty` enum, and optional `monster_cr_min`/`monster_cr_max` numeric range filters.
description: 'Build balanced D&D 5e encounters. Given party size and level, calculates XP budgets for each difficulty and suggests monster combinations.', inputSchema: { party_size: z.number().min(1).max(10).describe('Number of party members (1-10)'), party_level: z.number().min(1).max(20).describe('Average party level (1-20)'), difficulty: z .enum(['easy', 'medium', 'hard', 'deadly']) .optional() .describe('Target difficulty. If omitted, shows budgets and suggestions for all difficulties.'), monster_cr_min: z .number() .optional() .describe('Minimum monster CR to consider (numeric, e.g. 0.25 for 1/4)'), monster_cr_max: z .number() .optional() .describe('Maximum monster CR to consider (numeric)'), }, - src/tools/build-encounter.ts:33-40 (helper)Helper function `getEncounterMultiplier`: returns DMG encounter XP multiplier based on monster count (1→1x, 2→1.5x, 3-6→2x, 7-10→2.5x, 11-14→3x, 15+→4x).
function getEncounterMultiplier(monsterCount: number): number { if (monsterCount <= 1) return 1; if (monsterCount === 2) return 1.5; if (monsterCount <= 6) return 2; if (monsterCount <= 10) return 2.5; if (monsterCount <= 14) return 3; return 4; } - src/tools/build-encounter.ts:42-53 (helper)Helper function `calculatePartyBudget`: calculates the total XP budget for each difficulty threshold (easy/medium/hard/deadly) based on party size and average level, using the XP_THRESHOLDS table.
function calculatePartyBudget( partySize: number, partyLevel: number, ): Record<Difficulty, number> { const thresholds = XP_THRESHOLDS[partyLevel]; return { easy: thresholds.easy * partySize, medium: thresholds.medium * partySize, hard: thresholds.hard * partySize, deadly: thresholds.deadly * partySize, }; }