fpl-mcp-server 0.1.5__py3-none-any.whl → 0.1.7__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.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/METADATA +67 -58
- fpl_mcp_server-0.1.7.dist-info/RECORD +35 -0
- src/client.py +31 -14
- src/constants.py +4 -88
- src/models.py +15 -30
- src/prompts/__init__.py +2 -0
- src/prompts/captain_recommendation.py +149 -0
- src/prompts/league_analysis.py +12 -5
- src/prompts/player_analysis.py +2 -3
- src/prompts/squad_analysis.py +1 -1
- src/prompts/team_analysis.py +2 -1
- src/prompts/team_selection.py +105 -0
- src/prompts/transfers.py +4 -3
- src/resources/bootstrap.py +7 -1
- src/state.py +10 -4
- src/tools/__init__.py +0 -2
- src/tools/fixtures.py +194 -14
- src/tools/gameweeks.py +0 -198
- src/tools/leagues.py +365 -14
- src/tools/players.py +256 -295
- src/tools/teams.py +1 -189
- src/tools/transfers.py +248 -126
- fpl_mcp_server-0.1.5.dist-info/RECORD +0 -33
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/WHEEL +0 -0
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/entry_points.txt +0 -0
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/licenses/LICENSE +0 -0
src/tools/gameweeks.py
CHANGED
|
@@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
6
6
|
|
|
7
7
|
from ..client import FPLClient
|
|
8
8
|
from ..constants import CHARACTER_LIMIT
|
|
9
|
-
from ..formatting import format_gameweek_details
|
|
10
9
|
from ..state import store
|
|
11
10
|
from ..utils import (
|
|
12
11
|
ResponseFormat,
|
|
@@ -14,14 +13,8 @@ from ..utils import (
|
|
|
14
13
|
format_json_response,
|
|
15
14
|
handle_api_error,
|
|
16
15
|
)
|
|
17
|
-
|
|
18
|
-
# Import shared mcp instance
|
|
19
16
|
from . import mcp
|
|
20
17
|
|
|
21
|
-
# =============================================================================
|
|
22
|
-
# Pydantic Input Models
|
|
23
|
-
# =============================================================================
|
|
24
|
-
|
|
25
18
|
|
|
26
19
|
class GetCurrentGameweekInput(BaseModel):
|
|
27
20
|
"""Input model for getting current gameweek."""
|
|
@@ -34,34 +27,6 @@ class GetCurrentGameweekInput(BaseModel):
|
|
|
34
27
|
)
|
|
35
28
|
|
|
36
29
|
|
|
37
|
-
class GetGameweekInfoInput(BaseModel):
|
|
38
|
-
"""Input model for getting specific gameweek information."""
|
|
39
|
-
|
|
40
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
41
|
-
|
|
42
|
-
gameweek_number: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
43
|
-
response_format: ResponseFormat = Field(
|
|
44
|
-
default=ResponseFormat.MARKDOWN,
|
|
45
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class ListAllGameweeksInput(BaseModel):
|
|
50
|
-
"""Input model for listing all gameweeks."""
|
|
51
|
-
|
|
52
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
53
|
-
|
|
54
|
-
response_format: ResponseFormat = Field(
|
|
55
|
-
default=ResponseFormat.MARKDOWN,
|
|
56
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# =============================================================================
|
|
61
|
-
# Helper Functions
|
|
62
|
-
# =============================================================================
|
|
63
|
-
|
|
64
|
-
|
|
65
30
|
async def _create_client():
|
|
66
31
|
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
67
32
|
client = FPLClient(store=store)
|
|
@@ -70,11 +35,6 @@ async def _create_client():
|
|
|
70
35
|
return client
|
|
71
36
|
|
|
72
37
|
|
|
73
|
-
# =============================================================================
|
|
74
|
-
# MCP Tools
|
|
75
|
-
# =============================================================================
|
|
76
|
-
|
|
77
|
-
|
|
78
38
|
@mcp.tool(
|
|
79
39
|
name="fpl_get_current_gameweek",
|
|
80
40
|
annotations={
|
|
@@ -232,161 +192,3 @@ async def fpl_get_current_gameweek(params: GetCurrentGameweekInput) -> str:
|
|
|
232
192
|
|
|
233
193
|
except Exception as e:
|
|
234
194
|
return handle_api_error(e)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
@mcp.tool(
|
|
238
|
-
name="fpl_get_gameweek_info",
|
|
239
|
-
annotations={
|
|
240
|
-
"title": "Get FPL Gameweek Information",
|
|
241
|
-
"readOnlyHint": True,
|
|
242
|
-
"destructiveHint": False,
|
|
243
|
-
"idempotentHint": True,
|
|
244
|
-
"openWorldHint": True,
|
|
245
|
-
},
|
|
246
|
-
)
|
|
247
|
-
async def fpl_get_gameweek_info(params: GetGameweekInfoInput) -> str:
|
|
248
|
-
"""
|
|
249
|
-
Get detailed information about a specific Fantasy Premier League gameweek.
|
|
250
|
-
|
|
251
|
-
Returns comprehensive gameweek data including deadline, scores, top players, most
|
|
252
|
-
captained players, and statistics. Useful for analyzing past performance or planning
|
|
253
|
-
for future gameweeks.
|
|
254
|
-
|
|
255
|
-
Args:
|
|
256
|
-
params (GetGameweekInfoInput): Validated input parameters containing:
|
|
257
|
-
- gameweek_number (int): Gameweek number between 1-38
|
|
258
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
259
|
-
|
|
260
|
-
Returns:
|
|
261
|
-
str: Detailed gameweek information with statistics and top performers
|
|
262
|
-
|
|
263
|
-
Examples:
|
|
264
|
-
- Check GW1 results: gameweek_number=1
|
|
265
|
-
- Plan for GW10: gameweek_number=10
|
|
266
|
-
- Get as JSON: gameweek_number=5, response_format="json"
|
|
267
|
-
|
|
268
|
-
Error Handling:
|
|
269
|
-
- Returns error if gameweek number invalid (must be 1-38)
|
|
270
|
-
- Returns error if gameweek not found
|
|
271
|
-
- Returns formatted error message if data unavailable
|
|
272
|
-
"""
|
|
273
|
-
try:
|
|
274
|
-
await _create_client()
|
|
275
|
-
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
276
|
-
return "Error: Gameweek data not available. Please try again later."
|
|
277
|
-
|
|
278
|
-
gameweek = next(
|
|
279
|
-
(e for e in store.bootstrap_data.events if e.id == params.gameweek_number),
|
|
280
|
-
None,
|
|
281
|
-
)
|
|
282
|
-
if not gameweek:
|
|
283
|
-
return f"Error: Gameweek {params.gameweek_number} not found. Please use a number between 1-38."
|
|
284
|
-
|
|
285
|
-
if params.response_format == ResponseFormat.JSON:
|
|
286
|
-
return format_json_response(gameweek.model_dump())
|
|
287
|
-
else:
|
|
288
|
-
result = format_gameweek_details(gameweek)
|
|
289
|
-
|
|
290
|
-
# Add top element info manually since it differs from basic details
|
|
291
|
-
if gameweek.top_element_info:
|
|
292
|
-
top_player = store.get_player_by_id(gameweek.top_element_info.id)
|
|
293
|
-
top_player_name = (
|
|
294
|
-
top_player.web_name if top_player else f"ID {gameweek.top_element_info.id}"
|
|
295
|
-
)
|
|
296
|
-
result += (
|
|
297
|
-
f"\n**Top Player:** {top_player_name} ({gameweek.top_element_info.points} pts)"
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
301
|
-
return truncated
|
|
302
|
-
|
|
303
|
-
except Exception as e:
|
|
304
|
-
return handle_api_error(e)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
@mcp.tool(
|
|
308
|
-
name="fpl_list_all_gameweeks",
|
|
309
|
-
annotations={
|
|
310
|
-
"title": "List All FPL Gameweeks",
|
|
311
|
-
"readOnlyHint": True,
|
|
312
|
-
"destructiveHint": False,
|
|
313
|
-
"idempotentHint": True,
|
|
314
|
-
"openWorldHint": True,
|
|
315
|
-
},
|
|
316
|
-
)
|
|
317
|
-
async def fpl_list_all_gameweeks(params: ListAllGameweeksInput) -> str:
|
|
318
|
-
"""
|
|
319
|
-
List all Fantasy Premier League gameweeks with their status.
|
|
320
|
-
|
|
321
|
-
Returns all 38 gameweeks showing which are finished, current, or upcoming. Includes
|
|
322
|
-
deadlines and average scores for finished gameweeks. Useful for getting a season
|
|
323
|
-
overview and planning long-term strategy.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
params (ListAllGameweeksInput): Validated input parameters containing:
|
|
327
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
str: Complete list of all gameweeks with status and scores
|
|
331
|
-
|
|
332
|
-
Examples:
|
|
333
|
-
- View all gameweeks: response_format="markdown"
|
|
334
|
-
- Get season overview: response_format="json"
|
|
335
|
-
|
|
336
|
-
Error Handling:
|
|
337
|
-
- Returns error if gameweek data unavailable
|
|
338
|
-
- Returns formatted error message if API fails
|
|
339
|
-
"""
|
|
340
|
-
try:
|
|
341
|
-
await _create_client()
|
|
342
|
-
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
343
|
-
return "Error: Gameweek data not available. Please try again later."
|
|
344
|
-
|
|
345
|
-
if params.response_format == ResponseFormat.JSON:
|
|
346
|
-
result = {
|
|
347
|
-
"total_gameweeks": len(store.bootstrap_data.events),
|
|
348
|
-
"gameweeks": [
|
|
349
|
-
{
|
|
350
|
-
"id": event.id,
|
|
351
|
-
"name": event.name,
|
|
352
|
-
"deadline_time": event.deadline_time,
|
|
353
|
-
"is_current": event.is_current,
|
|
354
|
-
"is_previous": event.is_previous,
|
|
355
|
-
"is_next": event.is_next,
|
|
356
|
-
"finished": event.finished,
|
|
357
|
-
"average_entry_score": event.average_entry_score,
|
|
358
|
-
}
|
|
359
|
-
for event in store.bootstrap_data.events
|
|
360
|
-
],
|
|
361
|
-
}
|
|
362
|
-
return format_json_response(result)
|
|
363
|
-
else:
|
|
364
|
-
output = ["**All Gameweeks:**\n"]
|
|
365
|
-
|
|
366
|
-
for event in store.bootstrap_data.events:
|
|
367
|
-
status = []
|
|
368
|
-
if event.is_current:
|
|
369
|
-
status.append("CURRENT")
|
|
370
|
-
if event.is_previous:
|
|
371
|
-
status.append("PREVIOUS")
|
|
372
|
-
if event.is_next:
|
|
373
|
-
status.append("NEXT")
|
|
374
|
-
if event.finished:
|
|
375
|
-
status.append("FINISHED")
|
|
376
|
-
|
|
377
|
-
status_str = f" [{', '.join(status)}]" if status else ""
|
|
378
|
-
avg_score = (
|
|
379
|
-
f" | Avg: {event.average_entry_score}" if event.average_entry_score else ""
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
output.append(
|
|
383
|
-
f"GW{event.id}: {event.name}{status_str} | "
|
|
384
|
-
f"Deadline: {event.deadline_time[:10]}{avg_score}"
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
result = "\n".join(output)
|
|
388
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
389
|
-
return truncated
|
|
390
|
-
|
|
391
|
-
except Exception as e:
|
|
392
|
-
return handle_api_error(e)
|
src/tools/leagues.py
CHANGED
|
@@ -12,14 +12,8 @@ from ..utils import (
|
|
|
12
12
|
format_json_response,
|
|
13
13
|
handle_api_error,
|
|
14
14
|
)
|
|
15
|
-
|
|
16
|
-
# Import shared mcp instance
|
|
17
15
|
from . import mcp
|
|
18
16
|
|
|
19
|
-
# =============================================================================
|
|
20
|
-
# Pydantic Input Models
|
|
21
|
-
# =============================================================================
|
|
22
|
-
|
|
23
17
|
|
|
24
18
|
class GetLeagueStandingsInput(BaseModel):
|
|
25
19
|
"""Input model for getting league standings."""
|
|
@@ -99,9 +93,45 @@ class GetManagerSquadInput(BaseModel):
|
|
|
99
93
|
)
|
|
100
94
|
|
|
101
95
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
class GetManagerByTeamIdInput(BaseModel):
|
|
97
|
+
"""Input model for getting manager profile by team ID."""
|
|
98
|
+
|
|
99
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
100
|
+
|
|
101
|
+
team_id: int = Field(
|
|
102
|
+
...,
|
|
103
|
+
description="Manager's team ID (entry ID)",
|
|
104
|
+
ge=1,
|
|
105
|
+
)
|
|
106
|
+
gameweek: int | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="Gameweek number (1-38). If not provided, uses current gameweek",
|
|
109
|
+
ge=1,
|
|
110
|
+
le=38,
|
|
111
|
+
)
|
|
112
|
+
response_format: ResponseFormat = Field(
|
|
113
|
+
default=ResponseFormat.MARKDOWN,
|
|
114
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AnalyzeRivalInput(BaseModel):
|
|
119
|
+
"""Input model for analyzing a rival manager."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
122
|
+
|
|
123
|
+
my_team_id: int = Field(..., description="Your team ID", ge=1)
|
|
124
|
+
rival_team_id: int = Field(..., description="Rival's team ID", ge=1)
|
|
125
|
+
gameweek: int | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
description="Gameweek to analyze (defaults to current)",
|
|
128
|
+
ge=1,
|
|
129
|
+
le=38,
|
|
130
|
+
)
|
|
131
|
+
response_format: ResponseFormat = Field(
|
|
132
|
+
default=ResponseFormat.MARKDOWN,
|
|
133
|
+
description="Output format: 'markdown' or 'json'",
|
|
134
|
+
)
|
|
105
135
|
|
|
106
136
|
|
|
107
137
|
async def _create_client():
|
|
@@ -112,11 +142,6 @@ async def _create_client():
|
|
|
112
142
|
return client
|
|
113
143
|
|
|
114
144
|
|
|
115
|
-
# =============================================================================
|
|
116
|
-
# MCP Tools
|
|
117
|
-
# =============================================================================
|
|
118
|
-
|
|
119
|
-
|
|
120
145
|
@mcp.tool(
|
|
121
146
|
name="fpl_get_league_standings",
|
|
122
147
|
annotations={
|
|
@@ -588,3 +613,329 @@ async def fpl_get_manager_squad(params: GetManagerSquadInput) -> str:
|
|
|
588
613
|
|
|
589
614
|
except Exception as e:
|
|
590
615
|
return handle_api_error(e)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@mcp.tool(
|
|
619
|
+
name="fpl_get_manager_by_team_id",
|
|
620
|
+
annotations={
|
|
621
|
+
"title": "Get Manager Profile by Team ID",
|
|
622
|
+
"readOnlyHint": True,
|
|
623
|
+
"destructiveHint": False,
|
|
624
|
+
"idempotentHint": True,
|
|
625
|
+
"openWorldHint": True,
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
async def fpl_get_manager_by_team_id(params: GetManagerByTeamIdInput) -> str:
|
|
629
|
+
"""
|
|
630
|
+
Get manager profile and squad information using team ID directly.
|
|
631
|
+
|
|
632
|
+
This tool provides the same functionality as fpl_get_manager_squad but with
|
|
633
|
+
a name that better reflects its purpose - getting manager information without
|
|
634
|
+
requiring league context. Shows the 15 players picked, captain/vice-captain
|
|
635
|
+
choices, formation, points scored, transfers made, and automatic substitutions.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
params (GetManagerByTeamIdInput): Validated input parameters containing:
|
|
639
|
+
- team_id (int): Manager's team ID (entry ID)
|
|
640
|
+
- gameweek (int | None): Gameweek number (1-38), defaults to current GW
|
|
641
|
+
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
str: Complete manager profile with squad, statistics, and team info
|
|
645
|
+
|
|
646
|
+
Examples:
|
|
647
|
+
- View current squad: team_id=123456
|
|
648
|
+
- View specific gameweek: team_id=123456, gameweek=20
|
|
649
|
+
- Get as JSON: team_id=123456, response_format="json"
|
|
650
|
+
|
|
651
|
+
Error Handling:
|
|
652
|
+
- Returns error if team ID not found (404)
|
|
653
|
+
- Returns error if gameweek not started yet
|
|
654
|
+
- Returns formatted error message if API fails
|
|
655
|
+
"""
|
|
656
|
+
try:
|
|
657
|
+
client = await _create_client()
|
|
658
|
+
|
|
659
|
+
# Fetch manager entry to get team name and player name
|
|
660
|
+
try:
|
|
661
|
+
entry_data = await client.get_manager_entry(params.team_id)
|
|
662
|
+
except Exception:
|
|
663
|
+
return (
|
|
664
|
+
f"Manager with team ID {params.team_id} not found. Verify the team ID is correct."
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
team_name = entry_data.get("name", "Unknown Team")
|
|
668
|
+
player_name = f"{entry_data.get('player_first_name', '')} {entry_data.get('player_last_name', '')}".strip()
|
|
669
|
+
|
|
670
|
+
# Determine which gameweek to use
|
|
671
|
+
gameweek = params.gameweek
|
|
672
|
+
if gameweek is None:
|
|
673
|
+
current_gw = store.get_current_gameweek()
|
|
674
|
+
if not current_gw:
|
|
675
|
+
return (
|
|
676
|
+
"Error: Could not determine current gameweek. Please specify a gameweek number."
|
|
677
|
+
)
|
|
678
|
+
gameweek = current_gw.id
|
|
679
|
+
|
|
680
|
+
# Fetch gameweek picks from API
|
|
681
|
+
picks_data = await client.get_manager_gameweek_picks(params.team_id, gameweek)
|
|
682
|
+
|
|
683
|
+
picks = picks_data.get("picks", [])
|
|
684
|
+
entry_history = picks_data.get("entry_history", {})
|
|
685
|
+
auto_subs = picks_data.get("automatic_subs", [])
|
|
686
|
+
|
|
687
|
+
if not picks:
|
|
688
|
+
return f"No team data found for team ID {params.team_id} in gameweek {gameweek}. Gameweek {gameweek} may not have started yet. Please choose an earlier gameweek or wait until GW{gameweek} begins."
|
|
689
|
+
|
|
690
|
+
# Rehydrate player names
|
|
691
|
+
element_ids = [pick["element"] for pick in picks]
|
|
692
|
+
players_info = store.rehydrate_player_names(element_ids)
|
|
693
|
+
|
|
694
|
+
if params.response_format == ResponseFormat.JSON:
|
|
695
|
+
starting_xi = [p for p in picks if p["position"] <= 11]
|
|
696
|
+
bench = [p for p in picks if p["position"] > 11]
|
|
697
|
+
|
|
698
|
+
result = {
|
|
699
|
+
"team_id": params.team_id,
|
|
700
|
+
"team_name": team_name,
|
|
701
|
+
"player_name": player_name,
|
|
702
|
+
"gameweek": gameweek,
|
|
703
|
+
"stats": {
|
|
704
|
+
"points": entry_history.get("points", 0),
|
|
705
|
+
"total_points": entry_history.get("total_points", 0),
|
|
706
|
+
"overall_rank": entry_history.get("overall_rank"),
|
|
707
|
+
"team_value": entry_history.get("value", 0) / 10,
|
|
708
|
+
"bank": entry_history.get("bank", 0) / 10,
|
|
709
|
+
"transfers": entry_history.get("event_transfers", 0),
|
|
710
|
+
"transfer_cost": entry_history.get("event_transfers_cost", 0),
|
|
711
|
+
"points_on_bench": entry_history.get("points_on_bench", 0),
|
|
712
|
+
},
|
|
713
|
+
"active_chip": picks_data.get("active_chip"),
|
|
714
|
+
"starting_xi": [
|
|
715
|
+
{
|
|
716
|
+
"position": pick["position"],
|
|
717
|
+
"player_name": players_info.get(pick["element"], {}).get(
|
|
718
|
+
"web_name", "Unknown"
|
|
719
|
+
),
|
|
720
|
+
"team": players_info.get(pick["element"], {}).get("team", "UNK"),
|
|
721
|
+
"player_position": players_info.get(pick["element"], {}).get(
|
|
722
|
+
"position", "UNK"
|
|
723
|
+
),
|
|
724
|
+
"price": players_info.get(pick["element"], {}).get("price", 0),
|
|
725
|
+
"is_captain": pick["is_captain"],
|
|
726
|
+
"is_vice_captain": pick["is_vice_captain"],
|
|
727
|
+
"multiplier": pick["multiplier"],
|
|
728
|
+
}
|
|
729
|
+
for pick in starting_xi
|
|
730
|
+
],
|
|
731
|
+
"bench": [
|
|
732
|
+
{
|
|
733
|
+
"position": pick["position"],
|
|
734
|
+
"player_name": players_info.get(pick["element"], {}).get(
|
|
735
|
+
"web_name", "Unknown"
|
|
736
|
+
),
|
|
737
|
+
"team": players_info.get(pick["element"], {}).get("team", "UNK"),
|
|
738
|
+
"player_position": players_info.get(pick["element"], {}).get(
|
|
739
|
+
"position", "UNK"
|
|
740
|
+
),
|
|
741
|
+
"price": players_info.get(pick["element"], {}).get("price", 0),
|
|
742
|
+
}
|
|
743
|
+
for pick in bench
|
|
744
|
+
],
|
|
745
|
+
"automatic_subs": [
|
|
746
|
+
{
|
|
747
|
+
"player_out": store.get_player_name(sub["element_out"]),
|
|
748
|
+
"player_in": store.get_player_name(sub["element_in"]),
|
|
749
|
+
}
|
|
750
|
+
for sub in auto_subs
|
|
751
|
+
],
|
|
752
|
+
}
|
|
753
|
+
return format_json_response(result)
|
|
754
|
+
else:
|
|
755
|
+
result = format_manager_squad(
|
|
756
|
+
team_name=team_name,
|
|
757
|
+
player_name=player_name,
|
|
758
|
+
team_id=params.team_id,
|
|
759
|
+
gameweek=gameweek,
|
|
760
|
+
entry_history=entry_history,
|
|
761
|
+
picks=picks,
|
|
762
|
+
players_info=players_info,
|
|
763
|
+
active_chip=picks_data.get("active_chip"),
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if auto_subs:
|
|
767
|
+
result += "\n\n**Automatic Substitutions:**"
|
|
768
|
+
for sub in auto_subs:
|
|
769
|
+
player_out = store.get_player_name(sub["element_out"])
|
|
770
|
+
player_in = store.get_player_name(sub["element_in"])
|
|
771
|
+
result += f"\n├─ {player_out} → {player_in}"
|
|
772
|
+
|
|
773
|
+
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
774
|
+
return truncated
|
|
775
|
+
|
|
776
|
+
except Exception as e:
|
|
777
|
+
return handle_api_error(e)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@mcp.tool(
|
|
781
|
+
name="fpl_analyze_rival",
|
|
782
|
+
annotations={
|
|
783
|
+
"title": "Analyze FPL Rival",
|
|
784
|
+
"readOnlyHint": True,
|
|
785
|
+
"destructiveHint": False,
|
|
786
|
+
"idempotentHint": True,
|
|
787
|
+
"openWorldHint": True,
|
|
788
|
+
},
|
|
789
|
+
)
|
|
790
|
+
async def fpl_analyze_rival(params: AnalyzeRivalInput) -> str:
|
|
791
|
+
"""
|
|
792
|
+
Compare your team against a specific rival manager.
|
|
793
|
+
|
|
794
|
+
Provides a comprehensive head-to-head analysis including:
|
|
795
|
+
- Points and Rank comparison
|
|
796
|
+
- Chip usage history
|
|
797
|
+
- Captaincy comparison
|
|
798
|
+
- Key Differentials (players they own that you don't)
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
params (AnalyzeRivalInput): Validated input parameters containing:
|
|
802
|
+
- my_team_id (int): Your team ID
|
|
803
|
+
- rival_team_id (int): Rival's team ID
|
|
804
|
+
- gameweek (int | None): Gameweek number (defaults to current)
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
str: Detailed rival analysis and threat assessment
|
|
808
|
+
|
|
809
|
+
Examples:
|
|
810
|
+
- Compare me vs rival: my_team_id=123, rival_team_id=456
|
|
811
|
+
- Analyze past GW: my_team_id=123, rival_team_id=456, gameweek=10
|
|
812
|
+
|
|
813
|
+
Error Handling:
|
|
814
|
+
- Returns error if either team ID invalid
|
|
815
|
+
- Returns formatted error message if API fails
|
|
816
|
+
"""
|
|
817
|
+
try:
|
|
818
|
+
client = await _create_client()
|
|
819
|
+
|
|
820
|
+
# Determine gameweek
|
|
821
|
+
gameweek = params.gameweek
|
|
822
|
+
if not gameweek:
|
|
823
|
+
current = store.get_current_gameweek()
|
|
824
|
+
if not current:
|
|
825
|
+
return "Error: Could not determine current gameweek."
|
|
826
|
+
gameweek = current.id
|
|
827
|
+
|
|
828
|
+
# Fetch data for both managers
|
|
829
|
+
try:
|
|
830
|
+
my_entry = await client.get_manager_entry(params.my_team_id)
|
|
831
|
+
rival_entry = await client.get_manager_entry(params.rival_team_id)
|
|
832
|
+
except Exception:
|
|
833
|
+
return "Error: Could not retrieve manager details. Check Team IDs."
|
|
834
|
+
|
|
835
|
+
my_picks_data = await client.get_manager_gameweek_picks(params.my_team_id, gameweek)
|
|
836
|
+
rival_picks_data = await client.get_manager_gameweek_picks(params.rival_team_id, gameweek)
|
|
837
|
+
|
|
838
|
+
my_name = f"{my_entry.get('player_first_name')} {my_entry.get('player_last_name')}"
|
|
839
|
+
rival_name = f"{rival_entry.get('player_first_name')} {rival_entry.get('player_last_name')}"
|
|
840
|
+
|
|
841
|
+
my_team_name = my_entry.get("name")
|
|
842
|
+
rival_team_name = rival_entry.get("name")
|
|
843
|
+
|
|
844
|
+
# Extract picks (Starting XI + Bench)
|
|
845
|
+
my_picks = my_picks_data.get("picks", [])
|
|
846
|
+
rival_picks = rival_picks_data.get("picks", [])
|
|
847
|
+
|
|
848
|
+
# Helper to get active players (Starting XI - first 11)
|
|
849
|
+
# Note: Position 1-11 are starters, 12-15 bench
|
|
850
|
+
my_starters = {p["element"] for p in my_picks if p["position"] <= 11}
|
|
851
|
+
rival_starters = {p["element"] for p in rival_picks if p["position"] <= 11}
|
|
852
|
+
|
|
853
|
+
# Differentials (My unique vs Rival unique)
|
|
854
|
+
my_unique = my_starters - rival_starters
|
|
855
|
+
rival_unique = rival_starters - my_starters
|
|
856
|
+
common = my_starters & rival_starters
|
|
857
|
+
|
|
858
|
+
# Captains
|
|
859
|
+
my_cap = next((p for p in my_picks if p["is_captain"]), None)
|
|
860
|
+
rival_cap = next((p for p in rival_picks if p["is_captain"]), None)
|
|
861
|
+
|
|
862
|
+
my_cap_name = store.get_player_name(my_cap["element"]) if my_cap else "None"
|
|
863
|
+
rival_cap_name = store.get_player_name(rival_cap["element"]) if rival_cap else "None"
|
|
864
|
+
|
|
865
|
+
# Stats
|
|
866
|
+
my_history = my_picks_data.get("entry_history", {})
|
|
867
|
+
rival_history = rival_picks_data.get("entry_history", {})
|
|
868
|
+
|
|
869
|
+
if params.response_format == ResponseFormat.JSON:
|
|
870
|
+
result = {
|
|
871
|
+
"gameweek": gameweek,
|
|
872
|
+
"managers": {
|
|
873
|
+
"me": {
|
|
874
|
+
"name": my_name,
|
|
875
|
+
"team": my_team_name,
|
|
876
|
+
"points": my_history.get("points"),
|
|
877
|
+
"rank": my_history.get("overall_rank"),
|
|
878
|
+
},
|
|
879
|
+
"rival": {
|
|
880
|
+
"name": rival_name,
|
|
881
|
+
"team": rival_team_name,
|
|
882
|
+
"points": rival_history.get("points"),
|
|
883
|
+
"rank": rival_history.get("overall_rank"),
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
"comparison": {
|
|
887
|
+
"common_players_count": len(common),
|
|
888
|
+
"differentials_count": len(rival_unique),
|
|
889
|
+
"points_diff": my_history.get("points", 0) - rival_history.get("points", 0),
|
|
890
|
+
},
|
|
891
|
+
"captains": {"me": my_cap_name, "rival": rival_cap_name},
|
|
892
|
+
"rival_differentials": [store.get_player_name(pid) for pid in rival_unique],
|
|
893
|
+
}
|
|
894
|
+
return format_json_response(result)
|
|
895
|
+
|
|
896
|
+
# Markdown Output
|
|
897
|
+
output = [
|
|
898
|
+
f"## Rival Analysis: {my_name} vs {rival_name}",
|
|
899
|
+
f"**Gameweek {gameweek}**",
|
|
900
|
+
"",
|
|
901
|
+
"### 🏆 Performance & Rank",
|
|
902
|
+
f"| Metric | 👤 You ({my_team_name}) | 🆚 Rival ({rival_team_name}) | Diff |",
|
|
903
|
+
"| :--- | :--- | :--- | :--- |",
|
|
904
|
+
f"| **GW Points** | {my_history.get('points', 0)} | {rival_history.get('points', 0)} | {my_history.get('points', 0) - rival_history.get('points', 0):+d} |",
|
|
905
|
+
f"| **Total Pts** | {my_history.get('total_points', 0)} | {rival_history.get('total_points', 0)} | {my_history.get('total_points', 0) - rival_history.get('total_points', 0):+d} |",
|
|
906
|
+
f"| **Rank** | {my_history.get('overall_rank', 0):,} | {rival_history.get('overall_rank', 0):,} | --- |",
|
|
907
|
+
f"| **Captain** | {my_cap_name} | {rival_cap_name} | {'✅ Same' if my_cap['element'] == rival_cap['element'] else '⚠️ Diff'} |",
|
|
908
|
+
f"| **Chip** | {my_picks_data.get('active_chip') or 'None'} | {rival_picks_data.get('active_chip') or 'None'} | --- |",
|
|
909
|
+
"",
|
|
910
|
+
"### ⚠️ Threat Assessment (Differentials)",
|
|
911
|
+
"Players in their starting XI that you DO NOT have:",
|
|
912
|
+
"",
|
|
913
|
+
]
|
|
914
|
+
|
|
915
|
+
if rival_unique:
|
|
916
|
+
for pid in rival_unique:
|
|
917
|
+
p_name = store.get_player_name(pid)
|
|
918
|
+
# Get live points if possible
|
|
919
|
+
p_data = next((p for p in store.bootstrap_data.elements if p.id == pid), None)
|
|
920
|
+
points = p_data.event_points if p_data else "?"
|
|
921
|
+
output.append(f"- **{p_name}** ({points} pts)")
|
|
922
|
+
else:
|
|
923
|
+
output.append("No starting XI differentials! You have a full template match.")
|
|
924
|
+
|
|
925
|
+
output.append("")
|
|
926
|
+
output.append("### 🛡️ Your Advantages")
|
|
927
|
+
output.append("Players you have that they don't:")
|
|
928
|
+
|
|
929
|
+
if my_unique:
|
|
930
|
+
for pid in my_unique:
|
|
931
|
+
p_name = store.get_player_name(pid)
|
|
932
|
+
p_data = next((p for p in store.bootstrap_data.elements if p.id == pid), None)
|
|
933
|
+
points = p_data.event_points if p_data else "?"
|
|
934
|
+
output.append(f"- **{p_name}** ({points} pts)")
|
|
935
|
+
else:
|
|
936
|
+
output.append("No unique players.")
|
|
937
|
+
|
|
938
|
+
return "\n".join(output)
|
|
939
|
+
|
|
940
|
+
except Exception as e:
|
|
941
|
+
return handle_api_error(e)
|