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/transfers.py
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""FPL Transfer Tools - MCP tools for transfer statistics and live trends."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from ..client import FPLClient
|
|
6
|
+
from ..constants import CHARACTER_LIMIT
|
|
7
|
+
from ..state import store
|
|
8
|
+
from ..utils import (
|
|
9
|
+
ResponseFormat,
|
|
10
|
+
check_and_truncate,
|
|
11
|
+
format_json_response,
|
|
12
|
+
format_player_price,
|
|
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 GetPlayerTransfersByGameweekInput(BaseModel):
|
|
25
|
+
"""Input model for getting player transfer statistics."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
28
|
+
|
|
29
|
+
player_name: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="Player name (e.g., 'Haaland', 'Salah')",
|
|
32
|
+
min_length=2,
|
|
33
|
+
max_length=100,
|
|
34
|
+
)
|
|
35
|
+
gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GetTopTransferredPlayersInput(BaseModel):
|
|
39
|
+
"""Input model for getting top transferred players."""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
42
|
+
|
|
43
|
+
limit: int = Field(default=10, description="Number of players to return (1-50)", ge=1, le=50)
|
|
44
|
+
response_format: ResponseFormat = Field(
|
|
45
|
+
default=ResponseFormat.MARKDOWN,
|
|
46
|
+
description="Output format: 'markdown' or 'json'",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GetManagerTransfersByGameweekInput(BaseModel):
|
|
51
|
+
"""Input model for getting manager transfers."""
|
|
52
|
+
|
|
53
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
54
|
+
|
|
55
|
+
team_id: int = Field(..., description="Manager's team ID (entry ID) from FPL", ge=1)
|
|
56
|
+
gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Helper Functions
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _create_client():
|
|
65
|
+
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
66
|
+
client = FPLClient(store=store)
|
|
67
|
+
await store.ensure_bootstrap_data(client)
|
|
68
|
+
await store.ensure_fixtures_data(client)
|
|
69
|
+
return client
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# MCP Tools
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@mcp.tool(
|
|
78
|
+
name="fpl_get_player_transfers_by_gameweek",
|
|
79
|
+
annotations={
|
|
80
|
+
"title": "Get FPL Player Transfer Statistics",
|
|
81
|
+
"readOnlyHint": True,
|
|
82
|
+
"destructiveHint": False,
|
|
83
|
+
"idempotentHint": True,
|
|
84
|
+
"openWorldHint": True,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
async def fpl_get_player_transfers_by_gameweek(
|
|
88
|
+
params: GetPlayerTransfersByGameweekInput,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Get transfer statistics for a specific player in a specific gameweek.
|
|
92
|
+
|
|
93
|
+
Shows transfers in, transfers out, net transfers, ownership data, and performance.
|
|
94
|
+
Useful for understanding how manager sentiment towards a player changed during a
|
|
95
|
+
specific gameweek and correlation with performance.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
params (GetPlayerTransfersByGameweekInput): Validated input parameters containing:
|
|
99
|
+
- player_name (str): Player name (e.g., 'Haaland', 'Salah')
|
|
100
|
+
- gameweek (int): Gameweek number between 1-38
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: Transfer statistics and performance for the gameweek
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
- Check Haaland GW20: player_name="Haaland", gameweek=20
|
|
107
|
+
- Salah transfers: player_name="Salah", gameweek=15
|
|
108
|
+
|
|
109
|
+
Error Handling:
|
|
110
|
+
- Returns error if player not found
|
|
111
|
+
- Suggests using fpl_find_player if name ambiguous
|
|
112
|
+
- Returns error if no data for gameweek
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
client = await _create_client()
|
|
116
|
+
|
|
117
|
+
# Find player by name
|
|
118
|
+
matches = store.find_players_by_name(params.player_name, fuzzy=True)
|
|
119
|
+
if not matches:
|
|
120
|
+
return f"No player found matching '{params.player_name}'. Use fpl_search_players to find the correct name."
|
|
121
|
+
|
|
122
|
+
if len(matches) > 1 and matches[0][1] < 0.95:
|
|
123
|
+
return f"Ambiguous player name. Use fpl_find_player to see all matches for '{params.player_name}'"
|
|
124
|
+
|
|
125
|
+
player = matches[0][0]
|
|
126
|
+
player_id = player.id
|
|
127
|
+
|
|
128
|
+
# Fetch detailed summary from API
|
|
129
|
+
summary_data = await client.get_element_summary(player_id)
|
|
130
|
+
history = summary_data.get("history", [])
|
|
131
|
+
|
|
132
|
+
# Find the specific gameweek in history
|
|
133
|
+
gw_data = next((gw for gw in history if gw.get("round") == params.gameweek), None)
|
|
134
|
+
|
|
135
|
+
if not gw_data:
|
|
136
|
+
return f"No transfer data found for {player.web_name} in gameweek {params.gameweek}. The gameweek may not have started yet or data is unavailable."
|
|
137
|
+
|
|
138
|
+
# Enrich with team name
|
|
139
|
+
enriched_history = store.enrich_gameweek_history([gw_data])
|
|
140
|
+
if enriched_history:
|
|
141
|
+
gw_data = enriched_history[0]
|
|
142
|
+
|
|
143
|
+
output = [
|
|
144
|
+
f"**{player.web_name}** ({player.first_name} {player.second_name})",
|
|
145
|
+
f"Team: {player.team_name} | Position: {player.position} | Price: {format_player_price(player.now_cost)}",
|
|
146
|
+
"",
|
|
147
|
+
f"**Gameweek {params.gameweek} Transfer Statistics:**",
|
|
148
|
+
"",
|
|
149
|
+
f"├─ Transfers In: {gw_data.get('transfers_in', 0):,}",
|
|
150
|
+
f"├─ Transfers Out: {gw_data.get('transfers_out', 0):,}",
|
|
151
|
+
f"├─ Net Transfers: {gw_data.get('transfers_balance', gw_data.get('transfers_in', 0) - gw_data.get('transfers_out', 0)):+,}",
|
|
152
|
+
f"├─ Ownership at GW: {gw_data.get('selected', 0):,} teams",
|
|
153
|
+
"",
|
|
154
|
+
f"**Performance in GW{params.gameweek}:**",
|
|
155
|
+
f"├─ Points: {gw_data.get('total_points', 0)}",
|
|
156
|
+
f"├─ Minutes: {gw_data.get('minutes', 0)}",
|
|
157
|
+
f"├─ xGoal: {gw_data.get('expected_goals', '0.00')} | Goals: {gw_data.get('goals_scored', 0)}",
|
|
158
|
+
f"├─ xAssist: {gw_data.get('expected_assists', '0.00')} | Assists: {gw_data.get('assists', 0)}",
|
|
159
|
+
f"├─ Clean Sheets: {gw_data.get('clean_sheets', 0)} | Bonus: {gw_data.get('bonus', 0)}",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
opponent_name = gw_data.get("opponent_team_short", "Unknown")
|
|
163
|
+
home_away = "H" if gw_data.get("was_home") else "A"
|
|
164
|
+
output.append(f"├─ Opponent: vs {opponent_name} ({home_away})")
|
|
165
|
+
|
|
166
|
+
result = "\n".join(output)
|
|
167
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
168
|
+
return truncated
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return handle_api_error(e)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@mcp.tool(
|
|
175
|
+
name="fpl_get_top_transferred_players",
|
|
176
|
+
annotations={
|
|
177
|
+
"title": "Get Top Transferred FPL Players",
|
|
178
|
+
"readOnlyHint": True,
|
|
179
|
+
"destructiveHint": False,
|
|
180
|
+
"idempotentHint": True,
|
|
181
|
+
"openWorldHint": True,
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
async def fpl_get_top_transferred_players(params: GetTopTransferredPlayersInput) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Get the most transferred in and out players for the current gameweek.
|
|
187
|
+
|
|
188
|
+
Shows live transfer trends to identify popular moves happening right now. Uses
|
|
189
|
+
real-time data from bootstrap for instant response. Essential for understanding
|
|
190
|
+
the current template and finding differentials.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
params (GetTopTransferredPlayersInput): Validated input parameters containing:
|
|
194
|
+
- limit (int): Number of players to return, 1-50 (default: 10)
|
|
195
|
+
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str: Top transferred in and out players with net transfers
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
- Top 10: limit=10
|
|
202
|
+
- Top 20: limit=20
|
|
203
|
+
- Get as JSON: limit=15, response_format="json"
|
|
204
|
+
|
|
205
|
+
Error Handling:
|
|
206
|
+
- Returns error if no transfer data available
|
|
207
|
+
- Returns formatted error message if current gameweek unavailable
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
await _create_client()
|
|
211
|
+
if not store.bootstrap_data:
|
|
212
|
+
return "Error: Player data not available. Please try again later."
|
|
213
|
+
|
|
214
|
+
# Get current gameweek
|
|
215
|
+
current_gw = store.get_current_gameweek()
|
|
216
|
+
if not current_gw:
|
|
217
|
+
return "Error: Could not determine current gameweek. Data may be unavailable."
|
|
218
|
+
|
|
219
|
+
gameweek = current_gw.id
|
|
220
|
+
|
|
221
|
+
# Use bootstrap data directly - instant, no API calls!
|
|
222
|
+
players_with_transfers = []
|
|
223
|
+
|
|
224
|
+
for player in store.bootstrap_data.elements:
|
|
225
|
+
transfers_in = getattr(player, "transfers_in_event", 0)
|
|
226
|
+
transfers_out = getattr(player, "transfers_out_event", 0)
|
|
227
|
+
|
|
228
|
+
# Only include players with transfer activity
|
|
229
|
+
if transfers_in > 0 or transfers_out > 0:
|
|
230
|
+
players_with_transfers.append(
|
|
231
|
+
{
|
|
232
|
+
"player": player,
|
|
233
|
+
"transfers_in": transfers_in,
|
|
234
|
+
"transfers_out": transfers_out,
|
|
235
|
+
"net_transfers": transfers_in - transfers_out,
|
|
236
|
+
"points": getattr(player, "event_points", 0),
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not players_with_transfers:
|
|
241
|
+
return f"No transfer data available for gameweek {gameweek}. The gameweek may not have started yet."
|
|
242
|
+
|
|
243
|
+
# Sort by transfers in and out
|
|
244
|
+
most_transferred_in = sorted(
|
|
245
|
+
players_with_transfers, key=lambda x: x["transfers_in"], reverse=True
|
|
246
|
+
)[: params.limit]
|
|
247
|
+
most_transferred_out = sorted(
|
|
248
|
+
players_with_transfers, key=lambda x: x["transfers_out"], reverse=True
|
|
249
|
+
)[: params.limit]
|
|
250
|
+
|
|
251
|
+
if params.response_format == ResponseFormat.JSON:
|
|
252
|
+
result = {
|
|
253
|
+
"gameweek": gameweek,
|
|
254
|
+
"transferred_in": [
|
|
255
|
+
{
|
|
256
|
+
"rank": i + 1,
|
|
257
|
+
"player_name": data["player"].web_name,
|
|
258
|
+
"team": data["player"].team_name,
|
|
259
|
+
"position": data["player"].position,
|
|
260
|
+
"price": format_player_price(data["player"].now_cost),
|
|
261
|
+
"transfers_in": data["transfers_in"],
|
|
262
|
+
"net_transfers": data["net_transfers"],
|
|
263
|
+
"points": data["points"],
|
|
264
|
+
}
|
|
265
|
+
for i, data in enumerate(most_transferred_in)
|
|
266
|
+
],
|
|
267
|
+
"transferred_out": [
|
|
268
|
+
{
|
|
269
|
+
"rank": i + 1,
|
|
270
|
+
"player_name": data["player"].web_name,
|
|
271
|
+
"team": data["player"].team_name,
|
|
272
|
+
"position": data["player"].position,
|
|
273
|
+
"price": format_player_price(data["player"].now_cost),
|
|
274
|
+
"transfers_out": data["transfers_out"],
|
|
275
|
+
"net_transfers": data["net_transfers"],
|
|
276
|
+
"points": data["points"],
|
|
277
|
+
}
|
|
278
|
+
for i, data in enumerate(most_transferred_out)
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
return format_json_response(result)
|
|
282
|
+
else:
|
|
283
|
+
output = [
|
|
284
|
+
f"**Gameweek {gameweek} - Live Transfer Trends** 🔥",
|
|
285
|
+
"",
|
|
286
|
+
f"**Most Transferred IN (Top {min(params.limit, len(most_transferred_in))}):**",
|
|
287
|
+
"",
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
for i, data in enumerate(most_transferred_in, 1):
|
|
291
|
+
player = data["player"]
|
|
292
|
+
price = format_player_price(player.now_cost)
|
|
293
|
+
output.append(
|
|
294
|
+
f"{i:2d}. {player.web_name:20s} ({player.team_name:15s} {player.position}) | "
|
|
295
|
+
f"{price} | In: {data['transfers_in']:,} | "
|
|
296
|
+
f"Net: {data['net_transfers']:+,} | {data['points']}pts"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
output.extend(
|
|
300
|
+
[
|
|
301
|
+
"",
|
|
302
|
+
f"**Most Transferred OUT (Top {min(params.limit, len(most_transferred_out))}):**",
|
|
303
|
+
"",
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
for i, data in enumerate(most_transferred_out, 1):
|
|
308
|
+
player = data["player"]
|
|
309
|
+
price = format_player_price(player.now_cost)
|
|
310
|
+
output.append(
|
|
311
|
+
f"{i:2d}. {player.web_name:20s} ({player.team_name:15s} {player.position}) | "
|
|
312
|
+
f"{price} | Out: {data['transfers_out']:,} | "
|
|
313
|
+
f"Net: {data['net_transfers']:+,} | {data['points']}pts"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
result = "\n".join(output)
|
|
317
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
318
|
+
return truncated
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
return handle_api_error(e)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@mcp.tool(
|
|
325
|
+
name="fpl_get_manager_transfers_by_gameweek",
|
|
326
|
+
annotations={
|
|
327
|
+
"title": "Get Manager's FPL Transfers",
|
|
328
|
+
"readOnlyHint": True,
|
|
329
|
+
"destructiveHint": False,
|
|
330
|
+
"idempotentHint": True,
|
|
331
|
+
"openWorldHint": True,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
async def fpl_get_manager_transfers_by_gameweek(
|
|
335
|
+
params: GetManagerTransfersByGameweekInput,
|
|
336
|
+
) -> str:
|
|
337
|
+
"""
|
|
338
|
+
Get all transfers made by a specific manager in a specific gameweek.
|
|
339
|
+
|
|
340
|
+
Shows which players were transferred in and out, transfer costs, and timing.
|
|
341
|
+
Useful for analyzing manager strategy and understanding when/why they made moves.
|
|
342
|
+
Requires manager's team ID (entry ID) which can be found in the FPL URL.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
params (GetManagerTransfersByGameweekInput): Validated input parameters containing:
|
|
346
|
+
- team_id (int): Manager's team ID (entry ID)
|
|
347
|
+
- gameweek (int): Gameweek number (1-38)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
str: Complete transfer history for the gameweek with costs
|
|
351
|
+
|
|
352
|
+
Examples:
|
|
353
|
+
- View transfers: team_id=123456, gameweek=20
|
|
354
|
+
- Check costs: team_id=789012, gameweek=15
|
|
355
|
+
|
|
356
|
+
Error Handling:
|
|
357
|
+
- Returns error if team ID invalid
|
|
358
|
+
- Returns message if no transfers in gameweek
|
|
359
|
+
- Returns formatted error message if API fails
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
client = await _create_client()
|
|
363
|
+
|
|
364
|
+
# Fetch manager entry info to get team name
|
|
365
|
+
try:
|
|
366
|
+
manager_entry = await client.get_manager_entry(params.team_id)
|
|
367
|
+
manager_name = f"{manager_entry.get('player_first_name', '')} {manager_entry.get('player_last_name', '')}".strip()
|
|
368
|
+
team_name = manager_entry.get("name", f"Team {params.team_id}")
|
|
369
|
+
except Exception:
|
|
370
|
+
manager_name = f"Manager {params.team_id}"
|
|
371
|
+
team_name = f"Team {params.team_id}"
|
|
372
|
+
|
|
373
|
+
# Fetch all transfer history
|
|
374
|
+
transfers_data = await client.get_manager_transfers(params.team_id)
|
|
375
|
+
|
|
376
|
+
# Filter transfers for the specific gameweek
|
|
377
|
+
gw_transfers = [t for t in transfers_data if t.get("event") == params.gameweek]
|
|
378
|
+
|
|
379
|
+
if not gw_transfers:
|
|
380
|
+
return f"No transfers found for {manager_name} in gameweek {params.gameweek}. They may have used their free transfers or rolled them over."
|
|
381
|
+
|
|
382
|
+
output = [
|
|
383
|
+
f"**{team_name}** - {manager_name}",
|
|
384
|
+
f"Gameweek {params.gameweek} Transfers",
|
|
385
|
+
"",
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
total_cost = 0
|
|
389
|
+
for i, transfer in enumerate(gw_transfers, 1):
|
|
390
|
+
# Get player details
|
|
391
|
+
player_in_id = transfer.get("element_in")
|
|
392
|
+
player_out_id = transfer.get("element_out")
|
|
393
|
+
|
|
394
|
+
player_in_name = store.get_player_name(player_in_id)
|
|
395
|
+
player_out_name = store.get_player_name(player_out_id)
|
|
396
|
+
|
|
397
|
+
# Get player info for prices
|
|
398
|
+
player_in_info = next(
|
|
399
|
+
(p for p in store.bootstrap_data.elements if p.id == player_in_id), None
|
|
400
|
+
)
|
|
401
|
+
player_out_info = next(
|
|
402
|
+
(p for p in store.bootstrap_data.elements if p.id == player_out_id),
|
|
403
|
+
None,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
price_in = format_player_price(player_in_info.now_cost) if player_in_info else "£0.0m"
|
|
407
|
+
price_out = (
|
|
408
|
+
format_player_price(player_out_info.now_cost) if player_out_info else "£0.0m"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Transfer details
|
|
412
|
+
transfer_time = transfer.get("time", "Unknown")
|
|
413
|
+
cost = transfer.get("event_cost", 0)
|
|
414
|
+
total_cost += cost
|
|
415
|
+
|
|
416
|
+
output.append(f"**Transfer {i}:**")
|
|
417
|
+
output.append(
|
|
418
|
+
f"OUT: {player_out_name} ({player_out_info.team_name if player_out_info else 'Unknown'} "
|
|
419
|
+
f"{player_out_info.position if player_out_info else 'UNK'}) - {price_out}"
|
|
420
|
+
)
|
|
421
|
+
output.append(
|
|
422
|
+
f"IN: {player_in_name} ({player_in_info.team_name if player_in_info else 'Unknown'} "
|
|
423
|
+
f"{player_in_info.position if player_in_info else 'UNK'}) - {price_in}"
|
|
424
|
+
)
|
|
425
|
+
if cost > 0:
|
|
426
|
+
output.append(f"Cost: -{cost} points")
|
|
427
|
+
output.append(
|
|
428
|
+
f"Time: {transfer_time[:19] if transfer_time != 'Unknown' else 'Unknown'}"
|
|
429
|
+
)
|
|
430
|
+
output.append("")
|
|
431
|
+
|
|
432
|
+
output.extend(
|
|
433
|
+
[
|
|
434
|
+
"**Summary:**",
|
|
435
|
+
f"├─ Total Transfers: {len(gw_transfers)}",
|
|
436
|
+
f"├─ Total Cost: -{total_cost} points"
|
|
437
|
+
if total_cost > 0
|
|
438
|
+
else "├─ Free Transfers Used",
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
result = "\n".join(output)
|
|
443
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
444
|
+
return truncated
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
return handle_api_error(e)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class GetManagerChipsInput(BaseModel):
|
|
451
|
+
"""Input model for getting manager chip usage."""
|
|
452
|
+
|
|
453
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
454
|
+
|
|
455
|
+
team_id: int = Field(..., description="Manager's team ID (entry ID) from FPL", ge=1)
|
|
456
|
+
response_format: ResponseFormat = Field(
|
|
457
|
+
default=ResponseFormat.MARKDOWN,
|
|
458
|
+
description="Output format: 'markdown' or 'json'",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@mcp.tool(
|
|
463
|
+
name="fpl_get_manager_chips",
|
|
464
|
+
annotations={
|
|
465
|
+
"title": "Get Manager's Chip Usage",
|
|
466
|
+
"readOnlyHint": True,
|
|
467
|
+
"destructiveHint": False,
|
|
468
|
+
"idempotentHint": True,
|
|
469
|
+
"openWorldHint": True,
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
async def fpl_get_manager_chips(params: GetManagerChipsInput) -> str:
|
|
473
|
+
"""
|
|
474
|
+
Get a manager's chip usage showing which chips have been used and which are still available.
|
|
475
|
+
|
|
476
|
+
Since the 2025/2026 season, FPL provides 4 chips per half-season.
|
|
477
|
+
|
|
478
|
+
Shows used chips with gameweek and timing, plus remaining available chips.
|
|
479
|
+
Essential for strategic chip planning and recommendations.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
params (GetManagerChipsInput): Validated input parameters containing:
|
|
483
|
+
- team_id (int): Manager's team ID (entry ID)
|
|
484
|
+
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
str: Chip usage summary with used and available chips
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
- Check chip status: team_id=123456
|
|
491
|
+
- JSON format: team_id=123456, response_format="json"
|
|
492
|
+
|
|
493
|
+
Error Handling:
|
|
494
|
+
- Returns error if team ID invalid
|
|
495
|
+
- Returns formatted error message if API fails
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
client = await _create_client()
|
|
499
|
+
|
|
500
|
+
# Fetch manager history for used chips
|
|
501
|
+
history_data = await client.get_manager_history(params.team_id)
|
|
502
|
+
used_chips = history_data.get("chips", [])
|
|
503
|
+
|
|
504
|
+
# Get all available chips from bootstrap data
|
|
505
|
+
bootstrap_chips = store.bootstrap_data.chips if store.bootstrap_data else []
|
|
506
|
+
|
|
507
|
+
# Get current gameweek to determine which chips are available
|
|
508
|
+
current_gw_data = store.get_current_gameweek()
|
|
509
|
+
current_gw = current_gw_data.id if current_gw_data else 1
|
|
510
|
+
|
|
511
|
+
# Build chip availability map
|
|
512
|
+
# FPL chip names: "wildcard", "freehit", "bboost", "3xc"
|
|
513
|
+
chip_display_names = {
|
|
514
|
+
"wildcard": "Wildcard",
|
|
515
|
+
"freehit": "Free Hit",
|
|
516
|
+
"bboost": "Bench Boost",
|
|
517
|
+
"3xc": "Triple Captain",
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
# Build available chips list
|
|
521
|
+
# Match used chips to specific chip instances based on gameweek used
|
|
522
|
+
available_chips = []
|
|
523
|
+
for chip in bootstrap_chips:
|
|
524
|
+
chip_name = chip.get("name")
|
|
525
|
+
start_gw = chip.get("start_event", 1)
|
|
526
|
+
end_gw = chip.get("stop_event", 38)
|
|
527
|
+
|
|
528
|
+
# Check if this specific chip instance was used
|
|
529
|
+
# Match by checking if any used chip has this name and was used within this chip's GW range
|
|
530
|
+
chip_used = any(
|
|
531
|
+
used_chip["name"] == chip_name and start_gw <= used_chip["event"] <= end_gw
|
|
532
|
+
for used_chip in used_chips
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Chip is available if: within current gameweek range AND not used
|
|
536
|
+
if start_gw <= current_gw <= end_gw and not chip_used:
|
|
537
|
+
available_chips.append(
|
|
538
|
+
{
|
|
539
|
+
"name": chip_name,
|
|
540
|
+
"display_name": chip_display_names.get(chip_name, chip_name.title()),
|
|
541
|
+
"start_event": start_gw,
|
|
542
|
+
"stop_event": end_gw,
|
|
543
|
+
"half": "First Half" if end_gw <= 19 else "Second Half",
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if params.response_format == ResponseFormat.JSON:
|
|
548
|
+
result = {
|
|
549
|
+
"team_id": params.team_id,
|
|
550
|
+
"current_gameweek": current_gw,
|
|
551
|
+
"used_chips": [
|
|
552
|
+
{
|
|
553
|
+
"name": chip["name"],
|
|
554
|
+
"display_name": chip_display_names.get(chip["name"], chip["name"].title()),
|
|
555
|
+
"gameweek": chip["event"],
|
|
556
|
+
"time": chip["time"],
|
|
557
|
+
}
|
|
558
|
+
for chip in used_chips
|
|
559
|
+
],
|
|
560
|
+
"available_chips": available_chips,
|
|
561
|
+
}
|
|
562
|
+
return format_json_response(result)
|
|
563
|
+
else:
|
|
564
|
+
# Markdown output
|
|
565
|
+
output = [
|
|
566
|
+
f"**Chip Usage Summary** (Team ID: {params.team_id})",
|
|
567
|
+
f"Current Gameweek: {current_gw}",
|
|
568
|
+
"",
|
|
569
|
+
f"**Used Chips ({len(used_chips)}):**",
|
|
570
|
+
"",
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
if used_chips:
|
|
574
|
+
# Group used chips by half
|
|
575
|
+
first_half_used = [c for c in used_chips if c["event"] <= 19]
|
|
576
|
+
second_half_used = [c for c in used_chips if c["event"] >= 20]
|
|
577
|
+
|
|
578
|
+
if first_half_used:
|
|
579
|
+
output.append(" **First Half (GW1-19):**")
|
|
580
|
+
for chip in first_half_used:
|
|
581
|
+
display_name = chip_display_names.get(chip["name"], chip["name"].title())
|
|
582
|
+
gw = chip["event"]
|
|
583
|
+
time = chip["time"][:10] if chip.get("time") else "Unknown"
|
|
584
|
+
output.append(f" ✓ {display_name} - GW{gw} | Used: {time}")
|
|
585
|
+
output.append("")
|
|
586
|
+
|
|
587
|
+
if second_half_used:
|
|
588
|
+
output.append(" **Second Half (GW20-38):**")
|
|
589
|
+
for chip in second_half_used:
|
|
590
|
+
display_name = chip_display_names.get(chip["name"], chip["name"].title())
|
|
591
|
+
gw = chip["event"]
|
|
592
|
+
time = chip["time"][:10] if chip.get("time") else "Unknown"
|
|
593
|
+
output.append(f" ✓ {display_name} - GW{gw} | Used: {time}")
|
|
594
|
+
output.append("")
|
|
595
|
+
else:
|
|
596
|
+
output.append("No chips used yet")
|
|
597
|
+
|
|
598
|
+
output.extend(
|
|
599
|
+
[
|
|
600
|
+
"",
|
|
601
|
+
f"**Available Chips ({len(available_chips)}):**",
|
|
602
|
+
"",
|
|
603
|
+
]
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
if available_chips:
|
|
607
|
+
# Group by half
|
|
608
|
+
first_half_available = [c for c in available_chips if c["half"] == "First Half"]
|
|
609
|
+
second_half_available = [c for c in available_chips if c["half"] == "Second Half"]
|
|
610
|
+
|
|
611
|
+
if first_half_available:
|
|
612
|
+
output.append(" **First Half (GW1-19):**")
|
|
613
|
+
for chip in first_half_available:
|
|
614
|
+
output.append(f" • {chip['display_name']}")
|
|
615
|
+
output.append("")
|
|
616
|
+
|
|
617
|
+
if second_half_available:
|
|
618
|
+
output.append(" **Second Half (GW20-38):**")
|
|
619
|
+
for chip in second_half_available:
|
|
620
|
+
output.append(f" • {chip['display_name']}")
|
|
621
|
+
else:
|
|
622
|
+
output.append("All chips have been used")
|
|
623
|
+
|
|
624
|
+
result = "\n".join(output)
|
|
625
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
626
|
+
return truncated
|
|
627
|
+
|
|
628
|
+
except Exception as e:
|
|
629
|
+
return handle_api_error(e)
|