fpl-mcp-server 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
src/tools/teams.py ADDED
@@ -0,0 +1,397 @@
1
+ """FPL Team Tools - MCP tools for team information and fixture analysis."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from ..client import FPLClient
6
+ from ..constants import CHARACTER_LIMIT
7
+ from ..formatting import format_difficulty_indicator, format_team_details
8
+ from ..state import store
9
+ from ..utils import (
10
+ ResponseFormat,
11
+ check_and_truncate,
12
+ format_json_response,
13
+ handle_api_error,
14
+ )
15
+
16
+ # Import shared mcp instance
17
+ from . import mcp
18
+
19
+ # =============================================================================
20
+ # Pydantic Input Models
21
+ # =============================================================================
22
+
23
+
24
+ class GetTeamInfoInput(BaseModel):
25
+ """Input model for getting team information."""
26
+
27
+ model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
28
+
29
+ team_name: str = Field(
30
+ ...,
31
+ description="Team name or abbreviation (e.g., 'Arsenal', 'MCI', 'Liverpool')",
32
+ min_length=2,
33
+ max_length=50,
34
+ )
35
+ response_format: ResponseFormat = Field(
36
+ default=ResponseFormat.MARKDOWN,
37
+ description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
38
+ )
39
+
40
+
41
+ class ListAllTeamsInput(BaseModel):
42
+ """Input model for listing all teams."""
43
+
44
+ model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
45
+
46
+ response_format: ResponseFormat = Field(
47
+ default=ResponseFormat.MARKDOWN,
48
+ description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
49
+ )
50
+
51
+
52
+ class AnalyzeTeamFixturesInput(BaseModel):
53
+ """Input model for analyzing team fixtures."""
54
+
55
+ model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
56
+
57
+ team_name: str = Field(
58
+ ...,
59
+ description="Team name to analyze (e.g., 'Arsenal', 'Liverpool')",
60
+ min_length=2,
61
+ max_length=50,
62
+ )
63
+ num_gameweeks: int = Field(
64
+ default=5, description="Number of upcoming gameweeks to analyze", ge=1, le=15
65
+ )
66
+ response_format: ResponseFormat = Field(
67
+ default=ResponseFormat.MARKDOWN,
68
+ description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
69
+ )
70
+
71
+
72
+ # =============================================================================
73
+ # Helper Functions
74
+ # =============================================================================
75
+
76
+
77
+ async def _create_client():
78
+ """Create an unauthenticated FPL client for public API access and ensure data is loaded."""
79
+ client = FPLClient(store=store)
80
+ await store.ensure_bootstrap_data(client)
81
+ await store.ensure_fixtures_data(client)
82
+ return client
83
+
84
+
85
+ # =============================================================================
86
+ # MCP Tools
87
+ # =============================================================================
88
+
89
+
90
+ @mcp.tool(
91
+ name="fpl_get_team_info",
92
+ annotations={
93
+ "title": "Get FPL Team Information",
94
+ "readOnlyHint": True,
95
+ "destructiveHint": False,
96
+ "idempotentHint": True,
97
+ "openWorldHint": True,
98
+ },
99
+ )
100
+ async def fpl_get_team_info(params: GetTeamInfoInput) -> str:
101
+ """
102
+ Get detailed information about a specific Premier League team.
103
+
104
+ Returns team strength ratings for home/away attack/defence, useful for assessing
105
+ which teams have strong defensive or attacking potential.
106
+
107
+ Args:
108
+ params (GetTeamInfoInput): Validated input parameters containing:
109
+ - team_name (str): Team name or abbreviation (e.g., 'Arsenal', 'MCI')
110
+ - response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
111
+
112
+ Returns:
113
+ str: Detailed team information with strength ratings
114
+
115
+ Examples:
116
+ - Get Arsenal info: team_name="Arsenal"
117
+ - Use abbreviation: team_name="LIV"
118
+ - Get JSON format: team_name="Man City", response_format="json"
119
+
120
+ Error Handling:
121
+ - Returns error if no team found
122
+ - Returns error if multiple teams match (asks user to be more specific)
123
+ - Returns formatted error message if data unavailable
124
+ """
125
+ try:
126
+ await _create_client()
127
+ if not store.bootstrap_data:
128
+ return "Error: Team data not available. Please try again later."
129
+
130
+ matching_teams = [
131
+ t
132
+ for t in store.bootstrap_data.teams
133
+ if params.team_name.lower() in t.name.lower()
134
+ or params.team_name.lower() in t.short_name.lower()
135
+ ]
136
+
137
+ if not matching_teams:
138
+ return f"No team found matching '{params.team_name}'. Try using the full team name or abbreviation."
139
+
140
+ if len(matching_teams) > 1:
141
+ team_list = ", ".join([f"{t.name} ({t.short_name})" for t in matching_teams])
142
+ return f"Multiple teams found: {team_list}. Please be more specific."
143
+
144
+ team = matching_teams[0]
145
+ team_dict = store.get_team_by_id(team.id)
146
+
147
+ if params.response_format == ResponseFormat.JSON:
148
+ return format_json_response(team_dict)
149
+ else:
150
+ # Convert Team object to dict for formatter
151
+ team_dict = {
152
+ "name": team.name,
153
+ "short_name": team.short_name,
154
+ "strength": getattr(team, "strength", None),
155
+ "strength_overall_home": getattr(team, "strength_overall_home", None),
156
+ "strength_overall_away": getattr(team, "strength_overall_away", None),
157
+ "strength_attack_home": getattr(team, "strength_attack_home", None),
158
+ "strength_attack_away": getattr(team, "strength_attack_away", None),
159
+ "strength_defence_home": getattr(team, "strength_defence_home", None),
160
+ "strength_defence_away": getattr(team, "strength_defence_away", None),
161
+ }
162
+
163
+ result = format_team_details(team_dict)
164
+ truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
165
+ return truncated
166
+
167
+ except Exception as e:
168
+ return handle_api_error(e)
169
+
170
+
171
+ @mcp.tool(
172
+ name="fpl_list_all_teams",
173
+ annotations={
174
+ "title": "List All FPL Teams",
175
+ "readOnlyHint": True,
176
+ "destructiveHint": False,
177
+ "idempotentHint": True,
178
+ "openWorldHint": True,
179
+ },
180
+ )
181
+ async def fpl_list_all_teams(params: ListAllTeamsInput) -> str:
182
+ """
183
+ List all Premier League teams with their basic information.
184
+
185
+ Returns all 20 Premier League teams with their names, abbreviations, and average
186
+ strength ratings. Useful for finding exact team names or comparing team strengths.
187
+
188
+ Args:
189
+ params (ListAllTeamsInput): Validated input parameters containing:
190
+ - response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
191
+
192
+ Returns:
193
+ str: List of all teams with strength ratings
194
+
195
+ Examples:
196
+ - List all teams: response_format="markdown"
197
+ - Get as JSON: response_format="json"
198
+
199
+ Error Handling:
200
+ - Returns error if team data unavailable
201
+ - Returns formatted error message if API fails
202
+ """
203
+ try:
204
+ await _create_client()
205
+ teams = store.get_all_teams()
206
+ if not teams:
207
+ return "Error: Team data not available. Please try again later."
208
+
209
+ teams_sorted = sorted(teams, key=lambda t: t["name"])
210
+
211
+ if params.response_format == ResponseFormat.JSON:
212
+ return format_json_response({"count": len(teams_sorted), "teams": teams_sorted})
213
+ else:
214
+ output = ["**Premier League Teams:**\n"]
215
+
216
+ for team in teams_sorted:
217
+ strength_info = ""
218
+ if team.get("strength_overall_home") and team.get("strength_overall_away"):
219
+ avg_strength = (
220
+ team["strength_overall_home"] + team["strength_overall_away"]
221
+ ) / 2
222
+ strength_info = f" | Strength: {avg_strength:.0f}"
223
+
224
+ output.append(f"{team['name']:20s} ({team['short_name']}){strength_info}")
225
+
226
+ result = "\n".join(output)
227
+ truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
228
+ return truncated
229
+
230
+ except Exception as e:
231
+ return handle_api_error(e)
232
+
233
+
234
+ @mcp.tool(
235
+ name="fpl_analyze_team_fixtures",
236
+ annotations={
237
+ "title": "Analyze FPL Team Fixtures",
238
+ "readOnlyHint": True,
239
+ "destructiveHint": False,
240
+ "idempotentHint": True,
241
+ "openWorldHint": True,
242
+ },
243
+ )
244
+ async def fpl_analyze_team_fixtures(params: AnalyzeTeamFixturesInput) -> str:
245
+ """
246
+ Analyze upcoming fixtures for a specific Premier League team to assess difficulty.
247
+
248
+ Shows next N gameweeks with opponent strength and home/away status. Includes average
249
+ difficulty rating and assessment. Very useful for identifying good times to bring in
250
+ or sell team assets based on fixture difficulty.
251
+
252
+ Args:
253
+ params (AnalyzeTeamFixturesInput): Validated input parameters containing:
254
+ - team_name (str): Team name to analyze (e.g., 'Arsenal', 'Liverpool')
255
+ - num_gameweeks (int): Number of gameweeks to analyze, 1-15 (default: 5)
256
+ - response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
257
+
258
+ Returns:
259
+ str: Fixture difficulty analysis with ratings and assessment
260
+
261
+ Examples:
262
+ - Next 5 fixtures: team_name="Arsenal"
263
+ - Next 10 fixtures: team_name="Liverpool", num_gameweeks=10
264
+ - Long-term view: team_name="Man City", num_gameweeks=15
265
+
266
+ Error Handling:
267
+ - Returns error if team not found
268
+ - Returns error if no upcoming fixtures
269
+ - Returns formatted error message if data unavailable
270
+ """
271
+ try:
272
+ await _create_client()
273
+ if not store.bootstrap_data or not store.fixtures_data:
274
+ return "Error: Team or fixtures data not available. Please try again later."
275
+
276
+ matching_teams = [
277
+ t
278
+ for t in store.bootstrap_data.teams
279
+ if params.team_name.lower() in t.name.lower()
280
+ or params.team_name.lower() in t.short_name.lower()
281
+ ]
282
+
283
+ if not matching_teams:
284
+ return f"No team found matching '{params.team_name}'. Try using the full team name."
285
+
286
+ if len(matching_teams) > 1:
287
+ team_list = ", ".join([f"{t.name} ({t.short_name})" for t in matching_teams])
288
+ return f"Multiple teams found: {team_list}. Please be more specific."
289
+
290
+ team = matching_teams[0]
291
+
292
+ current_gw = store.get_current_gameweek()
293
+ if not current_gw:
294
+ return "Error: Could not determine current gameweek. Data may be unavailable."
295
+
296
+ start_gw = current_gw.id
297
+ # Fetch extra gameweeks to ensure we get enough after filtering out finished fixtures
298
+ end_gw = start_gw + params.num_gameweeks + 5 # Add buffer
299
+
300
+ team_fixtures = [
301
+ f
302
+ for f in store.fixtures_data
303
+ if (f.team_h == team.id or f.team_a == team.id)
304
+ and f.event
305
+ and start_gw <= f.event < end_gw
306
+ and not f.finished
307
+ ]
308
+
309
+ if not team_fixtures:
310
+ return f"No upcoming fixtures found for {team.name} in the next {params.num_gameweeks} gameweeks."
311
+
312
+ # Enrich and sort fixtures
313
+ team_fixtures_enriched = store.enrich_fixtures(team_fixtures)
314
+ team_fixtures_sorted = sorted(team_fixtures_enriched, key=lambda x: x.get("event") or 999)
315
+
316
+ # Limit to requested number of fixtures
317
+ team_fixtures_sorted = team_fixtures_sorted[: params.num_gameweeks]
318
+
319
+ if params.response_format == ResponseFormat.JSON:
320
+ total_difficulty = sum(
321
+ f.get(
322
+ "team_h_difficulty" if f.get("team_h") == team.id else "team_a_difficulty",
323
+ 3,
324
+ )
325
+ for f in team_fixtures_sorted
326
+ )
327
+ avg_difficulty = (
328
+ total_difficulty / len(team_fixtures_sorted) if team_fixtures_sorted else 0
329
+ )
330
+
331
+ result = {
332
+ "team": {"name": team.name, "short_name": team.short_name},
333
+ "num_fixtures": len(team_fixtures_sorted),
334
+ "average_difficulty": round(avg_difficulty, 2),
335
+ "assessment": "Favorable"
336
+ if avg_difficulty < 3
337
+ else "Moderate"
338
+ if avg_difficulty < 3.5
339
+ else "Difficult",
340
+ "fixtures": [
341
+ {
342
+ "gameweek": f.get("event"),
343
+ "opponent": f.get("team_a_name")
344
+ if f.get("team_h") == team.id
345
+ else f.get("team_h_name"),
346
+ "home_away": "H" if f.get("team_h") == team.id else "A",
347
+ "difficulty": f.get("team_h_difficulty")
348
+ if f.get("team_h") == team.id
349
+ else f.get("team_a_difficulty"),
350
+ "kickoff_time": f.get("kickoff_time", "TBD"),
351
+ }
352
+ for f in team_fixtures_sorted
353
+ ],
354
+ }
355
+ return format_json_response(result)
356
+ else:
357
+ output = [
358
+ f"**{team.name} ({team.short_name}) - Next {len(team_fixtures_sorted)} Fixtures**\n"
359
+ ]
360
+
361
+ total_difficulty = 0
362
+ for fixture in team_fixtures_sorted:
363
+ is_home = fixture.get("team_h") == team.id
364
+ opponent_name = (
365
+ fixture.get("team_a_name") if is_home else fixture.get("team_h_name", "Unknown")
366
+ )
367
+
368
+ difficulty = (
369
+ fixture.get("team_h_difficulty")
370
+ if is_home
371
+ else fixture.get("team_a_difficulty", 3)
372
+ )
373
+ home_away = "H" if is_home else "A"
374
+
375
+ total_difficulty += difficulty
376
+
377
+ difficulty_indicator = format_difficulty_indicator(difficulty)
378
+ output.append(
379
+ f"├─ GW{fixture.get('event')}: vs {opponent_name} ({home_away}) | "
380
+ f"Diff: {difficulty_indicator} ({difficulty}/5)"
381
+ )
382
+
383
+ avg_difficulty = total_difficulty / len(team_fixtures_sorted)
384
+ output.extend(
385
+ [
386
+ "",
387
+ f"**Average Difficulty:** {avg_difficulty:.1f}/5",
388
+ f"**Assessment:** {'Favorable' if avg_difficulty < 3 else 'Moderate' if avg_difficulty < 3.5 else 'Difficult'} run of fixtures",
389
+ ]
390
+ )
391
+
392
+ result = "\n".join(output)
393
+ truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
394
+ return truncated
395
+
396
+ except Exception as e:
397
+ return handle_api_error(e)