Skip to main content
Glama
bmurdock

Scryfall MCP Server

by bmurdock

batch_card_analysis

Analyze multiple Magic: The Gathering cards for legality, prices, synergies, or deck composition. Choose specific formats, currencies, and grouping options to streamline card evaluation and strategy planning.

Instructions

Analyze multiple cards for legality, prices, synergies, or deck composition

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
analysis_typeYesType of analysis to perform
card_listYesList of card names to analyze
currencyNoCurrency for price analysisusd
formatNoFormat for legality analysis
group_byNoHow to group analysis results
include_suggestionsNoInclude improvement suggestions

Implementation Reference

  • Complete handler implementation in BatchCardAnalysisTool class with execute method and all analysis logic
    export class BatchCardAnalysisTool {
      readonly name = 'batch_card_analysis';
      readonly description = 'Analyze multiple cards for legality, prices, synergies, or deck composition';
    
      readonly inputSchema = {
        type: 'object' as const,
        properties: {
          card_list: {
            type: 'array',
            items: { type: 'string' },
            description: 'List of card names to analyze',
            minItems: 1,
            maxItems: 100
          },
          analysis_type: {
            type: 'string',
            enum: ['legality', 'prices', 'synergy', 'composition', 'comprehensive'],
            description: 'Type of analysis to perform'
          },
          format: {
            type: 'string',
            enum: ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer'],
            description: 'Format for legality analysis'
          },
          currency: {
            type: 'string',
            enum: ['usd', 'eur', 'tix'],
            default: 'usd',
            description: 'Currency for price analysis'
          },
          include_suggestions: {
            type: 'boolean',
            default: false,
            description: 'Include improvement suggestions'
          },
          group_by: {
            type: 'string',
            enum: ['type', 'cmc', 'color', 'rarity', 'price_range'],
            description: 'How to group analysis results'
          }
        },
        required: ['card_list', 'analysis_type']
      };
    
      constructor(private readonly scryfallClient: ScryfallClient) {}
    
      /**
       * Validate parameters
       */
      private validateParams(args: unknown): {
        card_list: string[];
        analysis_type: string;
        format?: string;
        currency: string;
        include_suggestions: boolean;
        group_by?: string;
      } {
        if (!args || typeof args !== 'object') {
          throw new ValidationError('Invalid parameters');
        }
    
        const params = args as BatchCardAnalysisParams;
    
        if (!Array.isArray(params.card_list) || params.card_list.length === 0) {
          throw new ValidationError('Card list is required and must be a non-empty array');
        }
    
        if (params.card_list.length > 100) {
          throw new ValidationError('Card list cannot exceed 100 cards');
        }
    
        if (!params.card_list.every((card: string) => typeof card === 'string' && card.trim().length > 0)) {
          throw new ValidationError('All cards in the list must be non-empty strings');
        }
    
        if (!params.analysis_type || typeof params.analysis_type !== 'string') {
          throw new ValidationError('Analysis type is required and must be a string');
        }
    
        const validAnalysisTypes = ['legality', 'prices', 'synergy', 'composition', 'comprehensive'];
        if (!validAnalysisTypes.includes(params.analysis_type)) {
          throw new ValidationError(`Analysis type must be one of: ${validAnalysisTypes.join(', ')}`);
        }
    
        if (params.format) {
          const validFormats = ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer'];
          if (!validFormats.includes(params.format)) {
            throw new ValidationError(`Format must be one of: ${validFormats.join(', ')}`);
          }
        }
    
        const currency = params.currency || 'usd';
        const validCurrencies = ['usd', 'eur', 'tix'];
        if (!validCurrencies.includes(currency)) {
          throw new ValidationError(`Currency must be one of: ${validCurrencies.join(', ')}`);
        }
    
        const includeSuggestions = params.include_suggestions ?? false;
        if (typeof includeSuggestions !== 'boolean') {
          throw new ValidationError('Include suggestions must be a boolean');
        }
    
        if (params.group_by) {
          const validGroupBy = ['type', 'cmc', 'color', 'rarity', 'price_range'];
          if (!validGroupBy.includes(params.group_by)) {
            throw new ValidationError(`Group by must be one of: ${validGroupBy.join(', ')}`);
          }
        }
    
        return {
          card_list: params.card_list.map((card: string) => card.trim()),
          analysis_type: params.analysis_type,
          format: params.format,
          currency,
          include_suggestions: includeSuggestions,
          group_by: params.group_by
        };
      }
    
      async execute(args: unknown) {
        try {
          // Validate parameters
          const params = this.validateParams(args);
    
          // Fetch all cards
          const cards = await this.fetchCards(params.card_list);
    
          // Perform analysis based on type
          let analysisResult: string;
          switch (params.analysis_type) {
            case 'legality':
              analysisResult = this.analyzeLegality(cards, params.format);
              break;
            case 'prices':
              analysisResult = this.analyzePrices(cards, params.currency);
              break;
            case 'synergy':
              analysisResult = this.analyzeSynergy(cards);
              break;
            case 'composition':
              analysisResult = this.analyzeComposition(cards);
              break;
            case 'comprehensive':
              analysisResult = this.analyzeComprehensive(cards, params);
              break;
            default:
              throw new ValidationError(`Unknown analysis type: ${params.analysis_type}`);
          }
    
          // Add suggestions if requested
          if (params.include_suggestions) {
            analysisResult += this.generateSuggestions(cards, params);
          }
    
          return {
            content: [
              {
                type: 'text',
                text: analysisResult
              }
            ]
          };
    
        } catch (error) {
          // Handle validation errors
          if (error instanceof ValidationError) {
            return {
              content: [
                {
                  type: 'text',
                  text: `Validation error: ${error.message}`
                }
              ],
              isError: true
            };
          }
    
          // Generic error handling
          return {
            content: [
              {
                type: 'text',
                text: `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
              }
            ],
            isError: true
          };
        }
      }
    
      /**
       * Fetch all cards from the card list
       */
      private async fetchCards(cardList: string[]): Promise<any[]> {
        const cards: any[] = [];
        const notFound: string[] = [];
    
        // Use batch processing for efficiency
        for (const cardName of cardList) {
          try {
            const card = await this.scryfallClient.getCard({ identifier: cardName });
            cards.push(card);
          } catch (error) {
            if (error instanceof ScryfallAPIError && error.status === 404) {
              notFound.push(cardName);
            } else {
              throw error;
            }
          }
        }
    
        if (notFound.length > 0) {
          throw new ValidationError(`Cards not found: ${notFound.join(', ')}`);
        }
    
        return cards;
      }
    
      /**
       * Analyze format legality
       */
      private analyzeLegality(cards: any[], format?: string): string {
        let result = `# Legality Analysis\n\n`;
        result += `**Total Cards:** ${cards.length}\n\n`;
    
        if (format) {
          const legal = cards.filter(card => card.legalities[format] === 'legal');
          const banned = cards.filter(card => card.legalities[format] === 'banned');
          const restricted = cards.filter(card => card.legalities[format] === 'restricted');
          const notLegal = cards.filter(card => !['legal', 'banned', 'restricted'].includes(card.legalities[format]));
    
          result += `**${format.toUpperCase()} Legality:**\n`;
          result += `- Legal: ${legal.length} cards\n`;
          result += `- Banned: ${banned.length} cards\n`;
          result += `- Restricted: ${restricted.length} cards\n`;
          result += `- Not Legal: ${notLegal.length} cards\n\n`;
    
          if (banned.length > 0) {
            result += `**Banned Cards:**\n`;
            banned.forEach(card => result += `- ${card.name}\n`);
            result += '\n';
          }
    
          if (restricted.length > 0) {
            result += `**Restricted Cards:**\n`;
            restricted.forEach(card => result += `- ${card.name}\n`);
            result += '\n';
          }
        } else {
          // Show legality across all formats
          const formats = ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer'];
          result += `**Format Legality Summary:**\n`;
          
          for (const fmt of formats) {
            const legal = cards.filter(card => card.legalities[fmt] === 'legal').length;
            result += `- ${fmt.charAt(0).toUpperCase() + fmt.slice(1)}: ${legal}/${cards.length} legal\n`;
          }
        }
    
        return result;
      }
    
      /**
       * Analyze prices
       */
      private analyzePrices(cards: any[], currency: string): string {
        let result = `# Price Analysis (${currency.toUpperCase()})\n\n`;
        
        const cardsWithPrices = cards.filter(card => (card.prices as any)[currency]);
        const prices = cardsWithPrices.map(card => parseFloat((card.prices as any)[currency]));
        
        if (prices.length === 0) {
          return result + 'No price data available for the specified currency.\n';
        }
    
        const totalValue = prices.reduce((sum, price) => sum + price, 0);
        const averagePrice = totalValue / prices.length;
        const minPrice = Math.min(...prices);
        const maxPrice = Math.max(...prices);
    
        result += `**Summary:**\n`;
        result += `- Total Value: ${currency.toUpperCase()} ${totalValue.toFixed(2)}\n`;
        result += `- Average Price: ${currency.toUpperCase()} ${averagePrice.toFixed(2)}\n`;
        result += `- Price Range: ${currency.toUpperCase()} ${minPrice.toFixed(2)} - ${currency.toUpperCase()} ${maxPrice.toFixed(2)}\n`;
        result += `- Cards with Prices: ${cardsWithPrices.length}/${cards.length}\n\n`;
    
        // Price breakdown
        const expensive = cardsWithPrices.filter(card => parseFloat((card.prices as any)[currency]) >= 10);
        const moderate = cardsWithPrices.filter(card => {
          const price = parseFloat((card.prices as any)[currency]);
          return price >= 1 && price < 10;
        });
        const budget = cardsWithPrices.filter(card => parseFloat((card.prices as any)[currency]) < 1);
    
        result += `**Price Categories:**\n`;
        result += `- Expensive (≥$10): ${expensive.length} cards\n`;
        result += `- Moderate ($1-$10): ${moderate.length} cards\n`;
        result += `- Budget (<$1): ${budget.length} cards\n\n`;
    
        if (expensive.length > 0) {
          result += `**Most Expensive Cards:**\n`;
          expensive
            .sort((a, b) => parseFloat((b.prices as any)[currency]) - parseFloat((a.prices as any)[currency]))
            .slice(0, 5)
            .forEach(card => {
              result += `- ${card.name}: ${currency.toUpperCase()} ${(card.prices as any)[currency]}\n`;
            });
        }
    
        return result;
      }
    
      /**
       * Analyze synergy patterns
       */
      private analyzeSynergy(cards: any[]): string {
        let result = `# Synergy Analysis\n\n`;
        
        // Analyze types
        const types = new Map<string, number>();
        const keywords = new Map<string, number>();
        const mechanics = new Map<string, number>();
    
        cards.forEach(card => {
          // Extract types
          const cardTypes = card.type_line.toLowerCase().split(/[\s—]+/);
          cardTypes.forEach((type: string) => {
            if (type && type !== '—') {
              types.set(type, (types.get(type) || 0) + 1);
            }
          });
    
          // Extract keywords and mechanics from oracle text
          if (card.oracle_text) {
            const text = card.oracle_text.toLowerCase();
            
            // Common keywords
            const keywordList = ['flying', 'trample', 'haste', 'vigilance', 'lifelink', 'deathtouch', 'first strike', 'double strike'];
            keywordList.forEach(keyword => {
              if (text.includes(keyword)) {
                keywords.set(keyword, (keywords.get(keyword) || 0) + 1);
              }
            });
    
            // Common mechanics
            const mechanicList = ['proliferate', 'scry', 'surveil', 'explore', 'convoke', 'delve'];
            mechanicList.forEach(mechanic => {
              if (text.includes(mechanic)) {
                mechanics.set(mechanic, (mechanics.get(mechanic) || 0) + 1);
              }
            });
          }
        });
    
        // Report common types
        result += `**Common Types:**\n`;
        Array.from(types.entries())
          .sort((a, b) => b[1] - a[1])
          .slice(0, 10)
          .forEach(([type, count]) => {
            result += `- ${type.charAt(0).toUpperCase() + type.slice(1)}: ${count} cards\n`;
          });
    
        // Report common keywords
        if (keywords.size > 0) {
          result += `\n**Common Keywords:**\n`;
          Array.from(keywords.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 5)
            .forEach(([keyword, count]) => {
              result += `- ${keyword.charAt(0).toUpperCase() + keyword.slice(1)}: ${count} cards\n`;
            });
        }
    
        // Report common mechanics
        if (mechanics.size > 0) {
          result += `\n**Common Mechanics:**\n`;
          Array.from(mechanics.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 5)
            .forEach(([mechanic, count]) => {
              result += `- ${mechanic.charAt(0).toUpperCase() + mechanic.slice(1)}: ${count} cards\n`;
            });
        }
    
        return result;
      }
    
      /**
       * Analyze deck composition
       */
      private analyzeComposition(cards: any[]): string {
        let result = `# Composition Analysis\n\n`;
        result += `**Total Cards:** ${cards.length}\n\n`;
    
        // Mana curve analysis
        const manaCurve = new Map<number, number>();
        cards.forEach(card => {
          const cmc = card.cmc || 0;
          manaCurve.set(cmc, (manaCurve.get(cmc) || 0) + 1);
        });
    
        result += `**Mana Curve:**\n`;
        for (let i = 0; i <= 10; i++) {
          const count = manaCurve.get(i) || 0;
          if (count > 0 || i <= 7) {
            result += `- ${i}${i >= 7 ? '+' : ''}: ${count} cards\n`;
          }
        }
    
        // Color distribution
        const colors = new Map<string, number>();
        const colorIdentity = new Map<string, number>();
        
        cards.forEach(card => {
          // Count individual colors in mana cost
          if (card.mana_cost) {
            const colorMatches = card.mana_cost.match(/[WUBRG]/g) || [];
            colorMatches.forEach((color: string) => {
              colors.set(color, (colors.get(color) || 0) + 1);
            });
          }
    
          // Count color identity
          if (card.color_identity && card.color_identity.length > 0) {
            const identity = card.color_identity.sort().join('');
            colorIdentity.set(identity, (colorIdentity.get(identity) || 0) + 1);
          } else {
            colorIdentity.set('Colorless', (colorIdentity.get('Colorless') || 0) + 1);
          }
        });
    
        result += `\n**Color Distribution:**\n`;
        const colorNames = { W: 'White', U: 'Blue', B: 'Black', R: 'Red', G: 'Green' };
        Object.entries(colorNames).forEach(([symbol, name]) => {
          const count = colors.get(symbol) || 0;
          result += `- ${name}: ${count} symbols\n`;
        });
    
        result += `\n**Color Identity:**\n`;
        Array.from(colorIdentity.entries())
          .sort((a, b) => b[1] - a[1])
          .forEach(([identity, count]) => {
            result += `- ${identity || 'Colorless'}: ${count} cards\n`;
          });
    
        return result;
      }
    
      /**
       * Comprehensive analysis combining all types
       */
      private analyzeComprehensive(cards: any[], params: any): string {
        let result = `# Comprehensive Analysis\n\n`;
        
        result += this.analyzeLegality(cards, params.format);
        result += '\n---\n\n';
        result += this.analyzePrices(cards, params.currency);
        result += '\n---\n\n';
        result += this.analyzeComposition(cards);
        result += '\n---\n\n';
        result += this.analyzeSynergy(cards);
    
        return result;
      }
    
      /**
       * Generate improvement suggestions
       */
      private generateSuggestions(cards: any[], params: any): string {
        let suggestions = `\n\n# Suggestions\n\n`;
    
        // Format-specific suggestions
        if (params.format) {
          const illegal = cards.filter(card => card.legalities[params.format] !== 'legal');
          if (illegal.length > 0) {
            suggestions += `**Format Compliance:**\n`;
            suggestions += `- Consider replacing ${illegal.length} cards that are not legal in ${params.format}\n`;
          }
        }
    
        // Price suggestions
        const expensiveCards = cards.filter(card => {
          const price = parseFloat((card.prices as any)[params.currency] || '0');
          return price >= 20;
        });
        
        if (expensiveCards.length > 0) {
          suggestions += `**Budget Optimization:**\n`;
          suggestions += `- Consider finding alternatives for ${expensiveCards.length} expensive cards (≥$20)\n`;
        }
    
        // Mana curve suggestions
        const highCmc = cards.filter(card => (card.cmc || 0) >= 6).length;
        const lowCmc = cards.filter(card => (card.cmc || 0) <= 2).length;
        
        if (highCmc > cards.length * 0.2) {
          suggestions += `**Mana Curve:**\n`;
          suggestions += `- Consider reducing high-cost cards (${highCmc} cards with CMC ≥6)\n`;
        }
        
        if (lowCmc < cards.length * 0.3) {
          suggestions += `**Early Game:**\n`;
          suggestions += `- Consider adding more low-cost cards for early game presence\n`;
        }
    
        return suggestions;
      }
    }
  • Input schema definition for the batch_card_analysis tool, defining parameters like card_list, analysis_type, format, etc.
    readonly inputSchema = {
      type: 'object' as const,
      properties: {
        card_list: {
          type: 'array',
          items: { type: 'string' },
          description: 'List of card names to analyze',
          minItems: 1,
          maxItems: 100
        },
        analysis_type: {
          type: 'string',
          enum: ['legality', 'prices', 'synergy', 'composition', 'comprehensive'],
          description: 'Type of analysis to perform'
        },
        format: {
          type: 'string',
          enum: ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer'],
          description: 'Format for legality analysis'
        },
        currency: {
          type: 'string',
          enum: ['usd', 'eur', 'tix'],
          default: 'usd',
          description: 'Currency for price analysis'
        },
        include_suggestions: {
          type: 'boolean',
          default: false,
          description: 'Include improvement suggestions'
        },
        group_by: {
          type: 'string',
          enum: ['type', 'cmc', 'color', 'rarity', 'price_range'],
          description: 'How to group analysis results'
        }
      },
      required: ['card_list', 'analysis_type']
    };
  • src/server.ts:75-75 (registration)
    Registration of the batch_card_analysis tool in the MCP server constructor.
    this.tools.set("batch_card_analysis", new BatchCardAnalysisTool(this.scryfallClient));
  • TypeScript interface for BatchCardAnalysisParams used in tool validation.
    export interface BatchCardAnalysisParams {
      card_list: string[];
      analysis_type: "legality" | "prices" | "synergy" | "composition" | "comprehensive";
      format?: MagicFormat;
      currency?: "usd" | "eur" | "tix";
      include_images?: boolean;
      include_suggestions?: boolean;
      group_by?: string;
    }
Behavior2/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions analysis types but lacks details on permissions, rate limits, response format, or whether it's read-only or mutative. This is inadequate for a tool with 6 parameters and no output schema.

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 a single, efficient sentence that front-loads the core purpose without unnecessary words. Every part contributes directly to understanding the tool's function, making it appropriately sized and well-structured.

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

Completeness2/5

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

Given the tool's complexity (6 parameters, no annotations, no output schema), the description is insufficient. It doesn't cover behavioral traits, output expectations, or usage context, leaving significant gaps for an AI agent to invoke it correctly without additional information.

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%, so the schema already documents all parameters. The description adds minimal value by listing analysis types (e.g., 'legality, prices') but doesn't provide additional semantics beyond what the schema's enum values and descriptions offer.

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

Purpose4/5

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

The description clearly states the tool's purpose: analyzing multiple cards for specific aspects (legality, prices, synergies, deck composition). It specifies the verb 'analyze' and resource 'multiple cards,' but doesn't explicitly differentiate from sibling tools like 'analyze_deck_composition' or 'get_card_prices,' which reduces clarity.

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

Usage Guidelines2/5

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

No explicit guidance on when to use this tool versus alternatives is provided. The description lists analysis types but doesn't indicate scenarios for choosing this over siblings like 'analyze_deck_composition' or 'get_card_prices,' leaving usage context implied rather than stated.

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

Related 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/bmurdock/scryfall-mcp'

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