Skip to main content
Glama

ESPN MCP Server

core.tsβ€’17.5 kB
/** * ESPN MCP Server - Core Module with Lazy Loading * Complete implementation with intelligent caching and multiple transport options */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; // Lazy Loading Cache Implementation class LazyCache<T> { private cache = new Map<string, { data: T; timestamp: number; ttl: number }>(); private activeLoads = new Map<string, Promise<T>>(); async get<K>( key: string, loader: () => Promise<T>, ttl: number = 300000 // 5 minutes default ): Promise<T> { // Check if we have valid cached data const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < cached.ttl) { return cached.data; } // Check if we're already loading this data if (this.activeLoads.has(key)) { return this.activeLoads.get(key)!; } // Start loading the data const loadPromise = loader().then((data) => { // Cache the result this.cache.set(key, { data, timestamp: Date.now(), ttl }); // Remove from active loads this.activeLoads.delete(key); return data; }).catch((error) => { // Remove from active loads on error this.activeLoads.delete(key); throw error; }); this.activeLoads.set(key, loadPromise); return loadPromise; } clear(): void { this.cache.clear(); this.activeLoads.clear(); } size(): number { return this.cache.size; } getStats() { return { size: this.cache.size, activeLoads: this.activeLoads.size, keys: Array.from(this.cache.keys()) }; } } // ESPN Tool Definitions const ESPN_TOOLS = [ // NFL Tools { name: 'get_nfl_scores', description: 'Get NFL scores with lazy loading', inputSchema: { type: 'object', properties: { save: { type: 'boolean', description: 'Whether to save results to markdown' } }, }, }, { name: 'get_nfl_news', description: 'Get NFL news with lazy loading', inputSchema: { type: 'object', properties: { save: { type: 'boolean', description: 'Whether to save results to markdown' } }, }, }, { name: 'get_nfl_teams', description: 'Get all NFL teams with lazy loading', inputSchema: { type: 'object', properties: { save: { type: 'boolean', description: 'Whether to save results to markdown' } }, }, }, { name: 'get_nfl_standings', description: 'Get NFL standings with lazy loading', inputSchema: { type: 'object', properties: {} }, }, // NBA Tools { name: 'get_nba_scores', description: 'Get NBA scores with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nba_news', description: 'Get NBA news with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nba_teams', description: 'Get all NBA teams with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nba_standings', description: 'Get NBA standings with lazy loading', inputSchema: { type: 'object', properties: {} }, }, // MLB Tools { name: 'get_mlb_scores', description: 'Get MLB scores with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_mlb_news', description: 'Get MLB news with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_mlb_teams', description: 'Get all MLB teams with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_mlb_standings', description: 'Get MLB standings with lazy loading', inputSchema: { type: 'object', properties: {} }, }, // NHL Tools { name: 'get_nhl_scores', description: 'Get NHL scores with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nhl_news', description: 'Get NHL news with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nhl_teams', description: 'Get all NHL teams with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_nhl_standings', description: 'Get NHL standings with lazy loading', inputSchema: { type: 'object', properties: {} }, }, // College Football { name: 'get_college_football_scores', description: 'Get college football scores with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_college_football_news', description: 'Get college football news with lazy loading', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_college_football_rankings', description: 'Get college football rankings with lazy loading', inputSchema: { type: 'object', properties: {} }, }, // Cache Management Tools { name: 'get_cache_stats', description: 'Get current cache statistics and performance metrics', inputSchema: { type: 'object', properties: {} }, }, { name: 'clear_cache', description: 'Clear all cached data to force fresh API calls', inputSchema: { type: 'object', properties: {} }, }, ]; // Lazy ESPN Service class LazyESPNService { private cache = new LazyCache(); private baseURL = 'http://site.api.espn.com/apis/site/v2/sports'; // Cache TTL per sport type (milliseconds) private cacheTTL = { nfl: 180000, // 3 minutes - moderate pace nba: 60000, // 1 minute - fast-paced mlb: 120000, // 2 minutes - moderate pace nhl: 120000, // 2 minutes - moderate pace cfb: 300000, // 5 minutes - slower updates soccer: 240000, // 4 minutes - moderate pace default: 300000 // 5 minutes fallback }; private getTTL(toolName: string): number { // Extract sport from tool name const sport = this.extractSport(toolName); return this.cacheTTL[sport as keyof typeof this.cacheTTL] || this.cacheTTL.default; } private extractSport(toolName: string): string { if (toolName.includes('nfl')) return 'nfl'; if (toolName.includes('nba')) return 'nba'; if (toolName.includes('mlb')) return 'mlb'; if (toolName.includes('nhl')) return 'nhl'; if (toolName.includes('college_football') || toolName.includes('cfb')) return 'cfb'; if (toolName.includes('soccer')) return 'soccer'; return 'default'; } private async fetchData(url: string): Promise<any> { // Simple fetch implementation without axios dependency const fullUrl = `${this.baseURL}${url}`; try { const response = await fetch(fullUrl); if (!response.ok) { throw new Error(`ESPN API error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { throw new Error(`Failed to fetch ESPN data: ${error instanceof Error ? error.message : String(error)}`); } } async callTool(name: string, args: any): Promise<any> { // Handle cache management tools if (name === 'get_cache_stats') { return { stats: this.cache.getStats(), uptime: Math.floor(process.uptime()), memory: process.memoryUsage(), cacheTTL: this.cacheTTL }; } if (name === 'clear_cache') { this.cache.clear(); return { message: 'Cache cleared successfully' }; } const cacheKey = `${name}:${JSON.stringify(args)}`; const ttl = this.getTTL(name); return this.cache.get( cacheKey, () => this.fetchESPNData(name, args), ttl ); } private async fetchESPNData(name: string, args: any): Promise<any> { let url = ''; switch (name) { // NFL case 'get_nfl_scores': url = '/football/nfl/scoreboard'; break; case 'get_nfl_news': url = '/football/nfl/news'; break; case 'get_nfl_teams': url = '/football/nfl/teams'; break; case 'get_nfl_standings': url = '/football/nfl/standings'; break; // NBA case 'get_nba_scores': url = '/basketball/nba/scoreboard'; break; case 'get_nba_news': url = '/basketball/nba/news'; break; case 'get_nba_teams': url = '/basketball/nba/teams'; break; case 'get_nba_standings': url = '/basketball/nba/standings'; break; // MLB case 'get_mlb_scores': url = '/baseball/mlb/scoreboard'; break; case 'get_mlb_news': url = '/baseball/mlb/news'; break; case 'get_mlb_teams': url = '/baseball/mlb/teams'; break; case 'get_mlb_standings': url = '/baseball/mlb/standings'; break; // NHL case 'get_nhl_scores': url = '/hockey/nhl/scoreboard'; break; case 'get_nhl_news': url = '/hockey/nhl/news'; break; case 'get_nhl_teams': url = '/hockey/nhl/teams'; break; case 'get_nhl_standings': url = '/hockey/nhl/standings'; break; // College Football case 'get_college_football_scores': url = '/football/college-football/scoreboard'; break; case 'get_college_football_news': url = '/football/college-football/news'; break; case 'get_college_football_rankings': url = '/football/college-football/rankings'; break; default: throw new Error(`Unknown tool: ${name}`); } const data = await this.fetchData(url); // Format the response based on the data type return this.formatResponse(name, data); } private formatResponse(toolName: string, data: any): string { const date = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); let content = `# ESPN ${this.getLeagueFromTool(toolName)} ${this.getTypeFromTool(toolName)}\n\n`; content += `*Updated: ${date}*\n\n`; try { if (toolName.includes('scores') && data.events) { content += '## Scores\n\n'; data.events.slice(0, 10).forEach((event: any) => { const competition = event.competitions?.[0]; if (competition) { const homeTeam = competition.competitors?.find((c: any) => c.homeAway === 'home'); const awayTeam = competition.competitors?.find((c: any) => c.homeAway === 'away'); const status = event.status?.type?.description || 'Unknown'; content += `### ${awayTeam?.team?.name || 'Away'} vs ${homeTeam?.team?.name || 'Home'}\n`; content += `**Score:** ${awayTeam?.score || '0'} - ${homeTeam?.score || '0'}\n`; content += `**Status:** ${status}\n`; if (competition.venue?.fullName) { content += `**Venue:** ${competition.venue.fullName}\n`; } content += '\n'; } }); } else if (toolName.includes('news') && data.articles) { content += '## Latest News\n\n'; data.articles.slice(0, 10).forEach((article: any) => { content += `### ${article.headline || 'No Title'}\n`; content += `${article.description || 'No description available'}\n\n`; if (article.published) { const pubDate = new Date(article.published).toLocaleDateString(); content += `*Published: ${pubDate}*\n`; } if (article.links?.web?.href) { content += `[Read more](${article.links.web.href})\n`; } content += '\n---\n\n'; }); } else if (toolName.includes('teams') && data.sports?.[0]?.leagues?.[0]?.teams) { content += '## Teams\n\n'; data.sports[0].leagues[0].teams.slice(0, 32).forEach((teamData: any) => { const team = teamData.team; content += `### ${team.displayName || team.name}\n`; content += `**Location:** ${team.location || 'N/A'}\n`; content += `**Abbreviation:** ${team.abbreviation || 'N/A'}\n`; if (team.venue?.fullName) { content += `**Home Venue:** ${team.venue.fullName}\n`; } content += '\n'; }); } else if (toolName.includes('standings')) { content += '## Standings\n\n'; if (data.groups) { data.groups.forEach((group: any) => { content += `### ${group.name}\n\n`; content += '| Team | W | L | PCT |\n'; content += '|------|---|---|-----|\n'; group.standings?.entries?.forEach((entry: any) => { const team = entry.team; const wins = entry.stats?.find((s: any) => s.name === 'wins')?.value || 0; const losses = entry.stats?.find((s: any) => s.name === 'losses')?.value || 0; const pct = entry.stats?.find((s: any) => s.name === 'winPercent')?.value || '0.000'; content += `| ${team.name} | ${wins} | ${losses} | ${pct} |\n`; }); content += '\n'; }); } } else if (toolName.includes('rankings') && data.rankings?.[0]?.ranks) { content += '## Rankings\n\n'; data.rankings[0].ranks.slice(0, 25).forEach((rank: any) => { content += `${rank.current}. **${rank.team?.name || 'Unknown'}** (${rank.recordSummary || 'No record'})\n`; }); content += '\n'; } else { content += '## Raw Data\n\n'; content += '```json\n'; content += JSON.stringify(data, null, 2).slice(0, 2000); content += '\n```\n'; } } catch (error) { content += `## Error Processing Data\n\n`; content += `Error: ${error instanceof Error ? error.message : String(error)}\n\n`; content += '### Raw Response\n\n'; content += '```json\n'; content += JSON.stringify(data, null, 2).slice(0, 1000); content += '\n```\n'; } return content; } private getLeagueFromTool(toolName: string): string { if (toolName.includes('nfl')) return 'NFL'; if (toolName.includes('nba')) return 'NBA'; if (toolName.includes('mlb')) return 'MLB'; if (toolName.includes('nhl')) return 'NHL'; if (toolName.includes('college_football')) return 'College Football'; return 'Sports'; } private getTypeFromTool(toolName: string): string { if (toolName.includes('scores')) return 'Scores'; if (toolName.includes('news')) return 'News'; if (toolName.includes('teams')) return 'Teams'; if (toolName.includes('standings')) return 'Standings'; if (toolName.includes('rankings')) return 'Rankings'; return 'Data'; } getCacheStats() { return this.cache.getStats(); } clearCache() { this.cache.clear(); } } // Create the ESPN service instance const espnService = new LazyESPNService(); // Create and configure the server const server = new Server( { name: 'espn-mcp-lazy', version: '0.2.0', }, { capabilities: { tools: {}, }, } ); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: ESPN_TOOLS.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) }; }); // Call tool handler with lazy loading server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Use the lazy service to call the tool const result = await espnService.callTool(name, args || {}); return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Transport factory function export function createTransport(type: 'stdio' = 'stdio') { switch (type) { case 'stdio': return new StdioServerTransport(); default: throw new Error(`Unknown transport type: ${type}`); } } // Main function for STDIO server export async function main() { const transport = createTransport('stdio'); await server.connect(transport); console.error('ESPN MCP Lazy Server running on stdio'); console.error('Enhanced with intelligent caching and performance monitoring'); } // Export for testing and other modules export { server, espnService, ESPN_TOOLS }; // Start server if this is the main module (compatible with both ESM and CommonJS) let isMainModule = false; // Check if running as ESM module if (typeof import.meta !== 'undefined' && import.meta.url) { isMainModule = import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith('/core.js') || import.meta.url.endsWith('\\core.js'); } else { // Fallback for CommonJS - check if this is the main module isMainModule = require.main === module; } if (isMainModule) { main().catch((error) => { console.error('Server error:', error); process.exit(1); }); }

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