Skip to main content
Glama

ESPN MCP Server

lazy-server.jsβ€’10.7 kB
#!/usr/bin/env node import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import axios from 'axios'; import { formatScore, formatNews, formatTeam, formatStandings, createMarkdownTitle } from './formatters.js'; import { exportToMarkdown } from './markdown.js'; const BASE_URL = 'http://site.api.espn.com/apis/site/v2/sports'; class LazyCache { constructor(defaultTTL = 300000) { this.cache = new Map(); this.defaultTTL = defaultTTL; } get(key) { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiry) { this.cache.delete(key); return null; } return entry.data; } set(key, data, ttl = this.defaultTTL) { this.cache.set(key, { data, timestamp: Date.now(), expiry: Date.now() + ttl }); } clear() { this.cache.clear(); } size() { return this.cache.size; } } class LazyAPIService { constructor() { this.loadingPromises = new Map(); this.axiosInstance = axios.create({ baseURL: BASE_URL, timeout: 10000, headers: { 'User-Agent': 'ESPN-MCP-Server/1.0.0' } }); this.cache = new LazyCache(); } async lazyFetch(endpoint, ttl) { const cacheKey = endpoint; // Check cache first const cached = this.cache.get(cacheKey); if (cached) { return cached; } // Check if already loading if (this.loadingPromises.has(cacheKey)) { return await this.loadingPromises.get(cacheKey); } // Start loading const loadingPromise = this.axiosInstance.get(endpoint) .then((response) => { this.cache.set(cacheKey, response.data, ttl); return response.data; }) .finally(() => { this.loadingPromises.delete(cacheKey); }); this.loadingPromises.set(cacheKey, loadingPromise); return await loadingPromise; } getCacheStats() { return { size: this.cache.size(), activeLoads: this.loadingPromises.size }; } clearCache() { this.cache.clear(); this.loadingPromises.clear(); } } class ESPNLazyServer { constructor() { this.server = new McpServer({ name: 'espn-lazy-adventures', version: '2.0.0', }); this.apiService = new LazyAPIService(); this.setupLazyHandlers(); this.setupErrorHandlers(); } setupLazyHandlers() { // Sports configuration with lazy loading const sportsConfig = { nfl: { path: 'football/nfl', cache: 180000 }, // 3 minutes mlb: { path: 'baseball/mlb', cache: 120000 }, // 2 minutes nba: { path: 'basketball/nba', cache: 60000 }, // 1 minute nhl: { path: 'hockey/nhl', cache: 120000 }, cfb: { path: 'football/college-football', cache: 300000 }, // 5 minutes wnba: { path: 'basketball/wnba', cache: 180000 }, soccer: { path: 'soccer', cache: 240000 } }; // Register lazy-loaded sports resources Object.entries(sportsConfig).forEach(([sport, config]) => { this.server.registerResource(`${sport}-data`, new ResourceTemplate(`espn://${sport}/{type}`, { list: undefined, complete: { type: (value) => { return ['scores', 'news', 'teams', 'standings'].filter(t => t.startsWith(value)); } } }), { title: `${sport.toUpperCase()} Data`, description: `Lazy-loaded ${sport.toUpperCase()} sports data`, mimeType: 'application/json' }, async (uri, { type }) => { const endpoint = `/${config.path}/${type}`; const data = await this.apiService.lazyFetch(endpoint, config.cache); return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: 'application/json' }] }; }); }); // Register universal sports tool with lazy loading this.server.registerTool('get-sports-data', { title: 'Get Sports Data', description: 'Lazy-loaded sports data with intelligent caching', inputSchema: { sport: z.enum(['nfl', 'mlb', 'nba', 'nhl', 'cfb', 'wnba']), type: z.enum(['scores', 'news', 'teams', 'standings']), team: z.string().optional(), format: z.enum(['json', 'markdown']).default('markdown'), save: z.boolean().default(false) } }, async ({ sport, type, team, format, save }) => { try { const config = sportsConfig[sport]; let endpoint = `/${config.path}/${type}`; if (team && type === 'teams') { endpoint = `/${config.path}/teams/${team}`; } const data = await this.apiService.lazyFetch(endpoint, config.cache); if (format === 'json') { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; } // Format as markdown let content = createMarkdownTitle(type, sport.toUpperCase()); if (type === 'scores' && data.events) { content += data.events.map((event) => formatScore(event)).join('\n\n'); } else if (type === 'news' && data.articles) { content += data.articles.map((article) => formatNews(article)).join('\n\n'); } else if (type === 'teams' && data.sports?.[0]?.leagues?.[0]?.teams) { content += data.sports[0].leagues[0].teams .map((team) => formatTeam(team.team)) .join('\n\n'); } else if (type === 'standings' && data.standings) { data.standings.forEach((div) => { content += formatStandings(div); }); } // Add cache stats const stats = this.apiService.getCacheStats(); content += `\n\n---\n*Cache: ${stats.size} entries, ${stats.activeLoads} loading*`; if (save) { const filePath = exportToMarkdown(content, type, sport.toUpperCase()); content += `\n\nSaved to: ${filePath}`; } return { content: [{ type: 'text', text: content }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } }); // Cache management tool this.server.registerTool('cache-stats', { title: 'Cache Statistics', description: 'View cache performance and statistics', inputSchema: { action: z.enum(['stats', 'clear']).default('stats') } }, async ({ action }) => { if (action === 'clear') { this.apiService.clearCache(); return { content: [{ type: 'text', text: 'Cache cleared successfully!' }] }; } const stats = this.apiService.getCacheStats(); return { content: [{ type: 'text', text: `## Cache Statistics\n\n- **Cached entries:** ${stats.size}\n- **Active loads:** ${stats.activeLoads}\n- **Server uptime:** ${Math.floor(process.uptime())} seconds` }] }; }); // Health check resource this.server.registerResource('health', 'espn://health', { title: 'Server Health', description: 'Server health and performance metrics', mimeType: 'application/json' }, async () => { const stats = this.apiService.getCacheStats(); const health = { status: 'healthy', uptime: process.uptime(), memory: process.memoryUsage(), cache: stats, timestamp: new Date().toISOString() }; return { contents: [{ uri: 'espn://health', text: JSON.stringify(health, null, 2), mimeType: 'application/json' }] }; }); } setupErrorHandlers() { this.server.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { console.log('\nShutting down gracefully...'); this.apiService.clearCache(); await this.server.close(); process.exit(0); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); } getServer() { return this.server; } getCacheStats() { return this.apiService.getCacheStats(); } clearCache() { this.apiService.clearCache(); } async run() { try { this.transport = new StdioServerTransport(); await this.server.connect(this.transport); console.error('ESPN Lazy MCP server running on stdio with intelligent caching'); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } } const server = new ESPNLazyServer(); server.run().catch(console.error); export { ESPNLazyServer };

Latest Blog Posts

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/DynamicEndpoints/espn-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server