Skip to main content
Glama

plan_spells

Plan your D&D 5e character's spells by class and level. Track spell slots, detect concentration conflicts, highlight rituals, and calculate material component costs.

Instructions

Plan spells for a D&D 5e character. Shows available spells for a class at a given level, tracks spell slots, flags concentration conflicts, highlights rituals, and sums material component costs.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
class_nameYesSpellcasting class name (e.g. "Wizard", "Cleric")
levelYesCharacter level (1-20)
prepared_spellsNoList of spell names you plan to prepare. If provided, analyzes conflicts and costs.
remaining_slotsNoRemaining spell slots as an object mapping spell level (string) to count, e.g. {"1": 3, "2": 1}

Implementation Reference

  • The main handler function 'registerPlanSpells' that registers the 'plan_spells' tool on the MCP server. It defines the input schema (class_name, level, prepared_spells, remaining_slots), validates the class, computes spell slots and available spells, and returns a formatted text response with spell planning details, concentration conflict detection, ritual highlights, and material component cost analysis.
    export function registerPlanSpells(
      server: McpServer,
      db: Database.Database,
    ): void {
      server.registerTool(
        'plan_spells',
        {
          description:
            'Plan spells for a D&D 5e character. Shows available spells for a class at a given level, tracks spell slots, flags concentration conflicts, highlights rituals, and sums material component costs.',
          inputSchema: {
            class_name: z.string().describe('Spellcasting class name (e.g. "Wizard", "Cleric")'),
            level: z.number().min(1).max(20).describe('Character level (1-20)'),
            prepared_spells: z
              .array(z.string())
              .optional()
              .describe('List of spell names you plan to prepare. If provided, analyzes conflicts and costs.'),
            remaining_slots: z
              .record(z.string(), z.number())
              .optional()
              .describe('Remaining spell slots as an object mapping spell level (string) to count, e.g. {"1": 3, "2": 1}'),
          },
        },
        async ({ class_name, level, prepared_spells, remaining_slots }) => {
          const classRow = getClassByName(db, class_name);
          if (!classRow) {
            return {
              content: [{ type: 'text' as const, text: `Class "${class_name}" not found in the SRD. Available classes include: Bard, Cleric, Druid, Paladin, Ranger, Sorcerer, Warlock, Wizard.` }],
              isError: true,
            };
          }
    
          if (!classRow.spellcasting_ability) {
            return {
              content: [{ type: 'text' as const, text: `${classRow.name} is not a spellcasting class (no spellcasting ability). Non-caster classes like Fighter and Barbarian do not have spell lists.` }],
              isError: true,
            };
          }
    
          const maxSpellLevel = getMaxSpellLevel(classRow.name, level);
          if (maxSpellLevel === 0) {
            return {
              content: [{ type: 'text' as const, text: `${classRow.name} does not have spell slots at level ${level}. ${HALF_CASTERS.includes(classRow.name.toLowerCase()) ? 'Half casters gain spellcasting at level 2.' : ''}` }],
              isError: true,
            };
          }
    
          const spells = getSpellsByClassAndLevel(db, classRow.name, maxSpellLevel);
          const slots = getSpellSlots(classRow.name, level);
    
          const lines: string[] = [];
          lines.push(`# Spell Planning: ${classRow.name} Level ${level}`);
          lines.push('');
          lines.push(`**Spellcasting Ability:** ${classRow.spellcasting_ability}`);
          lines.push(`**Max Spell Level:** ${maxSpellLevel}`);
          lines.push(`**Available SRD Spells:** ${spells.length}`);
          lines.push('');
    
          // Spell slot table
          lines.push('## Spell Slots');
          lines.push('');
          if (slots.length > 0) {
            const headerCells = slots.map((_, i) => `${i + 1}st${i === 0 ? '' : i === 1 ? '' : ''}`.replace(/1st$/, '1st').replace(/2st$/, '2nd').replace(/3st$/, '3rd'));
            const levelLabels = slots.map((_, i) => {
              const n = i + 1;
              if (n === 1) return '1st';
              if (n === 2) return '2nd';
              if (n === 3) return '3rd';
              return `${n}th`;
            });
            lines.push('| ' + levelLabels.join(' | ') + ' |');
            lines.push('| ' + slots.map(() => '---').join(' | ') + ' |');
    
            if (remaining_slots) {
              const totalRow = slots.map((s) => `${s}`);
              const remainRow = slots.map((s, i) => {
                const key = `${i + 1}`;
                const rem = remaining_slots[key] ?? s;
                return `${rem}/${s}`;
              });
              lines.push('| ' + totalRow.join(' | ') + ' | *(Total)*');
              lines.push('| ' + remainRow.join(' | ') + ' | *(Remaining)*');
            } else {
              lines.push('| ' + slots.join(' | ') + ' |');
            }
          }
          lines.push('');
    
          // Available spells by level
          lines.push('## Available Spells by Level');
          lines.push('');
          const spellsByLevel = new Map<number, SpellRow[]>();
          for (const spell of spells) {
            const list = spellsByLevel.get(spell.level) ?? [];
            list.push(spell);
            spellsByLevel.set(spell.level, list);
          }
    
          for (let lvl = 0; lvl <= maxSpellLevel; lvl++) {
            const levelSpells = spellsByLevel.get(lvl);
            if (!levelSpells || levelSpells.length === 0) continue;
            const label = lvl === 0 ? 'Cantrips' : `Level ${lvl}`;
            lines.push(`### ${label} (${levelSpells.length} spells)`);
            for (const s of levelSpells) {
              const tags: string[] = [];
              if (s.concentration) tags.push('C');
              if (s.ritual) tags.push('R');
              const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
              lines.push(`- ${s.name}${tagStr} — ${s.school}, ${s.casting_time}`);
            }
            lines.push('');
          }
    
          // Prepared spells analysis
          if (prepared_spells && prepared_spells.length > 0) {
            lines.push('## Prepared Spell Analysis');
            lines.push('');
    
            const found: SpellRow[] = [];
            const notFound: string[] = [];
            for (const name of prepared_spells) {
              const spell = getSpellByName(db, name);
              if (spell) {
                found.push(spell);
              } else {
                notFound.push(name);
              }
            }
    
            if (notFound.length > 0) {
              lines.push(`**Not found in SRD:** ${notFound.join(', ')}`);
              lines.push('');
            }
    
            // Concentration conflicts
            const concentrationSpells = found.filter((s) => s.concentration);
            if (concentrationSpells.length > 1) {
              lines.push(`**⚠ Concentration Conflict:** You have ${concentrationSpells.length} concentration spells prepared. You can only concentrate on ONE at a time:`);
              for (const s of concentrationSpells) {
                lines.push(`  - ${s.name} (${s.duration})`);
              }
              lines.push('');
            } else if (concentrationSpells.length === 1) {
              lines.push(`**Concentration:** ${concentrationSpells[0].name} (${concentrationSpells[0].duration})`);
              lines.push('');
            }
    
            // Ritual spells
            const ritualSpells = found.filter((s) => s.ritual);
            if (ritualSpells.length > 0) {
              lines.push(`**Ritual Spells** (can cast without expending a slot, +10 min casting time):`);
              for (const s of ritualSpells) {
                lines.push(`  - ${s.name}`);
              }
              lines.push('');
            }
    
            // Material component costs
            let totalCost = 0;
            const costlyComponents: { name: string; cost: number; desc: string }[] = [];
            for (const s of found) {
              if (s.components_m && s.material_description) {
                const cost = extractCostFromMaterial(s.material_description);
                if (cost !== null && cost > 0) {
                  totalCost += cost;
                  costlyComponents.push({ name: s.name, cost, desc: s.material_description });
                }
              }
            }
            if (costlyComponents.length > 0) {
              lines.push('**Material Component Costs:**');
              for (const c of costlyComponents) {
                lines.push(`  - ${c.name}: ${c.cost} gp — ${c.desc}`);
              }
              lines.push(`  - **Total: ${totalCost} gp**`);
              lines.push('');
            }
    
            // Summary table
            lines.push('**Prepared Spells Summary:**');
            lines.push('');
            lines.push('| Spell | Level | School | Components | Concentration | Ritual |');
            lines.push('|-------|-------|--------|------------|---------------|--------|');
            for (const s of found) {
              lines.push(`| ${s.name} | ${s.level === 0 ? 'Cantrip' : s.level} | ${s.school} | ${formatComponents(s)} | ${s.concentration ? 'Yes' : 'No'} | ${s.ritual ? 'Yes' : 'No'} |`);
            }
            lines.push('');
          }
    
          return {
            content: [{ type: 'text' as const, text: lines.join('\n') }],
          };
        },
      );
    }
  • Input schema for the 'plan_spells' tool defined via Zod: class_name (string), level (number 1-20), optional prepared_spells (array of strings), optional remaining_slots (record mapping level strings to counts).
    server.registerTool(
      'plan_spells',
      {
        description:
          'Plan spells for a D&D 5e character. Shows available spells for a class at a given level, tracks spell slots, flags concentration conflicts, highlights rituals, and sums material component costs.',
        inputSchema: {
          class_name: z.string().describe('Spellcasting class name (e.g. "Wizard", "Cleric")'),
          level: z.number().min(1).max(20).describe('Character level (1-20)'),
          prepared_spells: z
            .array(z.string())
            .optional()
            .describe('List of spell names you plan to prepare. If provided, analyzes conflicts and costs.'),
          remaining_slots: z
            .record(z.string(), z.number())
            .optional()
            .describe('Remaining spell slots as an object mapping spell level (string) to count, e.g. {"1": 3, "2": 1}'),
        },
      },
  • Helper function 'getMaxSpellLevel' that determines the maximum spell level available for a given class and character level, handling full casters, half casters, and warlocks.
    function getMaxSpellLevel(className: string, classLevel: number): number {
      const name = className.toLowerCase();
      if (FULL_CASTERS.includes(name)) {
        if (classLevel >= 17) return 9;
        if (classLevel >= 15) return 8;
        if (classLevel >= 13) return 7;
        if (classLevel >= 11) return 6;
        if (classLevel >= 9) return 5;
        if (classLevel >= 7) return 4;
        if (classLevel >= 5) return 3;
        if (classLevel >= 3) return 2;
        return 1;
      }
      if (HALF_CASTERS.includes(name)) {
        if (classLevel < 2) return 0;
        const effective = Math.ceil(classLevel / 2);
        return Math.min(effective, 5);
      }
      if (name === 'warlock') {
        if (classLevel >= 9) return 5;
        if (classLevel >= 7) return 4;
        if (classLevel >= 5) return 3;
        if (classLevel >= 3) return 2;
        return 1;
      }
      return 0;
    }
  • Helper function 'getSpellSlots' that returns the array of spell slot counts per level for a given class and character level, including full caster, half caster, and warlock pact magic tables.
    function getSpellSlots(className: string, classLevel: number): number[] {
      const name = className.toLowerCase();
      if (FULL_CASTERS.includes(name)) {
        return FULL_CASTER_SLOTS[classLevel] ?? [];
      }
      if (HALF_CASTERS.includes(name)) {
        return HALF_CASTER_SLOTS[classLevel] ?? [];
      }
      if (name === 'warlock') {
        // Warlock pact magic: all slots are same level
        let slotLevel: number;
        let slotCount: number;
        if (classLevel >= 9) { slotLevel = 5; slotCount = classLevel >= 17 ? 4 : classLevel >= 11 ? 3 : 2; }
        else if (classLevel >= 7) { slotLevel = 4; slotCount = 2; }
        else if (classLevel >= 5) { slotLevel = 3; slotCount = 2; }
        else if (classLevel >= 3) { slotLevel = 2; slotCount = 2; }
        else if (classLevel >= 2) { slotLevel = 1; slotCount = 2; }
        else { slotLevel = 1; slotCount = 1; }
        const slots: number[] = new Array(slotLevel).fill(0);
        slots[slotLevel - 1] = slotCount;
        return slots;
      }
      return [];
    }
  • src/server.ts:13-13 (registration)
    Import of registerPlanSpells from the plan-spells module. The actual registration call is on line 55 where registerPlanSpells(server, db) is invoked within createServer().
    import { registerPlanSpells } from './tools/plan-spells.js';
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must fully convey behavioral traits. It mentions key internal actions (tracking slots, flagging concentration, etc.) but omits whether the tool is read-only, requires authentication, or has side effects. It suggests a safe planning utility but lacks explicit statements on mutability.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, information-dense sentence. Every clause adds meaningful capability (available spells, slot tracking, concentration flags, ritual highlights, cost sums), with no redundant or extraneous words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has 4 parameters (2 required), no output schema, and no annotations, the description covers core functionality well. It explains what happens when optional parameters are provided. However, it could clarify the return format or confirm it is read-only, but overall it is sufficiently complete for a planning tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 100% description coverage, establishing a baseline of 3. The description adds functional context for the optional parameters (prepared_spells and remaining_slots trigger analysis) but does not elaborate on parameter formats or constraints beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb 'plan' and clearly identifies the resource 'spells for a D&D 5e character'. It lists concrete actions (shows spells, tracks slots, flags concentration, highlights rituals, sums costs), making it distinct from sibling tools like search_spells or browse_classes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies appropriate use for character spell planning, but does not explicitly state when to use this tool over alternatives (e.g., search_spells for general lookup, analyze_loadout for broader character analysis). No exclusions or when-not-to-use guidance is provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gregario/dnd-oracle'

If you have feedback or need assistance with the MCP directory API, please join our Discord server