Skip to main content
Glama

Scryfall MCP Server

by bmurdock
find-synergistic-cards.ts36.5 kB
import { ScryfallClient } from '../services/scryfall-client.js'; import { ValidationError, ScryfallAPIError } from '../types/mcp-types.js'; // Removed unused import /** * MCP Tool for finding cards that synergize with a specific card, theme, or archetype */ export class FindSynergisticCardsTool { readonly name = 'find_synergistic_cards'; readonly description = 'Find cards that synergize with a specific card, theme, or archetype'; readonly inputSchema = { type: 'object' as const, properties: { focus_card: { type: 'string', description: 'Card name or theme to build around' }, synergy_type: { type: 'string', enum: ['tribal', 'combo', 'archetype', 'keyword', 'theme', 'mechanic'], description: 'Type of synergy to discover' }, format: { type: 'string', enum: ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer', 'brawl', 'standardbrawl'], description: 'Format legality requirement' }, exclude_colors: { type: 'string', description: 'Colors to exclude from results (e.g., "rb" to exclude red and black)' }, max_cmc: { type: 'number', minimum: 0, description: 'Maximum mana value' }, include_lands: { type: 'boolean', default: true, description: 'Include synergistic lands' }, limit: { type: 'number', default: 15, minimum: 1, maximum: 50, description: 'Number of synergistic cards to return' }, arena_only: { type: 'boolean', default: false, description: 'Only return cards available in Arena' } }, required: ['focus_card'] }; constructor(private readonly scryfallClient: ScryfallClient) {} /** * Validate parameters */ private validateParams(args: unknown): { focus_card: string; synergy_type?: string; format?: string; exclude_colors?: string; max_cmc?: number; include_lands: boolean; limit: number; arena_only: boolean; } { if (!args || typeof args !== 'object') { throw new ValidationError('Invalid parameters'); } const params = args as any; if (!params.focus_card || typeof params.focus_card !== 'string') { throw new ValidationError('Focus card is required and must be a string'); } if (params.synergy_type) { const validTypes = ['tribal', 'combo', 'archetype', 'keyword', 'theme', 'mechanic']; if (!validTypes.includes(params.synergy_type)) { throw new ValidationError(`Synergy type must be one of: ${validTypes.join(', ')}`); } } if (params.format) { const validFormats = ['standard', 'modern', 'legacy', 'vintage', 'commander', 'pioneer', 'brawl', 'standardbrawl']; if (!validFormats.includes(params.format)) { throw new ValidationError(`Format must be one of: ${validFormats.join(', ')}`); } } if (params.exclude_colors && typeof params.exclude_colors !== 'string') { throw new ValidationError('Exclude colors must be a string'); } if (params.max_cmc !== undefined) { if (typeof params.max_cmc !== 'number' || params.max_cmc < 0) { throw new ValidationError('Max CMC must be a non-negative number'); } } const includeLands = params.include_lands ?? true; if (typeof includeLands !== 'boolean') { throw new ValidationError('Include lands must be a boolean'); } const limit = params.limit || 15; if (typeof limit !== 'number' || limit < 1 || limit > 50) { throw new ValidationError('Limit must be a number between 1 and 50'); } const arenaOnly = params.arena_only ?? false; if (typeof arenaOnly !== 'boolean') { throw new ValidationError('Arena only must be a boolean'); } return { focus_card: params.focus_card.trim(), synergy_type: params.synergy_type, format: params.format, exclude_colors: params.exclude_colors, max_cmc: params.max_cmc, include_lands: includeLands, limit, arena_only: arenaOnly }; } async execute(args: unknown) { try { // Validate parameters const params = this.validateParams(args); // Try to get the focus card to analyze its properties let focusCard = null; try { focusCard = await this.scryfallClient.getCard({ identifier: params.focus_card }); } catch (error) { // If card not found, treat focus_card as a theme/keyword if (!(error instanceof ScryfallAPIError && error.status === 404)) { throw error; } } // Build synergy search queries const queries = this.buildSynergyQueries(focusCard, params); // Execute searches with multi-layered strategy const allResults: any[] = []; const resultsByLayer: { [key: string]: any[] } = { semantic: [], exact: [], thematic: [] }; for (let i = 0; i < queries.length; i++) { const query = queries[i]; try { const results = await this.scryfallClient.searchCards({ query, limit: Math.ceil(params.limit / queries.length) + 5, // Get extra to account for filtering order: 'edhrec' }); // Categorize results by search layer const layerResults = results.data.map(card => ({ ...card, _synergy_layer: i < 10 ? 'semantic' : (i < 20 ? 'exact' : 'thematic'), // Rough categorization _synergy_query: query })); if (i < 10) { resultsByLayer.semantic.push(...layerResults); } else if (i < 20) { resultsByLayer.exact.push(...layerResults); } else { resultsByLayer.thematic.push(...layerResults); } allResults.push(...layerResults); } catch (error) { // Continue with other queries if one fails continue; } } // If no results and theme-based, try broader searches if (allResults.length === 0 && !focusCard) { // Build a basic query with just the format/constraints let fallbackBaseQuery = ''; if (params.format) { fallbackBaseQuery += `legal:${params.format} `; } if (params.exclude_colors) { for (const color of params.exclude_colors.toLowerCase()) { if ('wubrg'.includes(color)) { fallbackBaseQuery += `-c:${color} `; } } } if (params.max_cmc !== undefined) { fallbackBaseQuery += `cmc<=${params.max_cmc} `; } if (!params.include_lands) { fallbackBaseQuery += '-t:land '; } if (params.arena_only) { fallbackBaseQuery += 'game:arena '; } const fallbackQueries = this.getFallbackQueries(params.focus_card, fallbackBaseQuery); for (const query of fallbackQueries) { try { const results = await this.scryfallClient.searchCards({ query, limit: params.limit, order: 'edhrec' }); allResults.push(...results.data); if (allResults.length > 0) break; // Stop once we find some results } catch (error) { continue; } } } // Remove duplicates and filter results with layered prioritization const uniqueResults = this.filterAndDeduplicateResults(allResults, focusCard, params); // Prioritize results by synergy layer (semantic > exact > thematic) const prioritizedResults = this.prioritizeResultsByLayer(uniqueResults); // Limit results const finalResults = prioritizedResults.slice(0, params.limit); // Format response let responseText = `**Synergistic cards for "${params.focus_card}"**`; if (params.synergy_type) { responseText += ` (${params.synergy_type} synergy)`; } responseText += ':\n\n'; if (finalResults.length === 0) { responseText += `No synergistic cards found for "${params.focus_card}"`; if (params.format) { responseText += ` in ${params.format}`; } responseText += '. Try adjusting your search criteria or synergy type.'; } else { const mockSearchResponse = { object: 'list' as const, total_cards: finalResults.length, has_more: false, data: finalResults }; responseText += this.formatResultsWithSynergyExplanations(mockSearchResponse, focusCard, params.focus_card); } return { content: [ { type: 'text', text: responseText } ] }; } 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 }; } } /** * Format results with synergy explanations */ private formatResultsWithSynergyExplanations(searchResponse: any, focusCard: any, focusCardName: string): string { let output = ''; // Group results by synergy layer const resultsByLayer = { semantic: searchResponse.data.filter((card: any) => card._synergy_layer === 'semantic'), exact: searchResponse.data.filter((card: any) => card._synergy_layer === 'exact'), thematic: searchResponse.data.filter((card: any) => card._synergy_layer === 'thematic') }; // Display semantic synergies first (highest priority) if (resultsByLayer.semantic.length > 0) { output += '**🎯 Strategic Synergies:**\n'; for (const card of resultsByLayer.semantic.slice(0, 8)) { output += this.formatCardWithSynergyExplanation(card, focusCard, focusCardName); } output += '\n'; } // Display exact synergies if (resultsByLayer.exact.length > 0) { output += '**⚡ Mechanical Synergies:**\n'; for (const card of resultsByLayer.exact.slice(0, 5)) { output += this.formatCardWithSynergyExplanation(card, focusCard, focusCardName); } output += '\n'; } // Display thematic synergies if (resultsByLayer.thematic.length > 0) { output += '**🌟 Thematic Support:**\n'; for (const card of resultsByLayer.thematic.slice(0, 5)) { output += this.formatCardWithSynergyExplanation(card, focusCard, focusCardName); } } return output; } /** * Format individual card with synergy explanation */ private formatCardWithSynergyExplanation(card: any, focusCard: any, focusCardName: string): string { const name = card.name; const manaCost = card.mana_cost || ''; const typeLine = card.type_line || ''; const oracleText = card.oracle_text || ''; const prices = card.prices || {}; let output = `• **${name}** ${manaCost}\n`; output += ` ${typeLine}\n`; // Add synergy explanation const synergyExplanation = this.generateSynergyExplanation(card, focusCard, focusCardName); if (synergyExplanation) { output += ` 💡 *${synergyExplanation}*\n`; } // Add shortened oracle text if (oracleText.length > 120) { output += ` ${oracleText.substring(0, 120)}...\n`; } else if (oracleText) { output += ` ${oracleText}\n`; } // Add price if available if (prices.usd) { output += ` 💰 $${prices.usd}\n`; } output += '\n'; return output; } /** * Generate synergy explanation for a card */ private generateSynergyExplanation(card: any, focusCard: any, focusCardName: string): string { const cardOracle = card.oracle_text?.toLowerCase() || ''; const focusOracle = focusCard?.oracle_text?.toLowerCase() || ''; const focusName = focusCardName.toLowerCase(); // Check for specific synergy patterns // Obeka-specific synergies if (focusName.includes('obeka')) { if (cardOracle.includes('beginning of your upkeep') || cardOracle.includes('at the beginning of your upkeep')) { return 'Triggers during extra upkeep steps created by Obeka'; } } // Upkeep synergies if (focusOracle.includes('upkeep') || focusOracle.includes('extra turn')) { if (cardOracle.includes('beginning of your upkeep')) { return 'Benefits from extra upkeep steps'; } } // Combat synergies if (focusOracle.includes('combat') || focusOracle.includes('attack')) { if (cardOracle.includes('attacks') || cardOracle.includes('combat damage')) { return 'Synergizes with extra combat steps'; } } // Token synergies if (focusOracle.includes('token') || focusOracle.includes('create')) { if (cardOracle.includes('creature enters') || cardOracle.includes('creatures you control')) { return 'Benefits from token generation'; } } // ETB synergies if (focusOracle.includes('enters the battlefield')) { if (cardOracle.includes('creature enters') || cardOracle.includes('enters the battlefield')) { return 'Creates ETB synergy chains'; } } // Spell synergies if (focusOracle.includes('instant') || focusOracle.includes('sorcery')) { if (cardOracle.includes('prowess') || cardOracle.includes('magecraft') || cardOracle.includes('whenever you cast')) { return 'Triggers from spell casting'; } } // Graveyard synergies if (focusOracle.includes('graveyard') || focusOracle.includes('dies')) { if (cardOracle.includes('graveyard') || cardOracle.includes('dies')) { return 'Graveyard value synergy'; } } // Sacrifice synergies if (focusOracle.includes('sacrifice')) { if (cardOracle.includes('sacrifice') || cardOracle.includes('dies')) { return 'Sacrifice synergy engine'; } } // Generic synergy based on layer if (card._synergy_layer === 'semantic') { return 'Strategic synergy with focus card'; } else if (card._synergy_layer === 'exact') { return 'Mechanical synergy match'; } else if (card._synergy_layer === 'thematic') { return 'Thematic support card'; } return ''; } /** * Build synergy search queries based on focus card and parameters */ private buildSynergyQueries(focusCard: any, params: { focus_card: string; synergy_type?: string; format?: string; exclude_colors?: string; max_cmc?: number; include_lands: boolean; arena_only: boolean; }): string[] { const queries: string[] = []; let baseQuery = ''; // Add format constraint if (params.format) { baseQuery += `legal:${params.format} `; } // Add color exclusion if (params.exclude_colors) { for (const color of params.exclude_colors.toLowerCase()) { if ('wubrg'.includes(color)) { baseQuery += `-c:${color} `; } } } // Add CMC constraint if (params.max_cmc !== undefined) { baseQuery += `cmc<=${params.max_cmc} `; } // Add land exclusion if not wanted if (!params.include_lands) { baseQuery += '-t:land '; } // Add Arena filtering if requested if (params.arena_only) { baseQuery += 'game:arena '; } if (focusCard) { // Card-based synergy analysis queries.push(...this.getCardBasedSynergies(focusCard, baseQuery, params.synergy_type, params.focus_card)); } else { // Theme-based synergy analysis queries.push(...this.getThemeBasedSynergies(params.focus_card, baseQuery, params.synergy_type)); } return queries.filter(q => q.trim().length > 0); } /** * Get synergies based on an actual card */ private getCardBasedSynergies(focusCard: any, baseQuery: string, synergyType?: string, originalSearchTerm?: string): string[] { const queries: string[] = []; // Theme override: If user explicitly wants theme analysis but we found a card, // fall back to theme-based analysis using the original search term if (synergyType === 'theme' && originalSearchTerm && originalSearchTerm.toLowerCase() !== focusCard.name.toLowerCase()) { const themeQueries = this.getThemeBasedSynergies(originalSearchTerm, baseQuery, synergyType); queries.push(...themeQueries); return queries; } // Extract card properties const types = focusCard.type_line.toLowerCase(); const oracleText = focusCard.oracle_text?.toLowerCase() || ''; const keywords = this.extractKeywords(oracleText); // **NEW: Semantic synergy detection** const semanticQueries = this.getSemanticSynergies(focusCard, baseQuery); queries.push(...semanticQueries); if (!synergyType || synergyType === 'tribal') { // Tribal synergies const creatureTypes = this.extractCreatureTypes(types); for (const type of creatureTypes) { queries.push(`${baseQuery}(o:"${type}" OR t:"${type}")`); } } if (!synergyType || synergyType === 'keyword') { // Keyword synergies for (const keyword of keywords) { queries.push(`${baseQuery}o:"${keyword}"`); } } if (!synergyType || synergyType === 'mechanic') { // Mechanic synergies based on oracle text patterns const mechanics = this.extractMechanics(oracleText); for (const mechanic of mechanics) { queries.push(`${baseQuery}o:"${mechanic}"`); } } if (!synergyType || synergyType === 'archetype') { // Archetype synergies based on card function const archetypeQueries = this.getArchetypeSynergies(focusCard, baseQuery); queries.push(...archetypeQueries); } if (synergyType === 'theme') { // When user specifies theme synergy but we found a card, // fall back to theme-based analysis using the original search term if (originalSearchTerm) { const themeQueries = this.getThemeBasedSynergies(originalSearchTerm, baseQuery, synergyType); queries.push(...themeQueries); } } return queries; } /** * Get semantic synergies based on strategic patterns and interactions */ private getSemanticSynergies(focusCard: any, baseQuery: string): string[] { const queries: string[] = []; const oracleText = focusCard.oracle_text?.toLowerCase() || ''; const name = focusCard.name.toLowerCase(); // Strategic synergy patterns mapping const synergyPatterns = [ // Upkeep/End step synergies { triggers: ['extra upkeep', 'additional upkeep', 'extra turn', 'additional turn', 'upkeep step'], synergizes_with: ['beginning of your upkeep', 'at the beginning of your upkeep', 'during your upkeep'] }, { triggers: ['extra combat', 'additional combat', 'extra attack', 'additional attack phase'], synergizes_with: ['whenever ~ attacks', 'whenever ~ deals combat damage', 'combat damage to a player'] }, { triggers: ['extra end step', 'additional end step', 'end step'], synergizes_with: ['at the beginning of your end step', 'at the beginning of the end step'] }, // Token synergies { triggers: ['create.*token', 'token.*creature', 'populate', 'amass'], synergizes_with: ['whenever a creature enters', 'creature tokens you control', 'token creatures', 'creatures you control'] }, // ETB/LTB synergies { triggers: ['enters the battlefield', 'enters tapped', 'when.*enters'], synergizes_with: ['whenever a creature enters', 'whenever.*enters the battlefield', 'creature entering'] }, { triggers: ['leaves the battlefield', 'dies', 'when.*dies'], synergizes_with: ['whenever a creature dies', 'whenever.*leaves the battlefield', 'creature dying'] }, // Spell synergies { triggers: ['whenever you cast', 'instant or sorcery', 'noncreature spell'], synergizes_with: ['prowess', 'magecraft', 'whenever you cast an instant or sorcery'] }, // Sacrifice synergies { triggers: ['sacrifice', 'sacrificed', 'sacrifice.*creature'], synergizes_with: ['whenever.*is sacrificed', 'sacrifice.*creature', 'aristocrats'] }, // Life gain synergies { triggers: ['you gain life', 'whenever you gain life', 'lifelink'], synergizes_with: ['whenever you gain life', 'lifegain', 'lifelink'] }, // Card draw synergies { triggers: ['draw.*card', 'whenever you draw', 'card draw'], synergizes_with: ['whenever you draw', 'draw additional', 'card advantage'] }, // Mana synergies { triggers: ['add.*mana', 'mana.*to your mana pool', 'treasure', 'ritual'], synergizes_with: ['x spell', 'costs.*less', 'mana value', 'expensive spell'] }, // Graveyard synergies { triggers: ['enters your graveyard', 'creature.*graveyard', 'mill'], synergizes_with: ['from your graveyard', 'graveyard.*battlefield', 'flashback', 'unearth'] }, // +1/+1 counter synergies { triggers: ['\\+1/\\+1 counter', 'put.*counter', 'counter.*creature'], synergizes_with: ['\\+1/\\+1 counter', 'proliferate', 'counter.*creature', 'adapt', 'evolve'] } ]; // Check each pattern for (const pattern of synergyPatterns) { // Check if focus card matches any trigger pattern const matchesTrigger = pattern.triggers.some(trigger => new RegExp(trigger, 'i').test(oracleText) || new RegExp(trigger, 'i').test(name) ); if (matchesTrigger) { // Search for cards that synergize with this pattern for (const synergy of pattern.synergizes_with) { queries.push(`${baseQuery}o:"${synergy}"`); } } } // Specific high-synergy card patterns if (name.includes('obeka')) { // Obeka specifically synergizes with upkeep triggers queries.push(`${baseQuery}o:"beginning of your upkeep"`); queries.push(`${baseQuery}o:"at the beginning of your upkeep"`); queries.push(`${baseQuery}o:"during your upkeep"`); queries.push(`${baseQuery}o:"upkeep, "`); } return queries.filter(q => q.trim().length > 0); } /** * Get synergies based on a theme or keyword */ private getThemeBasedSynergies(theme: string, baseQuery: string, synergyType?: string): string[] { const queries: string[] = []; const normalizedTheme = theme.toLowerCase(); const words = normalizedTheme.split(/\s+/); // For tribal themes, extract creature type if (normalizedTheme.includes('tribal') || synergyType === 'tribal') { const creatureType = words.find(word => !['tribal', 'deck', 'theme', 'synergy'].includes(word) ); if (creatureType) { // Search for creatures of that type queries.push(`${baseQuery}t:${creatureType}`); // Search for cards that care about that creature type queries.push(`${baseQuery}o:"${creatureType}"`); // Search for cards that reference the creature type in rules text queries.push(`${baseQuery}o:"${creatureType}s you control"`); } } // Direct oracle text search for shorter themes if (theme.length <= 20) { queries.push(`${baseQuery}o:"${theme}"`); } // Search for individual words in the theme for (const word of words) { if (word.length >= 4 && !['tribal', 'deck', 'theme', 'synergy', 'cards'].includes(word)) { queries.push(`${baseQuery}o:"${word}"`); queries.push(`${baseQuery}t:"${word}"`); } } // Common synergy patterns if (normalizedTheme.includes('counter')) { queries.push(`${baseQuery}(o:"counter" OR o:"+1/+1" OR o:"proliferate")`); } if (normalizedTheme.includes('artifact')) { // Comprehensive artifact ecosystem searches queries.push(`${baseQuery}t:artifact`); // Actual artifact cards queries.push(`${baseQuery}(o:"artifact" OR o:"metalcraft" OR o:"affinity")`); // Artifact synergy queries.push(`${baseQuery}(o:"equipment" OR o:"attach" OR o:"equip")`); // Equipment synergy queries.push(`${baseQuery}(o:"construct" OR o:"golem" OR o:"thopter")`); // Artifact creatures queries.push(`${baseQuery}(t:"artifact creature" OR o:"fabricate")`); // Artifact creature support } if (normalizedTheme.includes('graveyard')) { queries.push(`${baseQuery}(o:"graveyard" OR o:"from your graveyard" OR o:"flashback" OR o:"unearth")`); } if (normalizedTheme.includes('token')) { queries.push(`${baseQuery}(o:"token" OR o:"create" OR o:"populate")`); } if (normalizedTheme.includes('burn') || normalizedTheme.includes('damage')) { queries.push(`${baseQuery}(o:"damage" OR o:"burn" OR o:"deals damage")`); } if (normalizedTheme.includes('lifegain') || normalizedTheme.includes('life')) { queries.push(`${baseQuery}(o:"gain life" OR o:"lifelink" OR o:"life")`); } return queries.filter(q => q.trim().length > 0); } /** * Extract creature types from type line */ private extractCreatureTypes(typeLine: string): string[] { const commonTypes = [ 'human', 'elf', 'goblin', 'zombie', 'spirit', 'angel', 'demon', 'dragon', 'wizard', 'warrior', 'knight', 'soldier', 'beast', 'elemental', 'vampire', 'merfolk', 'pirate', 'dinosaur', 'cat', 'bird', 'horror', 'construct', 'artifact', 'enchantment', 'planeswalker', 'giant', 'dwarf', 'troll', 'ogre', 'orc', 'minotaur', 'centaur', 'sphinx', 'hydra', 'phoenix' ]; return commonTypes.filter(type => typeLine.includes(type)); } /** * Extract keywords from oracle text */ private extractKeywords(oracleText: string): string[] { const keywords = [ 'flying', 'trample', 'haste', 'vigilance', 'lifelink', 'deathtouch', 'first strike', 'double strike', 'hexproof', 'indestructible', 'flash', 'prowess', 'menace', 'reach', 'defender', 'ward', 'protection', 'shroud', 'fear', 'intimidate', 'landwalk', 'horsemanship', 'shadow', 'banding', 'rampage', 'bushido', 'ninjutsu', 'splice', 'affinity', 'convoke', 'delve', 'emerge', 'improvise', 'undaunted' ]; return keywords.filter(keyword => oracleText.includes(keyword)); } /** * Extract mechanics from oracle text with improved pattern matching */ private extractMechanics(oracleText: string): string[] { const mechanics = [ 'proliferate', 'scry', 'surveil', 'explore', 'adapt', 'amass', 'convoke', 'delve', 'emerge', 'escape', 'flashback', 'madness', 'morph', 'suspend', 'unearth', 'cycling', 'kicker', 'multikicker', 'cascade', 'storm', 'dredge', 'buyback', 'echo', 'evoke', 'splice', 'ripple', 'rebound', 'retrace', 'overload', 'cipher', 'populate', 'scavenge', 'unleash', 'detain', 'extort', 'evolve', 'bloodrush', 'battalion', 'devotion', 'inspired', 'tribute', 'dash', 'exploit', 'manifest', 'bolster', 'support', 'surge', 'awaken', 'devoid', 'ingest', 'myriad', 'crew', 'fabricate', 'energy', 'revolt', 'improvise', 'aftermath', 'embalm', 'eternalize', 'afflict', 'exert', 'explore', 'enrage', 'raid', 'ascend', 'assist', 'jump-start', 'mentor', 'undergrowth', 'spectacle', 'riot', 'addendum', 'afterlife', 'amass', 'proliferate', 'adapt', 'escape', 'mutate', 'companion', 'cycling', 'keyword counter', 'foretell', 'boast', 'disturb', 'daybound', 'nightbound', 'cleave', 'training', 'channel', 'ninjutsu', 'reconfigure', 'compleated', 'casualty', 'connive', 'hideaway', 'blitz', 'prototype', 'unearth', 'powerstone', 'enlist', 'domain', 'kicker', 'toxic', 'corrupted', 'backup', 'bargain', 'craft' ]; // Enhanced pattern matching for mechanics const foundMechanics: string[] = []; for (const mechanic of mechanics) { // Match mechanic as whole word or followed by a space/number const regex = new RegExp(`\\b${mechanic}(?:\\s|\\d|$)`, 'i'); if (regex.test(oracleText)) { foundMechanics.push(mechanic); } } // Additional pattern-based mechanic detection const mechanicPatterns = [ { pattern: /\+1\/\+1 counter/i, mechanic: '+1/+1 counters' }, { pattern: /enters the battlefield/i, mechanic: 'enters the battlefield' }, { pattern: /leaves the battlefield/i, mechanic: 'leaves the battlefield' }, { pattern: /sacrifice.*creature/i, mechanic: 'sacrifice creatures' }, { pattern: /whenever.*dies/i, mechanic: 'death triggers' }, { pattern: /whenever.*attacks/i, mechanic: 'attack triggers' }, { pattern: /whenever.*deals damage/i, mechanic: 'damage triggers' }, { pattern: /whenever you cast/i, mechanic: 'spell triggers' }, { pattern: /draw.*cards?/i, mechanic: 'card draw' }, { pattern: /discard.*cards?/i, mechanic: 'discard' }, { pattern: /mill.*cards?/i, mechanic: 'mill' }, { pattern: /create.*token/i, mechanic: 'token creation' }, { pattern: /return.*from.*graveyard/i, mechanic: 'graveyard recursion' }, { pattern: /costs.*less/i, mechanic: 'cost reduction' }, { pattern: /add.*mana/i, mechanic: 'mana generation' }, { pattern: /destroy target/i, mechanic: 'destruction' }, { pattern: /exile.*until/i, mechanic: 'temporary exile' }, { pattern: /double.*damage/i, mechanic: 'damage doubling' }, { pattern: /prevent.*damage/i, mechanic: 'damage prevention' }, { pattern: /can't be blocked/i, mechanic: 'evasion' }, { pattern: /flying/i, mechanic: 'flying' }, { pattern: /trample/i, mechanic: 'trample' }, { pattern: /haste/i, mechanic: 'haste' }, { pattern: /vigilance/i, mechanic: 'vigilance' }, { pattern: /lifelink/i, mechanic: 'lifelink' }, { pattern: /deathtouch/i, mechanic: 'deathtouch' }, { pattern: /first strike/i, mechanic: 'first strike' }, { pattern: /double strike/i, mechanic: 'double strike' }, { pattern: /hexproof/i, mechanic: 'hexproof' }, { pattern: /indestructible/i, mechanic: 'indestructible' }, { pattern: /flash/i, mechanic: 'flash' }, { pattern: /prowess/i, mechanic: 'prowess' }, { pattern: /menace/i, mechanic: 'menace' }, { pattern: /reach/i, mechanic: 'reach' }, { pattern: /defender/i, mechanic: 'defender' }, { pattern: /ward/i, mechanic: 'ward' } ]; for (const { pattern, mechanic } of mechanicPatterns) { if (pattern.test(oracleText) && !foundMechanics.includes(mechanic)) { foundMechanics.push(mechanic); } } return foundMechanics; } /** * Get archetype-based synergies */ private getArchetypeSynergies(focusCard: any, baseQuery: string): string[] { const queries: string[] = []; const oracleText = focusCard.oracle_text?.toLowerCase() || ''; const types = focusCard.type_line?.toLowerCase() || ''; const colors = focusCard.color_identity || []; // Spell-based synergies if (types.includes('instant') || types.includes('sorcery')) { if (oracleText.includes('damage')) { queries.push(`${baseQuery}(o:"damage" OR o:"burn" OR o:"direct damage")`); queries.push(`${baseQuery}(o:"prowess" OR o:"magecraft" OR o:"whenever you cast")`); } if (oracleText.includes('counter')) { queries.push(`${baseQuery}(o:"counter" OR o:"counterspell")`); } if (oracleText.includes('draw')) { queries.push(`${baseQuery}(o:"draw" OR o:"card advantage")`); } } // Creature synergies if (types.includes('creature')) { const power = parseInt(focusCard.power) || 0; const toughness = parseInt(focusCard.toughness) || 0; // Aggressive creatures if (power >= 3 && focusCard.cmc <= 3) { queries.push(`${baseQuery}(o:"haste" OR o:"attack" OR o:"combat")`); } // Defensive creatures if (toughness >= 4) { queries.push(`${baseQuery}(o:"defender" OR o:"vigilance" OR o:"prevent")`); } // Utility creatures if (oracleText.includes('when') || oracleText.includes('whenever')) { queries.push(`${baseQuery}(o:"enters" OR o:"leaves" OR o:"sacrifice")`); } } // Enchantment synergies if (types.includes('enchantment')) { queries.push(`${baseQuery}(t:enchantment OR o:"enchantment")`); if (types.includes('aura')) { queries.push(`${baseQuery}(o:"attach" OR o:"equipped" OR o:"enchant")`); } } // Artifact synergies if (types.includes('artifact')) { queries.push(`${baseQuery}(t:artifact OR o:"artifact" OR o:"metalcraft")`); } // Color-based synergies if (colors.length === 1) { const color = colors[0]; queries.push(`${baseQuery}(c:${color.toLowerCase()} OR o:"${this.getColorName(color)}")`); } return queries; } /** * Get color name from color code */ private getColorName(colorCode: string): string { const colorMap: Record<string, string> = { 'W': 'white', 'U': 'blue', 'B': 'black', 'R': 'red', 'G': 'green' }; return colorMap[colorCode.toUpperCase()] || colorCode; } /** * Get fallback queries when primary searches fail */ private getFallbackQueries(theme: string, baseQuery: string): string[] { const queries: string[] = []; const normalizedTheme = theme.toLowerCase(); // Single-word fallbacks const words = normalizedTheme.split(/\s+/); for (const word of words) { if (word.length >= 3) { queries.push(`${baseQuery}t:${word}`); queries.push(`${baseQuery}o:${word}`); } } // Popular archetype fallbacks if (normalizedTheme.includes('artifact')) { queries.push(`${baseQuery}t:artifact`); // Priority 1: Actual artifacts queries.push(`${baseQuery}o:"artifact"`); // Priority 2: Cards that mention artifacts queries.push(`${baseQuery}(o:"metalcraft" OR o:"affinity" OR o:"improvise")`); // Priority 3: Artifact mechanics queries.push(`${baseQuery}t:equipment`); // Priority 4: Equipment } if (normalizedTheme.includes('enchantment')) { queries.push(`${baseQuery}t:enchantment`); queries.push(`${baseQuery}o:enchantment`); } if (normalizedTheme.includes('token')) { queries.push(`${baseQuery}o:token`); queries.push(`${baseQuery}o:create`); } return queries; } /** * Prioritize results by synergy layer */ private prioritizeResultsByLayer(results: any[]): any[] { const layerPriority = { semantic: 3, exact: 2, thematic: 1 }; return results.sort((a, b) => { const aPriority = layerPriority[a._synergy_layer as keyof typeof layerPriority] || 0; const bPriority = layerPriority[b._synergy_layer as keyof typeof layerPriority] || 0; if (aPriority !== bPriority) { return bPriority - aPriority; // Higher priority first } // Secondary sort by EDHREC ranking (lower rank = more popular) return (a.edhrec_rank || 999999) - (b.edhrec_rank || 999999); }); } /** * Filter and deduplicate results */ private filterAndDeduplicateResults(results: any[], focusCard: any, params: any): any[] { const seen = new Set<string>(); const filtered: any[] = []; for (const card of results) { // Skip duplicates if (seen.has(card.id)) continue; seen.add(card.id); // Skip the focus card itself if (focusCard && card.id === focusCard.id) continue; // Skip if name matches focus card (for theme searches) if (card.name.toLowerCase() === params.focus_card.toLowerCase()) continue; filtered.push(card); } return filtered; } }

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