Skip to main content
Glama

Scryfall MCP Server

by bmurdock
query-rules.ts7.83 kB
import { readFileSync } from 'fs'; import { join } from 'path'; import { ValidationError } from '../types/mcp-types.js'; import { mcpLogger } from '../services/logger.js'; /** * MCP Tool for searching Magic: The Gathering comprehensive rules */ export class QueryRulesTool { readonly name = 'query_rules'; readonly description = 'Search Magic: The Gathering comprehensive rules for specific interactions and rule clarifications'; readonly inputSchema = { type: 'object' as const, properties: { query: { type: 'string', description: 'Search term or rules question' }, section: { type: 'string', description: 'Specific rule section (e.g., "7" for Additional Rules)', pattern: '^[1-9]\\d*$' }, context_lines: { type: 'number', default: 3, minimum: 1, maximum: 10, description: 'Lines of context around matches' }, exact_match: { type: 'boolean', default: false, description: 'Require exact phrase matching' } }, required: ['query'] }; private rulesContent: string | null = null; private rulesLines: string[] = []; constructor() { this.loadRulesFile(); } /** * Load the MTG rules file into memory */ private loadRulesFile(): void { try { // Try to load from the project root const rulesPath = join(process.cwd(), 'mtgrules.txt'); this.rulesContent = readFileSync(rulesPath, 'utf-8'); this.rulesLines = this.rulesContent.split('\n'); } catch (error) { mcpLogger.warn({ operation: 'rules_load', error }, 'Could not load MTG rules file'); this.rulesContent = null; this.rulesLines = []; } } /** * Validate parameters using Zod-like validation */ private validateParams(args: unknown): { query: string; section?: string; context_lines: number; exact_match: boolean; } { if (!args || typeof args !== 'object') { throw new ValidationError('Invalid parameters'); } const params = args as any; if (!params.query || typeof params.query !== 'string' || params.query.trim().length === 0) { throw new ValidationError('Query is required and must be a non-empty string'); } if (params.section && (typeof params.section !== 'string' || !/^[1-9]\d*$/.test(params.section))) { throw new ValidationError('Section must be a positive number as string'); } const contextLines = params.context_lines ?? 3; if (typeof contextLines !== 'number' || contextLines < 1 || contextLines > 10) { throw new ValidationError('Context lines must be a number between 1 and 10'); } const exactMatch = params.exact_match ?? false; if (typeof exactMatch !== 'boolean') { throw new ValidationError('Exact match must be a boolean'); } return { query: params.query.trim(), section: params.section, context_lines: contextLines, exact_match: exactMatch }; } /** * Search for rules matching the query */ private searchRules(params: { query: string; section?: string; context_lines: number; exact_match: boolean; }): Array<{ lineNumber: number; line: string; context: string[]; section: string; }> { if (!this.rulesContent || this.rulesLines.length === 0) { return []; } const results: Array<{ lineNumber: number; line: string; context: string[]; section: string; }> = []; // Create search pattern let searchPattern: RegExp; if (params.exact_match) { // Exact phrase match, case insensitive searchPattern = new RegExp(params.query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); } else { // Flexible search - split query into words and match all const words = params.query.toLowerCase().split(/\s+/).filter(word => word.length > 0); const pattern = words.map(word => `(?=.*\\b${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`).join(''); searchPattern = new RegExp(pattern, 'i'); } // Track current section for context let currentSection = ''; for (let i = 0; i < this.rulesLines.length; i++) { const line = this.rulesLines[i]; // Update current section if we hit a section header const sectionMatch = line.match(/^(\d+)\.\s/); if (sectionMatch) { currentSection = sectionMatch[1]; } // Skip if section filter is specified and doesn't match if (params.section && currentSection !== params.section) { continue; } // Check if line matches search pattern if (searchPattern.test(line)) { // Get context lines const contextStart = Math.max(0, i - params.context_lines); const contextEnd = Math.min(this.rulesLines.length - 1, i + params.context_lines); const context: string[] = []; for (let j = contextStart; j <= contextEnd; j++) { const contextLine = this.rulesLines[j]; if (j === i) { // Mark the matching line context.push(`>>> ${contextLine}`); } else { context.push(` ${contextLine}`); } } results.push({ lineNumber: i + 1, line: line, context: context, section: currentSection || 'Unknown' }); } } return results; } async execute(args: unknown) { try { // Check if rules file is available if (!this.rulesContent) { return { content: [ { type: 'text', text: 'MTG rules file is not available. Please ensure mtgrules.txt is present in the project root.' } ], isError: true }; } // Validate parameters const params = this.validateParams(args); // Search for matching rules const results = this.searchRules(params); // Handle no results if (results.length === 0) { let message = `No rules found matching "${params.query}"`; if (params.section) { message += ` in section ${params.section}`; } message += '. Try adjusting your search terms or removing section filters.'; return { content: [ { type: 'text', text: message } ] }; } // Format results let responseText = `Found ${results.length} rule${results.length === 1 ? '' : 's'} matching "${params.query}"`; if (params.section) { responseText += ` in section ${params.section}`; } responseText += ':\n\n'; for (const result of results.slice(0, 10)) { // Limit to 10 results responseText += `**Section ${result.section}, Line ${result.lineNumber}:**\n`; responseText += result.context.join('\n') + '\n\n'; } if (results.length > 10) { responseText += `... and ${results.length - 10} more results. Refine your search for more specific results.\n`; } 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 }; } } }

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