Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
queryNoFree-text search across card name and text (uses FTS5)
player_classNoFilter by class (e.g. MAGE, WARRIOR, NEUTRAL)
mana_costNoFilter by mana cost (combine with cost_op for range queries)
cost_opNoCost comparison operator: eq, lt, lte, gt, gte (default: eq)eq
typeNoFilter by card type: MINION, SPELL, WEAPON, HERO, LOCATION
rarityNoFilter by rarity: FREE, COMMON, RARE, EPIC, LEGENDARY
card_setNoFilter by set code (e.g. CORE, CLASSIC, LOE)
keywordNoFilter by keyword in keywords JSON (e.g. BATTLECRY, TAUNT, CHARGE)
raceNoFilter by minion race/tribe (e.g. BEAST, DRAGON, MURLOC)
collectible_onlyNoOnly return collectible cards (default: true)
limitNoMax results to return, 1-50 (default: 25)

Implementation Reference

  • 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 };
    }
  • 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,
          };
        }
      },
    );
  • SearchCardsResult type and CardSummary interface used to represent the output of the search_cards tool.
    export interface SearchCardsResult {
      cards: CardSummary[];
      total: number;
    }
  • 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');
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden for behavioral disclosure. It only mentions 'Returns a summary list,' implying read-only behavior and limited details, but does not explicitly state read-only, no side effects, or other behavioral traits like pagination or performance. This leaves significant gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is only two sentences, front-loading the purpose and tool distinction. Every word is necessary, with no extraneous content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 11 parameters and no output schema, the description is adequate for basic use but lacks details on how queries work (e.g., fuzzy search, boolean operators) or result behavior. It is sufficient but not rich.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% (all 11 parameters have descriptions in the schema). The description adds no additional parameter semantics beyond listing criteria, so it scores the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it 'Search for Hearthstone cards' and lists numerous filter criteria, specifying the verb and resource. It distinguishes itself from sibling tool 'get_card' by noting that search returns a summary list and get_card provides full details.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description advises to use this tool 'when you need to find cards matching specific criteria' and points to 'get_card' for full details, giving clear context. It does not explicitly state when not to use, but the guidance is sufficient.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gregario/hearthstone-oracle'

If you have feedback or need assistance with the MCP directory API, please join our Discord server