Skip to main content
Glama

Scryfall MCP Server

by bmurdock
validators.ts9.17 kB
import { z } from "zod"; import { SearchCardsParamsSchema, GetCardParamsSchema, GetCardPricesParamsSchema, RandomCardParamsSchema, SearchSetsParamsSchema, BuildQueryParamsSchema, ValidationError, } from "../types/mcp-types.js"; /** * Validates search cards parameters */ export function validateSearchCardsParams(params: unknown) { try { return SearchCardsParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Invalid parameter '${firstError.path.join(".")}': ${firstError.message}`, firstError.path.join(".") ); } throw error; } } /** * Validates get card parameters */ export function validateGetCardParams(params: unknown) { try { return GetCardParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Invalid parameter '${firstError.path.join(".")}': ${firstError.message}`, firstError.path.join(".") ); } throw error; } } /** * Validates get card prices parameters */ export function validateGetCardPricesParams(params: unknown) { try { return GetCardPricesParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Invalid parameter '${firstError.path.join(".")}': ${firstError.message}`, firstError.path.join(".") ); } throw error; } } /** * Validates random card parameters */ export function validateRandomCardParams(params: unknown) { try { return RandomCardParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Invalid parameter '${firstError.path.join(".")}': ${firstError.message}`, firstError.path.join(".") ); } throw error; } } /** * Validates search sets parameters */ export function validateSearchSetsParams(params: unknown) { try { return SearchSetsParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Invalid parameter '${firstError.path.join(".")}': ${firstError.message}`, firstError.path.join(".") ); } throw error; } } /** * Validates build query parameters */ export function validateBuildQueryParams(params: unknown) { try { return BuildQueryParamsSchema.parse(params); } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; const fieldPath = firstError.path.join("."); // Provide helpful error messages for common issues let message = `Invalid parameter '${fieldPath}': ${firstError.message}`; if (fieldPath === "natural_query" && firstError.code === "too_small") { message = "Natural query cannot be empty. Please provide a description of what you want to find."; } else if (fieldPath === "natural_query" && firstError.code === "too_big") { message = "Natural query is too long. Please keep it under 500 characters."; } else if (fieldPath === "format" && firstError.code === "invalid_enum_value") { message = `Invalid format. Valid formats are: standard, modern, legacy, vintage, commander, pioneer, brawl, pauper, penny, historic, alchemy.`; } else if (fieldPath === "optimize_for" && firstError.code === "invalid_enum_value") { message = `Invalid optimization strategy. Valid options are: precision, recall, discovery, budget.`; } else if (fieldPath === "max_results") { message = "Max results must be between 1 and 175."; } throw new ValidationError(message, fieldPath); } throw error; } } /** * Result interface for query validation */ export interface ValidationResult { isValid: boolean; errors: Array<{ message: string; position?: number }>; warnings: Array<{ message: string; position?: number }>; suggestions?: string[]; } /** * Enhanced Scryfall query validation with detailed feedback */ export function validateScryfallQuery(query: string): Promise<ValidationResult> { return Promise.resolve(validateScryfallQuerySync(query)); } /** * Synchronous Scryfall query validation with comprehensive checks */ export function validateScryfallQuerySync(query: string): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], suggestions: [] }; if (!query || query.trim().length === 0) { result.isValid = false; result.errors.push({ message: "Search query cannot be empty" }); return result; } const trimmedQuery = query.trim(); // Check for basic syntax errors const openParens = (trimmedQuery.match(/\(/g) || []).length; const closeParens = (trimmedQuery.match(/\)/g) || []).length; if (openParens !== closeParens) { result.isValid = false; result.errors.push({ message: "Unmatched parentheses in search query" }); } // Check for invalid operators at the start if (/^(AND|OR|NOT)\s/i.test(trimmedQuery)) { result.isValid = false; result.errors.push({ message: "Search query cannot start with a boolean operator" }); } // Check for consecutive operators if (/\b(AND|OR|NOT)\s+(AND|OR|NOT)\b/i.test(trimmedQuery)) { result.isValid = false; result.errors.push({ message: "Consecutive boolean operators are not allowed" }); } // Check for common issues and provide warnings if (/[<>]=?/.test(trimmedQuery) && !/\b(cmc|pow|tou|loy)\b/.test(trimmedQuery)) { result.warnings.push({ message: "Comparison operators should typically be used with numeric fields like cmc, pow, tou, or loy" }); } // Check for potentially misspelled operators if (/\bcolou?r\b/i.test(trimmedQuery)) { result.warnings.push({ message: "Use 'c:' or 'color:' instead of 'color' for color searches" }); result.suggestions?.push("Try 'c:red' instead of 'color red'"); } // Check for excessive complexity const operatorCount = (trimmedQuery.match(/\b(AND|OR|NOT)\b/gi) || []).length; if (operatorCount > 10) { result.warnings.push({ message: "Query is very complex and may be slow to execute" }); } // Add helpful suggestions for common patterns if (result.errors.length === 0 && trimmedQuery.split(/\s+/).length > 5 && !/[:()"']/.test(trimmedQuery)) { result.suggestions?.push("Consider using operators like 'c:', 't:', or 'o:' for more precise searches"); } return result; } // ValidationResult type is already exported above /** * Validates card identifier format */ export function validateCardIdentifier(identifier: string): void { if (!identifier || identifier.trim().length === 0) { throw new ValidationError("Card identifier cannot be empty"); } // Check if it's a UUID (Scryfall ID) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(identifier)) { return; // Valid UUID } // Check if it's a set code + collector number format (e.g., "dom/123") const setCollectorRegex = /^[a-z0-9]{3,4}\/\d+[a-z]?$/i; if (setCollectorRegex.test(identifier)) { return; // Valid set/collector format } // Otherwise, assume it's a card name - validate basic constraints if (identifier.length > 200) { throw new ValidationError("Card name is too long (max 200 characters)"); } // Check for potentially problematic characters if (/[<>{}[\]\\]/.test(identifier)) { throw new ValidationError("Card name contains invalid characters"); } } /** * Validates set code format */ export function validateSetCode(setCode: string): void { if (!/^[a-z0-9]{3,4}$/i.test(setCode)) { throw new ValidationError("Set code must be 3-4 alphanumeric characters"); } } /** * Validates language code format */ export function validateLanguageCode(langCode: string): void { const validLanguages = [ "en", "es", "fr", "de", "it", "pt", "ja", "ko", "ru", "zhs", "zht", "he", "la", "grc", "ar", "sa", "ph", ]; if (!validLanguages.includes(langCode.toLowerCase())) { throw new ValidationError( `Invalid language code. Supported languages: ${validLanguages.join(", ")}` ); } } /** * Validates date string format (ISO 8601) */ export function validateDateString(dateStr: string): void { const date = new Date(dateStr); if (isNaN(date.getTime())) { throw new ValidationError("Invalid date format. Use ISO 8601 format (YYYY-MM-DD)"); } // Check if the date is reasonable (not too far in the past or future) const now = new Date(); const minDate = new Date("1993-01-01"); // Magic's first release const maxDate = new Date(now.getFullYear() + 5, 11, 31); // 5 years in the future if (date < minDate || date > maxDate) { throw new ValidationError("Date must be between 1993 and 5 years from now"); } }

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