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.
- fpl_mcp_server-0.1.3.dist-info/METADATA +137 -0
- fpl_mcp_server-0.1.3.dist-info/RECORD +33 -0
- fpl_mcp_server-0.1.3.dist-info/WHEEL +4 -0
- fpl_mcp_server-0.1.3.dist-info/entry_points.txt +2 -0
- fpl_mcp_server-0.1.3.dist-info/licenses/LICENSE +21 -0
- src/cache.py +162 -0
- src/client.py +273 -0
- src/config.py +33 -0
- src/constants.py +118 -0
- src/exceptions.py +114 -0
- src/formatting.py +299 -0
- src/main.py +41 -0
- src/models.py +526 -0
- src/prompts/__init__.py +18 -0
- src/prompts/chips.py +127 -0
- src/prompts/league_analysis.py +250 -0
- src/prompts/player_analysis.py +141 -0
- src/prompts/squad_analysis.py +136 -0
- src/prompts/team_analysis.py +121 -0
- src/prompts/transfers.py +167 -0
- src/rate_limiter.py +101 -0
- src/resources/__init__.py +13 -0
- src/resources/bootstrap.py +183 -0
- src/state.py +443 -0
- src/tools/__init__.py +25 -0
- src/tools/fixtures.py +162 -0
- src/tools/gameweeks.py +392 -0
- src/tools/leagues.py +590 -0
- src/tools/players.py +840 -0
- src/tools/teams.py +397 -0
- src/tools/transfers.py +629 -0
- src/utils.py +226 -0
- src/validators.py +290 -0
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)
|