# Directory Structure
```
├── Dockerfile
├── nba_server.py
├── README.md
└── requirements.txt
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | [](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)
```