search_spells
Find D&D 5e spells by name, level, school, class, concentration, ritual, components, damage type, or saving throw using SRD data.
Instructions
Search D&D 5e SRD spells by name, level, school, class, concentration, ritual, components, damage type, or save type.
Input Schema
TableJSON 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:112-160 (handler)The MCP tool handler function for 'search_spells', which invokes the DB query and formats the results for the client.
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)The input schema definition for the 'search_spells' tool using Zod validation.
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/tools/search-spells.ts:89-162 (registration)Registration function that adds 'search_spells' to the MCP server instance.
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/data/db.ts:177-249 (helper)The underlying data helper function that performs the SQL query to search the spells database based on the provided filters.
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 };