Skip to main content
Glama

Scryfall MCP Server

by bmurdock
formatters.ts8.48 kB
import { ScryfallCard, ScryfallSet, ScryfallSearchResponse, Legalities } from '../types/scryfall-api.js'; import { FormattedCard, FormattedSearchResult, FormattedSet } from '../types/mcp-types.js'; /** * Formats a Scryfall card for MCP response */ export function formatCard(card: ScryfallCard, includeImage = true): FormattedCard { // Handle multi-faced cards const mainFace = card.card_faces?.[0] || card; const name = card.card_faces ? `${card.card_faces[0].name} // ${card.card_faces[1].name}` : card.name; return { name, mana_cost: mainFace.mana_cost || card.mana_cost, type_line: mainFace.type_line || card.type_line, oracle_text: formatOracleText(card), power: mainFace.power || card.power, toughness: mainFace.toughness || card.toughness, set_name: card.set_name, rarity: card.rarity, prices: { usd: card.prices.usd, eur: card.prices.eur, tix: card.prices.tix }, legalities: formatLegalities(card.legalities), image_url: includeImage ? getCardImageUrl(card) : undefined, color_identity: card.color_identity, games: card.games }; } /** * Formats oracle text for multi-faced cards */ function formatOracleText(card: ScryfallCard): string | undefined { if (card.card_faces && card.card_faces.length > 0) { return card.card_faces .map((face, index) => { const faceLabel = index === 0 ? 'Front' : 'Back'; return `${faceLabel}: ${face.oracle_text || '(No text)'}`; }) .join('\n\n'); } return card.oracle_text; } /** * Gets the appropriate image URL for a card */ function getCardImageUrl(card: ScryfallCard): string | undefined { // Prefer card-level image, fall back to first face const imageUris = card.image_uris || card.card_faces?.[0]?.image_uris; return imageUris?.normal; } /** * Formats legalities object to a more readable format */ function formatLegalities(legalities: Legalities): Record<string, string> { const formatted: Record<string, string> = {}; // Only include formats where the card is legal or restricted Object.entries(legalities).forEach(([format, legality]) => { if (legality === 'legal' || legality === 'restricted') { formatted[format] = legality; } }); return formatted; } /** * Formats search results for text display */ export function formatSearchResultsAsText( searchResponse: ScryfallSearchResponse, currentPage = 1, pageSize = 20 ): string { const { total_cards, has_more, data: cards, warnings } = searchResponse; const displayed = Array.isArray(cards) ? cards.slice(0, Math.max(0, pageSize)) : []; let result = `Found ${total_cards ?? cards.length} card${(total_cards ?? cards.length) !== 1 ? 's' : ''}`; if (has_more && total_cards) { const totalPages = Math.ceil(total_cards / pageSize); result += ` (Page ${currentPage} of ${totalPages})`; } result += ':\n\n'; // Add warnings if present if (warnings && warnings.length > 0) { result += '⚠️ **Warnings:**\n'; warnings.forEach(warning => { result += `- ${warning}\n`; }); result += '\n'; } displayed.forEach((card, index) => { const cardNumber = (currentPage - 1) * pageSize + index + 1; result += formatCardAsText(card, cardNumber); result += '\n'; }); if (has_more || (total_cards && total_cards > currentPage * pageSize)) { const remainingCount = total_cards ? Math.max(0, total_cards - currentPage * pageSize) : 'many'; result += `\n... and ${remainingCount} more cards. Use page parameter to see more results.`; } return result; } /** * Formats a single card for text display */ function formatCardAsText(card: ScryfallCard, index?: number): string { const formatted = formatCard(card, false); let result = ''; if (index) { result += `${index}. `; } result += `**${formatted.name}**`; if (formatted.mana_cost) { result += ` ${formatted.mana_cost}`; } result += `\n ${formatted.type_line}`; if (formatted.power && formatted.toughness) { result += ` (${formatted.power}/${formatted.toughness})`; } result += `\n *${formatted.set_name}* - ${formatted.rarity}`; if (formatted.prices.usd) { result += ` - $${formatted.prices.usd}`; } if (formatted.oracle_text) { // Truncate long oracle text for list view const truncatedText = formatted.oracle_text.length > 100 ? formatted.oracle_text.substring(0, 100) + '...' : formatted.oracle_text; result += `\n ${truncatedText}`; } return result; } /** * Formats search results for JSON display */ export function formatSearchResultsAsJson( searchResponse: ScryfallSearchResponse, currentPage = 1, pageSize = 20 ): FormattedSearchResult { const { total_cards, has_more, data: cards, warnings } = searchResponse; const displayed = Array.isArray(cards) ? cards.slice(0, Math.max(0, pageSize)) : []; const totalPages = total_cards ? Math.ceil(total_cards / pageSize) : undefined; return { total_cards: total_cards ?? cards.length, has_more, cards: displayed.map(card => formatCard(card)), warnings, page_info: { current_page: currentPage, total_pages: totalPages, next_page: has_more ? `page=${currentPage + 1}` : undefined } }; } /** * Formats a single card for detailed display */ export function formatCardDetails(card: ScryfallCard, includeImage = true): string { const formatted = formatCard(card, includeImage); let result = `# ${formatted.name}\n\n`; if (formatted.mana_cost) { result += `**Mana Cost:** ${formatted.mana_cost}\n`; } result += `**Type:** ${formatted.type_line}\n`; if (formatted.power && formatted.toughness) { result += `**Power/Toughness:** ${formatted.power}/${formatted.toughness}\n`; } if (formatted.oracle_text) { result += `\n**Oracle Text:**\n${formatted.oracle_text}\n`; } result += `\n**Set:** ${formatted.set_name}\n`; result += `**Rarity:** ${formatted.rarity}\n`; // Prices const prices = Object.entries(formatted.prices) .filter(([, price]) => price) .map(([currency, price]) => `${currency.toUpperCase()}: $${price}`) .join(', '); if (prices) { result += `**Prices:** ${prices}\n`; } // Legalities const legalFormats = Object.entries(formatted.legalities) .map(([format, legality]) => `${format}: ${legality}`) .join(', '); if (legalFormats) { result += `**Legal in:** ${legalFormats}\n`; } if (formatted.image_url) { result += `\n**Image:** ${formatted.image_url}\n`; } return result; } /** * Formats a Scryfall set for MCP response */ export function formatSet(set: ScryfallSet): FormattedSet { return { name: set.name, code: set.code, type: set.set_type, released_at: set.released_at, card_count: set.card_count, digital: set.digital, icon_url: set.icon_svg_uri }; } /** * Formats sets for text display */ export function formatSetsAsText(sets: ScryfallSet[]): string { if (sets.length === 0) { return 'No sets found.'; } let result = `Found ${sets.length} set${sets.length !== 1 ? 's' : ''}:\n\n`; sets.forEach((set, index) => { result += `${index + 1}. **${set.name}** (${set.code.toUpperCase()})\n`; result += ` Type: ${set.set_type}`; if (set.released_at) { result += ` | Released: ${set.released_at}`; } result += ` | Cards: ${set.card_count}`; if (set.digital) { result += ' | Digital'; } result += '\n\n'; }); return result; } /** * Formats card prices for display */ export function formatCardPrices(card: ScryfallCard, currency = 'usd'): string { let result = `# Prices for ${card.name}\n\n`; const prices = card.prices; if (currency === 'usd') { if (prices.usd) result += `**Paper:** $${prices.usd}\n`; if (prices.usd_foil) result += `**Foil:** $${prices.usd_foil}\n`; if (prices.usd_etched) result += `**Etched:** $${prices.usd_etched}\n`; } else if (currency === 'eur') { if (prices.eur) result += `**Paper:** €${prices.eur}\n`; if (prices.eur_foil) result += `**Foil:** €${prices.eur_foil}\n`; } else if (currency === 'tix') { if (prices.tix) result += `**MTGO:** ${prices.tix} tix\n`; } if (result === `# Prices for ${card.name}\n\n`) { result += 'No price data available for this currency.'; } return result; }

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