Skip to main content
Glama

Scryfall MCP Server

by bmurdock
set-database.ts8.06 kB
import { ScryfallClient } from '../services/scryfall-client.js'; import { CacheService } from '../services/cache-service.js'; import { ScryfallSet } from '../types/scryfall-api.js'; import { ScryfallAPIError } from '../types/mcp-types.js'; import { mcpLogger } from '../services/logger.js'; import { REQUIRED_HEADERS } from '../types/mcp-types.js'; import { EnvValidators } from '../utils/env-parser.js'; /** * MCP Resource for accessing set database */ export class SetDatabaseResource { readonly uri = 'set-database://all'; readonly name = 'Set Database'; readonly description = 'Complete Magic: The Gathering sets database with metadata and icons'; readonly mimeType = 'application/json'; private lastUpdateCheck = 0; private readonly updateCheckInterval = 7 * 24 * 60 * 60 * 1000; // 1 week constructor( private readonly scryfallClient: ScryfallClient, private readonly cache: CacheService ) {} /** * Gets the set database, checking for updates if needed */ async getData(): Promise<string> { try { const now = Date.now(); // Check if we need to update if (now - this.lastUpdateCheck > this.updateCheckInterval) { await this.checkForUpdates(); this.lastUpdateCheck = now; } // Try to get from cache first const cacheKey = CacheService.createSetKey(); const cached = this.cache.getWithStats<ScryfallSet[]>(cacheKey); if (cached) { return JSON.stringify({ object: 'list', type: 'sets', updated_at: new Date().toISOString(), total_sets: cached.length, data: cached, source: 'cache' }, null, 2); } // If not in cache, download fresh data const sets = await this.downloadSetData(); // Cache the result this.cache.setWithType(cacheKey, sets, 'set_data'); return JSON.stringify({ object: 'list', type: 'sets', updated_at: new Date().toISOString(), total_sets: sets.length, data: sets, source: 'fresh' }, null, 2); } catch (error) { throw new ScryfallAPIError( `Failed to retrieve set database: ${error instanceof Error ? error.message : 'Unknown error'}`, 500, 'resource_error' ); } } /** * Checks for updates to set data */ private async checkForUpdates(): Promise<void> { try { // Sets don't have a bulk data endpoint, so we check periodically const cacheKey = CacheService.createSetKey(); const lastUpdateKey = CacheService.createSetKey('last_update'); const lastUpdate = this.cache.get<string>(lastUpdateKey); const weekAgo = new Date(Date.now() - this.updateCheckInterval).toISOString(); if (!lastUpdate || lastUpdate < weekAgo) { // Clear old cache and mark for refresh this.cache.delete(cacheKey); this.cache.set(lastUpdateKey, new Date().toISOString(), this.updateCheckInterval); } } catch (error) { // Log error but don't fail - we can still serve cached data mcpLogger.warn({ operation: 'set_update_check', error }, 'Failed to check for set data updates'); } } /** * Downloads fresh set data */ private async downloadSetData(): Promise<ScryfallSet[]> { const sets = await this.scryfallClient.getSets(); // Enhance sets with additional metadata and convert icons to base64 return Promise.all(sets.map(set => this.enhanceSetData(set))); } /** * Enhances set data with additional metadata */ private async enhanceSetData(set: ScryfallSet): Promise<ScryfallSet> { try { // Download and convert icon to base64 if available if (set.icon_svg_uri) { const iconBase64 = await this.downloadIconAsBase64(set.icon_svg_uri); return { ...set, icon_base64: iconBase64 } as ScryfallSet & { icon_base64?: string }; } return set; } catch (error) { // If icon download fails, just return the set without it mcpLogger.warn({ operation: 'set_icon_download', setCode: set.code, error }, 'Failed to download set icon'); return set; } } /** * Downloads an SVG icon and converts it to base64 */ private async downloadIconAsBase64(iconUri: string): Promise<string> { try { const timeoutMs = EnvValidators.scryfallTimeoutMs(process.env.SCRYFALL_TIMEOUT_MS); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); const response = await fetch(iconUri, { headers: { 'User-Agent': REQUIRED_HEADERS['User-Agent'], 'Accept': 'image/svg+xml', }, signal: controller.signal, }).finally(() => clearTimeout(timeout)); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const svgText = await response.text(); const base64 = Buffer.from(svgText).toString('base64'); return `data:image/svg+xml;base64,${base64}`; } catch (error) { throw new Error(`Failed to download icon: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Gets sets filtered by criteria */ async getFilteredSets(filters: { type?: string; released_after?: string; released_before?: string; digital?: boolean; }): Promise<string> { const allSetsData = await this.getData(); const allSets = JSON.parse(allSetsData); let filteredSets = allSets.data as ScryfallSet[]; // Apply filters if (filters.type) { filteredSets = filteredSets.filter(set => set.set_type === filters.type); } if (filters.digital !== undefined) { filteredSets = filteredSets.filter(set => set.digital === filters.digital); } if (filters.released_after) { const afterDate = new Date(filters.released_after); filteredSets = filteredSets.filter(set => set.released_at && new Date(set.released_at) >= afterDate ); } if (filters.released_before) { const beforeDate = new Date(filters.released_before); filteredSets = filteredSets.filter(set => set.released_at && new Date(set.released_at) <= beforeDate ); } return JSON.stringify({ ...allSets, total_sets: filteredSets.length, data: filteredSets, filters_applied: filters }, null, 2); } /** * Gets resource metadata */ getMetadata() { const stats = this.cache.getStats(); const cacheKey = CacheService.createSetKey(); const ttl = this.cache.getTTL(cacheKey); return { uri: this.uri, name: this.name, description: this.description, mimeType: this.mimeType, cache_stats: stats, cache_ttl_remaining: ttl, last_update_check: new Date(this.lastUpdateCheck).toISOString(), next_update_check: new Date(this.lastUpdateCheck + this.updateCheckInterval).toISOString() }; } /** * Forces a refresh of the set data */ async forceRefresh(): Promise<void> { const cacheKey = CacheService.createSetKey(); this.cache.delete(cacheKey); this.lastUpdateCheck = 0; await this.getData(); // This will trigger a fresh download } /** * Gets cache statistics */ getCacheStats() { return this.cache.getStats(); } /** * Gets available set types */ async getSetTypes(): Promise<string[]> { const allSetsData = await this.getData(); const allSets = JSON.parse(allSetsData); const types = new Set<string>(); (allSets.data as ScryfallSet[]).forEach(set => { types.add(set.set_type); }); return Array.from(types).sort(); } /** * Gets sets by release year */ async getSetsByYear(year: number): Promise<string> { const startDate = `${year}-01-01`; const endDate = `${year}-12-31`; return this.getFilteredSets({ released_after: startDate, released_before: endDate }); } }

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