search_cards
Find Magic: The Gathering cards by name, type, color, mana cost, rarity, set, format legality, or keyword. Supports full-text search across card text.
Instructions
Search for Magic: The Gathering cards by name, type, color, mana cost, rarity, set, format legality, or keyword. Use this when you need to find cards matching specific criteria. Supports full-text search across card names, type lines, and oracle text. 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 (FTS5) across name, type_line, oracle_text | |
| name | No | Filter by card name (LIKE match) | |
| type | No | Filter by type_line (LIKE match) | |
| colors | No | Filter by colors (JSON array contains all specified) | |
| cmc | No | Filter by converted mana cost | |
| cmcOp | No | CMC comparison operator (default: eq) | |
| rarity | No | Filter by rarity (common, uncommon, rare, mythic) | |
| set | No | Filter by set code | |
| format | No | Filter by format legality (legal status) | |
| keyword | No | Filter by keyword in keywords JSON array | |
| limit | No | Max results (default 25, max 50) |
Implementation Reference
- src/tools/search-cards.ts:39-143 (handler)Main handler function for the search_cards tool. Accepts SearchCardsParams and returns SearchCardsResult. Builds dynamic SQL with optional filters (name, type, colors, cmc, rarity, set, format, keyword) and FTS5 full-text search over cards_fts. Returns up to `limit` cards (default 25, max 50) with a preview of oracle_text.
export function handler(db: Database.Database, params: SearchCardsParams): SearchCardsResult { const limit = params.limit ?? 25; const conditions: string[] = []; const bindings: unknown[] = []; const joins: string[] = []; // Determine base query source let usesFts = false; if (params.query) { usesFts = true; } // Build WHERE clauses for structured filters if (params.name) { conditions.push('c.name LIKE ?'); bindings.push(`%${params.name}%`); } if (params.type) { conditions.push('c.type_line LIKE ?'); bindings.push(`%${params.type}%`); } if (params.colors && params.colors.length > 0) { // Each color must be present in the JSON array for (const color of params.colors) { conditions.push("c.colors LIKE ?"); bindings.push(`%"${color}"%`); } } if (params.cmc !== undefined) { const op = params.cmcOp ?? 'eq'; const sqlOp = op === 'eq' ? '=' : op === 'lt' ? '<' : op === 'lte' ? '<=' : op === 'gt' ? '>' : '>='; conditions.push(`c.cmc ${sqlOp} ?`); bindings.push(params.cmc); } if (params.rarity) { conditions.push('c.rarity = ?'); bindings.push(params.rarity); } if (params.set) { conditions.push('c.set_code = ?'); bindings.push(params.set); } if (params.format) { joins.push('JOIN legalities l ON l.card_id = c.id'); conditions.push("l.format = ? AND l.status = 'legal'"); bindings.push(params.format); } if (params.keyword) { conditions.push("c.keywords LIKE ?"); bindings.push(`%"${params.keyword}"%`); } let sql: string; const allBindings: unknown[] = []; if (usesFts) { // FTS5 query joined with cards table for structured filters sql = `SELECT c.name, c.mana_cost, c.type_line, c.oracle_text, c.colors FROM cards_fts fts JOIN cards c ON c.rowid = fts.rowid ${joins.join(' ')} WHERE cards_fts MATCH ?`; allBindings.push(params.query!); for (const cond of conditions) { sql += ` AND ${cond}`; } allBindings.push(...bindings); sql += ' ORDER BY fts.rank LIMIT ?'; } else { sql = `SELECT c.name, c.mana_cost, c.type_line, c.oracle_text, c.colors FROM cards c ${joins.join(' ')}`; if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); allBindings.push(...bindings); } sql += ' ORDER BY c.name LIMIT ?'; } allBindings.push(limit); const rows = db.prepare(sql).all(...allBindings) as Array<{ name: string; mana_cost: string | null; type_line: string | null; oracle_text: string | null; colors: string | null; }>; const cards: CardSummary[] = rows.map(row => ({ name: row.name, mana_cost: row.mana_cost, type_line: row.type_line, oracle_text_preview: row.oracle_text ? row.oracle_text.split('\n')[0] : null, colors: row.colors ? JSON.parse(row.colors) as string[] : [], })); return { cards, total: cards.length }; } - src/tools/search-cards.ts:6-18 (schema)Zod input schema for search_cards. Defines optional fields: query (FTS5 free-text), name, type, colors (array), cmc with cmcOp comparison operator, rarity, set, format (legality), keyword, and limit (1-50, default 25).
export const SearchCardsInput = z.object({ query: z.string().optional().describe('Free-text search (FTS5) across name, type_line, oracle_text'), name: z.string().optional().describe('Filter by card name (LIKE match)'), type: z.string().optional().describe('Filter by type_line (LIKE match)'), colors: z.array(z.string()).optional().describe('Filter by colors (JSON array contains all specified)'), cmc: z.number().optional().describe('Filter by converted mana cost'), cmcOp: z.enum(['eq', 'lt', 'lte', 'gt', 'gte']).optional().describe('CMC comparison operator (default: eq)'), rarity: z.string().optional().describe('Filter by rarity (common, uncommon, rare, mythic)'), set: z.string().optional().describe('Filter by set code'), format: z.string().optional().describe('Filter by format legality (legal status)'), keyword: z.string().optional().describe('Filter by keyword in keywords JSON array'), limit: z.number().min(1).max(50).optional().describe('Max results (default 25, max 50)'), }); - src/tools/search-cards.ts:24-35 (schema)Output types: CardSummary (name, mana_cost, type_line, oracle_text_preview, colors) and SearchCardsResult (cards array + total count).
export interface CardSummary { name: string; mana_cost: string | null; type_line: string | null; oracle_text_preview: string | null; colors: string[]; } export interface SearchCardsResult { cards: CardSummary[]; total: number; } - src/server.ts:79-91 (registration)Registration of the 'search_cards' tool with the MCP server. Uses server.tool() with name 'search_cards', description, SearchCardsInput.shape for schema, and an async handler that calls searchCardsHandler(db, params) and formats via formatSearchCards().
server.tool( 'search_cards', 'Search for Magic: The Gathering cards by name, type, color, mana cost, rarity, set, format legality, or keyword. Use this when you need to find cards matching specific criteria. Supports full-text search across card names, type lines, and oracle text. Returns a summary list — use get_card for full details on a specific card.', SearchCardsInput.shape, async (params) => { try { const result = searchCardsHandler(db, params); return { content: [{ type: 'text' as const, text: formatSearchCards(result) }] }; } catch (err) { return { content: [{ type: 'text' as const, text: `Error searching cards: ${err instanceof Error ? err.message : String(err)}` }], isError: true }; } }, ); - src/format.ts:23-40 (helper)Formatting helper for search_cards results. Converts SearchCardsResult into a human-readable markdown string listing each card with name, mana cost, type line, colors, and oracle 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) { const costPart = card.mana_cost ? ` ${card.mana_cost}` : ''; const colorPart = card.colors.length > 0 ? ` [${card.colors.join('')}]` : ''; lines.push(`- **${card.name}**${costPart} — ${card.type_line ?? 'Unknown Type'}${colorPart}`); if (card.oracle_text_preview) { lines.push(` ${card.oracle_text_preview}`); } } return lines.join('\n'); }