search_by_mechanic
Find Magic: The Gathering cards by keyword or mechanic like Flying or Trample, with optional rules definition and format filter.
Instructions
Find cards that have a specific keyword or mechanic (e.g., "Flying", "Trample", "Scry", "Cascade"). Use this when a user asks about cards with a particular ability or mechanic. Optionally includes the keyword's official rules definition.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | Keyword/mechanic to search for (e.g., "Flying", "Trample") | |
| include_definition | No | Include keyword definition from keywords table | |
| format | No | Filter by format legality | |
| limit | No | Max results (default 25, max 50) |
Implementation Reference
- src/tools/search-by-mechanic.ts:41-153 (handler)Main handler function that searches for cards by keyword/mechanic. Queries the cards table using JSON keyword matching, optionally filters by format legality, optionally fetches the keyword definition, and falls back to FTS5 oracle_text search. Returns a SearchByMechanicResult with matching cards.
export function handler(db: Database.Database, params: SearchByMechanicParams): SearchByMechanicResult { const limit = params.limit ?? 25; const result: SearchByMechanicResult = { keyword: params.keyword, cards: [], total: 0, }; // Optionally fetch keyword definition if (params.include_definition) { const kwRow = db.prepare( 'SELECT * FROM keywords WHERE LOWER(name) = LOWER(?)' ).get(params.keyword) as KeywordRow | undefined; if (kwRow) { result.definition = { name: kwRow.name, section: kwRow.section, type: kwRow.type, rules_text: kwRow.rules_text, }; } } // Primary: search keywords JSON array const formatJoin = params.format ? 'JOIN legalities l ON l.card_id = c.id' : ''; const formatWhere = params.format ? "AND l.format = ? AND l.status = 'legal'" : ''; const keywordSql = ` SELECT c.name, c.mana_cost, c.type_line, c.oracle_text FROM cards c ${formatJoin} WHERE c.keywords LIKE ? ${formatWhere} ORDER BY c.name LIMIT ? `; const keywordBindings: unknown[] = [`%"${params.keyword}"%`]; if (params.format) { keywordBindings.push(params.format); } keywordBindings.push(limit); const keywordRows = db.prepare(keywordSql).all(...keywordBindings) as Array<{ name: string; mana_cost: string | null; type_line: string | null; oracle_text: string | null; }>; const seenNames = new Set<string>(); for (const row of keywordRows) { seenNames.add(row.name); result.cards.push({ 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, }); } // Fallback: FTS5 oracle_text search if we haven't hit the limit if (result.cards.length < limit) { const remaining = limit - result.cards.length; try { const ftsSql = ` SELECT c.name, c.mana_cost, c.type_line, c.oracle_text FROM cards_fts fts JOIN cards c ON c.rowid = fts.rowid ${params.format ? 'JOIN legalities l ON l.card_id = c.id' : ''} WHERE cards_fts MATCH ? ${formatWhere} ORDER BY fts.rank LIMIT ? `; const ftsBindings: unknown[] = [params.keyword]; if (params.format) { ftsBindings.push(params.format); } ftsBindings.push(remaining); const ftsRows = db.prepare(ftsSql).all(...ftsBindings) as Array<{ name: string; mana_cost: string | null; type_line: string | null; oracle_text: string | null; }>; for (const row of ftsRows) { if (!seenNames.has(row.name)) { seenNames.add(row.name); result.cards.push({ 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, }); } } } catch { // FTS5 query might fail with certain keyword patterns — that's OK, // we already have results from the JSON keyword search } } result.total = result.cards.length; return result; } - src/tools/search-by-mechanic.ts:7-37 (schema)Input schema (SearchByMechanicInput) using Zod with fields: keyword (string), include_definition (optional boolean), format (optional string), limit (optional number 1-50). Also defines output types: SearchByMechanicResult, KeywordDefinition, and MechanicCardSummary.
export const SearchByMechanicInput = z.object({ keyword: z.string().describe('Keyword/mechanic to search for (e.g., "Flying", "Trample")'), include_definition: z.boolean().optional().describe('Include keyword definition from keywords table'), format: z.string().optional().describe('Filter by format legality'), limit: z.number().min(1).max(50).optional().describe('Max results (default 25, max 50)'), }); export type SearchByMechanicParams = z.infer<typeof SearchByMechanicInput>; // --- Output types --- export interface MechanicCardSummary { name: string; mana_cost: string | null; type_line: string | null; oracle_text_preview: string | null; } export interface KeywordDefinition { name: string; section: string; type: string; rules_text: string; } export interface SearchByMechanicResult { keyword: string; definition?: KeywordDefinition; cards: MechanicCardSummary[]; total: number; } - src/server.ts:135-147 (registration)Tool registration on the MCP server using server.tool() with name 'search_by_mechanic', description, input schema (SearchByMechanicInput.shape), and handler callback that calls searchByMechanicHandler and formats with formatSearchByMechanic.
server.tool( 'search_by_mechanic', 'Find cards that have a specific keyword or mechanic (e.g., "Flying", "Trample", "Scry", "Cascade"). Use this when a user asks about cards with a particular ability or mechanic. Optionally includes the keyword\'s official rules definition.', SearchByMechanicInput.shape, async (params) => { try { const result = searchByMechanicHandler(db, params); return { content: [{ type: 'text' as const, text: formatSearchByMechanic(result) }] }; } catch (err) { return { content: [{ type: 'text' as const, text: `Error searching by mechanic: ${err instanceof Error ? err.message : String(err)}` }], isError: true }; } }, ); - src/format.ts:201-227 (helper)Formatting helper that converts a SearchByMechanicResult into readable text for the LLM, including keyword definition (if present) and a list of matching cards with mana cost, type line, and oracle text preview.
export function formatSearchByMechanic(result: SearchByMechanicResult): string { const lines: string[] = []; lines.push(`# Cards with "${result.keyword}"\n`); if (result.definition) { lines.push(`**${result.definition.name}** (${result.definition.type}, Section ${result.definition.section})`); lines.push(result.definition.rules_text); lines.push(''); } if (result.cards.length === 0) { lines.push('No cards found with this keyword/mechanic.'); return lines.join('\n'); } lines.push(`Found ${result.total} card(s):\n`); for (const card of result.cards) { const costPart = card.mana_cost ? ` ${card.mana_cost}` : ''; lines.push(`- **${card.name}**${costPart} — ${card.type_line ?? 'Unknown Type'}`); if (card.oracle_text_preview) { lines.push(` ${card.oracle_text_preview}`); } } return lines.join('\n'); }