Skip to main content
Glama
gedin-eth

College Football MCP

by gedin-eth
server.py17.5 kB
""" MCP Server for College Football Data This module implements the FastAPI server that exposes MCP endpoints for college football game information, odds, and statistics. """ from fastapi import FastAPI, HTTPException, Query from fastapi.responses import JSONResponse from pydantic import BaseModel from typing import Optional import os from dotenv import load_dotenv import logging from src.odds_api import ( get_ncaaf_odds, find_game_by_teams, find_next_game_for_team, format_game_odds_response, format_next_game_odds ) from src.cfbd_api import ( search_players, get_player_game_stats, get_player_game_logs, get_team_games, get_team_records, get_team_rankings, format_player_recent_stats, format_team_recent_results, format_team_info ) # Load environment variables load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Load API keys from environment ODDS_API_KEY = os.getenv("ODDS_API_KEY") CFB_API_KEY = os.getenv("CFB_API_KEY") # Validate API keys on startup def validate_api_keys(): """Validate that required API keys are present""" missing_keys = [] if not ODDS_API_KEY: missing_keys.append("ODDS_API_KEY") if not CFB_API_KEY: missing_keys.append("CFB_API_KEY") if missing_keys: logger.warning(f"Missing API keys: {', '.join(missing_keys)}. Some functions may not work.") else: logger.info("All API keys loaded successfully") # Validate on module load validate_api_keys() app = FastAPI( title="College Football MCP Server", description="MCP server for college football game data and betting odds", version="0.1.0" ) @app.get("/") async def root(): """Health check endpoint""" return { "status": "running", "service": "cfb-mcp", "version": "0.1.0" } @app.get("/health") async def health(): """Health check endpoint""" return { "status": "healthy", "api_keys_configured": { "odds_api": bool(ODDS_API_KEY), "cfb_api": bool(CFB_API_KEY) } } # Request models for MCP endpoints class GameOddsRequest(BaseModel): """Request model for get_game_odds_and_score""" team1: str team2: Optional[str] = None class PlayerStatsRequest(BaseModel): """Request model for get_recent_player_stats""" player_name: str team: Optional[str] = None class TeamResultsRequest(BaseModel): """Request model for get_team_recent_results""" team: str class TeamInfoRequest(BaseModel): """Request model for get_team_info""" team: str class NextGameOddsRequest(BaseModel): """Request model for get_next_game_odds""" team: str # MCP Function Endpoints @app.post("/mcp/get_game_odds_and_score") async def get_game_odds_and_score(request: GameOddsRequest): """ Get live game scores and betting odds for a specific matchup. This endpoint retrieves real-time odds and scores from The Odds API for NCAA college football games. Args: request: GameOddsRequest with team1 (required) and team2 (optional) Returns: JSON response with game details, scores, and odds """ if not ODDS_API_KEY: raise HTTPException( status_code=503, detail="ODDS_API_KEY is not configured" ) try: # Fetch odds from The Odds API games = get_ncaaf_odds(ODDS_API_KEY) if not games: raise HTTPException( status_code=404, detail="No games found or API request failed" ) # Find the matching game game = find_game_by_teams(games, request.team1, request.team2) if not game: raise HTTPException( status_code=404, detail=f"Game not found for team: {request.team1}" ) # Format the response formatted_response = format_game_odds_response(game) return JSONResponse(content=formatted_response) except HTTPException: raise except Exception as e: logger.error(f"Error in get_game_odds_and_score: {e}") raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" ) @app.get("/api/get_game_odds_and_score") async def get_game_odds_and_score_get( team1: str = Query(..., description="First team name (required)"), team2: Optional[str] = Query(None, description="Second team name (optional, for exact match)") ): """ GET endpoint for get_game_odds_and_score (for easier testing). Same functionality as POST endpoint but accessible via GET with query parameters. """ request = GameOddsRequest(team1=team1, team2=team2) return await get_game_odds_and_score(request) @app.post("/mcp/get_recent_player_stats") async def get_recent_player_stats(request: PlayerStatsRequest): """ Get player statistics from the last 5 games. This endpoint retrieves detailed game-by-game statistics for a player from the CollegeFootballData API. Args: request: PlayerStatsRequest with player_name (required) and team (optional) Returns: JSON response with player stats from last 5 games """ if not CFB_API_KEY: raise HTTPException( status_code=503, detail="CFB_API_KEY is not configured" ) try: # Search for the player players = search_players(CFB_API_KEY, request.player_name, request.team) if not players or len(players) == 0: raise HTTPException( status_code=404, detail=f"Player not found: {request.player_name}" ) # Use the first matching player (could be enhanced to handle multiple matches) player = players[0] player_id = player.get("id") player_name = player.get("name", request.player_name) player_team = player.get("team", request.team) if not player_id: raise HTTPException( status_code=404, detail=f"Player ID not found for: {request.player_name}" ) # Get current year for stats from datetime import datetime current_year = datetime.now().year # Try to get game-by-game stats game_stats = get_player_game_logs(CFB_API_KEY, player_id, current_year) # If game logs not available, try season stats if not game_stats: game_stats = get_player_game_stats(CFB_API_KEY, player_id, current_year) # If still no stats, try previous year if not game_stats: game_stats = get_player_game_logs(CFB_API_KEY, player_id, current_year - 1) if not game_stats: game_stats = get_player_game_stats(CFB_API_KEY, player_id, current_year - 1) if not game_stats: raise HTTPException( status_code=404, detail=f"No game statistics found for player: {request.player_name}" ) # Sort games by date (most recent first) if date information is available # For now, we'll take the first/last entries depending on API response order # Format the response formatted_response = format_player_recent_stats( player_name, player_team, game_stats ) return JSONResponse(content=formatted_response) except HTTPException: raise except Exception as e: logger.error(f"Error in get_recent_player_stats: {e}") import traceback logger.error(traceback.format_exc()) raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" ) @app.get("/api/get_recent_player_stats") async def get_recent_player_stats_get( player_name: str = Query(..., description="Player name (required)"), team: Optional[str] = Query(None, description="Team name (optional, for disambiguation)") ): """ GET endpoint for get_recent_player_stats (for easier testing). Same functionality as POST endpoint but accessible via GET with query parameters. """ request = PlayerStatsRequest(player_name=player_name, team=team) return await get_recent_player_stats(request) @app.post("/mcp/get_team_recent_results") async def get_team_recent_results(request: TeamResultsRequest): """ Get a team's last 5 game results. This endpoint retrieves recent game results for a team from the CollegeFootballData API, including opponents, dates, scores, and outcomes. Args: request: TeamResultsRequest with team name (required) Returns: JSON response with team's last 5 games including scores and win/loss """ if not CFB_API_KEY: raise HTTPException( status_code=503, detail="CFB_API_KEY is not configured" ) try: # Get current year for stats from datetime import datetime from src.team_normalizer import normalize_team_name current_year = datetime.now().year team_name = request.team original_team = request.team # Start with previous year (most recent completed season) as college football season # typically ends in January, so December 2025 means 2024 season is most recent year_to_try = current_year - 1 normalized_name = normalize_team_name(original_team) # Try combinations: [original, normalized] x [previous_year, current_year] name_variations = [original_team] if normalized_name and normalized_name != original_team.lower(): name_variations.append(normalized_name) games = None # Try current year first (most recent), then previous year # This ensures 2025 games are prioritized when they exist for year in [current_year, current_year - 1]: for name_var in name_variations: logger.info(f"Trying team '{name_var}' for year {year}") games = get_team_games(CFB_API_KEY, name_var, year, "both") if games and isinstance(games, list) and len(games) > 0: team_name = name_var logger.info(f"Found {len(games)} games for '{name_var}' in {year}") break if games and isinstance(games, list) and len(games) > 0: break if not games or (isinstance(games, list) and len(games) == 0): raise HTTPException( status_code=404, detail=f"No games found for team: {request.team} (tried variations: {team_name})" ) # Format the response (use the team name that worked) formatted_response = format_team_recent_results(team_name, games) return JSONResponse(content=formatted_response) except HTTPException: raise except Exception as e: logger.error(f"Error in get_team_recent_results: {e}") import traceback logger.error(traceback.format_exc()) raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" ) @app.get("/api/get_team_recent_results") async def get_team_recent_results_get( team: str = Query(..., description="Team name (required)") ): """ GET endpoint for get_team_recent_results (for easier testing). Same functionality as POST endpoint but accessible via GET with query parameters. """ request = TeamResultsRequest(team=team) return await get_team_recent_results(request) @app.post("/mcp/get_team_info") async def get_team_info(request: TeamInfoRequest): """ Get a team's current season overview. This endpoint retrieves team season information from the CollegeFootballData API, including overall record, conference record, and rankings. Args: request: TeamInfoRequest with team name (required) Returns: JSON response with team season overview including record and rankings """ if not CFB_API_KEY: raise HTTPException( status_code=503, detail="CFB_API_KEY is not configured" ) try: # Get current year from datetime import datetime from src.team_normalizer import normalize_team_name current_year = datetime.now().year team_name = request.team # Try original team name first record = get_team_records(CFB_API_KEY, team_name, current_year) # If no record, try normalized team name if not record: normalized_name = normalize_team_name(team_name) if normalized_name and normalized_name != team_name.lower(): logger.info(f"No record found for '{team_name}', trying normalized name '{normalized_name}'") record = get_team_records(CFB_API_KEY, normalized_name, current_year) if record: team_name = normalized_name # If no record for current year, try previous year if not record: record = get_team_records(CFB_API_KEY, team_name, current_year - 1) if not record: normalized_name = normalize_team_name(request.team) if normalized_name and normalized_name != team_name.lower(): record = get_team_records(CFB_API_KEY, normalized_name, current_year - 1) if record: team_name = normalized_name # Get rankings (AP Poll) - try both names rankings = get_team_rankings( CFB_API_KEY, team=team_name, year=current_year, poll="ap" ) if not rankings: normalized_name = normalize_team_name(request.team) if normalized_name and normalized_name != team_name.lower(): rankings = get_team_rankings( CFB_API_KEY, team=normalized_name, year=current_year, poll="ap" ) # Format the response formatted_response = format_team_info(team_name, record, rankings) return JSONResponse(content=formatted_response) except HTTPException: raise except Exception as e: logger.error(f"Error in get_team_info: {e}") import traceback logger.error(traceback.format_exc()) raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" ) @app.get("/api/get_team_info") async def get_team_info_get( team: str = Query(..., description="Team name (required)") ): """ GET endpoint for get_team_info (for easier testing). Same functionality as POST endpoint but accessible via GET with query parameters. """ request = TeamInfoRequest(team=team) return await get_team_info(request) @app.post("/mcp/get_next_game_odds") async def get_next_game_odds(request: NextGameOddsRequest): """ Get next scheduled game and betting odds for a team. This endpoint finds a team's next upcoming game from The Odds API and returns the betting odds for that matchup. Args: request: NextGameOddsRequest with team name (required) Returns: JSON response with next game details and betting odds """ if not ODDS_API_KEY: raise HTTPException( status_code=503, detail="ODDS_API_KEY is not configured" ) try: # Fetch upcoming games from The Odds API games = get_ncaaf_odds(ODDS_API_KEY) if not games: raise HTTPException( status_code=404, detail="No upcoming games found or API request failed" ) # Find the next game for the team next_game = find_next_game_for_team(games, request.team) if not next_game: raise HTTPException( status_code=404, detail=f"No upcoming game found for team: {request.team}" ) # Format the response formatted_response = format_next_game_odds(request.team, next_game) return JSONResponse(content=formatted_response) except HTTPException: raise except Exception as e: logger.error(f"Error in get_next_game_odds: {e}") import traceback logger.error(traceback.format_exc()) raise HTTPException( status_code=500, detail=f"Internal server error: {str(e)}" ) @app.get("/api/get_next_game_odds") async def get_next_game_odds_get( team: str = Query(..., description="Team name (required)") ): """ GET endpoint for get_next_game_odds (for easier testing). Same functionality as POST endpoint but accessible via GET with query parameters. """ request = NextGameOddsRequest(team=team) return await get_next_game_odds(request) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)

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/gedin-eth/cfb-mcp'

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