Skip to main content
Glama

ESPN MCP Server

modern-server.ts64.7 kB
/** * Modern ESPN MCP Server with Latest 2025 Features * Supports: Resources, Prompts, Tools, HTTP Streaming, Session Management */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListToolsRequestSchema, Tool, Resource, Prompt, PromptMessage, TextContent, ResourceContents } from "@modelcontextprotocol/sdk/types.js"; import express from "express"; import cors from "cors"; import { EventEmitter } from "events"; import { z } from "zod"; // Configuration schema for HTTP deployment export const configSchema = z.object({ cacheTimeout: z.number().optional().default(300000).describe("Cache timeout in milliseconds"), maxConcurrentRequests: z.number().optional().default(5).describe("Maximum number of concurrent ESPN API requests"), enableStreaming: z.boolean().optional().default(true).describe("Enable Server-Side Events for real-time updates"), debug: z.boolean().optional().default(false).describe("Enable debug logging"), }); // Parse configuration from query parameters (for HTTP deployment) function parseConfig(req: express.Request) { const configParam = req.query.config as string; if (configParam) { try { return JSON.parse(Buffer.from(configParam, 'base64').toString()); } catch (e) { console.warn('Failed to parse config parameter:', e); return {}; } } return {}; } // Modern cache implementation class ModernCache extends EventEmitter { private cache = new Map<string, { data: any; timestamp: number; ttl: number }>(); private cleanupInterval: NodeJS.Timeout; constructor(private defaultTtl = 300000) { // 5 minutes super(); this.cleanupInterval = setInterval(() => this.cleanup(), 60000); } async get<T>(key: string, fetcher: () => Promise<T>, ttl = this.defaultTtl): Promise<T> { const entry = this.cache.get(key); const now = Date.now(); if (entry && now - entry.timestamp < entry.ttl) { return entry.data; } const data = await fetcher(); this.cache.set(key, { data, timestamp: now, ttl }); this.emit('resourceUpdated', key, data); return data; } private cleanup() { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > entry.ttl) { this.cache.delete(key); this.emit('resourceExpired', key); } } } invalidate(key: string) { this.cache.delete(key); this.emit('resourceInvalidated', key); } destroy() { clearInterval(this.cleanupInterval); this.cache.clear(); } } // ESPN API with modern patterns class ModernESPNClient { // Football async getCollegeFootballNews() { return this.fetchData('/football/college-football/news'); } async getCollegeFootballScores(params: {calendar?: string, dates?: string} = {}) { let url = '/football/college-football/scoreboard'; const q = []; if (params.calendar) q.push(`calendar=${params.calendar}`); if (params.dates) q.push(`dates=${params.dates}`); if (q.length) url += '?' + q.join('&'); return this.fetchData(url); } async getCollegeFootballGameSummary(gameId: string) { return this.fetchData(`/football/college-football/summary?event=${gameId}`); } async getCollegeFootballTeam(team: string) { return this.fetchData(`/football/college-football/teams/${team}`); } async getCollegeFootballRankings() { return this.fetchData('/football/college-football/rankings'); } async getNFLNews() { return this.fetchData('/football/nfl/news'); } async getNFLScores(params: {seasontype?: string, week?: string, dates?: string} = {}) { let url = '/football/nfl/scoreboard'; const q = []; if (params.seasontype) q.push(`seasontype=${params.seasontype}`); if (params.week) q.push(`week=${params.week}`); if (params.dates) q.push(`dates=${params.dates}`); if (q.length) url += '?' + q.join('&'); return this.fetchData(url); } async getNFLTeams() { return this.fetchData('/football/nfl/teams'); } async getNFLTeam(team: string) { return this.fetchData(`/football/nfl/teams/${team}`); } // Baseball async getMLBScores() { return this.fetchData('/baseball/mlb/scoreboard'); } async getMLBNews() { return this.fetchData('/baseball/mlb/news'); } async getMLBTeams() { return this.fetchData('/baseball/mlb/teams'); } async getMLBTeam(team: string) { return this.fetchData(`/baseball/mlb/teams/${team}`); } async getCollegeBaseballScores() { return this.fetchData('/baseball/college-baseball/scoreboard'); } // Hockey async getNHLScores() { return this.fetchData('/hockey/nhl/scoreboard'); } async getNHLNews() { return this.fetchData('/hockey/nhl/news'); } async getNHLTeams() { return this.fetchData('/hockey/nhl/teams'); } async getNHLTeam(team: string) { return this.fetchData(`/hockey/nhl/teams/${team}`); } // Basketball async getNBAScores() { return this.fetchData('/basketball/nba/scoreboard'); } async getNBANews() { return this.fetchData('/basketball/nba/news'); } async getNBATeams() { return this.fetchData('/basketball/nba/teams'); } async getNBATeam(team: string) { return this.fetchData(`/basketball/nba/teams/${team}`); } async getWNBAScores() { return this.fetchData('/basketball/wnba/scoreboard'); } async getWNBANews() { return this.fetchData('/basketball/wnba/news'); } async getWNBATeams() { return this.fetchData('/basketball/wnba/teams'); } async getWNBATeam(team: string) { return this.fetchData(`/basketball/wnba/teams/${team}`); } async getWomensCollegeBasketballScores() { return this.fetchData('/basketball/womens-college-basketball/scoreboard'); } async getWomensCollegeBasketballNews() { return this.fetchData('/basketball/womens-college-basketball/news'); } async getWomensCollegeBasketballTeams() { return this.fetchData('/basketball/womens-college-basketball/teams'); } async getWomensCollegeBasketballTeam(team: string) { return this.fetchData(`/basketball/womens-college-basketball/teams/${team}`); } async getMensCollegeBasketballScores() { return this.fetchData('/basketball/mens-college-basketball/scoreboard'); } async getMensCollegeBasketballNews() { return this.fetchData('/basketball/mens-college-basketball/news'); } async getMensCollegeBasketballTeams() { return this.fetchData('/basketball/mens-college-basketball/teams'); } async getMensCollegeBasketballTeam(team: string) { return this.fetchData(`/basketball/mens-college-basketball/teams/${team}`); } // Soccer (generic, league required) async getSoccerScores(league: string) { return this.fetchData(`/soccer/${league}/scoreboard`); } async getMLSScores() { return this.fetchData('/soccer/mls/scoreboard'); } async getPremierLeagueScores() { return this.fetchData('/soccer/eng.1/scoreboard'); } async getChampionsLeagueScores() { return this.fetchData('/soccer/uefa.champions/scoreboard'); } async getSoccerNews(league: string) { return this.fetchData(`/soccer/${league}/news`); } async getSoccerTeams(league: string) { return this.fetchData(`/soccer/${league}/teams`); } async getSoccerTeam(league: string, team: string) { return this.fetchData(`/soccer/${league}/teams/${team}`); } private cache: ModernCache; private baseUrl = "https://site.api.espn.com/apis/site/v2/sports"; constructor(cacheTimeout = 300000) { this.cache = new ModernCache(cacheTimeout); } async fetchData(endpoint: string): Promise<any> { return this.cache.get(endpoint, async () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); try { const response = await fetch(`${this.baseUrl}${endpoint}`, { headers: { 'User-Agent': 'ESPN-MCP-Server/2.0', 'Accept': 'application/json', }, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`ESPN API error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error('ESPN API request timeout'); } throw error instanceof Error ? error : new Error(String(error)); } }); } // Specific data methods async getScoreboard(sport: string, league?: string): Promise<any> { const leagueParam = league ? `/${league}` : ''; return this.fetchData(`/${sport}${leagueParam}/scoreboard`); } async getTeams(sport: string, league?: string): Promise<any> { const leagueParam = league ? `/${league}` : ''; return this.fetchData(`/${sport}${leagueParam}/teams`); } async getStandings(sport: string, league?: string): Promise<any> { try { const leagueParam = league ? `/${league}` : ''; const standingsData = await this.fetchData(`/${sport}${leagueParam}/standings`); // Check if we got minimal response (just a link) if (standingsData?.fullViewLink && !standingsData.standings) { // Return informative response about the limitation return { message: "ESPN API provides limited standings data via this endpoint", fullViewLink: standingsData.fullViewLink, recommendation: "Use the fullViewLink to access complete standings on ESPN's website", sport: sport, league: league, alternativeEndpoint: `/${sport}${leagueParam}/teams can provide team information` }; } return standingsData; } catch (error) { console.error('Error fetching standings:', error); throw new Error(`Failed to fetch standings: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getNews(sport?: string): Promise<any> { const sportParam = sport ? `/${sport}` : ''; return this.fetchData(`${sportParam}/news`); } async getAthletes(sport: string, league?: string): Promise<any> { try { // Get all teams first const leagueParam = league ? `/${league}` : ''; const teamsData = await this.fetchData(`/${sport}${leagueParam}/teams`); if (!teamsData?.sports?.[0]?.leagues?.[0]?.teams) { throw new Error('No teams found'); } const teams = teamsData.sports[0].leagues[0].teams; const allAthletes: any[] = []; // Get roster for each team for (const team of teams) { try { const rosterData = await this.fetchData(`/${sport}${leagueParam}/teams/${team.id}/roster`); if (rosterData?.athletes) { // Add team info to each athlete const athletesWithTeam = rosterData.athletes.map((athlete: any) => ({ ...athlete, team: { id: team.id, name: team.displayName, abbreviation: team.abbreviation } })); allAthletes.push(...athletesWithTeam); } } catch (teamError) { // Continue if individual team roster fails console.warn(`Failed to fetch roster for team ${team.id}:`, teamError); } } return { athletes: allAthletes, count: allAthletes.length, sport: sport, league: league }; } catch (error) { console.error('Error fetching athletes:', error); throw new Error(`Failed to fetch athletes: ${error instanceof Error ? error.message : 'Unknown error'}`); } } onResourceUpdate(callback: (key: string, data: any) => void) { this.cache.on('resourceUpdated', callback); } // Generic fallback methods for unsupported combinations async getTeamDetails(sport: string, league: string, team: string) { return this.fetchData(`/${sport}/${league}/teams/${team}`); } async getCollegeBaseballTeam(team: string) { return this.fetchData(`/baseball/college-baseball/teams/${team}`); } async getCollegeBaseballTeams() { return this.fetchData('/baseball/college-baseball/teams'); } async getCollegeFootballTeams() { return this.fetchData('/football/college-football/teams'); } destroy() { this.cache.destroy(); } } // Tools with modern schemas (defined globally for HTTP handlers) const tools: Tool[] = [ { name: "get_live_scores", description: "Get live scores and game information for any sport and league", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football", "basketball", "baseball", "hockey", "soccer", "tennis", "golf"], description: "The sport to get scores for" }, league: { type: "string", description: "Specific league (optional)", enum: ["nfl", "college-football", "nba", "mens-college-basketball", "womens-college-basketball", "wnba", "mlb", "college-baseball", "nhl", "mls", "premier-league", "champions-league"] }, dates: { type: "string", description: "Date in YYYYMMDD format (optional)", pattern: "^\\d{8}$" }, week: { type: "string", description: "NFL week number (optional, for NFL only)" }, seasontype: { type: "string", description: "Season type (1=preseason, 2=regular, 3=postseason) (optional, for NFL only)", enum: ["1", "2", "3"] } }, required: ["sport"] } }, { name: "get_team_information", description: "Get comprehensive team information including roster and statistics", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football", "basketball", "baseball", "hockey", "soccer"], description: "The sport" }, league: { type: "string", description: "Specific league (optional)", enum: ["nfl", "college-football", "nba", "mens-college-basketball", "womens-college-basketball", "wnba", "mlb", "college-baseball", "nhl"] } }, required: ["sport"] } }, { name: "get_specific_team", description: "Get detailed information about a specific team by team abbreviation", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football", "basketball", "baseball", "hockey", "soccer"], description: "The sport" }, league: { type: "string", description: "Specific league", enum: ["nfl", "college-football", "nba", "mens-college-basketball", "womens-college-basketball", "wnba", "mlb", "nhl"] }, team: { type: "string", description: "Team abbreviation (e.g., 'all' for Allegheny, 'gt' for Georgia Tech, 'patriots' for New England)" } }, required: ["sport", "league", "team"] } }, { name: "get_college_football_rankings", description: "Get current college football rankings", inputSchema: { type: "object", properties: {}, additionalProperties: false } }, { name: "get_game_summary", description: "Get detailed game summary information for a specific game", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football"], description: "The sport (currently only football supported)" }, league: { type: "string", enum: ["college-football"], description: "The league (currently only college-football supported)" }, gameId: { type: "string", description: "Game identifier (e.g., '400934572' for 2017 Army vs Navy)" } }, required: ["sport", "league", "gameId"] } }, { name: "get_league_standings", description: "Get current standings and playoff information for any league", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football", "basketball", "baseball", "hockey", "soccer"], description: "The sport" }, league: { type: "string", description: "Specific league (optional)" } }, required: ["sport"] } }, { name: "get_sports_news", description: "Get latest sports news and breaking stories", inputSchema: { type: "object", properties: { sport: { type: "string", description: "Specific sport for news (optional, defaults to general sports)", enum: ["football", "basketball", "baseball", "hockey"] }, limit: { type: "number", minimum: 1, maximum: 50, default: 10, description: "Number of articles to return" } } } }, { name: "search_athletes", description: "Search for athlete information and career statistics", inputSchema: { type: "object", properties: { sport: { type: "string", enum: ["football", "basketball", "baseball", "hockey", "soccer", "tennis", "golf"], description: "The sport" }, league: { type: "string", description: "Specific league (optional)" } }, required: ["sport"] } } ]; // Dynamic resources (defined globally for HTTP handlers) const staticResources: Resource[] = [ { uri: "espn://live-dashboard", name: "Live Sports Dashboard", description: "Real-time sports scores and updates across all major leagues", mimeType: "application/json" }, { uri: "espn://breaking-news", name: "Breaking Sports News", description: "Latest breaking news stories from the sports world", mimeType: "application/json" }, { uri: "espn://trending-athletes", name: "Trending Athletes", description: "Currently trending athletes and their recent performances", mimeType: "application/json" }, { uri: "espn://playoff-picture", name: "Playoff Picture", description: "Current playoff standings and scenarios across all leagues", mimeType: "application/json" } ]; // Interactive prompts (defined globally for HTTP handlers) const prompts: Prompt[] = [ { name: "analyze_game_performance", description: "Analyze team or player performance in a specific game with detailed insights", arguments: [ { name: "sport", description: "The sport (e.g., football, basketball, baseball)", required: true }, { name: "team_or_player", description: "Team name or player name to analyze", required: true }, { name: "game_context", description: "Specific game or recent games context", required: false } ] }, { name: "compare_head_to_head", description: "Generate head-to-head comparison between two teams or players", arguments: [ { name: "sport", description: "The sport for comparison", required: true }, { name: "entity1", description: "First team or player name", required: true }, { name: "entity2", description: "Second team or player name", required: true }, { name: "comparison_type", description: "Type of comparison (season, career, recent)", required: false } ] }, { name: "predict_season_outcomes", description: "Generate predictions and analysis for season outcomes and playoffs", arguments: [ { name: "sport", description: "The sport to analyze", required: true }, { name: "league", description: "Specific league", required: true }, { name: "prediction_scope", description: "Scope of prediction (playoffs, championship, awards)", required: false } ] } ]; export function createModernESPNServer(config: Partial<z.infer<typeof configSchema>> = {}): Server { const finalConfig = configSchema.parse(config); const server = new Server( { name: "modern-espn-server", version: "2.0.0", }, { capabilities: { resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, tools: { listChanged: true }, logging: {}, experimental: { httpStreaming: finalConfig.enableStreaming, sessionManagement: true, resourceTemplates: true } }, } ); const espnClient = new ModernESPNClient(finalConfig.cacheTimeout); const resourceSubscriptions = new Map<string, Set<string>>(); if (finalConfig.debug) { console.log('ESPN MCP Server initialized with config:', finalConfig); } // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "get_live_scores": { const { sport, league, dates, week, seasontype } = args as any; let data; // Use specific ESPN endpoints for each sport/league combination switch (sport) { case "football": if (league === "nfl") { data = await espnClient.getNFLScores({ dates, week, seasontype }); } else if (league === "college-football") { data = await espnClient.getCollegeFootballScores({ dates }); } else { data = await espnClient.getScoreboard(sport, league); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBAScores(); } else if (league === "wnba") { data = await espnClient.getWNBAScores(); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballScores(); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBScores(); } else if (league === "college-baseball") { data = await espnClient.getCollegeBaseballScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "soccer": if (league) { data = await espnClient.getSoccerScores(league); } else { data = await espnClient.getScoreboard(sport, league); } break; default: data = await espnClient.getScoreboard(sport, league); } return { content: [ { type: "text", text: `Live scores for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_team_information": { const { sport, league } = args as any; let data; // Use specific ESPN endpoints for each sport/league combination switch (sport) { case "football": if (league === "nfl") { data = await espnClient.getNFLTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBATeams(); } else if (league === "wnba") { data = await espnClient.getWNBATeams(); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballTeams(); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "soccer": if (league) { data = await espnClient.getSoccerTeams(league); } else { data = await espnClient.getTeams(sport, league); } break; default: data = await espnClient.getTeams(sport, league); } return { content: [ { type: "text", text: `Team information for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_league_standings": { const { sport, league } = args as any; const data = await espnClient.getStandings(sport, league); return { content: [ { type: "text", text: `Standings for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_sports_news": { const { sport, limit = 10 } = args as any; let data; // Use specific ESPN endpoints for each sport switch (sport) { case "football": // For football, we'll get both NFL and college football news const [nflNews, cfbNews] = await Promise.all([ espnClient.getNFLNews().catch(() => null), espnClient.getCollegeFootballNews().catch(() => null) ]); data = { nfl: nflNews, collegefootball: cfbNews }; break; case "basketball": // For basketball, get NBA, WNBA, and college basketball news const [nbaNews, wnbaNews, mensCbbNews, womensCbbNews] = await Promise.all([ espnClient.getNBANews().catch(() => null), espnClient.getWNBANews().catch(() => null), espnClient.getMensCollegeBasketballNews().catch(() => null), espnClient.getWomensCollegeBasketballNews().catch(() => null) ]); data = { nba: nbaNews, wnba: wnbaNews, menscollege: mensCbbNews, womenscollege: womensCbbNews }; break; case "baseball": data = await espnClient.getMLBNews(); break; case "hockey": data = await espnClient.getNHLNews(); break; case "soccer": // For soccer, get news from major leagues const [premierLeagueNews, mlsNews, championsLeagueNews, laLigaNews] = await Promise.all([ espnClient.getSoccerNews('eng.1').catch(() => null), espnClient.getSoccerNews('usa.1').catch(() => null), espnClient.getSoccerNews('uefa.champions').catch(() => null), espnClient.getSoccerNews('esp.1').catch(() => null) ]); data = { premierLeague: premierLeagueNews, mls: mlsNews, championsLeague: championsLeagueNews, laLiga: laLigaNews }; break; case "golf": // Golf requires PGA tour endpoint data = await espnClient.getNews('golf/pga'); break; case "tennis": // Tennis requires ATP and WTA tour endpoints const [atpNews, wtaNews] = await Promise.all([ espnClient.getNews('tennis/atp').catch(() => null), espnClient.getNews('tennis/wta').catch(() => null) ]); data = { atp: atpNews, wta: wtaNews }; break; case "f1": case "formula1": // F1 racing news data = await espnClient.getNews('racing/f1'); break; case "ncaa-basketball": case "college-basketball": // NCAA basketball (already handled above in basketball case) const [mensCbbNewsOnly, womensCbbNewsOnly] = await Promise.all([ espnClient.getNews('basketball/mens-college-basketball').catch(() => null), espnClient.getNews('basketball/womens-college-basketball').catch(() => null) ]); data = { mens: mensCbbNewsOnly, womens: womensCbbNewsOnly }; break; case "ncaa-football": case "college-football": // NCAA football (already handled above in football case) data = await espnClient.getNews('football/college-football'); break; default: data = await espnClient.getNews(sport); } return { content: [ { type: "text", text: `Sports news${sport ? ` for ${sport}` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "search_athletes": { const { sport, league } = args as any; const data = await espnClient.getAthletes(sport, league); return { content: [ { type: "text", text: `Athletes for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_specific_team": { const { sport, league, team } = args as any; let data; switch (sport) { case "football": if (league === "college-football") { data = await espnClient.getCollegeFootballTeam(team); } else if (league === "nfl") { data = await espnClient.getNFLTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBATeam(team); } else if (league === "wnba") { data = await espnClient.getWNBATeam(team); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballTeam(team); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBTeam(team); } else if (league === "college-baseball") { data = await espnClient.getCollegeBaseballTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; default: data = await espnClient.getTeamDetails(sport, league, team); } return { content: [ { type: "text", text: `Team information for ${team} in ${sport} (${league}):\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_college_football_rankings": { const data = await espnClient.getCollegeFootballRankings(); return { content: [ { type: "text", text: `College Football Rankings:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_game_summary": { const { sport, league, gameId } = args as any; if (sport === "football" && league === "college-football") { const data = await espnClient.getCollegeFootballGameSummary(gameId); return { content: [ { type: "text", text: `Game Summary for ${gameId}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } else { throw new Error(`Game summary only supported for college football currently`); } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); // Resource handlers server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: staticResources }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; try { let contents: ResourceContents[]; switch (uri) { case "espn://live-dashboard": { const [nflScores, nbaScores, mlbScores, nhlScores] = await Promise.all([ espnClient.getNFLScores().catch(() => null), espnClient.getNBAScores().catch(() => null), espnClient.getMLBScores().catch(() => null), espnClient.getNHLScores().catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ lastUpdated: new Date().toISOString(), nfl: nflScores, nba: nbaScores, mlb: mlbScores, nhl: nhlScores }, null, 2) }]; break; } case "espn://breaking-news": { const [nflNews, nbaNews, mlbNews, nhlNews] = await Promise.all([ espnClient.getNFLNews().catch(() => null), espnClient.getNBANews().catch(() => null), espnClient.getMLBNews().catch(() => null), espnClient.getNHLNews().catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ lastUpdated: new Date().toISOString(), nfl: nflNews, nba: nbaNews, mlb: mlbNews, nhl: nhlNews }, null, 2) }]; break; } case "espn://trending-athletes": { const [nflTeams, nbaTeams, mlbTeams, nhlTeams] = await Promise.all([ espnClient.getNFLTeams().catch(() => null), espnClient.getNBATeams().catch(() => null), espnClient.getMLBTeams().catch(() => null), espnClient.getNHLTeams().catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ lastUpdated: new Date().toISOString(), nfl: nflTeams, nba: nbaTeams, mlb: mlbTeams, nhl: nhlTeams }, null, 2) }]; break; } case "espn://playoff-picture": { const [nflStandings, nbaStandings] = await Promise.all([ espnClient.getStandings("football", "nfl").catch(() => null), espnClient.getStandings("basketball", "nba").catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ lastUpdated: new Date().toISOString(), nfl: nflStandings, nba: nbaStandings }, null, 2) }]; break; } default: throw new Error(`Resource not found: ${uri}`); } return { contents }; } catch (error) { throw new Error(`Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`); } }); // Prompt handlers server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; const prompt = prompts.find(p => p.name === name); if (!prompt) { throw new Error(`Prompt not found: ${name}`); } const messages: PromptMessage[] = []; try { switch (name) { case "analyze_game_performance": { const { sport, team_or_player, game_context } = args as any; messages.push({ role: "user", content: { type: "text", text: `Analyze the performance of ${team_or_player} in ${sport}${game_context ? ` (${game_context})` : ''}.` } as TextContent }); // Add relevant data as resource const scoreboardData = await espnClient.getScoreboard(sport); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://analysis/${sport}/${encodeURIComponent(team_or_player)}`, mimeType: "application/json", text: JSON.stringify(scoreboardData, null, 2) } } }); break; } case "compare_head_to_head": { const { sport, entity1, entity2, comparison_type } = args as any; messages.push({ role: "user", content: { type: "text", text: `Compare ${entity1} vs ${entity2} in ${sport}${comparison_type ? ` (${comparison_type} comparison)` : ''}.` } as TextContent }); // Add team data const teamsData = await espnClient.getTeams(sport); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://comparison/${sport}/${encodeURIComponent(entity1)}-vs-${encodeURIComponent(entity2)}`, mimeType: "application/json", text: JSON.stringify(teamsData, null, 2) } } }); break; } case "predict_season_outcomes": { const { sport, league, prediction_scope } = args as any; messages.push({ role: "user", content: { type: "text", text: `Predict season outcomes for ${league} ${sport}${prediction_scope ? ` focusing on ${prediction_scope}` : ''}.` } as TextContent }); // Add standings data const standingsData = await espnClient.getStandings(sport, league); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://predictions/${sport}/${league}`, mimeType: "application/json", text: JSON.stringify(standingsData, null, 2) } } }); break; } default: throw new Error(`Unknown prompt: ${name}`); } } catch (error) { throw new Error(`Failed to generate prompt: ${error instanceof Error ? error.message : String(error)}`); } return { description: prompt.description, messages }; }); // Resource update notifications espnClient.onResourceUpdate((key: string, data: any) => { // Notify subscribers of resource updates server.notification({ method: "notifications/resources/updated", params: { uri: `espn://cache/${key}` } }); }); // Cleanup on server close const originalClose = server.close.bind(server); server.close = async () => { espnClient.destroy(); return originalClose(); }; return server; } // Global references for HTTP handlers let globalTools: Tool[] = []; let globalResources: Resource[] = []; let globalPrompts: Prompt[] = []; // Helper functions for HTTP endpoint handling async function handleToolCall(params: any) { const { name, arguments: args } = params; try { switch (name) { case "get_live_scores": { const { sport, league, dates, week, seasontype } = args as any; const espnClient = new ModernESPNClient(); let data; // Use specific ESPN endpoints for each sport/league combination switch (sport) { case "football": if (league === "nfl") { data = await espnClient.getNFLScores({ dates, week, seasontype }); } else if (league === "college-football") { data = await espnClient.getCollegeFootballScores({ dates }); } else { data = await espnClient.getScoreboard(sport, league); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBAScores(); } else if (league === "wnba") { data = await espnClient.getWNBAScores(); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballScores(); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBScores(); } else if (league === "college-baseball") { data = await espnClient.getCollegeBaseballScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLScores(); } else { data = await espnClient.getScoreboard(sport, league); } break; case "soccer": if (league === "mls") { data = await espnClient.getMLSScores(); } else if (league === "premier-league") { data = await espnClient.getPremierLeagueScores(); } else if (league === "champions-league") { data = await espnClient.getChampionsLeagueScores(); } else { data = await espnClient.getSoccerScores('mls'); } break; default: data = await espnClient.getScoreboard(sport, league); } espnClient.destroy(); return { content: [ { type: "text", text: `Live scores for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_team_information": { const { sport, league } = args as any; const espnClient = new ModernESPNClient(); let data; // Use specific ESPN endpoints for each sport/league combination switch (sport) { case "football": if (league === "nfl") { data = await espnClient.getNFLTeams(); } else if (league === "college-football") { data = await espnClient.getCollegeFootballTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBATeams(); } else if (league === "wnba") { data = await espnClient.getWNBATeams(); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballTeams(); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBTeams(); } else if (league === "college-baseball") { data = await espnClient.getCollegeBaseballTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLTeams(); } else { data = await espnClient.getTeams(sport, league); } break; case "soccer": if (league) { data = await espnClient.getSoccerTeams(league); } else { data = await espnClient.getTeams(sport, league); } break; default: data = await espnClient.getTeams(sport, league); } espnClient.destroy(); return { content: [ { type: "text", text: `Team information for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_league_standings": { const { sport, league } = args as any; const espnClient = new ModernESPNClient(); const data = await espnClient.getStandings(sport, league); espnClient.destroy(); return { content: [ { type: "text", text: `Standings for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_sports_news": { const { sport, limit = 10 } = args as any; const espnClient = new ModernESPNClient(); let data; switch (sport) { case "football": // For football, we'll get both NFL and college football news const [nflNews, cfbNews] = await Promise.all([ espnClient.getNFLNews().catch(() => null), espnClient.getCollegeFootballNews().catch(() => null) ]); data = { nfl: nflNews, collegefootball: cfbNews }; break; case "basketball": // For basketball, get NBA, WNBA, and college basketball news const [nbaNews, wnbaNews, mensCbbNews, womensCbbNews] = await Promise.all([ espnClient.getNBANews().catch(() => null), espnClient.getWNBANews().catch(() => null), espnClient.getMensCollegeBasketballNews().catch(() => null), espnClient.getWomensCollegeBasketballNews().catch(() => null) ]); data = { nba: nbaNews, wnba: wnbaNews, menscollege: mensCbbNews, womenscollege: womensCbbNews }; break; case "baseball": data = await espnClient.getMLBNews(); break; case "hockey": data = await espnClient.getNHLNews(); break; default: data = await espnClient.getNews(sport); } espnClient.destroy(); return { content: [ { type: "text", text: `Sports news${sport ? ` for ${sport}` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "search_athletes": { const { sport, league } = args as any; const espnClient = new ModernESPNClient(); const data = await espnClient.getAthletes(sport, league); espnClient.destroy(); return { content: [ { type: "text", text: `Athletes for ${sport}${league ? ` (${league})` : ''}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_specific_team": { const { sport, league, team } = args as any; const espnClient = new ModernESPNClient(); let data; switch (sport) { case "football": if (league === "college-football") { data = await espnClient.getCollegeFootballTeam(team); } else if (league === "nfl") { data = await espnClient.getNFLTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "basketball": if (league === "nba") { data = await espnClient.getNBATeam(team); } else if (league === "wnba") { data = await espnClient.getWNBATeam(team); } else if (league === "mens-college-basketball") { data = await espnClient.getMensCollegeBasketballTeam(team); } else if (league === "womens-college-basketball") { data = await espnClient.getWomensCollegeBasketballTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "baseball": if (league === "mlb") { data = await espnClient.getMLBTeam(team); } else if (league === "college-baseball") { data = await espnClient.getCollegeBaseballTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; case "hockey": if (league === "nhl") { data = await espnClient.getNHLTeam(team); } else { data = await espnClient.getTeamDetails(sport, league, team); } break; default: data = await espnClient.getTeamDetails(sport, league, team); } espnClient.destroy(); return { content: [ { type: "text", text: `Team information for ${team} in ${sport} (${league}):\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_college_football_rankings": { const espnClient = new ModernESPNClient(); const data = await espnClient.getCollegeFootballRankings(); espnClient.destroy(); return { content: [ { type: "text", text: `College Football Rankings:\n\n${JSON.stringify(data, null, 2)}` } ] }; } case "get_game_summary": { const { sport, league, gameId } = args as any; const espnClient = new ModernESPNClient(); if (sport === "football" && league === "college-football") { const data = await espnClient.getCollegeFootballGameSummary(gameId); espnClient.destroy(); return { content: [ { type: "text", text: `Game Summary for ${gameId}:\n\n${JSON.stringify(data, null, 2)}` } ] }; } else { espnClient.destroy(); throw new Error(`Game summary only supported for college football currently`); } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } async function handleResourceRead(params: any) { const { uri } = params; const espnClient = new ModernESPNClient(); try { let contents: ResourceContents[]; switch (uri) { case "espn://live-dashboard": { const [football, basketball, baseball] = await Promise.all([ espnClient.getScoreboard("football", "nfl").catch(() => null), espnClient.getScoreboard("basketball", "nba").catch(() => null), espnClient.getScoreboard("baseball", "mlb").catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ lastUpdated: new Date().toISOString(), football: football, basketball: basketball, baseball: baseball }, null, 2) }]; break; } case "espn://breaking-news": { const news = await espnClient.getNews(); contents = [{ uri, mimeType: "application/json", text: JSON.stringify(news, null, 2) }]; break; } case "espn://trending-athletes": { const [footballAthletes, basketballAthletes] = await Promise.all([ espnClient.getAthletes("football", "nfl").catch(() => null), espnClient.getAthletes("basketball", "nba").catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ football: footballAthletes, basketball: basketballAthletes }, null, 2) }]; break; } case "espn://playoff-picture": { const [nflStandings, nbaStandings] = await Promise.all([ espnClient.getStandings("football", "nfl").catch(() => null), espnClient.getStandings("basketball", "nba").catch(() => null) ]); contents = [{ uri, mimeType: "application/json", text: JSON.stringify({ nfl: nflStandings, nba: nbaStandings }, null, 2) }]; break; } default: throw new Error(`Resource not found: ${uri}`); } return { contents }; } finally { espnClient.destroy(); } } async function handlePromptGet(params: any) { const { name, arguments: args } = params; const prompt = globalPrompts.find((p: Prompt) => p.name === name); if (!prompt) { throw new Error(`Prompt not found: ${name}`); } const espnClient = new ModernESPNClient(); const messages: PromptMessage[] = []; try { switch (name) { case "analyze_game_performance": { const { sport, team_or_player, game_context } = args as any; messages.push({ role: "user", content: { type: "text", text: `Analyze the performance of ${team_or_player} in ${sport}${game_context ? ` (${game_context})` : ''}.` } as TextContent }); const scoreboardData = await espnClient.getScoreboard(sport); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://analysis/${sport}/${encodeURIComponent(team_or_player)}`, mimeType: "application/json", text: JSON.stringify(scoreboardData, null, 2) } } }); break; } case "compare_head_to_head": { const { sport, entity1, entity2, comparison_type } = args as any; messages.push({ role: "user", content: { type: "text", text: `Compare ${entity1} vs ${entity2} in ${sport}${comparison_type ? ` (${comparison_type} comparison)` : ''}.` } as TextContent }); const teamsData = await espnClient.getTeams(sport); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://comparison/${sport}/${encodeURIComponent(entity1)}-vs-${encodeURIComponent(entity2)}`, mimeType: "application/json", text: JSON.stringify(teamsData, null, 2) } } }); break; } case "predict_season_outcomes": { const { sport, league, prediction_scope } = args as any; messages.push({ role: "user", content: { type: "text", text: `Predict season outcomes for ${league} ${sport}${prediction_scope ? ` focusing on ${prediction_scope}` : ''}.` } as TextContent }); const standingsData = await espnClient.getStandings(sport, league); messages.push({ role: "user", content: { type: "resource", resource: { uri: `espn://predictions/${sport}/${league}`, mimeType: "application/json", text: JSON.stringify(standingsData, null, 2) } } }); break; } default: throw new Error(`Unknown prompt: ${name}`); } return { description: prompt.description, messages }; } finally { espnClient.destroy(); } } // HTTP Server with proper MCP StreamableHTTPServerTransport export function createHTTPServer(): express.Application { const app = express(); const PORT = process.env.PORT || 8081; // CORS configuration for deployment app.use(cors({ origin: '*', // Configure appropriately for production exposedHeaders: ['Mcp-Session-Id', 'mcp-protocol-version'], allowedHeaders: ['Content-Type', 'mcp-session-id'], })); app.use(express.json()); // Main MCP endpoint using StreamableHTTPServerTransport app.all('/mcp', async (req: express.Request, res: express.Response) => { try { // Parse configuration from query parameters const rawConfig = parseConfig(req); const config = configSchema.parse({ cacheTimeout: rawConfig.cacheTimeout || Number(process.env.CACHE_TIMEOUT) || 300000, maxConcurrentRequests: rawConfig.maxConcurrentRequests || Number(process.env.MAX_CONCURRENT_REQUESTS) || 5, enableStreaming: rawConfig.enableStreaming !== undefined ? rawConfig.enableStreaming : (process.env.ENABLE_STREAMING !== 'false'), debug: rawConfig.debug !== undefined ? rawConfig.debug : (process.env.DEBUG === 'true'), }); const server = createModernESPNServer(config); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); // Clean up on request close res.on('close', () => { transport.close(); server.close(); }); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null, }); } } }); // Health endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', version: '2.0.0', timestamp: new Date().toISOString(), capabilities: { resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, tools: { listChanged: true }, streaming: true, sessionManagement: true } }); }); return app; } // STDIO Server export async function runSTDIOServer() { const server = createModernESPNServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Modern ESPN MCP Server (v2.0) running on stdio"); } // HTTP Server export async function runHTTPServer(port?: number) { try { const app = createHTTPServer(); const serverPort = port || Number(process.env.PORT) || 8081; const httpServer = app.listen(serverPort, () => { console.error(`Modern ESPN MCP Server (v2.0) running on port ${serverPort}`); console.error('Features: Resources, Prompts, Tools, HTTP Streaming, Session Management'); }); httpServer.on('error', (error: Error) => { console.error('HTTP Server error:', error); process.exit(1); }); process.on('SIGTERM', () => { console.error('Shutting down gracefully...'); httpServer.close(() => process.exit(0)); }); process.on('SIGINT', () => { console.error('Shutting down gracefully...'); httpServer.close(() => process.exit(0)); }); return httpServer; } catch (error) { console.error('Failed to start HTTP server:', error); process.exit(1); } } // Auto-detect transport (compatible with both ESM and CommonJS) let isMainModule = false; try { // 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('/modern-server.js') || import.meta.url.endsWith('\\modern-server.js'); } } catch (e) { // Fallback for CommonJS environments try { // @ts-ignore - CommonJS check isMainModule = require.main === module; } catch (e2) { // Final fallback - check if this file is being executed directly isMainModule = !!(process.argv[1] && process.argv[1].includes('modern-server')); } } if (isMainModule) { const transport = process.env.TRANSPORT || 'stdio'; const args = process.argv.slice(2); if (transport === 'http' || args.includes('--http')) { const portIndex = args.indexOf('--port'); const port = portIndex !== -1 ? parseInt(args[portIndex + 1]) : undefined; console.error('Starting HTTP server...'); runHTTPServer(port).catch((error) => { console.error('HTTP server startup error:', error); process.exit(1); }); } else { console.error('Starting STDIO server...'); runSTDIOServer().catch((error) => { console.error('STDIO server startup 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