@kaminaduck/scryfall-mcp-server
Version:
A Model Context Protocol (MCP) server that provides access to the Scryfall API for Magic: The Gathering card data
140 lines • 5.16 kB
JavaScript
/**
* Scryfall API client for Magic: The Gathering card data.
*/
import fetch from 'node-fetch';
import { logger } from './logger.js';
export class ScryfallClient {
baseUrl = 'https://api.scryfall.com';
rateLimitDelay = 200; // 200ms between requests
/**
* Add a delay between requests to respect rate limits.
*/
async delay() {
return new Promise(resolve => setTimeout(resolve, this.rateLimitDelay));
}
/**
* Make a request to the Scryfall API with retry logic.
*/
async makeRequest(url, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Not found: ${url}`);
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Add delay after successful request
if (attempt < retries) {
await this.delay();
}
return data;
}
catch (error) {
if (attempt === retries) {
throw error;
}
logger.warn(`Request failed (attempt ${attempt}/${retries}):`, error);
await this.delay();
}
}
throw new Error('All retry attempts failed');
}
/**
* Search for cards using Scryfall's search API.
*/
async searchCards(query) {
const encodedQuery = encodeURIComponent(query);
const searchUrl = `${this.baseUrl}/cards/search?q=${encodedQuery}`;
logger.info(`Searching Scryfall for: ${query}`);
const allCards = [];
let currentUrl = searchUrl;
try {
while (currentUrl) {
const data = await this.makeRequest(currentUrl);
if (data.object === 'error') {
throw new Error(data.details || 'Unknown API error');
}
// Add cards from current page
allCards.push(...data.data);
// Check if there are more pages
if (data.has_more && data.next_page) {
currentUrl = data.next_page;
logger.debug('Fetching additional results...');
}
else {
currentUrl = undefined;
}
}
logger.info(`Found ${allCards.length} cards`);
return allCards;
}
catch (error) {
if (error instanceof Error && error.message.includes('Not found')) {
logger.info(`No cards found for query: ${query}`);
return [];
}
logger.error('Error searching for cards:', error);
throw error;
}
}
/**
* Get a card by its Scryfall ID.
*/
async getCardById(cardId) {
const url = `${this.baseUrl}/cards/${cardId}`;
logger.debug(`Getting card by ID: ${cardId}`);
return await this.makeRequest(url);
}
/**
* Get a card by its name (exact match).
*/
async getCardByName(cardName) {
const encodedName = encodeURIComponent(cardName);
const url = `${this.baseUrl}/cards/named?exact=${encodedName}`;
logger.debug(`Getting card by name: ${cardName}`);
return await this.makeRequest(url);
}
/**
* Get a random card.
*/
async getRandomCard() {
const url = `${this.baseUrl}/cards/random`;
logger.debug('Getting random card');
return await this.makeRequest(url);
}
/**
* Get a card by set and collector number.
*/
async getCardBySetAndNumber(setCode, collectorNumber) {
const url = `${this.baseUrl}/cards/${setCode}/${collectorNumber}`;
logger.debug(`Getting card by set/number: ${setCode}/${collectorNumber}`);
return await this.makeRequest(url);
}
/**
* Group cards by name and identify alternate artworks.
*/
groupCardsByNameAndArt(cards) {
const cardGroups = {};
for (const card of cards) {
const name = card.name || 'Unknown';
if (!cardGroups[name]) {
cardGroups[name] = [];
}
// Add a display name to help identify alternate arts
const setName = card.set_name || 'Unknown Set';
const setCode = (card.set || '???').toUpperCase();
const collectorNumber = card.collector_number || '??';
const artist = card.artist || 'Unknown Artist';
// Add a display_name field to help with showing alternate arts
card.display_name = `${name} [${setCode} - ${setName}, #${collectorNumber}, Art: ${artist}]`;
cardGroups[name].push(card);
}
return cardGroups;
}
}
// Export a singleton instance
export const scryfallClient = new ScryfallClient();
//# sourceMappingURL=scryfallClient.js.map