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/leagues.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""FPL League Tools - MCP tools for league standings and manager analysis."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
4
|
+
|
|
5
|
+
from ..client import FPLClient
|
|
6
|
+
from ..constants import CHARACTER_LIMIT
|
|
7
|
+
from ..formatting import format_manager_squad
|
|
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 GetLeagueStandingsInput(BaseModel):
|
|
25
|
+
"""Input model for getting league standings."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
28
|
+
|
|
29
|
+
league_id: int = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="League ID from FPL URL (e.g., for /leagues/12345/standings/ use 12345)",
|
|
32
|
+
ge=1,
|
|
33
|
+
)
|
|
34
|
+
page: int = Field(default=1, description="Page number for pagination (default: 1)", ge=1)
|
|
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 GetManagerGameweekTeamInput(BaseModel):
|
|
42
|
+
"""Input model for getting manager's team for a gameweek."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
45
|
+
|
|
46
|
+
manager_name: str = Field(
|
|
47
|
+
...,
|
|
48
|
+
description="Manager's name or team name (e.g., 'John Smith', 'FC Warriors')",
|
|
49
|
+
min_length=2,
|
|
50
|
+
max_length=100,
|
|
51
|
+
)
|
|
52
|
+
league_id: int = Field(..., description="League ID where the manager is found", ge=1)
|
|
53
|
+
gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CompareManagersInput(BaseModel):
|
|
57
|
+
"""Input model for comparing managers."""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
60
|
+
|
|
61
|
+
manager_names: list[str] = Field(
|
|
62
|
+
...,
|
|
63
|
+
description="List of 2-4 manager names to compare (e.g., ['John', 'Sarah', 'Mike'])",
|
|
64
|
+
min_length=2,
|
|
65
|
+
max_length=4,
|
|
66
|
+
)
|
|
67
|
+
league_id: int = Field(..., description="League ID where managers are found", ge=1)
|
|
68
|
+
gameweek: int = Field(..., description="Gameweek number to compare (1-38)", ge=1, le=38)
|
|
69
|
+
|
|
70
|
+
@field_validator("manager_names")
|
|
71
|
+
@classmethod
|
|
72
|
+
def validate_manager_names(cls, v: list[str]) -> list[str]:
|
|
73
|
+
if len(v) < 2:
|
|
74
|
+
raise ValueError("Must provide at least 2 managers to compare")
|
|
75
|
+
if len(v) > 4:
|
|
76
|
+
raise ValueError("Cannot compare more than 4 managers at once")
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class GetManagerSquadInput(BaseModel):
|
|
81
|
+
"""Input model for getting manager's squad by team ID."""
|
|
82
|
+
|
|
83
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
84
|
+
|
|
85
|
+
team_id: int = Field(
|
|
86
|
+
...,
|
|
87
|
+
description="Manager's team ID (entry ID)",
|
|
88
|
+
ge=1,
|
|
89
|
+
)
|
|
90
|
+
gameweek: int | None = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description="Gameweek number (1-38). If not provided, uses current gameweek",
|
|
93
|
+
ge=1,
|
|
94
|
+
le=38,
|
|
95
|
+
)
|
|
96
|
+
response_format: ResponseFormat = Field(
|
|
97
|
+
default=ResponseFormat.MARKDOWN,
|
|
98
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Helper Functions
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _create_client():
|
|
108
|
+
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
109
|
+
client = FPLClient(store=store)
|
|
110
|
+
await store.ensure_bootstrap_data(client)
|
|
111
|
+
await store.ensure_fixtures_data(client)
|
|
112
|
+
return client
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# MCP Tools
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@mcp.tool(
|
|
121
|
+
name="fpl_get_league_standings",
|
|
122
|
+
annotations={
|
|
123
|
+
"title": "Get FPL League Standings",
|
|
124
|
+
"readOnlyHint": True,
|
|
125
|
+
"destructiveHint": False,
|
|
126
|
+
"idempotentHint": True,
|
|
127
|
+
"openWorldHint": True,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
async def fpl_get_league_standings(params: GetLeagueStandingsInput) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get standings for a specific Fantasy Premier League league.
|
|
133
|
+
|
|
134
|
+
Returns manager rankings, points, team names, and rank changes within the league.
|
|
135
|
+
Supports pagination for large leagues. Find league ID in the FPL website URL
|
|
136
|
+
(e.g., for /leagues/12345/standings/ use league_id=12345).
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
params (GetLeagueStandingsInput): Validated input parameters containing:
|
|
140
|
+
- league_id (int): League ID from FPL URL
|
|
141
|
+
- page (int): Page number for pagination (default: 1)
|
|
142
|
+
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
str: League standings with rankings and pagination info
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
- View league: league_id=12345
|
|
149
|
+
- Next page: league_id=12345, page=2
|
|
150
|
+
- Get as JSON: league_id=12345, response_format="json"
|
|
151
|
+
|
|
152
|
+
Error Handling:
|
|
153
|
+
- Returns error if league not found
|
|
154
|
+
- Returns error if page number invalid
|
|
155
|
+
- Returns formatted error message if API fails
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
client = await _create_client()
|
|
159
|
+
standings_data = await client.get_league_standings(
|
|
160
|
+
league_id=params.league_id, page_standings=params.page
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
league_data = standings_data.get("league", {})
|
|
164
|
+
standings = standings_data.get("standings", {})
|
|
165
|
+
results = standings.get("results", [])
|
|
166
|
+
|
|
167
|
+
if not results:
|
|
168
|
+
return f"No standings found for league ID {params.league_id}. The league may not exist or you may not have access."
|
|
169
|
+
|
|
170
|
+
if params.response_format == ResponseFormat.JSON:
|
|
171
|
+
result = {
|
|
172
|
+
"league": {
|
|
173
|
+
"id": params.league_id,
|
|
174
|
+
"name": league_data.get("name", f"League {params.league_id}"),
|
|
175
|
+
},
|
|
176
|
+
"pagination": {
|
|
177
|
+
"page": params.page,
|
|
178
|
+
"has_next": standings.get("has_next", False),
|
|
179
|
+
},
|
|
180
|
+
"standings": [
|
|
181
|
+
{
|
|
182
|
+
"rank": entry["rank"],
|
|
183
|
+
"last_rank": entry["last_rank"],
|
|
184
|
+
"rank_change": entry["rank"] - entry["last_rank"],
|
|
185
|
+
"team_id": entry["entry"],
|
|
186
|
+
"entry_name": entry["entry_name"],
|
|
187
|
+
"player_name": entry["player_name"],
|
|
188
|
+
"gameweek_points": entry["event_total"],
|
|
189
|
+
"total_points": entry["total"],
|
|
190
|
+
}
|
|
191
|
+
for entry in results
|
|
192
|
+
],
|
|
193
|
+
}
|
|
194
|
+
return format_json_response(result)
|
|
195
|
+
else:
|
|
196
|
+
output = [
|
|
197
|
+
f"**{league_data.get('name', f'League {params.league_id}')}**",
|
|
198
|
+
f"Page: {params.page}",
|
|
199
|
+
"",
|
|
200
|
+
"**Standings:**",
|
|
201
|
+
"",
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for entry in results:
|
|
205
|
+
rank_change = entry["rank"] - entry["last_rank"]
|
|
206
|
+
rank_indicator = "ā" if rank_change < 0 else "ā" if rank_change > 0 else "="
|
|
207
|
+
|
|
208
|
+
output.append(
|
|
209
|
+
f"{entry['rank']:3d}. {rank_indicator} {entry['entry_name']:30s} | "
|
|
210
|
+
f"{entry['player_name']:20s} | "
|
|
211
|
+
f"Team ID: {entry['entry']:7d} | "
|
|
212
|
+
f"GW: {entry['event_total']:3d} | Total: {entry['total']:4d}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if standings.get("has_next"):
|
|
216
|
+
output.append(
|
|
217
|
+
f"\nš More entries available. Use page={params.page + 1} to see the next page."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
result = "\n".join(output)
|
|
221
|
+
truncated, _ = check_and_truncate(
|
|
222
|
+
result, CHARACTER_LIMIT, f"Use page={params.page + 1} for more results"
|
|
223
|
+
)
|
|
224
|
+
return truncated
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return handle_api_error(e)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool(
|
|
231
|
+
name="fpl_get_manager_gameweek_team",
|
|
232
|
+
annotations={
|
|
233
|
+
"title": "Get Manager's FPL Gameweek Team",
|
|
234
|
+
"readOnlyHint": True,
|
|
235
|
+
"destructiveHint": False,
|
|
236
|
+
"idempotentHint": True,
|
|
237
|
+
"openWorldHint": True,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
async def fpl_get_manager_gameweek_team(params: GetManagerGameweekTeamInput) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Get a manager's team selection for a specific gameweek.
|
|
243
|
+
|
|
244
|
+
Shows the 15 players picked, captain/vice-captain choices, formation, points scored,
|
|
245
|
+
transfers made, and automatic substitutions. Find manager by their name or team name
|
|
246
|
+
within a specific league.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
params (GetManagerGameweekTeamInput): Validated input parameters containing:
|
|
250
|
+
- manager_name (str): Manager's name or team name
|
|
251
|
+
- league_id (int): League ID where manager is found
|
|
252
|
+
- gameweek (int): Gameweek number (1-38)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
str: Complete team sheet with starting XI, bench, and statistics
|
|
256
|
+
|
|
257
|
+
Examples:
|
|
258
|
+
- View team: manager_name="John Smith", league_id=12345, gameweek=13
|
|
259
|
+
- Check transfers: manager_name="FC Warriors", league_id=12345, gameweek=15
|
|
260
|
+
|
|
261
|
+
Error Handling:
|
|
262
|
+
- Returns error if manager not found in league
|
|
263
|
+
- Returns helpful message suggesting correct name if ambiguous
|
|
264
|
+
- Returns formatted error message if API fails
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
client = await _create_client()
|
|
268
|
+
|
|
269
|
+
# Find manager in league
|
|
270
|
+
manager_info = await store.find_manager_by_name(
|
|
271
|
+
client, params.league_id, params.manager_name
|
|
272
|
+
)
|
|
273
|
+
if not manager_info:
|
|
274
|
+
return f"Could not find manager '{params.manager_name}' in league ID {params.league_id}. Try using the exact name from the league standings."
|
|
275
|
+
|
|
276
|
+
manager_team_id = manager_info["entry"]
|
|
277
|
+
|
|
278
|
+
# Fetch gameweek picks from API
|
|
279
|
+
picks_data = await client.get_manager_gameweek_picks(manager_team_id, params.gameweek)
|
|
280
|
+
|
|
281
|
+
picks = picks_data.get("picks", [])
|
|
282
|
+
entry_history = picks_data.get("entry_history", {})
|
|
283
|
+
auto_subs = picks_data.get("automatic_subs", [])
|
|
284
|
+
|
|
285
|
+
if not picks:
|
|
286
|
+
return f"No team data found for {manager_info['player_name']} in gameweek {params.gameweek}. The gameweek may not have started yet."
|
|
287
|
+
|
|
288
|
+
# Rehydrate player names
|
|
289
|
+
element_ids = [pick["element"] for pick in picks]
|
|
290
|
+
players_info = store.rehydrate_player_names(element_ids)
|
|
291
|
+
|
|
292
|
+
result = format_manager_squad(
|
|
293
|
+
team_name=manager_info["entry_name"],
|
|
294
|
+
player_name=manager_info["player_name"],
|
|
295
|
+
team_id=manager_team_id,
|
|
296
|
+
gameweek=params.gameweek,
|
|
297
|
+
entry_history=entry_history,
|
|
298
|
+
picks=picks,
|
|
299
|
+
players_info=players_info,
|
|
300
|
+
active_chip=picks_data.get("active_chip"),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if auto_subs:
|
|
304
|
+
result += "\n\n**Automatic Substitutions:**"
|
|
305
|
+
for sub in auto_subs:
|
|
306
|
+
player_out = store.get_player_name(sub["element_out"])
|
|
307
|
+
player_in = store.get_player_name(sub["element_in"])
|
|
308
|
+
result += f"\nāā {player_out} ā {player_in}"
|
|
309
|
+
|
|
310
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
311
|
+
return truncated
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
return handle_api_error(e)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool(
|
|
318
|
+
name="fpl_compare_managers",
|
|
319
|
+
annotations={
|
|
320
|
+
"title": "Compare FPL Managers",
|
|
321
|
+
"readOnlyHint": True,
|
|
322
|
+
"destructiveHint": False,
|
|
323
|
+
"idempotentHint": True,
|
|
324
|
+
"openWorldHint": True,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
async def fpl_compare_managers(params: CompareManagersInput) -> str:
|
|
328
|
+
"""
|
|
329
|
+
Compare multiple managers' teams for a specific gameweek side-by-side.
|
|
330
|
+
|
|
331
|
+
Shows differences in player selection, captaincy choices, points scored, common
|
|
332
|
+
players, and unique differentials. Useful for mini-league rivalry analysis and
|
|
333
|
+
understanding what sets top managers apart.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
params (CompareManagersInput): Validated input parameters containing:
|
|
337
|
+
- manager_names (list[str]): 2-4 manager names to compare
|
|
338
|
+
- league_id (int): League ID where managers are found
|
|
339
|
+
- gameweek (int): Gameweek number to compare (1-38)
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
str: Side-by-side manager comparison with differentials
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
- Compare 2 managers: manager_names=["John", "Sarah"], league_id=12345, gameweek=13
|
|
346
|
+
- Compare 4 managers: manager_names=["A", "B", "C", "D"], league_id=12345, gameweek=10
|
|
347
|
+
|
|
348
|
+
Error Handling:
|
|
349
|
+
- Returns error if fewer than 2 or more than 4 managers provided
|
|
350
|
+
- Returns error if any manager not found (with helpful message)
|
|
351
|
+
- Returns formatted error message if API fails
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
client = await _create_client()
|
|
355
|
+
|
|
356
|
+
# Find all managers
|
|
357
|
+
manager_ids = []
|
|
358
|
+
manager_infos = []
|
|
359
|
+
for name in params.manager_names:
|
|
360
|
+
manager_info = await store.find_manager_by_name(client, params.league_id, name)
|
|
361
|
+
if not manager_info:
|
|
362
|
+
return f"Could not find manager '{name}' in league ID {params.league_id}. Try using the exact name from league standings."
|
|
363
|
+
manager_ids.append(manager_info["entry"])
|
|
364
|
+
manager_infos.append(manager_info)
|
|
365
|
+
|
|
366
|
+
# Fetch all teams
|
|
367
|
+
teams_data = []
|
|
368
|
+
for team_id in manager_ids:
|
|
369
|
+
picks_data = await client.get_manager_gameweek_picks(team_id, params.gameweek)
|
|
370
|
+
teams_data.append((team_id, picks_data))
|
|
371
|
+
|
|
372
|
+
output = [f"**Manager Comparison - Gameweek {params.gameweek}**\n"]
|
|
373
|
+
|
|
374
|
+
# Performance summary
|
|
375
|
+
output.append("**Performance Summary:**")
|
|
376
|
+
for i, (_team_id, data) in enumerate(teams_data):
|
|
377
|
+
entry_history = data.get("entry_history", {})
|
|
378
|
+
manager_info = manager_infos[i]
|
|
379
|
+
output.append(
|
|
380
|
+
f"āā {manager_info['player_name']} ({manager_info['entry_name']}): "
|
|
381
|
+
f"{entry_history.get('points', 0)}pts | "
|
|
382
|
+
f"Rank: {entry_history.get('overall_rank', 'N/A'):,} | "
|
|
383
|
+
f"Transfers: {entry_history.get('event_transfers', 0)} "
|
|
384
|
+
f"(-{entry_history.get('event_transfers_cost', 0)}pts)"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Captain choices
|
|
388
|
+
output.append("\n**Captain Choices:**")
|
|
389
|
+
for i, (_team_id, data) in enumerate(teams_data):
|
|
390
|
+
picks = data.get("picks", [])
|
|
391
|
+
captain_pick = next((p for p in picks if p["is_captain"]), None)
|
|
392
|
+
if captain_pick:
|
|
393
|
+
captain_name = store.get_player_name(captain_pick["element"])
|
|
394
|
+
multiplier = captain_pick.get("multiplier", 2)
|
|
395
|
+
manager_info = manager_infos[i]
|
|
396
|
+
output.append(f"āā {manager_info['player_name']}: {captain_name} (x{multiplier})")
|
|
397
|
+
|
|
398
|
+
# Find common and unique players
|
|
399
|
+
all_players = {}
|
|
400
|
+
for _i, (team_id, data) in enumerate(teams_data):
|
|
401
|
+
picks = data.get("picks", [])
|
|
402
|
+
starting_xi = [p["element"] for p in picks if p["position"] <= 11]
|
|
403
|
+
all_players[team_id] = set(starting_xi)
|
|
404
|
+
|
|
405
|
+
common_players = set.intersection(*all_players.values()) if len(all_players) > 1 else set()
|
|
406
|
+
|
|
407
|
+
if common_players:
|
|
408
|
+
output.append(f"\n**Common Players ({len(common_players)}):**")
|
|
409
|
+
for element_id in list(common_players)[:10]:
|
|
410
|
+
player_name = store.get_player_name(element_id)
|
|
411
|
+
output.append(f"āā {player_name}")
|
|
412
|
+
|
|
413
|
+
# Unique players per team (differentials)
|
|
414
|
+
output.append("\n**Unique Selections (Differentials):**")
|
|
415
|
+
for i, team_id in enumerate(manager_ids):
|
|
416
|
+
other_teams = [t for t in manager_ids if t != team_id]
|
|
417
|
+
other_players = set()
|
|
418
|
+
for other_id in other_teams:
|
|
419
|
+
other_players.update(all_players.get(other_id, set()))
|
|
420
|
+
|
|
421
|
+
unique = all_players[team_id] - other_players
|
|
422
|
+
if unique:
|
|
423
|
+
manager_info = manager_infos[i]
|
|
424
|
+
output.append(f"\n{manager_info['player_name']} only:")
|
|
425
|
+
for element_id in list(unique)[:5]:
|
|
426
|
+
player_name = store.get_player_name(element_id)
|
|
427
|
+
output.append(f"āā {player_name}")
|
|
428
|
+
|
|
429
|
+
result = "\n".join(output)
|
|
430
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
431
|
+
return truncated
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
return handle_api_error(e)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@mcp.tool(
|
|
438
|
+
name="fpl_get_manager_squad",
|
|
439
|
+
annotations={
|
|
440
|
+
"title": "Get Manager's FPL Squad by Team ID",
|
|
441
|
+
"readOnlyHint": True,
|
|
442
|
+
"destructiveHint": False,
|
|
443
|
+
"idempotentHint": True,
|
|
444
|
+
"openWorldHint": True,
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
async def fpl_get_manager_squad(params: GetManagerSquadInput) -> str:
|
|
448
|
+
"""
|
|
449
|
+
Get a manager's squad selection for a specific gameweek using their team ID.
|
|
450
|
+
|
|
451
|
+
Shows the 15 players picked, captain/vice-captain choices, formation, points scored,
|
|
452
|
+
transfers made, and automatic substitutions. This is a simpler alternative to
|
|
453
|
+
fpl_get_manager_gameweek_team that uses team ID directly instead of requiring
|
|
454
|
+
manager name and league ID lookup.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
params (GetManagerSquadInput): Validated input parameters containing:
|
|
458
|
+
- team_id (int): Manager's team ID (entry ID)
|
|
459
|
+
- gameweek (int | None): Gameweek number (1-38), defaults to current GW
|
|
460
|
+
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
str: Complete team sheet with starting XI, bench, and statistics
|
|
464
|
+
|
|
465
|
+
Examples:
|
|
466
|
+
- View current team: team_id=123456
|
|
467
|
+
- View specific gameweek: team_id=123456, gameweek=13
|
|
468
|
+
- Get as JSON: team_id=123456, gameweek=15, response_format="json"
|
|
469
|
+
|
|
470
|
+
Error Handling:
|
|
471
|
+
- Returns error if team ID not found
|
|
472
|
+
- Returns error if gameweek not started yet
|
|
473
|
+
- Returns formatted error message if API fails
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
client = await _create_client()
|
|
477
|
+
|
|
478
|
+
# Fetch manager entry to get team name
|
|
479
|
+
entry_data = await client.get_manager_entry(params.team_id)
|
|
480
|
+
team_name = entry_data.get("name", "Unknown Team")
|
|
481
|
+
player_name = f"{entry_data.get('player_first_name', '')} {entry_data.get('player_last_name', '')}".strip()
|
|
482
|
+
|
|
483
|
+
# Determine which gameweek to use
|
|
484
|
+
gameweek = params.gameweek
|
|
485
|
+
if gameweek is None:
|
|
486
|
+
current_gw = store.get_current_gameweek()
|
|
487
|
+
if not current_gw:
|
|
488
|
+
return (
|
|
489
|
+
"Error: Could not determine current gameweek. Please specify a gameweek number."
|
|
490
|
+
)
|
|
491
|
+
gameweek = current_gw.id
|
|
492
|
+
|
|
493
|
+
# Fetch gameweek picks from API
|
|
494
|
+
picks_data = await client.get_manager_gameweek_picks(params.team_id, gameweek)
|
|
495
|
+
|
|
496
|
+
picks = picks_data.get("picks", [])
|
|
497
|
+
entry_history = picks_data.get("entry_history", {})
|
|
498
|
+
auto_subs = picks_data.get("automatic_subs", [])
|
|
499
|
+
|
|
500
|
+
if not picks:
|
|
501
|
+
return f"No team data found for team ID {params.team_id} in gameweek {gameweek}. The gameweek may not have started yet or the team ID may be invalid."
|
|
502
|
+
|
|
503
|
+
# Rehydrate player names
|
|
504
|
+
element_ids = [pick["element"] for pick in picks]
|
|
505
|
+
players_info = store.rehydrate_player_names(element_ids)
|
|
506
|
+
|
|
507
|
+
if params.response_format == ResponseFormat.JSON:
|
|
508
|
+
starting_xi = [p for p in picks if p["position"] <= 11]
|
|
509
|
+
bench = [p for p in picks if p["position"] > 11]
|
|
510
|
+
|
|
511
|
+
result = {
|
|
512
|
+
"team_id": params.team_id,
|
|
513
|
+
"team_name": team_name,
|
|
514
|
+
"player_name": player_name,
|
|
515
|
+
"gameweek": gameweek,
|
|
516
|
+
"stats": {
|
|
517
|
+
"points": entry_history.get("points", 0),
|
|
518
|
+
"total_points": entry_history.get("total_points", 0),
|
|
519
|
+
"overall_rank": entry_history.get("overall_rank"),
|
|
520
|
+
"team_value": entry_history.get("value", 0) / 10,
|
|
521
|
+
"bank": entry_history.get("bank", 0) / 10,
|
|
522
|
+
"transfers": entry_history.get("event_transfers", 0),
|
|
523
|
+
"transfer_cost": entry_history.get("event_transfers_cost", 0),
|
|
524
|
+
"points_on_bench": entry_history.get("points_on_bench", 0),
|
|
525
|
+
},
|
|
526
|
+
"active_chip": picks_data.get("active_chip"),
|
|
527
|
+
"starting_xi": [
|
|
528
|
+
{
|
|
529
|
+
"position": pick["position"],
|
|
530
|
+
"player_name": players_info.get(pick["element"], {}).get(
|
|
531
|
+
"web_name", "Unknown"
|
|
532
|
+
),
|
|
533
|
+
"team": players_info.get(pick["element"], {}).get("team", "UNK"),
|
|
534
|
+
"player_position": players_info.get(pick["element"], {}).get(
|
|
535
|
+
"position", "UNK"
|
|
536
|
+
),
|
|
537
|
+
"price": players_info.get(pick["element"], {}).get("price", 0),
|
|
538
|
+
"is_captain": pick["is_captain"],
|
|
539
|
+
"is_vice_captain": pick["is_vice_captain"],
|
|
540
|
+
"multiplier": pick["multiplier"],
|
|
541
|
+
}
|
|
542
|
+
for pick in starting_xi
|
|
543
|
+
],
|
|
544
|
+
"bench": [
|
|
545
|
+
{
|
|
546
|
+
"position": pick["position"],
|
|
547
|
+
"player_name": players_info.get(pick["element"], {}).get(
|
|
548
|
+
"web_name", "Unknown"
|
|
549
|
+
),
|
|
550
|
+
"team": players_info.get(pick["element"], {}).get("team", "UNK"),
|
|
551
|
+
"player_position": players_info.get(pick["element"], {}).get(
|
|
552
|
+
"position", "UNK"
|
|
553
|
+
),
|
|
554
|
+
"price": players_info.get(pick["element"], {}).get("price", 0),
|
|
555
|
+
}
|
|
556
|
+
for pick in bench
|
|
557
|
+
],
|
|
558
|
+
"automatic_subs": [
|
|
559
|
+
{
|
|
560
|
+
"player_out": store.get_player_name(sub["element_out"]),
|
|
561
|
+
"player_in": store.get_player_name(sub["element_in"]),
|
|
562
|
+
}
|
|
563
|
+
for sub in auto_subs
|
|
564
|
+
],
|
|
565
|
+
}
|
|
566
|
+
return format_json_response(result)
|
|
567
|
+
else:
|
|
568
|
+
result = format_manager_squad(
|
|
569
|
+
team_name=team_name,
|
|
570
|
+
player_name=player_name,
|
|
571
|
+
team_id=params.team_id,
|
|
572
|
+
gameweek=gameweek,
|
|
573
|
+
entry_history=entry_history,
|
|
574
|
+
picks=picks,
|
|
575
|
+
players_info=players_info,
|
|
576
|
+
active_chip=picks_data.get("active_chip"),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if auto_subs:
|
|
580
|
+
result += "\n\n**Automatic Substitutions:**"
|
|
581
|
+
for sub in auto_subs:
|
|
582
|
+
player_out = store.get_player_name(sub["element_out"])
|
|
583
|
+
player_in = store.get_player_name(sub["element_in"])
|
|
584
|
+
result += f"\nāā {player_out} ā {player_in}"
|
|
585
|
+
|
|
586
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
587
|
+
return truncated
|
|
588
|
+
|
|
589
|
+
except Exception as e:
|
|
590
|
+
return handle_api_error(e)
|