Skip to main content
Glama

plan_spells

Plan D&D 5e spells by analyzing available options, tracking spell slots, identifying concentration conflicts, highlighting rituals, and calculating material costs for your character.

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 for 'plan_spells', which retrieves class information, determines spell slots, and formats the planning output.
      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') }],
        };
      },
    );
  • The registration of the 'plan_spells' tool with its schema definition.
    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}'),
        },
      },

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