search_spells
Search D&D 5e SRD spells by name, level, school, class, concentration, ritual, components, damage type, or save type.
Instructions
Search D&D 5e SRD spells by name, level, school, class, concentration, ritual, components, damage type, or save type.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Search term for spell name or description | |
| level | No | Spell level (0 for cantrips, 1-9 for leveled spells) | |
| school | No | School of magic (e.g. "evocation", "necromancy") | |
| class_name | No | Spellcasting class (e.g. "Wizard", "Cleric") | |
| concentration | No | Filter by concentration requirement | |
| ritual | No | Filter by ritual casting | |
| has_material | No | Filter by material component requirement | |
| damage_type | No | Damage type (e.g. "fire", "necrotic", "radiant") | |
| save_type | No | Saving throw type (e.g. "DEX", "WIS", "CON") | |
| limit | No | Results per page (max 50) | |
| offset | No | Offset for pagination |
Implementation Reference
- src/tools/search-spells.ts:89-162 (handler)Registration and handler function for the 'search_spells' tool. Registers the tool with an MCP server, defines its schema (query, level, school, class_name, concentration, ritual, has_material, damage_type, save_type, limit, offset), and implements the handler that calls searchSpells in the DB layer and formats results using formatSpell.
export function registerSearchSpells( server: McpServer, db: Database.Database, ): void { server.registerTool( 'search_spells', { description: 'Search D&D 5e SRD spells by name, level, school, class, concentration, ritual, components, damage type, or save type.', inputSchema: { query: z.string().optional().describe('Search term for spell name or description'), level: z.number().min(0).max(9).optional().describe('Spell level (0 for cantrips, 1-9 for leveled spells)'), school: z.string().optional().describe('School of magic (e.g. "evocation", "necromancy")'), class_name: z.string().optional().describe('Spellcasting class (e.g. "Wizard", "Cleric")'), concentration: z.boolean().optional().describe('Filter by concentration requirement'), ritual: z.boolean().optional().describe('Filter by ritual casting'), has_material: z.boolean().optional().describe('Filter by material component requirement'), damage_type: z.string().optional().describe('Damage type (e.g. "fire", "necrotic", "radiant")'), save_type: z.string().optional().describe('Saving throw type (e.g. "DEX", "WIS", "CON")'), limit: z.number().min(1).max(50).default(10).describe('Results per page (max 50)'), offset: z.number().min(0).default(0).describe('Offset for pagination'), }, }, async ({ query, level, school, class_name, concentration, ritual, has_material, damage_type, save_type, limit, offset, }) => { const result = searchSpells(db, { query, level, school, class_name, concentration, ritual, has_material, damage_type, save_type, limit, offset, }); if (result.rows.length === 0) { return { content: [ { type: 'text' as const, text: 'No spells found matching your criteria. Try a broader search — for example, remove some filters or use a partial name.', }, ], isError: true, }; } const start = (offset ?? 0) + 1; const end = (offset ?? 0) + result.rows.length; const header = `Found ${result.total} spell${result.total === 1 ? '' : 's'} (showing ${start}-${end})\n`; const spells = result.rows.map(formatSpell).join('\n\n---\n\n'); return { content: [{ type: 'text' as const, text: header + '\n' + spells }], }; }, ); } - src/tools/search-spells.ts:98-110 (schema)Input schema for the search_spells tool, defined inline in the registerTool call using Zod. All fields are optional except limit and offset which have defaults.
inputSchema: { query: z.string().optional().describe('Search term for spell name or description'), level: z.number().min(0).max(9).optional().describe('Spell level (0 for cantrips, 1-9 for leveled spells)'), school: z.string().optional().describe('School of magic (e.g. "evocation", "necromancy")'), class_name: z.string().optional().describe('Spellcasting class (e.g. "Wizard", "Cleric")'), concentration: z.boolean().optional().describe('Filter by concentration requirement'), ritual: z.boolean().optional().describe('Filter by ritual casting'), has_material: z.boolean().optional().describe('Filter by material component requirement'), damage_type: z.string().optional().describe('Damage type (e.g. "fire", "necrotic", "radiant")'), save_type: z.string().optional().describe('Saving throw type (e.g. "DEX", "WIS", "CON")'), limit: z.number().min(1).max(50).default(10).describe('Results per page (max 50)'), offset: z.number().min(0).default(0).describe('Offset for pagination'), }, - src/data/db.ts:177-250 (helper)Database query helper that builds a dynamic SQL query from optional filters. Supports full-text search via FTS5 (spells_fts), filtering by level, school, class_name, concentration, ritual, has_material, damage_type, save_type, and pagination via limit/offset. Returns typed SearchResult with rows and total count.
export function searchSpells( db: Database.Database, filters: SpellFilters, ): SearchResult<SpellRow> { const conditions: string[] = []; const params: (string | number)[] = []; if (filters.query) { const ftsQuery = sanitizeFtsQuery(filters.query); if (ftsQuery.length > 0) { conditions.push( 's.id IN (SELECT rowid FROM spells_fts WHERE spells_fts MATCH ?)', ); params.push(ftsQuery); } } if (filters.level !== undefined) { conditions.push('s.level = ?'); params.push(filters.level); } if (filters.school) { conditions.push('LOWER(s.school) = LOWER(?)'); params.push(filters.school); } if (filters.class_name) { conditions.push('LOWER(s.classes) LIKE LOWER(?)'); params.push(`%${filters.class_name}%`); } if (filters.concentration !== undefined) { conditions.push('s.concentration = ?'); params.push(filters.concentration ? 1 : 0); } if (filters.ritual !== undefined) { conditions.push('s.ritual = ?'); params.push(filters.ritual ? 1 : 0); } if (filters.has_material !== undefined) { conditions.push('s.components_m = ?'); params.push(filters.has_material ? 1 : 0); } if (filters.damage_type) { conditions.push('LOWER(s.damage_type) = LOWER(?)'); params.push(filters.damage_type); } if (filters.save_type) { conditions.push('LOWER(s.save_type) = LOWER(?)'); params.push(filters.save_type); } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const limit = filters.limit ?? 20; const offset = filters.offset ?? 0; const countRow = db .prepare(`SELECT COUNT(*) as count FROM spells s ${where}`) .get(...params) as { count: number }; const rows = db .prepare( `SELECT s.* FROM spells s ${where} ORDER BY s.level ASC, s.name ASC LIMIT ? OFFSET ?`, ) .all(...params, limit, offset) as SpellRow[]; return { rows, total: countRow.count }; } - src/tools/search-spells.ts:42-87 (helper)Helper function that formats a SpellRow into a Markdown string including name (h2), level/school, tags (concentration/ritual), casting time, range, components, duration, description, higher levels, damage type, save, and classes.
function formatSpell(spell: SpellRow): string { const lines: string[] = []; lines.push(`## ${spell.name}`); const schoolStr = spell.school.charAt(0).toUpperCase() + spell.school.slice(1); const levelStr = levelLabel(spell.level); const tags: string[] = []; if (spell.concentration) tags.push('concentration'); if (spell.ritual) tags.push('ritual'); const tagStr = tags.length > 0 ? ` (${tags.join(', ')})` : ''; if (spell.level === 0) { lines.push(`*${schoolStr} cantrip${tagStr}*`); } else { lines.push(`*${levelStr} ${schoolStr.toLowerCase()}${tagStr}*`); } lines.push(''); lines.push(`**Casting Time:** ${spell.casting_time}`); lines.push(`**Range:** ${spell.range}`); lines.push(`**Components:** ${formatComponents(spell)}`); lines.push(`**Duration:** ${spell.duration}`); lines.push(''); lines.push(spell.description); if (spell.higher_level) { lines.push(''); lines.push(`**At Higher Levels.** ${spell.higher_level}`); } if (spell.damage_type) { lines.push(''); lines.push(`**Damage Type:** ${spell.damage_type}`); } if (spell.save_type) { lines.push(`**Save:** ${spell.save_type}`); } lines.push(''); lines.push(`**Classes:** ${formatClasses(spell.classes)}`); return lines.join('\n'); } - src/server.ts:47-47 (registration)Registration call in the main server file. The registerSearchSpells function is imported from './tools/search-spells.js' and called during server initialization.
registerSearchSpells(server, db);