Skip to main content
Glama

Scryfall MCP Server

by bmurdock
parser.ts10.1 kB
/** * @fileoverview Natural Language Parser for Scryfall Query Building * * This module coordinates all pattern recognition engines to parse natural * language queries into structured concepts that can be converted to Scryfall * search syntax. */ import { ParsedQuery, QueryContext, Ambiguity, LegalityConcept, RoleConcept, StrategyConcept, SetConcept, RarityConcept, TimeConcept, NameConcept, FlavorConcept, ArtistConcept } from './types.js'; import { ColorPatternEngine } from './extractors/color-extractor.js'; import { ArchetypePatternEngine } from './extractors/archetype-extractor.js'; import { PricePatternEngine } from './extractors/price-extractor.js'; import { TypePatternEngine } from './extractors/type-extractor.js'; import { FormatPatternEngine } from './extractors/format-extractor.js'; /** * Main natural language parser that coordinates all extraction engines */ export class NaturalLanguageParser { private readonly colorEngine = new ColorPatternEngine(); private readonly typeEngine = new TypePatternEngine(); private readonly archetypeEngine = new ArchetypePatternEngine(); private readonly priceEngine = new PricePatternEngine(); private readonly formatEngine = new FormatPatternEngine(); /** * Parse natural language query into structured concepts */ parse(naturalQuery: string, context?: QueryContext): ParsedQuery { // Preprocess text const cleanedText = this.preprocessText(naturalQuery); // Extract all concepts const colors = this.colorEngine.extract(cleanedText); const types = this.typeEngine.extract(cleanedText); const subtypes = this.typeEngine.extractSubtypes(cleanedText); const archetypes = this.archetypeEngine.extract(cleanedText); const priceConstraints = this.priceEngine.extract(cleanedText); const formats = this.formatEngine.extract(cleanedText); // Extract additional concepts (placeholder implementations) const keywords = this.extractKeywords(cleanedText); const abilities = this.extractAbilities(cleanedText); const mechanics = this.extractMechanics(cleanedText); const manaCost = this.extractManaCost(cleanedText); const powerToughness = this.extractPowerToughness(cleanedText); const legality = this.extractLegality(cleanedText); const deckRoles = this.extractDeckRoles(cleanedText); const strategies = this.extractStrategies(cleanedText); const sets = this.extractSets(cleanedText); const rarity = this.extractRarity(cleanedText); const timeConstraints = this.extractTimeConstraints(cleanedText); const namePatterns = this.extractNamePatterns(cleanedText); const flavorText = this.extractFlavorText(cleanedText); const artist = this.extractArtist(cleanedText); // Resolve conflicts and calculate confidence const resolved = this.resolveConflicts({ colors, types, subtypes, archetypes, priceConstraints, formats, keywords, abilities, mechanics, manaCost, powerToughness, legality, deckRoles, strategies, sets, rarity, timeConstraints, namePatterns, flavorText, artist }); return { ...resolved, confidence: this.calculateOverallConfidence(resolved), ambiguities: this.detectAmbiguities(resolved, cleanedText), context: context || {} }; } /** * Preprocess text for better pattern matching */ private preprocessText(text: string): string { return text .toLowerCase() .replace(/[^\w\s$.-]/g, ' ') // Remove special chars except $, ., - .replace(/\s+/g, ' ') // Normalize whitespace .trim(); } /** * Extract keyword abilities (placeholder implementation) */ private extractKeywords(text: string) { const keywords = ['flying', 'trample', 'haste', 'vigilance', 'lifelink', 'deathtouch', 'first strike', 'double strike']; return keywords .filter(keyword => text.includes(keyword)) .map(keyword => ({ keyword, confidence: 0.85 })); } /** * Extract complex abilities (placeholder implementation) */ private extractAbilities(text: string) { const abilities = ['enters the battlefield', 'when dies', 'tap to add', 'sacrifice to']; return abilities .filter(ability => text.includes(ability)) .map(ability => ({ ability, confidence: 0.80 })); } /** * Extract game mechanics (placeholder implementation) */ private extractMechanics(text: string) { const mechanics = ['storm', 'cascade', 'flashback', 'madness', 'cycling']; return mechanics .filter(mechanic => text.includes(mechanic)) .map(mechanic => ({ mechanic, confidence: 0.85 })); } /** * Extract mana cost constraints (placeholder implementation) */ private extractManaCost(text: string) { const patterns = [ /cmc\s*(\d+)/i, /mana cost\s*(\d+)/i, /costs?\s*(\d+)/i ]; const constraints = []; for (const pattern of patterns) { const match = text.match(pattern); if (match) { constraints.push({ exact: parseInt(match[1]), confidence: 0.80 }); } } return constraints; } /** * Extract power/toughness constraints (placeholder implementation) */ private extractPowerToughness(text: string) { const patterns = [ /power\s*(\d+)/i, /toughness\s*(\d+)/i, /(\d+)\/(\d+)/ ]; const constraints = []; for (const pattern of patterns) { const match = text.match(pattern); if (match) { if (pattern.source.includes('power')) { constraints.push({ stat: 'power' as const, exact: parseInt(match[1]), confidence: 0.85 }); } else if (pattern.source.includes('toughness')) { constraints.push({ stat: 'toughness' as const, exact: parseInt(match[1]), confidence: 0.85 }); } } } return constraints; } /** * Placeholder implementations for remaining extractors */ private extractLegality(_text: string): LegalityConcept[] { return []; } private extractDeckRoles(_text: string): RoleConcept[] { return []; } private extractStrategies(_text: string): StrategyConcept[] { return []; } private extractSets(_text: string): SetConcept[] { return []; } private extractRarity(_text: string): RarityConcept[] { return []; } private extractTimeConstraints(_text: string): TimeConcept[] { return []; } private extractNamePatterns(_text: string): NameConcept[] { return []; } private extractFlavorText(_text: string): FlavorConcept[] { return []; } private extractArtist(_text: string): ArtistConcept[] { return []; } /** * Resolve conflicts between extracted concepts */ private resolveConflicts(concepts: any): ParsedQuery { // For now, return concepts as-is // TODO: Implement conflict resolution logic return { colors: this.resolveColorConflicts(concepts.colors), types: concepts.types, subtypes: concepts.subtypes, keywords: concepts.keywords, abilities: concepts.abilities, mechanics: concepts.mechanics, manaCost: concepts.manaCost, powerToughness: concepts.powerToughness, priceConstraints: concepts.priceConstraints, formats: concepts.formats, legality: concepts.legality, archetypes: concepts.archetypes, deckRoles: concepts.deckRoles, strategies: concepts.strategies, sets: concepts.sets, rarity: concepts.rarity, timeConstraints: concepts.timeConstraints, namePatterns: concepts.namePatterns, flavorText: concepts.flavorText, artist: concepts.artist, confidence: 0, ambiguities: [], context: {} }; } /** * Resolve color conflicts (e.g., "red and blue" vs "only red") */ private resolveColorConflicts(colors: any[]) { if (colors.length <= 1) return colors; // If we have both exact and inclusive, prefer exact const hasExact = colors.some(c => c.exact); const hasInclusive = colors.some(c => c.inclusive); if (hasExact && hasInclusive) { return colors.filter(c => c.exact); } return colors; } /** * Calculate overall confidence score */ private calculateOverallConfidence(parsed: ParsedQuery): number { const conceptConfidences = [ ...parsed.colors.map(c => c.confidence), ...parsed.types.map(t => t.confidence), ...parsed.archetypes.map(a => a.confidence), ...parsed.priceConstraints.map(p => p.confidence), ...parsed.formats.map(f => f.confidence) ]; if (conceptConfidences.length === 0) return 0; // Weighted average with bias toward higher confidence concepts return conceptConfidences.reduce((sum, conf) => { const weight = Math.pow(conf, 1.5); // Higher confidence gets more weight return sum + (conf * weight); }, 0) / conceptConfidences.length; } /** * Detect ambiguities in the parsed query */ private detectAmbiguities(parsed: ParsedQuery, _originalText: string): Ambiguity[] { const ambiguities: Ambiguity[] = []; // Check for color ambiguities if (parsed.colors.length > 1) { const colorNames = parsed.colors.map(c => c.colors.join('')).join(', '); ambiguities.push({ type: 'color', description: `Multiple color interpretations found: ${colorNames}`, alternatives: parsed.colors.map(c => c.colors.join('')), confidence: 0.7 }); } // Check for format ambiguities if (parsed.formats.length > 1) { const formatNames = parsed.formats.map(f => f.name).join(', '); ambiguities.push({ type: 'format', description: `Multiple format interpretations found: ${formatNames}`, alternatives: parsed.formats.map(f => f.name), confidence: 0.7 }); } return ambiguities; } }

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