fpl-mcp-server 0.1.6__py3-none-any.whl → 0.2.0__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.6.dist-info → fpl_mcp_server-0.2.0.dist-info}/METADATA +3 -3
- fpl_mcp_server-0.2.0.dist-info/RECORD +36 -0
- src/prompts/__init__.py +1 -0
- src/prompts/captain_recommendation.py +13 -16
- src/prompts/gameweek_analysis.py +72 -0
- src/prompts/league_analysis.py +12 -5
- src/prompts/player_analysis.py +2 -3
- src/prompts/squad_analysis.py +54 -84
- src/prompts/team_analysis.py +2 -1
- src/prompts/transfers.py +4 -3
- src/tools/fixtures.py +355 -19
- src/tools/gameweeks.py +0 -182
- src/tools/leagues.py +189 -2
- src/tools/players.py +259 -287
- src/tools/teams.py +1 -173
- src/tools/transfers.py +250 -112
- fpl_mcp_server-0.1.6.dist-info/RECORD +0 -35
- {fpl_mcp_server-0.1.6.dist-info → fpl_mcp_server-0.2.0.dist-info}/WHEEL +0 -0
- {fpl_mcp_server-0.1.6.dist-info → fpl_mcp_server-0.2.0.dist-info}/entry_points.txt +0 -0
- {fpl_mcp_server-0.1.6.dist-info → fpl_mcp_server-0.2.0.dist-info}/licenses/LICENSE +0 -0
src/tools/players.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
4
4
|
|
|
5
5
|
from ..client import FPLClient
|
|
6
|
-
from ..constants import CHARACTER_LIMIT
|
|
6
|
+
from ..constants import CHARACTER_LIMIT
|
|
7
7
|
from ..formatting import format_player_details
|
|
8
8
|
from ..state import store
|
|
9
9
|
from ..utils import (
|
|
@@ -18,43 +18,6 @@ from ..utils import (
|
|
|
18
18
|
from . import mcp
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class SearchPlayersInput(BaseModel):
|
|
22
|
-
"""Input model for searching players by name."""
|
|
23
|
-
|
|
24
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
25
|
-
|
|
26
|
-
name_query: str = Field(
|
|
27
|
-
...,
|
|
28
|
-
description="Player name to search for (e.g., 'Salah', 'Haaland', 'Son')",
|
|
29
|
-
min_length=2,
|
|
30
|
-
max_length=100,
|
|
31
|
-
)
|
|
32
|
-
limit: int | None = Field(
|
|
33
|
-
default=10, description="Maximum number of results to return", ge=1, le=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 SearchPlayersByTeamInput(BaseModel):
|
|
42
|
-
"""Input model for searching players by team."""
|
|
43
|
-
|
|
44
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
45
|
-
|
|
46
|
-
team_name: str = Field(
|
|
47
|
-
...,
|
|
48
|
-
description="Team name to search for (e.g., 'Arsenal', 'Liverpool', 'Man City')",
|
|
49
|
-
min_length=2,
|
|
50
|
-
max_length=50,
|
|
51
|
-
)
|
|
52
|
-
response_format: ResponseFormat = Field(
|
|
53
|
-
default=ResponseFormat.MARKDOWN,
|
|
54
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
21
|
class FindPlayerInput(BaseModel):
|
|
59
22
|
"""Input for finding a player with fuzzy matching."""
|
|
60
23
|
|
|
@@ -120,6 +83,21 @@ class GetTopPlayersByMetricInput(BaseModel):
|
|
|
120
83
|
)
|
|
121
84
|
|
|
122
85
|
|
|
86
|
+
class GetCaptainRecommendationsInput(BaseModel):
|
|
87
|
+
"""Input model for captain recommendations."""
|
|
88
|
+
|
|
89
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
90
|
+
|
|
91
|
+
team_id: int | None = Field(
|
|
92
|
+
default=None, description="Your team ID (to analyze your specific squad)"
|
|
93
|
+
)
|
|
94
|
+
gameweek: int | None = Field(default=None, description="Gameweek to analyze (defaults to next)")
|
|
95
|
+
response_format: ResponseFormat = Field(
|
|
96
|
+
default=ResponseFormat.MARKDOWN,
|
|
97
|
+
description="Output format: 'markdown' or 'json'",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
123
101
|
async def _create_client():
|
|
124
102
|
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
125
103
|
client = FPLClient(store=store)
|
|
@@ -269,216 +247,6 @@ async def _aggregate_player_stats_from_fixtures(client: FPLClient, num_gameweeks
|
|
|
269
247
|
}
|
|
270
248
|
|
|
271
249
|
|
|
272
|
-
@mcp.tool(
|
|
273
|
-
name="fpl_search_players",
|
|
274
|
-
annotations={
|
|
275
|
-
"title": "Search FPL Players",
|
|
276
|
-
"readOnlyHint": True,
|
|
277
|
-
"destructiveHint": False,
|
|
278
|
-
"idempotentHint": True,
|
|
279
|
-
"openWorldHint": True,
|
|
280
|
-
},
|
|
281
|
-
)
|
|
282
|
-
async def fpl_search_players(params: SearchPlayersInput) -> str:
|
|
283
|
-
"""
|
|
284
|
-
Search for Fantasy Premier League players by name.
|
|
285
|
-
|
|
286
|
-
Returns basic player information including price, form, and stats. Use player names
|
|
287
|
-
(not IDs) for all operations. Supports partial name matching.
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
params (SearchPlayersInput): Validated input parameters containing:
|
|
291
|
-
- name_query (str): Player name to search (e.g., 'Salah', 'Haaland')
|
|
292
|
-
- limit (Optional[int]): Max results to return, 1-50 (default: 10)
|
|
293
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
str: Formatted player search results
|
|
297
|
-
|
|
298
|
-
Examples:
|
|
299
|
-
- Search for Egyptian players: name_query="Salah"
|
|
300
|
-
- Find strikers named Kane: name_query="Kane"
|
|
301
|
-
- Get top 20 results: name_query="Son", limit=20
|
|
302
|
-
|
|
303
|
-
Error Handling:
|
|
304
|
-
- Returns "No players found" if no matches
|
|
305
|
-
- Returns formatted error message if API fails
|
|
306
|
-
"""
|
|
307
|
-
try:
|
|
308
|
-
await _create_client()
|
|
309
|
-
if not store.bootstrap_data:
|
|
310
|
-
return "Error: Player data not available. Please try again later."
|
|
311
|
-
|
|
312
|
-
# Use bootstrap data which has all attributes
|
|
313
|
-
players = store.bootstrap_data.elements
|
|
314
|
-
matches = [p for p in players if params.name_query.lower() in p.web_name.lower()]
|
|
315
|
-
|
|
316
|
-
if not matches:
|
|
317
|
-
return f"No players found matching '{params.name_query}'. Try a different search term."
|
|
318
|
-
|
|
319
|
-
# Limit results
|
|
320
|
-
matches = matches[: params.limit]
|
|
321
|
-
|
|
322
|
-
if params.response_format == ResponseFormat.JSON:
|
|
323
|
-
result = {
|
|
324
|
-
"query": params.name_query,
|
|
325
|
-
"count": len(matches),
|
|
326
|
-
"players": [
|
|
327
|
-
{
|
|
328
|
-
"name": p.web_name,
|
|
329
|
-
"full_name": f"{p.first_name} {p.second_name}",
|
|
330
|
-
"team": p.team_name,
|
|
331
|
-
"position": p.position,
|
|
332
|
-
"price": format_player_price(p.now_cost),
|
|
333
|
-
"form": str(p.form),
|
|
334
|
-
"points_per_game": str(p.points_per_game),
|
|
335
|
-
"status": p.status,
|
|
336
|
-
"news": p.news or None,
|
|
337
|
-
}
|
|
338
|
-
for p in matches
|
|
339
|
-
],
|
|
340
|
-
}
|
|
341
|
-
return format_json_response(result)
|
|
342
|
-
else:
|
|
343
|
-
output = [
|
|
344
|
-
f"# Player Search Results: '{params.name_query}'",
|
|
345
|
-
f"\nFound {len(matches)} players:\n",
|
|
346
|
-
]
|
|
347
|
-
for p in matches:
|
|
348
|
-
price = format_player_price(p.now_cost)
|
|
349
|
-
status_ind = format_status_indicator(p.status, p.news)
|
|
350
|
-
output.append(
|
|
351
|
-
f"├─ **{p.web_name}** ({p.team_name}) | {price} | Form: {p.form} | PPG: {p.points_per_game}{status_ind}"
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
result = "\n".join(output)
|
|
355
|
-
truncated, was_truncated = check_and_truncate(
|
|
356
|
-
result,
|
|
357
|
-
CHARACTER_LIMIT,
|
|
358
|
-
"Use a more specific name_query to narrow results",
|
|
359
|
-
)
|
|
360
|
-
return truncated
|
|
361
|
-
|
|
362
|
-
except Exception as e:
|
|
363
|
-
return handle_api_error(e)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
@mcp.tool(
|
|
367
|
-
name="fpl_search_players_by_team",
|
|
368
|
-
annotations={
|
|
369
|
-
"title": "Search FPL Players by Team",
|
|
370
|
-
"readOnlyHint": True,
|
|
371
|
-
"destructiveHint": False,
|
|
372
|
-
"idempotentHint": True,
|
|
373
|
-
"openWorldHint": True,
|
|
374
|
-
},
|
|
375
|
-
)
|
|
376
|
-
async def fpl_search_players_by_team(params: SearchPlayersByTeamInput) -> str:
|
|
377
|
-
"""
|
|
378
|
-
Search for all Fantasy Premier League players from a specific team.
|
|
379
|
-
|
|
380
|
-
Returns all players from the team organized by position, with prices, form, and stats.
|
|
381
|
-
Useful for analyzing team squads or finding budget options from specific teams.
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
params (SearchPlayersByTeamInput): Validated input parameters containing:
|
|
385
|
-
- team_name (str): Team name (e.g., 'Arsenal', 'Liverpool', 'Man City')
|
|
386
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
str: Formatted team squad listing organized by position
|
|
390
|
-
|
|
391
|
-
Examples:
|
|
392
|
-
- Get Arsenal squad: team_name="Arsenal"
|
|
393
|
-
- Search by short name: team_name="MCI"
|
|
394
|
-
- Get Liverpool in JSON: team_name="Liverpool", response_format="json"
|
|
395
|
-
|
|
396
|
-
Error Handling:
|
|
397
|
-
- Returns error if no team found
|
|
398
|
-
- Returns error if multiple teams match (asks user to be more specific)
|
|
399
|
-
- Returns formatted error message if API fails
|
|
400
|
-
"""
|
|
401
|
-
try:
|
|
402
|
-
await _create_client()
|
|
403
|
-
if not store.bootstrap_data:
|
|
404
|
-
return "Error: Player data not available. Please try again later."
|
|
405
|
-
|
|
406
|
-
matching_teams = [
|
|
407
|
-
t
|
|
408
|
-
for t in store.bootstrap_data.teams
|
|
409
|
-
if params.team_name.lower() in t.name.lower()
|
|
410
|
-
or params.team_name.lower() in t.short_name.lower()
|
|
411
|
-
]
|
|
412
|
-
|
|
413
|
-
if not matching_teams:
|
|
414
|
-
return f"No teams found matching '{params.team_name}'. Try using the full team name or abbreviation."
|
|
415
|
-
|
|
416
|
-
if len(matching_teams) > 1:
|
|
417
|
-
team_list = ", ".join([f"{t.name} ({t.short_name})" for t in matching_teams])
|
|
418
|
-
return f"Multiple teams found: {team_list}. Please be more specific."
|
|
419
|
-
|
|
420
|
-
team = matching_teams[0]
|
|
421
|
-
players = [p for p in store.bootstrap_data.elements if p.team == team.id]
|
|
422
|
-
|
|
423
|
-
if not players:
|
|
424
|
-
return f"No players found for {team.name}. This may be a data issue."
|
|
425
|
-
|
|
426
|
-
# Sort by position and price
|
|
427
|
-
position_order = {
|
|
428
|
-
PlayerPosition.GOALKEEPER.value: 1,
|
|
429
|
-
PlayerPosition.DEFENDER.value: 2,
|
|
430
|
-
PlayerPosition.MIDFIELDER.value: 3,
|
|
431
|
-
PlayerPosition.FORWARD.value: 4,
|
|
432
|
-
}
|
|
433
|
-
players_sorted = sorted(
|
|
434
|
-
players,
|
|
435
|
-
key=lambda p: (position_order.get(p.position or "ZZZ", 5), -p.now_cost),
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
if params.response_format == ResponseFormat.JSON:
|
|
439
|
-
result = {
|
|
440
|
-
"team": {"name": team.name, "short_name": team.short_name},
|
|
441
|
-
"player_count": len(players_sorted),
|
|
442
|
-
"players": [
|
|
443
|
-
{
|
|
444
|
-
"name": p.web_name,
|
|
445
|
-
"full_name": f"{p.first_name} {p.second_name}",
|
|
446
|
-
"position": p.position,
|
|
447
|
-
"price": format_player_price(p.now_cost),
|
|
448
|
-
"form": str(p.form),
|
|
449
|
-
"points_per_game": str(p.points_per_game),
|
|
450
|
-
"status": p.status,
|
|
451
|
-
"news": p.news or None,
|
|
452
|
-
}
|
|
453
|
-
for p in players_sorted
|
|
454
|
-
],
|
|
455
|
-
}
|
|
456
|
-
return format_json_response(result)
|
|
457
|
-
else:
|
|
458
|
-
output = [f"**{team.name} ({team.short_name}) Squad:**\n"]
|
|
459
|
-
|
|
460
|
-
current_position = None
|
|
461
|
-
for p in players_sorted:
|
|
462
|
-
if p.position != current_position:
|
|
463
|
-
current_position = p.position
|
|
464
|
-
output.append(f"\n**{current_position}:**")
|
|
465
|
-
|
|
466
|
-
price = format_player_price(p.now_cost)
|
|
467
|
-
status_ind = format_status_indicator(p.status, p.news)
|
|
468
|
-
|
|
469
|
-
output.append(
|
|
470
|
-
f"├─ {p.web_name:20s} | {price} | "
|
|
471
|
-
f"Form: {p.form:4s} | PPG: {p.points_per_game:4s}{status_ind}"
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
result = "\n".join(output)
|
|
475
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
476
|
-
return truncated
|
|
477
|
-
|
|
478
|
-
except Exception as e:
|
|
479
|
-
return handle_api_error(e)
|
|
480
|
-
|
|
481
|
-
|
|
482
250
|
@mcp.tool(
|
|
483
251
|
name="fpl_find_player",
|
|
484
252
|
annotations={
|
|
@@ -684,48 +452,51 @@ async def fpl_compare_players(params: ComparePlayersInput) -> str:
|
|
|
684
452
|
output.append("\nPlease use more specific names or full names.")
|
|
685
453
|
return "\n".join(output)
|
|
686
454
|
|
|
687
|
-
|
|
688
|
-
|
|
455
|
+
# Construct Markdown Table
|
|
456
|
+
headers = ["Metric"] + [f"**{p.web_name}**" for p in players_to_compare]
|
|
457
|
+
output = [
|
|
458
|
+
f"## Player Comparison ({len(players_to_compare)} players)",
|
|
459
|
+
"",
|
|
460
|
+
"| " + " | ".join(headers) + " |",
|
|
461
|
+
"| :--- | " + " | ".join([":---"] * len(players_to_compare)) + " |",
|
|
462
|
+
]
|
|
689
463
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
)
|
|
464
|
+
def get_stat(p, attr, default="0"):
|
|
465
|
+
return str(getattr(p, attr, default))
|
|
466
|
+
|
|
467
|
+
metrics = [
|
|
468
|
+
("Team", lambda p: p.team_name),
|
|
469
|
+
("Position", lambda p: p.position),
|
|
470
|
+
("Price", lambda p: format_player_price(p.now_cost)),
|
|
471
|
+
("Form", lambda p: str(p.form)),
|
|
472
|
+
("PPG", lambda p: str(p.points_per_game)),
|
|
473
|
+
("Total Points", lambda p: get_stat(p, "total_points", "N/A")),
|
|
474
|
+
("Selected By", lambda p: f"{get_stat(p, 'selected_by_percent', '0')}%"),
|
|
475
|
+
(
|
|
476
|
+
"Status",
|
|
477
|
+
lambda p: f"{format_player_status(p.status)} {format_status_indicator(p.status, p.news)}",
|
|
478
|
+
),
|
|
479
|
+
("Minutes", lambda p: get_stat(p, "minutes", "0")),
|
|
480
|
+
("Goals", lambda p: get_stat(p, "goals_scored")),
|
|
481
|
+
("xG", lambda p: get_stat(p, "expected_goals", "0.00")),
|
|
482
|
+
("Assists", lambda p: get_stat(p, "assists")),
|
|
483
|
+
("xA", lambda p: get_stat(p, "expected_assists", "0.00")),
|
|
484
|
+
("Clean Sheets", lambda p: get_stat(p, "clean_sheets")),
|
|
485
|
+
("BPS", lambda p: get_stat(p, "bps")),
|
|
486
|
+
("Bonus", lambda p: get_stat(p, "bonus")),
|
|
487
|
+
("Def. Contribution", lambda p: get_stat(p, "defensive_contribution")),
|
|
488
|
+
("Yellow Cards", lambda p: get_stat(p, "yellow_cards")),
|
|
489
|
+
("Red Cards", lambda p: get_stat(p, "red_cards")),
|
|
490
|
+
]
|
|
705
491
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
# Popularity stats
|
|
710
|
-
if hasattr(player, "selected_by_percent"):
|
|
711
|
-
output.append(f"├─ Selected by: {getattr(player, 'selected_by_percent', 'N/A')}%")
|
|
712
|
-
|
|
713
|
-
if hasattr(player, "minutes"):
|
|
714
|
-
output.append(f"├─ Minutes: {getattr(player, 'minutes', 'N/A')}")
|
|
715
|
-
|
|
716
|
-
# Detailed Season Statistics
|
|
717
|
-
output.extend(
|
|
718
|
-
[
|
|
719
|
-
f"├─ Goals: {getattr(player, 'goals_scored', 0)} | xG: {getattr(player, 'expected_goals', '0.00')}",
|
|
720
|
-
f"├─ Assists: {getattr(player, 'assists', 0)} | xA: {getattr(player, 'expected_assists', '0.00')}",
|
|
721
|
-
f"├─ BPS: {getattr(player, 'bps', 0)} | Bonus: {getattr(player, 'bonus', 0)}",
|
|
722
|
-
f"├─ Clean Sheets: {getattr(player, 'clean_sheets', 0)}",
|
|
723
|
-
f"├─ Defensive Contribution: {getattr(player, 'defensive_contribution', 0)}",
|
|
724
|
-
f"├─ Yellow Cards: {getattr(player, 'yellow_cards', 0)} | Red Cards: {getattr(player, 'red_cards', 0)}",
|
|
725
|
-
]
|
|
726
|
-
)
|
|
492
|
+
for label, getter in metrics:
|
|
493
|
+
row = [f"**{label}**"] + [getter(p) for p in players_to_compare]
|
|
494
|
+
output.append("| " + " | ".join(row) + " |")
|
|
727
495
|
|
|
728
|
-
|
|
496
|
+
# News row
|
|
497
|
+
if any(p.news for p in players_to_compare):
|
|
498
|
+
row = ["**News**"] + [(p.news if p.news else "") for p in players_to_compare]
|
|
499
|
+
output.append("| " + " | ".join(row) + " |")
|
|
729
500
|
|
|
730
501
|
result = "\n".join(output)
|
|
731
502
|
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
@@ -781,6 +552,12 @@ async def fpl_get_top_performers(params: GetTopPlayersByMetricInput) -> str:
|
|
|
781
552
|
# Aggregate stats from fixtures
|
|
782
553
|
result = await _aggregate_player_stats_from_fixtures(client, params.num_gameweeks)
|
|
783
554
|
|
|
555
|
+
if not result:
|
|
556
|
+
msg = "No data available for the specified gameweek range."
|
|
557
|
+
if params.response_format == ResponseFormat.JSON:
|
|
558
|
+
return format_json_response({"error": msg})
|
|
559
|
+
return msg
|
|
560
|
+
|
|
784
561
|
if "error" in result:
|
|
785
562
|
return f"Error: {result['error']}\nGameweek range: {result.get('gameweek_range', 'Unknown')}"
|
|
786
563
|
|
|
@@ -827,3 +604,198 @@ async def fpl_get_top_performers(params: GetTopPlayersByMetricInput) -> str:
|
|
|
827
604
|
|
|
828
605
|
except Exception as e:
|
|
829
606
|
return handle_api_error(e)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@mcp.tool(
|
|
610
|
+
name="fpl_get_captain_recommendations",
|
|
611
|
+
annotations={
|
|
612
|
+
"title": "Get Captain Recommendations",
|
|
613
|
+
"readOnlyHint": True,
|
|
614
|
+
"destructiveHint": False,
|
|
615
|
+
"idempotentHint": True,
|
|
616
|
+
"openWorldHint": True,
|
|
617
|
+
},
|
|
618
|
+
)
|
|
619
|
+
async def fpl_get_captain_recommendations(params: GetCaptainRecommendationsInput) -> str:
|
|
620
|
+
"""
|
|
621
|
+
Get captaincy recommendations for the upcoming gameweek.
|
|
622
|
+
|
|
623
|
+
Analyzes fixtures, form, and home/away advantage to recommend the best captain choices.
|
|
624
|
+
If team_id is provided, analyzes YOUR specific squad. Otherwise, provides
|
|
625
|
+
general recommendations from all players.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
params (GetCaptainRecommendationsInput): Validated input parameters containing:
|
|
629
|
+
- team_id (int | None): Your team ID to analyze your specific squad
|
|
630
|
+
- gameweek (int | None): Gameweek to analyze (defaults to next)
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
str: Top 3-5 captain recommendations with analysis
|
|
634
|
+
|
|
635
|
+
Examples:
|
|
636
|
+
- Analyze my team: team_id=123456
|
|
637
|
+
- General picks for GW15: gameweek=15
|
|
638
|
+
- General picks: (no args)
|
|
639
|
+
|
|
640
|
+
Error Handling:
|
|
641
|
+
- Returns error if gameweek invalid
|
|
642
|
+
- Returns helpful message if team ID not found
|
|
643
|
+
- Returns formatted error message if API fails
|
|
644
|
+
"""
|
|
645
|
+
try:
|
|
646
|
+
client = await _create_client()
|
|
647
|
+
if not store.bootstrap_data:
|
|
648
|
+
return "Error: Player data not available. Please try again later."
|
|
649
|
+
|
|
650
|
+
# Determine gameweek
|
|
651
|
+
gw = params.gameweek
|
|
652
|
+
if not gw:
|
|
653
|
+
current = store.get_current_gameweek()
|
|
654
|
+
gw = (current.id + 1) if current else 1
|
|
655
|
+
if gw > 38:
|
|
656
|
+
gw = 38
|
|
657
|
+
|
|
658
|
+
candidates = []
|
|
659
|
+
|
|
660
|
+
# If team_id provided, fetch squad
|
|
661
|
+
if params.team_id:
|
|
662
|
+
try:
|
|
663
|
+
# Fetch picks for previous GW (or current if live) to guess squad
|
|
664
|
+
# For simplicity, we fetch picks for current event - 1 or current
|
|
665
|
+
fetch_gw = gw - 1 if gw > 1 else 1
|
|
666
|
+
picks_data = await client.get_manager_gameweek_picks(params.team_id, fetch_gw)
|
|
667
|
+
picks = picks_data.get("picks", [])
|
|
668
|
+
|
|
669
|
+
# Get element IDs
|
|
670
|
+
element_ids = [p["element"] for p in picks]
|
|
671
|
+
# Filter to playing players (bootstrap)
|
|
672
|
+
candidates = [p for p in store.bootstrap_data.elements if p.id in element_ids]
|
|
673
|
+
except Exception:
|
|
674
|
+
return f"Error: Could not fetch squad for team ID {params.team_id}. Ensure the ID is correct."
|
|
675
|
+
else:
|
|
676
|
+
# General recommendations: Focus on top 50 by form and price > 6.0 (likely captains)
|
|
677
|
+
candidates = [
|
|
678
|
+
p
|
|
679
|
+
for p in store.bootstrap_data.elements
|
|
680
|
+
if float(p.form) > 3.0 and p.now_cost > 60 and p.status == "a"
|
|
681
|
+
]
|
|
682
|
+
if len(candidates) < 10:
|
|
683
|
+
candidates = sorted(
|
|
684
|
+
store.bootstrap_data.elements, key=lambda x: float(x.form), reverse=True
|
|
685
|
+
)[:50]
|
|
686
|
+
|
|
687
|
+
# Analyze candidates
|
|
688
|
+
scored_candidates = []
|
|
689
|
+
|
|
690
|
+
for p in candidates:
|
|
691
|
+
# Skip GKP, DEF unless exceptional? Usually cap MID/FWD
|
|
692
|
+
if p.element_type == 1: # Skip GKP
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
# Fetch Fixture
|
|
696
|
+
player_fixture = None
|
|
697
|
+
if store.fixtures_data:
|
|
698
|
+
# Find fixture involving this player's team in target GW
|
|
699
|
+
fixtures = [
|
|
700
|
+
f
|
|
701
|
+
for f in store.fixtures_data
|
|
702
|
+
if f.event == gw and (f.team_h == p.team or f.team_a == p.team)
|
|
703
|
+
]
|
|
704
|
+
if fixtures:
|
|
705
|
+
player_fixture = fixtures[0] # Assume 1 fixture
|
|
706
|
+
|
|
707
|
+
difficulty = 3 # Default average
|
|
708
|
+
is_home = False
|
|
709
|
+
opponent = "Unknown"
|
|
710
|
+
|
|
711
|
+
if player_fixture:
|
|
712
|
+
if player_fixture.team_h == p.team:
|
|
713
|
+
difficulty = player_fixture.team_h_difficulty
|
|
714
|
+
is_home = True
|
|
715
|
+
opp_team = next(
|
|
716
|
+
(t for t in store.bootstrap_data.teams if t.id == player_fixture.team_a),
|
|
717
|
+
None,
|
|
718
|
+
)
|
|
719
|
+
opponent = opp_team.short_name if opp_team else "UNK"
|
|
720
|
+
else:
|
|
721
|
+
difficulty = player_fixture.team_a_difficulty
|
|
722
|
+
is_home = False
|
|
723
|
+
opp_team = next(
|
|
724
|
+
(t for t in store.bootstrap_data.teams if t.id == player_fixture.team_h),
|
|
725
|
+
None,
|
|
726
|
+
)
|
|
727
|
+
opponent = opp_team.short_name if opp_team else "UNK"
|
|
728
|
+
|
|
729
|
+
# Score Calculation
|
|
730
|
+
form = float(p.form)
|
|
731
|
+
ppg = float(p.points_per_game)
|
|
732
|
+
|
|
733
|
+
score = (form * 1.5) + ((5 - difficulty) * 2.5) + (ppg * 0.5)
|
|
734
|
+
if is_home:
|
|
735
|
+
score += 1.0
|
|
736
|
+
|
|
737
|
+
if p.status != "a":
|
|
738
|
+
score = -1
|
|
739
|
+
|
|
740
|
+
scored_candidates.append(
|
|
741
|
+
{
|
|
742
|
+
"player": p,
|
|
743
|
+
"score": score,
|
|
744
|
+
"fixture": f"{opponent} ({'H' if is_home else 'A'})",
|
|
745
|
+
"difficulty": difficulty,
|
|
746
|
+
"is_home": is_home,
|
|
747
|
+
"form": form,
|
|
748
|
+
}
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Sort and take top 5
|
|
752
|
+
top_picks = sorted(scored_candidates, key=lambda x: x["score"], reverse=True)[:5]
|
|
753
|
+
|
|
754
|
+
if params.response_format == ResponseFormat.JSON:
|
|
755
|
+
result = {
|
|
756
|
+
"gameweek": gw,
|
|
757
|
+
"recommendations": [
|
|
758
|
+
{
|
|
759
|
+
"rank": i + 1,
|
|
760
|
+
"name": pick["player"].web_name,
|
|
761
|
+
"team": pick["player"].team_name,
|
|
762
|
+
"score": round(pick["score"], 1),
|
|
763
|
+
"fixture": pick["fixture"],
|
|
764
|
+
"difficulty": pick["difficulty"],
|
|
765
|
+
"form": pick["form"],
|
|
766
|
+
}
|
|
767
|
+
for i, pick in enumerate(top_picks)
|
|
768
|
+
],
|
|
769
|
+
}
|
|
770
|
+
return format_json_response(result)
|
|
771
|
+
|
|
772
|
+
# Markdown
|
|
773
|
+
output = [
|
|
774
|
+
f"## 👑 Captain Recommendations (GW{gw})",
|
|
775
|
+
"Based on form, fixture difficulty, and home advantage.",
|
|
776
|
+
"",
|
|
777
|
+
]
|
|
778
|
+
|
|
779
|
+
if params.team_id:
|
|
780
|
+
output.append(f"**Analysis for Team ID: {params.team_id}**\n")
|
|
781
|
+
|
|
782
|
+
for i, pick in enumerate(top_picks, 1):
|
|
783
|
+
p = pick["player"]
|
|
784
|
+
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
|
|
785
|
+
|
|
786
|
+
output.append(f"### {medal} {p.web_name} ({p.team_name})")
|
|
787
|
+
output.append(f"**Fixture:** {pick['fixture']} | **Diff:** {pick['difficulty']}/5")
|
|
788
|
+
output.append(f"**Form:** {p.form} | **PPG:** {p.points_per_game}")
|
|
789
|
+
output.append(
|
|
790
|
+
f"**Rationale:** {'Great home fixture' if pick['is_home'] and pick['difficulty'] <= 2 else 'In excellent form'} "
|
|
791
|
+
f"vs {pick['fixture'].split(' ')[0]}"
|
|
792
|
+
)
|
|
793
|
+
output.append("")
|
|
794
|
+
|
|
795
|
+
if not top_picks:
|
|
796
|
+
output.append("No suitable captain options found. Check your team ID or season status.")
|
|
797
|
+
|
|
798
|
+
return "\n".join(output)
|
|
799
|
+
|
|
800
|
+
except Exception as e:
|
|
801
|
+
return handle_api_error(e)
|