Skip to main content
Glama

browse_classes

Browse D&D 5e SRD classes: list all, view features per level, and calculate multiclass spell slots and features.

Instructions

Browse D&D 5e SRD classes. List all classes, view a specific class's features at any level, or calculate multiclass feature combinations.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
class_nameNoName of a specific class to look up
levelNoFilter features to this level or below (1-20)
multiclassNoArray of class/level combos to calculate multiclass features and spell slots

Implementation Reference

  • Main tool handler functions: handleListClasses (line 133), handleSingleClass (line 154), and handleMulticlass (line 222) - the three modes of the browse_classes tool.
    function handleListClasses(db: Database.Database) {
      const classes = listClasses(db);
      if (classes.length === 0) {
        return {
          content: [{ type: 'text' as const, text: 'No classes found in the database.' }],
        };
      }
    
      const lines = classes.map((cls) => {
        const savingThrows = safeJsonParse<string[]>(cls.saving_throws, []);
        const subclasses = safeJsonParse<string[]>(cls.subclasses, []);
        const subclassText =
          subclasses.length > 0 ? `\n  Subclasses: ${subclasses.join(', ')}` : '';
    
        return `${cls.name}\n  Hit Die: d${cls.hit_die}\n  Saving Throws: ${savingThrows.join(', ') || 'None'}${subclassText}`;
      });
    
      const text = `D&D 5e SRD Classes (${classes.length})\n${'='.repeat(40)}\n\n${lines.join('\n\n')}`;
      return { content: [{ type: 'text' as const, text }] };
    }
    
    function handleSingleClass(
      db: Database.Database,
      className: string,
      level?: number,
    ) {
      const cls = getClassByName(db, className);
      if (!cls) {
        const allClasses = listClasses(db);
        const available = allClasses.map((c) => c.name).join(', ');
        return {
          content: [
            {
              type: 'text' as const,
              text: `Class "${className}" not found. Available classes: ${available}`,
            },
          ],
          isError: true,
        };
      }
    
      const savingThrows = safeJsonParse<string[]>(cls.saving_throws, []);
      const proficiencies = safeJsonParse<Record<string, unknown>>(cls.proficiencies, {});
      const features = safeJsonParse<ClassFeature[]>(cls.features, []);
      const subclasses = safeJsonParse<string[]>(cls.subclasses, []);
    
      const filteredFeatures = level
        ? features.filter((f) => f.level <= level)
        : features;
    
      const sections: string[] = [
        `${cls.name}`,
        '='.repeat(40),
        `Hit Die: d${cls.hit_die}`,
        `Saving Throws: ${savingThrows.join(', ') || 'None'}`,
        `Spellcasting Ability: ${cls.spellcasting_ability || 'None'}`,
      ];
    
      // Proficiencies
      const profLines: string[] = [];
      for (const [key, value] of Object.entries(proficiencies)) {
        if (Array.isArray(value)) {
          profLines.push(`  ${key}: ${value.join(', ')}`);
        } else if (typeof value === 'string') {
          profLines.push(`  ${key}: ${value}`);
        }
      }
      if (profLines.length > 0) {
        sections.push(`\nProficiencies:\n${profLines.join('\n')}`);
      }
    
      // Subclasses
      if (subclasses.length > 0) {
        sections.push(`\nSubclasses: ${subclasses.join(', ')}`);
      }
    
      // Features
      const levelLabel = level ? ` (up to level ${level})` : '';
      if (filteredFeatures.length > 0) {
        sections.push(
          `\nFeatures${levelLabel} (${filteredFeatures.length}):\n${filteredFeatures.map(formatFeature).join('\n\n')}`,
        );
      } else {
        sections.push(`\nNo features found${levelLabel}.`);
      }
    
      return { content: [{ type: 'text' as const, text: sections.join('\n') }] };
    }
    
    function handleMulticlass(db: Database.Database, entries: MulticlassEntry[]) {
      const totalLevel = entries.reduce((sum, e) => sum + e.level, 0);
      if (totalLevel > 20) {
        return {
          content: [
            {
              type: 'text' as const,
              text: `Total multiclass level (${totalLevel}) exceeds maximum of 20.`,
            },
          ],
          isError: true,
        };
      }
    
      const sections: string[] = [
        `Multiclass Build (Total Level ${totalLevel})`,
        '='.repeat(40),
      ];
    
      let combinedCasterLevel = 0;
      const allFeatures: { className: string; feature: ClassFeature }[] = [];
      const notFound: string[] = [];
    
      for (const entry of entries) {
        const cls = getClassByName(db, entry.class_name);
        if (!cls) {
          notFound.push(entry.class_name);
          continue;
        }
    
        sections.push(`\n${cls.name} (Level ${entry.level})`);
        sections.push('-'.repeat(30));
    
        const features = safeJsonParse<ClassFeature[]>(cls.features, []);
        const classFeatures = features.filter((f) => f.level <= entry.level);
    
        for (const feature of classFeatures) {
          allFeatures.push({ className: cls.name, feature });
          sections.push(`  Level ${feature.level}: ${feature.name}`);
        }
    
        combinedCasterLevel += getCasterLevel(cls.name, entry.level);
      }
    
      if (notFound.length > 0) {
        const allClasses = listClasses(db);
        const available = allClasses.map((c) => c.name).join(', ');
        sections.push(
          `\nWarning: Classes not found: ${notFound.join(', ')}. Available: ${available}`,
        );
      }
    
      // Spell slots
      sections.push(`\nMulticlass Spell Slots`);
      sections.push('-'.repeat(30));
      sections.push(`Combined Caster Level: ${combinedCasterLevel}`);
      sections.push(formatSpellSlots(combinedCasterLevel));
    
      return { content: [{ type: 'text' as const, text: sections.join('\n') }] };
    }
  • Registration function registerBrowseClasses that registers the 'browse_classes' tool with its inputSchema and handler on the McpServer.
    export function registerBrowseClasses(
      server: McpServer,
      db: Database.Database,
    ): void {
      server.registerTool(
        'browse_classes',
        {
          description:
            'Browse D&D 5e SRD classes. List all classes, view a specific class\'s features at any level, or calculate multiclass feature combinations.',
          inputSchema: {
            class_name: z
              .string()
              .optional()
              .describe('Name of a specific class to look up'),
            level: z
              .number()
              .int()
              .min(1)
              .max(20)
              .optional()
              .describe('Filter features to this level or below (1-20)'),
            multiclass: z
              .array(
                z.object({
                  class_name: z.string().describe('Class name'),
                  level: z.number().int().min(1).max(20).describe('Level in this class'),
                }),
              )
              .optional()
              .describe(
                'Array of class/level combos to calculate multiclass features and spell slots',
              ),
          },
        },
        async ({ class_name, level, multiclass }) => {
          // Mode 3: Multiclass combination
          if (multiclass && multiclass.length > 0) {
            return handleMulticlass(db, multiclass);
          }
    
          // Mode 2: Specific class
          if (class_name) {
            return handleSingleClass(db, class_name, level);
          }
    
          // Mode 1: List all classes
          return handleListClasses(db);
        },
      );
    }
  • Input schema definition for browse_classes: optional class_name, optional level (1-20), optional multiclass array.
    {
      description:
        'Browse D&D 5e SRD classes. List all classes, view a specific class\'s features at any level, or calculate multiclass feature combinations.',
      inputSchema: {
        class_name: z
          .string()
          .optional()
          .describe('Name of a specific class to look up'),
        level: z
          .number()
          .int()
          .min(1)
          .max(20)
          .optional()
          .describe('Filter features to this level or below (1-20)'),
        multiclass: z
          .array(
            z.object({
              class_name: z.string().describe('Class name'),
              level: z.number().int().min(1).max(20).describe('Level in this class'),
            }),
          )
          .optional()
          .describe(
            'Array of class/level combos to calculate multiclass features and spell slots',
          ),
      },
  • src/server.ts:49-49 (registration)
    Registration call in the server: registerBrowseClasses(server, db) wires the tool into the MCP server.
    registerBrowseClasses(server, db);
  • Helper data and utilities: SPELL_SLOT_TABLE, caster type sets, formatFeature, getCasterLevel, formatSpellSlots, etc.
    const SPELL_SLOT_TABLE: Record<number, number[]> = {
      1: [2],
      2: [3],
      3: [4, 2],
      4: [4, 3],
      5: [4, 3, 2],
      6: [4, 3, 3],
      7: [4, 3, 3, 1],
      8: [4, 3, 3, 2],
      9: [4, 3, 3, 3, 1],
      10: [4, 3, 3, 3, 2],
      11: [4, 3, 3, 3, 2, 1],
      12: [4, 3, 3, 3, 2, 1],
      13: [4, 3, 3, 3, 2, 1, 1],
      14: [4, 3, 3, 3, 2, 1, 1],
      15: [4, 3, 3, 3, 2, 1, 1, 1],
      16: [4, 3, 3, 3, 2, 1, 1, 1],
      17: [4, 3, 3, 3, 2, 1, 1, 1, 1],
      18: [4, 3, 3, 3, 3, 1, 1, 1, 1],
      19: [4, 3, 3, 3, 3, 2, 1, 1, 1],
      20: [4, 3, 3, 3, 3, 2, 2, 1, 1],
    };
    
    const FULL_CASTERS = new Set(['bard', 'cleric', 'druid', 'sorcerer', 'wizard']);
    const HALF_CASTERS = new Set(['paladin', 'ranger']);
    // Third casters are subclass-dependent (Eldritch Knight, Arcane Trickster)
    const THIRD_CASTER_CLASSES = new Set(['fighter', 'rogue']);
    
    interface ClassFeature {
      level: number;
      name: string;
      description: string;
    }
    
    interface MulticlassEntry {
      class_name: string;
      level: number;
    }
    
    function safeJsonParse<T>(value: string | null, fallback: T): T {
      if (!value) return fallback;
      try {
        return JSON.parse(value) as T;
      } catch {
        return fallback;
      }
    }
    
    function formatFeature(feature: ClassFeature): string {
      return `  Level ${feature.level}: ${feature.name}\n    ${feature.description}`;
    }
    
    function getCasterLevel(className: string, classLevel: number): number {
      const lower = className.toLowerCase();
      if (FULL_CASTERS.has(lower)) return classLevel;
      if (HALF_CASTERS.has(lower)) return Math.floor(classLevel / 2);
      if (THIRD_CASTER_CLASSES.has(lower)) return Math.floor(classLevel / 3);
      return 0;
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It does not disclose basic behavioral traits such as whether the operation is read-only, if authentication is required, or any side effects. The description implies a read-only browse operation but does not state it explicitly.

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 sentence that front-loads the core purpose and enumerates capabilities concisely. No unnecessary words or repetition.

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 no output schema and optional parameters, the description covers the main use cases. It could clarify the interaction between parameters (e.g., class_name and multiclass are likely mutually exclusive), but the current level of detail is sufficient for straightforward browsing.

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?

Schema description coverage is 100%, so baseline is 3. The description adds some context (e.g., 'view a specific class's features' for class_name, 'filter features to this level or below' for level) but does not significantly expand on the schema's own descriptions.

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?

Description clearly states the tool browses D&D 5e SRD classes and lists its capabilities: list all classes, view features by level, calculate multiclass combos. It distinguishes from sibling tools like browse_races and search_spells by focusing specifically on classes.

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

Usage Guidelines4/5

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

The description outlines when to use the tool (browsing classes, looking up features, multiclass calculations). It does not explicitly mention when not to use it, but the sibling tool names provide context for alternatives.

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