#
tokens: 11792/50000 4/4 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── Dockerfile
├── nba_server.py
├── README.md
└── requirements.txt
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/obinopaul-nba-mcp-server-badge.png)](https://mseep.ai/app/obinopaul-nba-mcp-server)
  2 | 
  3 | # NBA MCP Server
  4 | 
  5 | A Python server implementing Model Context Protocol (MCP) for NBA statistics and live game data.
  6 | 
  7 | ## Overview
  8 | 
  9 | This server provides a set of tools for accessing NBA data through the NBA API. It serves as a bridge between applications and the NBA's data services, offering both live game information and historical statistics.
 10 | 
 11 | ## Features
 12 | 
 13 | - Live game data (scoreboard, box scores, play-by-play)
 14 | - Player information and career statistics
 15 | - Team game logs and statistics
 16 | - League standings
 17 | - Game results and schedules
 18 | 
 19 | ## Tools
 20 | 
 21 | ### Live Game Data
 22 | 
 23 | - **nba_live_scoreboard**
 24 |   - Fetch today's NBA scoreboard (live or latest)
 25 |   - Returns game IDs, start times, scores, and broadcast details
 26 | 
 27 | - **nba_live_boxscore**
 28 |   - Fetch real-time box score for a given NBA game ID
 29 |   - Provides detailed player and team statistics
 30 | 
 31 | - **nba_live_play_by_play**
 32 |   - Retrieve live play-by-play actions for a specific game
 33 |   - Includes scoring plays, fouls, timeouts, and substitutions
 34 | 
 35 | ### Player Information
 36 | 
 37 | - **nba_common_player_info**
 38 |   - Retrieve basic information about a player
 39 |   - Includes biographical data, height, weight, team, position
 40 | 
 41 | - **nba_player_career_stats**
 42 |   - Obtain a player's career statistics
 43 |   - Available in different formats (per game, totals, per 36 minutes)
 44 | 
 45 | - **nba_list_active_players**
 46 |   - Return a list of all currently active NBA players
 47 | 
 48 | - **nba_player_game_logs**
 49 |   - Obtain a player's game statistics within a specified date range
 50 | 
 51 | ### Team Data
 52 | 
 53 | - **nba_team_game_logs_by_name**
 54 |   - Fetch a team's game logs using the team name
 55 |   - Avoids needing to know the team's numeric ID
 56 | 
 57 | - **nba_fetch_game_results**
 58 |   - Fetch game results for a given team ID and date range
 59 | 
 60 | - **nba_team_standings**
 61 |   - Fetch NBA team standings for a given season and season type
 62 | 
 63 | - **nba_team_stats_by_name**
 64 |   - Fetch team statistics using the team name
 65 |   - Supports different aggregation methods (totals, per game, etc.)
 66 | 
 67 | - **nba_all_teams_stats**
 68 |   - Fetch statistics for all NBA teams across multiple seasons
 69 | 
 70 | ### Schedule Information
 71 | 
 72 | - **nba_list_todays_games**
 73 |   - Returns scoreboard data for any specific date
 74 | 
 75 | ## Usage
 76 | 
 77 | The server is implemented using the MCP framework and can be run as a standalone service.
 78 | 
 79 | ```python
 80 | # Start the server
 81 | python nba_server.py
 82 | # or
 83 | mcp run nba_server.py
 84 | ```
 85 | 
 86 | ### Configuration
 87 | 
 88 | - The server runs with a 30-second timeout for more reliable operation
 89 | - Signal handlers are implemented for graceful shutdown (Ctrl+C)
 90 | 
 91 | ### Usage with Claude Desktop
 92 | 
 93 | #### Option 1: Using Docker (Recommended)
 94 | 
 95 | 1. Clone this repository
 96 | ```
 97 | git clone https://github.com/obinopaul/nba-mcp-server.git
 98 | cd nba-mcp-server
 99 | ```
100 | 
101 | 2. Install dependencies
102 | ```
103 | pip install -r requirements.txt
104 | ```
105 | 
106 | 3. Build the Docker image
107 | ```
108 | docker build -t nba_mcp_server .
109 | ```
110 | 
111 | 4. Run the Docker container
112 | ```
113 | docker run -d -p 5000:5000 --name nba_mcp_server nba_mcp_server
114 | ```
115 | 
116 | 5. Add this to your `claude_desktop_config.json`:
117 | 
118 | ```json
119 | {
120 |   "mcpServers": {
121 |     "nba_mcp_server": {
122 |       "command": "docker",
123 |       "args": [
124 |         "exec",
125 |         "-i",
126 |         "nba_mcp_server",
127 |         "python",
128 |         "nba_server.py"
129 |       ]
130 |     }
131 |   }
132 | }
133 | ```
134 | 
135 | #### Option 2: Direct Python Execution
136 | 
137 | 1. Clone this repository
138 | ```
139 | git clone https://github.com/obinopaul/nba-mcp-server.git
140 | cd nba-mcp-server
141 | ```
142 | 
143 | 2. Create a new environment
144 | ```
145 | conda create --name your_env_name python=3.13
146 | conda activate your_env_name
147 | ```
148 | 
149 | 3. Install dependencies
150 | ```
151 | pip install -r requirements.txt
152 | ```
153 | 
154 | 4. Run NBA mcp server on the terminal
155 | ```
156 | mcp run nba_server.py
157 | ```
158 | 
159 | 5. Add this to your `claude_desktop_config.json`, adjusting the Python path as needed:
160 | 
161 | ```json
162 | {
163 |   "mcpServers": {
164 |     "nba_mcp_server": {
165 |       "command": "/path/to/your/python",
166 |       "args": [
167 |         "/path/to/nba_server.py"
168 |       ]
169 |     }
170 |   }
171 | }
172 | ```
173 | 
174 | After adding your chosen configuration, restart Claude Desktop to load the NBA server. You'll then be able to use all the NBA data tools in your conversations with Claude.
175 | 
176 | 
177 | ## Technical Details
178 | 
179 | The server is built on:
180 | - NBA API (nba_api) Python package
181 | - MCP for API interface
182 | - Pydantic for input validation
183 | - Pandas for data manipulation
184 | 
185 | ## License
186 | 
187 | This MCP server is available under the MIT License.
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | nba_api
2 | langchain
3 | langgraph
4 | mcp[cli]
5 | pandas
6 | pydantic
7 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Use an official Python runtime as a parent image
 2 | FROM python:3.10-slim
 3 | 
 4 | # Set the working directory in the container
 5 | WORKDIR /nba-mcp-sever
 6 | 
 7 | # Copy the current directory contents into the container
 8 | COPY . /nba-mcp-sever
 9 | 
10 | # Install any necessary dependencies
11 | RUN pip install --no-cache-dir -r requirements.txt
12 | 
13 | # Expose the port that your app will run on
14 | EXPOSE 5000
15 | 
16 | # Run the server when the container launches
17 | CMD ["python", "nba_server.py"]
18 | 
19 | # docker build -t nba_server .
20 | # docker run -p 4000:5000 nba_server
21 | 
22 | 
```

--------------------------------------------------------------------------------
/nba_server.py:
--------------------------------------------------------------------------------

```python
  1 | from mcp.server.fastmcp import FastMCP
  2 | import time
  3 | import signal
  4 | import sys
  5 | from nba_api.live.nba.endpoints import scoreboard, boxscore, playbyplay
  6 | from nba_api.stats.static import players, teams
  7 | from pydantic import BaseModel, Field, field_validator, ValidationError
  8 | from typing import Optional, List, Dict, Any
  9 | from datetime import datetime, timedelta
 10 | import pandas as pd
 11 | import os
 12 | from nba_api.live.nba.endpoints import scoreboard, boxscore, playbyplay
 13 | from nba_api.stats.endpoints import commonplayerinfo, playercareerstats, scoreboardv2, teamgamelogs, leaguegamefinder, leaguestandingsv3, teamyearbyyearstats
 14 | from nba_api.stats.static import players, teams
 15 | from nba_api.stats.library.parameters import SeasonType, SeasonYear
 16 | 
 17 | # print(f"Python executable: {sys.executable}", file=sys.stderr)
 18 | # print(f"Python path: {sys.path}", file=sys.stderr)
 19 | print(f"Current working directory: {os.getcwd()}", file=sys.stderr)
 20 | 
 21 | # Handle SIGINT (Ctrl+C) gracefully
 22 | def signal_handler(sig, frame):
 23 |     print("Shutting down server gracefully...")
 24 |     sys.exit(0)
 25 | 
 26 | signal.signal(signal.SIGINT, signal_handler)
 27 | 
 28 | # Create an MCP server with increased timeout
 29 | mcp = FastMCP(
 30 |     name="nba_mcp_server",
 31 |     # host="127.0.0.1",
 32 |     # port=5000,
 33 |     # Add this to make the server more resilient
 34 |     timeout=30  # Increase timeout to 30 seconds
 35 | )
 36 | 
 37 | # -------------------------------------------------------------------
 38 | # 1) ScoreBoard Tool (Live Endpoint)
 39 | # -------------------------------------------------------------------
 40 | 
 41 | class LiveScoreBoardInput(BaseModel):
 42 |     dummy_param: Optional[str] = Field(default="", description="Not used.")
 43 | 
 44 | @mcp.tool()
 45 | def nba_live_scoreboard(dummy_param: Optional[str] = "") -> Dict[str, Any]:
 46 |     """Fetch today's NBA scoreboard (live or latest).
 47 | 
 48 |     This tool retrieves data from the `nba_api.live.nba.endpoints.scoreboard` endpoint.  It provides
 49 |     information about games happening *today* (or the most recent games if no games are live).
 50 |     This includes game IDs, start times, scores, period information, and broadcast details.
 51 | 
 52 |     **Args:**
 53 | 
 54 |         dummy_param (str, optional):  This parameter is not used. It exists for compatibility
 55 |             with the MCP framework and should be left as an empty string. Defaults to "".
 56 | 
 57 |     **Returns:**
 58 | 
 59 |         Dict[str, Any]: A dictionary containing scoreboard data.  The structure follows the
 60 |             `nba_api`'s `ScoreBoard` object. Key elements include:
 61 | 
 62 |             * "games": A list of dictionaries, one for each game.  Each game dictionary contains:
 63 |                 * "gameId": (str) The 10-digit game ID.  **Crucially, this is needed for other live tools.**
 64 |                 * "gameStatus": (int)  A numeric representation of the game status (1 = scheduled, 2 = in progress, 3 = final).
 65 |                 * "gameStatusText": (str) A textual representation of the game status (e.g., "Final", "Q4 05:30", "8:00 pm ET").
 66 |                 * "homeTeam": (dict) Information about the home team.
 67 |                 * "awayTeam": (dict) Information about the away team.
 68 |                 *  ...and many other fields.
 69 | 
 70 |             * "gameDate": (str) - The date of the game
 71 |             *  "scoreboard": (dict) - Contains overall scoreboard of that date.
 72 | 
 73 |             If an error occurs, the dictionary will contain a single "error" key with a
 74 |             description of the problem.
 75 |     """
 76 |     try:
 77 |         sb = scoreboard.ScoreBoard()
 78 |         return sb.get_dict()
 79 |     except Exception as e:
 80 |         return {"error": str(e)}
 81 | 
 82 | # -------------------------------------------------------------------
 83 | # 2) BoxScore Tool (Live Endpoint)
 84 | # -------------------------------------------------------------------
 85 | 
 86 | class LiveBoxScoreInput(BaseModel):
 87 |     game_id: str = Field(..., description="A 10-digit NBA game ID (e.g., '0022200017').")
 88 | 
 89 | @mcp.tool()
 90 | def nba_live_boxscore(game_id: str) -> Dict[str, Any]:
 91 |     """Fetch the real-time box score for a given NBA game ID.
 92 | 
 93 |     This tool retrieves live box score data from the `nba_api.live.nba.endpoints.boxscore`
 94 |     endpoint.  It provides detailed statistics for a *specific* game, including:
 95 | 
 96 |     * Player statistics (points, rebounds, assists, etc.)
 97 |     * Team statistics (points by quarter, totals)
 98 |     * Active players
 99 |     * Game officials
100 | 
101 |     **Args:**
102 | 
103 |         game_id (str): The 10-digit NBA game ID.  This is typically obtained from
104 |             `nba_live_scoreboard`.  Example: "0022300123"
105 | 
106 |     **Returns:**
107 | 
108 |         Dict[str, Any]: A dictionary containing the box score data. The structure follows
109 |             the `nba_api`'s `BoxScore` object. Key elements include:
110 | 
111 |             * "gameId": The game ID.
112 |             * "gameStatus": Numeric game status.
113 |             *  "boxScoreTraditional": (dict) - contains player and team stats.
114 |             * "teams": A list of two dictionaries (one for each team), containing:
115 |                 * "teamId": The team ID.
116 |                 * "teamName": The team name.
117 |                 * "teamCity": The team city.
118 |                 * "players": A list of dictionaries, one for each player, with detailed stats.
119 |             * ...and many other fields.
120 | 
121 |             If an error occurs, the dictionary will contain a single "error" key.
122 | 
123 |     """
124 |     if not isinstance(game_id, str):
125 |         game_id = str(game_id)
126 |         
127 |     try:
128 |         bs = boxscore.BoxScore(game_id=game_id)
129 |         return bs.get_dict()
130 |     except Exception as e:
131 |         return {"error": str(e)}
132 | 
133 | # -------------------------------------------------------------------
134 | # 3) PlayByPlay Tool (Live Endpoint)
135 | # -------------------------------------------------------------------
136 | 
137 | class LivePlayByPlayInput(BaseModel):
138 |     game_id: str = Field(..., description="A 10-digit NBA game ID.")
139 | 
140 | @mcp.tool()
141 | def nba_live_play_by_play(game_id: str) -> Dict[str, Any]:
142 |     """Retrieve the live play-by-play actions for a specific NBA game ID.
143 | 
144 |     This tool retrieves data from the `nba_api.live.nba.endpoints.playbyplay` endpoint.
145 |     It provides a chronological list of events that occur during a game, including:
146 | 
147 |     * Scoring plays
148 |     * Fouls
149 |     * Timeouts
150 |     * Substitutions
151 |     * Descriptions of each play
152 | 
153 |     **Args:**
154 | 
155 |         game_id (str): The 10-digit NBA game ID.  Obtain this from `nba_live_scoreboard`.
156 |             Example: "0022300123"
157 | 
158 |     **Returns:**
159 | 
160 |         Dict[str, Any]: A dictionary containing the play-by-play data. The structure follows
161 |             the `nba_api`'s `PlayByPlay` object.  Key elements include:
162 | 
163 |             * "gameId": The game ID.
164 |             * "actions": A list of dictionaries, one for each play.  Each play dictionary contains:
165 |                 * "actionNumber": A sequential number for the play.
166 |                 * "clock": The game clock time when the play occurred.
167 |                 * "period": The quarter/overtime period.
168 |                 * "teamId": The ID of the team involved in the play (if applicable).
169 |                 * "personId": The ID of the player involved in the play (if applicable).
170 |                 * "description": A textual description of the play.
171 |                 * ...and many other fields.
172 | 
173 |             If an error occurs, the dictionary will contain a single "error" key.
174 |     """
175 |     if not isinstance(game_id, str):
176 |         game_id = str(game_id)
177 |         
178 |     try:
179 |         pbp = playbyplay.PlayByPlay(game_id=game_id)
180 |         return pbp.get_dict()
181 |     except Exception as e:
182 |         return {"error": str(e)}
183 | 
184 | # -------------------------------------------------------------------
185 | # 4) CommonPlayerInfo Tool (Stats Endpoint)
186 | # -------------------------------------------------------------------
187 | 
188 | class CommonPlayerInfoInput(BaseModel):
189 |     player_id: str = Field(..., description="NBA player ID (e.g., '2544').")
190 | 
191 | @mcp.tool()
192 | def nba_common_player_info(player_id: str) -> Dict[str, Any]:
193 |     """Retrieve basic information about a player.
194 | 
195 |     This tool retrieves data from the `nba_api.stats.endpoints.commonplayerinfo` endpoint.
196 |     It provides biographical and basic information about a specific NBA player, including:
197 | 
198 |     * Player ID
199 |     * Full Name
200 |     * Birthdate
201 |     * Height
202 |     * Weight
203 |     * Current Team
204 |     * Jersey Number
205 |     * Position
206 |     * Draft information
207 |     * College
208 | 
209 |     **Args:**
210 | 
211 |         player_id (str): The NBA player ID.  This is typically a number, like "2544" (LeBron James).
212 |             You can use `nba_search_players` (not yet documented here, but in your original code) to
213 |             find a player ID by name.
214 | 
215 |     **Returns:**
216 | 
217 |         Dict[str, Any]: A dictionary containing player information.  The structure follows the
218 |             `nba_api`'s `CommonPlayerInfo` object. Key elements include:
219 | 
220 |             * "CommonPlayerInfo": A list containing a single dictionary with player details.
221 |                 * "personId": The player ID.
222 |                 * "displayFirstLast": The player's full name.
223 |                 * "birthdate": The player's birthdate.
224 |                 * "height": Player height.
225 |                 * "weight": Player weight.
226 |                 * "teamId":  The ID of the player's current team.
227 |                 * "teamName": The name of the player's current team.
228 |                 * ... and many other fields
229 | 
230 |              * "ResultSets": (list) - Contains the results in sets.
231 | 
232 |             If an error occurs, the dictionary will contain a single "error" key.
233 | 
234 |     """
235 |     if not isinstance(player_id, str):
236 |         player_id = str(player_id)
237 |         
238 |     try:
239 |         info = commonplayerinfo.CommonPlayerInfo(player_id=player_id)
240 |         return info.get_dict()
241 |     except Exception as e:
242 |         return {"error": str(e)}
243 | 
244 | # -------------------------------------------------------------------
245 | # 5) PlayerCareerStats Tool (Stats Endpoint)
246 | # -------------------------------------------------------------------
247 | 
248 | class PlayerCareerStatsInput(BaseModel):
249 |     player_id: str = Field(..., description="NBA player ID.")
250 |     per_mode: Optional[str] = Field(default="PerGame", description="One of 'Totals', 'PerGame', 'Per36'.")
251 | 
252 | @mcp.tool()
253 | def nba_player_career_stats(player_id: str, per_mode: str = "PerGame") -> Dict[str, Any]:
254 |     """Obtain an NBA player's career statistics.
255 | 
256 |     This tool retrieves career statistics (regular season, playoffs, and potentially All-Star games)
257 |     from the `nba_api.stats.endpoints.playercareerstats` endpoint.  It provides aggregated
258 |     statistics for a player across their entire career or specific seasons.
259 | 
260 |     **Args:**
261 | 
262 |         player_id (str): The NBA player ID (e.g., "2544").
263 |         per_mode (str, optional):  Determines the statistical aggregation.  Valid options are:
264 |             * "PerGame":  Stats averaged per game played.
265 |             * "Totals":  Total career statistics.
266 |             * "Per36": Stats per 36 minutes played.
267 |             Defaults to "PerGame".
268 | 
269 |     **Returns:**
270 | 
271 |         Dict[str, Any]: A dictionary containing the player's career statistics. The structure
272 |             follows the `nba_api`'s `PlayerCareerStats` object.  Key elements include:
273 | 
274 |             * "SeasonTotalsRegularSeason": A list of dictionaries, one for each season the
275 |               player played in the regular season.  Each dictionary contains aggregated stats
276 |               for that season (e.g., games played, points, rebounds, assists, etc.).
277 |             * "CareerTotalsRegularSeason":  A list containing a single dictionary with the
278 |               player's total career regular season stats.
279 |             * "SeasonTotalsPostSeason", "CareerTotalsPostSeason": Similar data for playoff games.
280 |             * "SeasonTotalsAllStarSeason", "CareerTotalsAllStarSeason": Similar data for All-Star games.
281 |             * "resultSets" (list): Contains different sets of career stats.
282 | 
283 |             If an error occurs, the dictionary will contain a single "error" key.
284 | 
285 |     """
286 |     # Convert player_id to a string if it's not already.
287 |     if not isinstance(player_id, str):
288 |         player_id = str(player_id)
289 |         
290 |     try:
291 |         career = playercareerstats.PlayerCareerStats(player_id=player_id, per_mode36=per_mode)
292 |         return career.get_dict()
293 |     except Exception as e:
294 |         return {"error": str(e)}
295 | 
296 | 
297 | # -------------------------------------------------------------------
298 | # 8) List All Active Players
299 | # -------------------------------------------------------------------
300 | class ListActivePlayersInput(BaseModel):
301 |     # no arguments needed
302 |     dummy: str = "unused"
303 | 
304 | @mcp.tool()
305 | def nba_list_active_players(dummy: str = "") -> List[Dict[str, Any]]:
306 |     """Return a list of all currently active NBA players.
307 | 
308 |     This tool uses the `nba_api.stats.static.players` module to retrieve a list of all players
309 |     marked as active in the NBA API's database.
310 | 
311 |     **Args:**
312 | 
313 |         dummy (str, optional): This parameter is not used. It is included for compatibility with the MCP framework.
314 | 
315 |     **Returns:**
316 | 
317 |         List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents an active player.
318 |             Each player dictionary contains:
319 |             * "id": (int) The player's ID.
320 |             * "full_name": (str) The player's full name.
321 |             * "first_name": (str) The player's first name.
322 |             * "last_name": (str) The player's last name.
323 |             * "is_active": (bool) Always True for this function.
324 |             If there's an issue, a list containing a dictionary with an "error" key is returned.
325 | 
326 |     """
327 |     try:
328 |         all_active = players.get_active_players()
329 |         return all_active
330 |     except Exception as e:
331 |         return [{"error": str(e)}]
332 | 
333 | # -------------------------------------------------------------------
334 | # 9) List Today’s Games (Stats vs. Live)
335 | # -------------------------------------------------------------------
336 | 
337 | class TodayGamesInput(BaseModel):
338 |     game_date: str = Field(..., description="A date in 'YYYY-MM-DD' format.")
339 |     league_id: str = Field(default="00", description="League ID (default=00 for NBA).")
340 | 
341 | @mcp.tool()
342 | def nba_list_todays_games(game_date: str, league_id: str = "00") -> Dict[str, Any]:
343 |     """Returns scoreboard data from stats.nba.com for a given date.
344 | 
345 |     This tool retrieves game information for a specific date from the
346 |     `nba_api.stats.endpoints.scoreboardv2` endpoint.  It's similar to `nba_live_scoreboard`,
347 |     but it allows you to query for games on *any* date (past, present, or future), not just
348 |     today's games.
349 | 
350 |     **Args:**
351 | 
352 |         game_date (str): The date for which to retrieve game information, in "YYYY-MM-DD" format.
353 |             Example: "2023-12-25"
354 |         league_id (str, optional):  The league ID.  "00" represents the NBA. Defaults to "00".
355 | 
356 |     **Returns:**
357 | 
358 |         Dict[str, Any]: A dictionary containing game data for the specified date.  The structure
359 |             follows the `nba_api`'s `ScoreboardV2` object (but is normalized). Key elements:
360 | 
361 |             * "GameHeader": A list of dictionaries, one for each game, containing:
362 |                 * "GAME_DATE_EST":  The game date in YYYY-MM-DD format.
363 |                 * "GAME_ID": The 10-digit game ID. **Important for other tools.**
364 |                 * "HOME_TEAM_ID": The ID of the home team.
365 |                 * "VISITOR_TEAM_ID": The ID of the away team.
366 |                 * "GAME_STATUS_TEXT": Textual game status (e.g., "Final", "8:00 PM ET").
367 |                 * ...and other fields.
368 |             * "LineScore": A list of dictionaries with detailed scoring information for each team
369 |                in each game.
370 |             *  "SeriesStandings": A list of series standings
371 |             *   "LastMeeting": A list of last meeting
372 |             * ... other fields
373 | 
374 |             If an error occurs, the dictionary will contain a single "error" key.
375 | 
376 |     """
377 |     try:
378 |         sb = scoreboardv2.ScoreboardV2(game_date=game_date, league_id=league_id)
379 |         return sb.get_normalized_dict()
380 |     except Exception as e:
381 |         return {"error": str(e)}
382 | 
383 | # # -------------------------------------------------------------------
384 | # # 10) TeamGameLogsTool: Fetch a Team's Game Logs
385 | # # -------------------------------------------------------------------
386 | 
387 | # class TeamGameLogsInput(BaseModel):
388 | #     team_id: str = Field(..., description="The NBA Team ID.")
389 | #     season: str = Field(default="2022-23", description="Season in 'YYYY-YY' format.")
390 | #     season_type: str = Field(default="Regular Season", description="'Regular Season', 'Playoffs', etc.")
391 | 
392 | # @mcp.tool()
393 | # def nba_team_game_logs(team_id: str, season: str, season_type: str) -> List[Dict[str, Any]]:
394 | #     """Fetch a list of all games for a given Team ID in a specified season."""
395 | #     try:
396 | #         logs = teamgamelogs.TeamGameLogs(team_id_nullable=team_id, season_nullable=season, season_type_nullable=season_type)
397 | #         df = logs.get_data_frames()[0]
398 | #         selected_columns = ["TEAM_ID", "GAME_ID", "GAME_DATE", "MATCHUP", "WL"]
399 | #         partial_df = df[selected_columns]
400 | #         return partial_df.to_dict("records")
401 | #     except Exception as e:
402 | #         return [{"error": str(e)}]
403 | 
404 | # -------------------------------------------------------------------
405 | # 11) team_game_logs_by_name_tool: Fetch a Team's Game Logs by Name
406 | # -------------------------------------------------------------------
407 | 
408 | class TeamGameLogsByNameInput(BaseModel):
409 |     team_name: str = Field(..., description="Partial or full NBA team name.")
410 |     season: str = Field(default="2022-23", description="Season in 'YYYY-YY' format.")
411 |     season_type: str = Field(default="Regular Season", description="'Regular Season', 'Playoffs', etc.")
412 | 
413 | @mcp.tool()
414 | def nba_team_game_logs_by_name(team_name: str, season: str, season_type: str) -> List[Dict[str, Any]]:
415 |     """Fetch a team's game logs by providing the team name.
416 | 
417 |     This tool retrieves a team's game log (list of games) for a given season and season type,
418 |     using the team's *name* as input.  This avoids needing to know the team's numeric ID.  It uses
419 |     the `nba_api.stats.static.teams` module to find the team and then the
420 |     `nba_api.stats.endpoints.teamgamelogs` endpoint to get the game log.
421 | 
422 |     **Args:**
423 | 
424 |         team_name (str): The full or partial name of the NBA team (e.g., "Lakers", "Los Angeles Lakers").
425 |         season (str): The season in "YYYY-YY" format (e.g., "2023-24").
426 |         season_type (str): The type of season.  Valid options are:
427 |             * "Regular Season"
428 |             * "Playoffs"
429 |             * "Pre Season"
430 |             * "All Star"
431 | 
432 |     **Returns:**
433 | 
434 |         List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a game in the
435 |             team's game log.  The selected columns are:
436 | 
437 |             * "TEAM_ID": The team's numeric ID.
438 |             * "GAME_ID": The 10-digit game ID.
439 |             * "GAME_DATE": The date of the game.
440 |             * "MATCHUP":  A string showing the matchup (e.g., "LAL vs. GSW").
441 |             * "WL":  The game result ("W" for win, "L" for loss, or None if the game hasn't been played).
442 | 
443 |             If no team is found or an error occurs, the list will contain a single dictionary
444 |             with an "error" key.
445 | 
446 |     """
447 |     try:
448 |         found = teams.find_teams_by_full_name(team_name)
449 |         if not found:
450 |             return [{"error": f"No NBA team found matching name '{team_name}'."}]
451 |         best_match = found[0]
452 |         team_id = best_match["id"]
453 |         logs = teamgamelogs.TeamGameLogs(team_id_nullable=str(team_id), season_nullable=season, season_type_nullable=season_type)
454 |         df = logs.get_data_frames()[0]
455 |         columns_we_want = ["TEAM_ID", "GAME_ID", "GAME_DATE", "MATCHUP", "WL"]
456 |         partial_df = df[columns_we_want]
457 |         return partial_df.to_dict("records")
458 |     except Exception as e:
459 |         return [{"error": str(e)}]
460 | 
461 | # -------------------------------------------------------------------
462 | # 12) nba_fetch_game_results: Fetch Game Results for a Team
463 | # -------------------------------------------------------------------
464 | class GameResultsInput(BaseModel):
465 | 
466 |     team_id: str = Field(..., description="A valid NBA team ID.")
467 |     dates: List[str] = Field(..., description="A list of dates in 'YYYY-MM-DD' format.", min_items=1)
468 | 
469 | @mcp.tool()
470 | def nba_fetch_game_results(team_id: str, dates: List[str]) -> List[Dict[str, Any]]:
471 |     """Fetch game results for a given NBA team ID and date range.
472 | 
473 |     This tool retrieves game results and statistics for a specified team within a given range of dates.
474 |     It leverages the `nba_api.stats.endpoints.leaguegamefinder` to efficiently find games and then filters
475 |     the results to include only the dates requested.
476 | 
477 |     **Args:**
478 | 
479 |         team_id (str): The NBA team ID (e.g., "1610612744" for the Golden State Warriors).
480 |         dates (List[str]): A list of dates in "YYYY-MM-DD" format, representing the date range for which
481 |                            to fetch game results. The order of dates does not matter; the function will
482 |                            automatically determine the start and end dates.  Must contain at least one date.
483 | 
484 |     **Returns:**
485 | 
486 |         List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a game played by
487 |             the specified team within the provided date range.  Includes all columns returned by
488 |             the `nba_api`'s `LeagueGameFinder`.
489 | 
490 |             If an error occurs or no games are found, a list with a single dictionary containing an "error"
491 |             key is returned.
492 | 
493 |     """
494 |     # Convert player_id to a string if it's not already.
495 |     if not isinstance(team_id, str):
496 |         team_id = str(team_id)
497 |         
498 |     try:
499 |         date_objects = [datetime.strptime(date, '%Y-%m-%d') for date in dates]
500 |         gamefinder = leaguegamefinder.LeagueGameFinder(
501 |             team_id_nullable=team_id,
502 |             season_type_nullable=SeasonType.regular,
503 |             date_from_nullable=min(date_objects).strftime('%m/%d/%Y'),
504 |             date_to_nullable=max(date_objects).strftime('%m/%d/%Y')
505 |         )
506 |         games = gamefinder.get_data_frames()[0]
507 |         games['GAME_DATE'] = pd.to_datetime(games['GAME_DATE'])
508 |         start_date = min(date_objects)
509 |         end_date = max(date_objects)
510 |         all_dates = []
511 |         current_date = start_date
512 |         while current_date <= end_date:
513 |             all_dates.append(current_date)
514 |             current_date += timedelta(days=1)
515 |         games = games[games['GAME_DATE'].dt.date.isin([d.date() for d in all_dates])]
516 |         return games.to_dict('records')
517 |     except Exception as e:
518 |         return {"error": str(e)}
519 | 
520 | 
521 | # -------------------------------------------------------------------------
522 | # nba_team_standings: Retrieve NBA Team Standings
523 | # -------------------------------------------------------------------------
524 | class LeagueStandingsInput(BaseModel):
525 |     season: str = Field(default=SeasonYear.default, description="The NBA season (e.g., '2023-24').")
526 |     season_type: str = Field(default="Regular Season", description="The season type (e.g., 'Regular Season').")
527 | 
528 | @mcp.tool()
529 | def nba_team_standings(season: str = SeasonYear.default, season_type: str = "Regular Season") -> List[Dict[str, Any]]:
530 |     """Fetch the NBA team standings for a given season and season type.
531 | 
532 |     Retrieves team standings data from `nba_api.stats.endpoints.leaguestandingsv3`.  This includes
533 |     wins, losses, win percentage, conference and division rankings, and other relevant information.
534 | 
535 |     **Args:**
536 | 
537 |         season (str, optional): The NBA season in "YYYY-YY" format (e.g., "2023-24"). Defaults to the
538 |             current season as defined by `nba_api.stats.library.parameters.SeasonYear.default`.
539 |         season_type (str, optional): The type of season. Valid options include:
540 |             * "Regular Season"
541 |             * "Playoffs"
542 |             * "Pre Season"
543 |             * "All Star"
544 |             Defaults to "Regular Season".
545 | 
546 |     **Returns:**
547 | 
548 |         List[Dict[str, Any]]: A list of dictionaries, each representing a team's standing and
549 |             associated statistics.  The structure is based on the `nba_api`'s `LeagueStandingsV3`
550 |             data frame output. Includes fields like:
551 | 
552 |             * "TeamID": The team's ID.
553 |             * "TeamCity": The team's city.
554 |             * "TeamName": The team's name.
555 |             * "Conference": The team's conference (e.g., "East", "West").
556 |             * "ConferenceRecord": The team's record within its conference.
557 |             * "PlayoffRank": The team's rank for playoff seeding within its conference.
558 |             * "WINS": Number of wins.
559 |             * "LOSSES": Number of losses.
560 |             * "Win_PCT": Win percentage.
561 |             * ...and many other statistical fields.
562 |             If an error occurs, returns a list containing a single dictionary with an "error" key.
563 |     """
564 |     try:
565 |         standings = leaguestandingsv3.LeagueStandingsV3(season=season, season_type=season_type)
566 |         return standings.get_data_frames()[0].to_dict('records')
567 |     except Exception as e:
568 |         return [{"error": str(e)}]
569 | 
570 | # -------------------------------------------------------------------------
571 | # nba_team_stats_by_name: Retrieve NBA Team Stats by Team Name
572 | # -------------------------------------------------------------------------
573 | class TeamStatsInput(BaseModel):
574 |     team_name: str = Field(..., description="The NBA team name (e.g., 'Cleveland Cavaliers').")
575 |     season_type: str = Field(default="Regular Season", description="The season type (e.g., 'Regular Season').")
576 |     per_mode: str = Field(default="PerGame", description="Options are Totals, PerGame, Per48, Per40, etc.")
577 | 
578 |     @field_validator("team_name")
579 |     def validate_team_name(cls, value):
580 |         found_teams = teams.find_teams_by_full_name(value)
581 |         if not found_teams:
582 |             raise ValueError(f"No NBA team found with the name '{value}'.")
583 |         return value
584 | 
585 | @mcp.tool()
586 | def nba_team_stats_by_name(team_name: str, season_type: str = "Regular Season", per_mode: str = "PerGame") -> List[Dict[str, Any]]:
587 |     """Fetches NBA team statistics from stats.nba.com using the team name.
588 | 
589 |     This tool retrieves detailed team statistics for a specified team, season type, and aggregation
590 |     method.  It first uses `nba_api.stats.static.teams` to find the team ID based on the provided
591 |     name, then uses `nba_api.stats.endpoints.teamyearbyyearstats` to get the statistics.
592 | 
593 |     **Args:**
594 | 
595 |         team_name (str): The full or partial name of the NBA team (e.g., "Celtics", "Boston Celtics").
596 |                           This argument is validated to ensure a team with provided name exists.
597 |         season_type (str, optional): The type of season. Valid options are:
598 |             * "Regular Season"
599 |             * "Playoffs"
600 |             * "Pre Season"
601 |              * "All Star"
602 |             Defaults to "Regular Season".
603 |         per_mode (str, optional):  Determines how the statistics are aggregated.  Valid options include:
604 |             * "Totals":  Total season statistics.
605 |             * "PerGame":  Stats averaged per game.
606 |             * "Per48": Stats per 48 minutes.
607 |             * "Per40": Stats per 40 minutes.
608 |             * "Per36" : Stats per 36 minutes
609 |             * ...and other per-minute options.
610 |             Defaults to "PerGame".
611 | 
612 |     **Returns:**
613 | 
614 |         List[Dict[str, Any]]: A list of dictionaries. If the data for provided `team_name` is not found
615 |             or is empty, this will contain a single error dictionary. Otherwise, each dictionary represents
616 |             a season for the team and includes a wide range of statistics, based on the
617 |             `nba_api`'s `TeamYearByYearStats` data frame output. Some key fields include:
618 | 
619 |             * "TEAM_ID": The team's ID.
620 |             * "TEAM_CITY": The team's city.
621 |             * "TEAM_NAME": The team's name.
622 |             * "YEAR": The year of the season.
623 |             * "WINS":, "LOSSES":, "Win_PCT": Basic win-loss information.
624 |             * Numerous statistical fields (e.g., "PTS", "REB", "AST", "STL", "BLK", etc.)
625 | 
626 |     """
627 |     try:
628 |         found_teams = teams.find_teams_by_full_name(team_name)
629 |         if not found_teams:
630 |             return [{"error": f"No NBA team found with the name '{team_name}'."}]
631 |         team_id = found_teams[0]['id']
632 |         team_stats = teamyearbyyearstats.TeamYearByYearStats(team_id=team_id, per_mode_simple=per_mode, season_type_all_star=season_type)
633 |         team_stats_data = team_stats.get_data_frames()[0]
634 |         if team_stats_data.empty:
635 |             return [{"error": f"No stats found for {team_name},  season_type {season_type}."}]
636 |         return team_stats_data.to_dict('records')
637 |     except Exception as e:
638 |         return [{"error": str(e)}]
639 | 
640 | # -------------------------------------------------------------------
641 | # 15) nba_all_teams_stats: Retrieve NBA Team Stats for All Teams
642 | # -------------------------------------------------------------------
643 | class AllTeamsStatsInput(BaseModel):
644 |     years: List[str] = Field(default=["2023"], description="A list of NBA season years (e.g., ['2022', '2023']).")
645 |     season_type: str = Field(default="Regular Season", description="The season type (e.g., 'Regular Season').")
646 | 
647 |     @field_validator("years")
648 |     def validate_years(cls, value):
649 |         for year in value:
650 |             if not year.isdigit() or len(year) != 4:
651 |                 raise ValueError("Each year must be a 4-digit string (e.g., '2023')")
652 |         return value
653 | 
654 | @mcp.tool()
655 | def nba_all_teams_stats(years: List[str] = ["2023"], season_type: str = "Regular Season") -> List[Dict[str, Any]]:
656 |     """Fetch the NBA team statistics for all teams for a given list of season years and a season type.
657 | 
658 |     This tool retrieves comprehensive team statistics for *all* NBA teams across one or more seasons.
659 |     It uses the `nba_api.stats.endpoints.leaguestandingsv3` endpoint to gather the data.  This is
660 |     useful for comparing teams or tracking league-wide trends over time.
661 | 
662 |     **Args:**
663 | 
664 |         years (List[str], optional): A list of NBA season years in "YYYY" format (e.g., ["2022", "2023"]).
665 |             Defaults to ["2023"].  Each year must be a 4-digit string.
666 |         season_type (str, optional): The type of season. Valid options are:
667 |             * "Regular Season"
668 |             * "Playoffs"
669 |             * "Pre Season"
670 |             * "All Star"
671 |             Defaults to "Regular Season".
672 | 
673 |     **Returns:**
674 | 
675 |         List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a team's
676 |             statistics *for a specific season*.  The data includes a wide range of statistics,
677 |             similar to `nba_team_standings`, but aggregated for all teams. Key fields:
678 | 
679 |             * "TeamID": The team's ID.
680 |             * "TeamCity": The team's city.
681 |             * "TeamName": The team's name.
682 |             * "Conference": The team's conference.
683 |             * "ConferenceRecord":  Record within the conference.
684 |             * "WINS", "LOSSES", "Win_PCT": Win-loss statistics.
685 |             * "Season": The year of the season (taken from the input `years`).
686 |             * ...and many other statistical fields.
687 |             If no data available, returns an error message.
688 |     """
689 |     all_seasons_stats = []
690 |     try:
691 |         for year in years:
692 |             team_stats = leaguestandingsv3.LeagueStandingsV3(
693 |                 season=year,
694 |                 season_type=season_type,
695 |                 league_id='00',
696 |             )
697 |             team_stats_data = team_stats.get_data_frames()[0]
698 |             if team_stats_data.empty:
699 |                 all_seasons_stats.append({"error": f"No stats found for season {year}, season_type {season_type}."})
700 |                 continue
701 |             for col in ['PlayoffRank', 'ConferenceRank', 'DivisionRank', 'WINS', 'LOSSES', 'ConferenceGamesBack', 'DivisionGamesBack']:
702 |                 if col in team_stats_data.columns:
703 |                     try:
704 |                         team_stats_data[col] = pd.to_numeric(team_stats_data[col], errors='coerce')
705 |                     except (ValueError, TypeError):
706 |                         pass
707 |             team_stats_data['Season'] = year
708 |             all_seasons_stats.extend(team_stats_data.to_dict('records'))
709 |         return all_seasons_stats
710 |     except Exception as e:
711 |         return [{"error": str(e)}]
712 | 
713 | # -------------------------------------------------------------------
714 | # 16) nba_player_game_logs: Retrieve NBA Player Game Logs and stats
715 | # -------------------------------------------------------------------
716 | @mcp.tool()
717 | def nba_player_game_logs(player_id: str, date_range: List[str], season_type: str = "Regular Season") -> List[Dict[str, Any]]:
718 |     """Obtain an NBA player's game statistics for dates within a specified date range.
719 | 
720 |     This tool retrieves individual game statistics for a given player within a specific date range. It uses
721 |     the `nba_api.stats.endpoints.leaguegamefinder` to find games played by the player and filters the
722 |     results to include only games within the specified dates.
723 | 
724 |     **Args:**
725 | 
726 |         player_id (str): The NBA player ID (e.g., "2544" for LeBron James).
727 |         date_range (List[str]): A list containing two strings representing the start and end dates
728 |             of the desired range, in "YYYY-MM-DD" format. Example: ["2024-01-01", "2024-01-31"]
729 |         season_type (str, optional): The type of season. Valid options are:
730 |             * "Regular Season"
731 |             * "Playoffs"
732 |             * "Pre Season"
733 |             * "All Star"
734 |             Defaults to "Regular Season".
735 | 
736 |     **Returns:**
737 | 
738 |         List[Dict[str, Any]]: A list of dictionaries, where each dictionary represents a game played
739 |             by the specified player within the provided date range. Includes all columns returned
740 |             by the underlying `nba_api` call, including detailed game statistics.  Key fields:
741 | 
742 |             *   "PLAYER_ID": The player's ID
743 |             *   "PLAYER_NAME": The player's name.
744 |             *   "TEAM_ID": The ID of the player's team.
745 |             *   "TEAM_ABBREVIATION": Team abbreviation.
746 |             *   "GAME_ID": The 10-digit Game ID.
747 |             *   "GAME_DATE": The date of the game.
748 |             *   "MATCHUP": Text showing the matchup.
749 |             *   "WL": Win ('W') or Loss ('L')
750 |             *   "MIN": Minutes played.
751 |             *   ...and many other statistical fields (PTS, REB, AST, etc.).
752 | 
753 |             If no games are found or an error occurs, returns a list containing a single dictionary
754 |             with an "error" key.
755 |     """
756 |     # Convert player_id to a string if it's not already.
757 |     if not isinstance(player_id, str):
758 |         player_id = str(player_id)
759 |         
760 |     try:
761 |         start_date_str, end_date_str = date_range
762 |         start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
763 |         end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
764 |         gamefinder = leaguegamefinder.LeagueGameFinder(
765 |             player_id_nullable=player_id,
766 |             season_type_nullable=season_type,
767 |             date_from_nullable=start_date.strftime('%m/%d/%Y'),
768 |             date_to_nullable=end_date.strftime('%m/%d/%Y')
769 |         )
770 |         games = gamefinder.get_data_frames()[0]
771 |         games['GAME_DATE'] = pd.to_datetime(games['GAME_DATE'])
772 |         all_dates = []
773 |         current_date = start_date
774 |         while current_date <= end_date:
775 |             all_dates.append(current_date)
776 |             current_date += timedelta(days=1)
777 |         games = games[games['GAME_DATE'].dt.date.isin([d.date() for d in all_dates])]
778 |         return games.to_dict('records')
779 |     except Exception as e:
780 |         return [{"error": str(e)}]
781 | 
782 | 
783 | if __name__ == "__main__":
784 |     try:
785 |         print("Starting MCP server 'nba_mcp_server' on 127.0.0.1:5000")
786 |         # Use this approach to keep the server running
787 |         mcp.run()
788 |     except Exception as e:
789 |         print(f"Error: {e}")
790 |         # Sleep before exiting to give time for error logs
791 |         time.sleep(5)
```