search_cards
Search for Hearthstone cards by name, text, class, mana cost, type, rarity, set, keyword, or race. Returns a summary list of matching cards.
Instructions
Search for Hearthstone cards by name, text, class, mana cost, type, rarity, set, or keyword. Use this when you need to find cards matching specific criteria. Returns a summary list — use get_card for full details on a specific card.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Free-text search across card name and text (uses FTS5) | |
| player_class | No | Filter by class (e.g. MAGE, WARRIOR, NEUTRAL) | |
| mana_cost | No | Filter by mana cost (combine with cost_op for range queries) | |
| cost_op | No | Cost comparison operator: eq, lt, lte, gt, gte (default: eq) | eq |
| type | No | Filter by card type: MINION, SPELL, WEAPON, HERO, LOCATION | |
| rarity | No | Filter by rarity: FREE, COMMON, RARE, EPIC, LEGENDARY | |
| card_set | No | Filter by set code (e.g. CORE, CLASSIC, LOE) | |
| keyword | No | Filter by keyword in keywords JSON (e.g. BATTLECRY, TAUNT, CHARGE) | |
| race | No | Filter by minion race/tribe (e.g. BEAST, DRAGON, MURLOC) | |
| collectible_only | No | Only return collectible cards (default: true) | |
| limit | No | Max results to return, 1-50 (default: 25) |
Implementation Reference
- src/tools/search-cards.ts:92-200 (handler)The main handler function for the 'search_cards' tool. It builds SQL queries (with optional FTS5 full-text search), applies filters (class, mana cost, type, rarity, set, keyword, race, collectible), executes the query, and maps results to CardSummary objects.
export function searchCards( db: Database.Database, input: SearchCardsInputType, ): SearchCardsResult { const params: unknown[] = []; const conditions: string[] = []; const useFts = !!input.query; // Collectible filter if (input.collectible_only !== false) { conditions.push('c.collectible = 1'); } // Class filter if (input.player_class) { conditions.push('UPPER(c.player_class) = UPPER(?)'); params.push(input.player_class); } // Cost filter if (input.mana_cost != null) { const op = COST_OPS[input.cost_op ?? 'eq'] ?? '='; conditions.push(`c.mana_cost ${op} ?`); params.push(input.mana_cost); } // Type filter if (input.type) { conditions.push('UPPER(c.type) = UPPER(?)'); params.push(input.type); } // Rarity filter if (input.rarity) { conditions.push('UPPER(c.rarity) = UPPER(?)'); params.push(input.rarity); } // Set filter if (input.card_set) { conditions.push('UPPER(c.card_set) = UPPER(?)'); params.push(input.card_set); } // Keyword filter (search in JSON string) if (input.keyword) { conditions.push("c.keywords LIKE '%' || ? || '%'"); params.push(input.keyword); } // Race filter if (input.race) { conditions.push('UPPER(c.race) = UPPER(?)'); params.push(input.race); } const limit = input.limit ?? 25; let sql: string; const allParams: unknown[] = []; if (useFts) { // FTS5 query allParams.push(input.query); allParams.push(...params); allParams.push(limit); const whereClause = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : ''; sql = ` SELECT c.* FROM cards_fts fts JOIN cards c ON c.rowid = fts.rowid WHERE cards_fts MATCH ?${whereClause} ORDER BY fts.rank LIMIT ? `; } else { // Regular query allParams.push(...params); allParams.push(limit); const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : ''; sql = ` SELECT c.* FROM cards c ${whereClause} ORDER BY c.name LIMIT ? `; } const rows = db.prepare(sql).all(...allParams) as CardRow[]; const cards: CardSummary[] = rows.map((row) => ({ name: row.name, mana_cost: row.mana_cost, type: row.type, player_class: row.player_class, rarity: row.rarity, text: textPreview(row.text), attack: row.attack, health: row.health, keywords: parseKeywords(row.keywords), })); return { cards, total: cards.length }; } - src/tools/search-cards.ts:8-58 (schema)The Zod input schema for search_cards, defining all search/filter parameters: query (free text), player_class, mana_cost, cost_op, type, rarity, card_set, keyword, race, collectible_only, and limit.
export const SearchCardsInput = z.object({ query: z .string() .optional() .describe('Free-text search across card name and text (uses FTS5)'), player_class: z .string() .optional() .describe('Filter by class (e.g. MAGE, WARRIOR, NEUTRAL)'), mana_cost: z .number() .optional() .describe('Filter by mana cost (combine with cost_op for range queries)'), cost_op: z .enum(['eq', 'lt', 'lte', 'gt', 'gte']) .optional() .default('eq') .describe('Cost comparison operator: eq, lt, lte, gt, gte (default: eq)'), type: z .string() .optional() .describe('Filter by card type: MINION, SPELL, WEAPON, HERO, LOCATION'), rarity: z .string() .optional() .describe('Filter by rarity: FREE, COMMON, RARE, EPIC, LEGENDARY'), card_set: z .string() .optional() .describe('Filter by set code (e.g. CORE, CLASSIC, LOE)'), keyword: z .string() .optional() .describe('Filter by keyword in keywords JSON (e.g. BATTLECRY, TAUNT, CHARGE)'), race: z .string() .optional() .describe('Filter by minion race/tribe (e.g. BEAST, DRAGON, MURLOC)'), collectible_only: z .boolean() .optional() .default(true) .describe('Only return collectible cards (default: true)'), limit: z .number() .min(1) .max(50) .optional() .default(25) .describe('Max results to return, 1-50 (default: 25)'), }); - src/server.ts:56-81 (registration)Registration of the 'search_cards' tool on the MCP server. Calls server.tool() with the tool name, description, input schema (SearchCardsInput.shape), and an async handler that invokes searchCards() and formats the result via formatSearchCards().
// 1. search_cards server.tool( 'search_cards', 'Search for Hearthstone cards by name, text, class, mana cost, type, rarity, set, or keyword. Use this when you need to find cards matching specific criteria. Returns a summary list — use get_card for full details on a specific card.', SearchCardsInput.shape, async (params) => { try { const result = searchCards(db, params); return { content: [ { type: 'text' as const, text: formatSearchCards(result) }, ], }; } catch (err) { return { content: [ { type: 'text' as const, text: `Error: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } }, ); - src/format.ts:23-26 (helper)SearchCardsResult type and CardSummary interface used to represent the output of the search_cards tool.
export interface SearchCardsResult { cards: CardSummary[]; total: number; } - src/format.ts:58-75 (helper)The formatSearchCards function which formats search results into a human-readable string (listing card name, mana cost, type, class, and text preview).
export function formatSearchCards(result: SearchCardsResult): string { if (result.cards.length === 0) { return 'No cards found matching your search criteria.'; } const lines: string[] = [`Found ${result.total} card(s):\n`]; for (const card of result.cards) { lines.push( `- **${card.name}** (${card.mana_cost ?? '?'} mana) — ${card.type ?? 'Unknown'} [${card.player_class ?? 'NEUTRAL'}]` ); if (card.text) { // Show first line only as preview const preview = card.text.split('\n')[0]; lines.push(` ${preview}`); } } return lines.join('\n'); }