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/teams.py
CHANGED
|
@@ -4,7 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
4
4
|
|
|
5
5
|
from ..client import FPLClient
|
|
6
6
|
from ..constants import CHARACTER_LIMIT
|
|
7
|
-
from ..formatting import format_difficulty_indicator
|
|
7
|
+
from ..formatting import format_difficulty_indicator
|
|
8
8
|
from ..state import store
|
|
9
9
|
from ..utils import (
|
|
10
10
|
ResponseFormat,
|
|
@@ -15,34 +15,6 @@ from ..utils import (
|
|
|
15
15
|
from . import mcp
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class GetTeamInfoInput(BaseModel):
|
|
19
|
-
"""Input model for getting team information."""
|
|
20
|
-
|
|
21
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
22
|
-
|
|
23
|
-
team_name: str = Field(
|
|
24
|
-
...,
|
|
25
|
-
description="Team name or abbreviation (e.g., 'Arsenal', 'MCI', 'Liverpool')",
|
|
26
|
-
min_length=2,
|
|
27
|
-
max_length=50,
|
|
28
|
-
)
|
|
29
|
-
response_format: ResponseFormat = Field(
|
|
30
|
-
default=ResponseFormat.MARKDOWN,
|
|
31
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ListAllTeamsInput(BaseModel):
|
|
36
|
-
"""Input model for listing all teams."""
|
|
37
|
-
|
|
38
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
39
|
-
|
|
40
|
-
response_format: ResponseFormat = Field(
|
|
41
|
-
default=ResponseFormat.MARKDOWN,
|
|
42
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
18
|
class AnalyzeTeamFixturesInput(BaseModel):
|
|
47
19
|
"""Input model for analyzing team fixtures."""
|
|
48
20
|
|
|
@@ -71,150 +43,6 @@ async def _create_client():
|
|
|
71
43
|
return client
|
|
72
44
|
|
|
73
45
|
|
|
74
|
-
@mcp.tool(
|
|
75
|
-
name="fpl_get_team_info",
|
|
76
|
-
annotations={
|
|
77
|
-
"title": "Get FPL Team Information",
|
|
78
|
-
"readOnlyHint": True,
|
|
79
|
-
"destructiveHint": False,
|
|
80
|
-
"idempotentHint": True,
|
|
81
|
-
"openWorldHint": True,
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
async def fpl_get_team_info(params: GetTeamInfoInput) -> str:
|
|
85
|
-
"""
|
|
86
|
-
Get detailed information about a specific Premier League team.
|
|
87
|
-
|
|
88
|
-
Returns team strength ratings for home/away attack/defence, useful for assessing
|
|
89
|
-
which teams have strong defensive or attacking potential.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
params (GetTeamInfoInput): Validated input parameters containing:
|
|
93
|
-
- team_name (str): Team name or abbreviation (e.g., 'Arsenal', 'MCI')
|
|
94
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
str: Detailed team information with strength ratings
|
|
98
|
-
|
|
99
|
-
Examples:
|
|
100
|
-
- Get Arsenal info: team_name="Arsenal"
|
|
101
|
-
- Use abbreviation: team_name="LIV"
|
|
102
|
-
- Get JSON format: team_name="Man City", response_format="json"
|
|
103
|
-
|
|
104
|
-
Error Handling:
|
|
105
|
-
- Returns error if no team found
|
|
106
|
-
- Returns error if multiple teams match (asks user to be more specific)
|
|
107
|
-
- Returns formatted error message if data unavailable
|
|
108
|
-
"""
|
|
109
|
-
try:
|
|
110
|
-
await _create_client()
|
|
111
|
-
if not store.bootstrap_data:
|
|
112
|
-
return "Error: Team data not available. Please try again later."
|
|
113
|
-
|
|
114
|
-
matching_teams = [
|
|
115
|
-
t
|
|
116
|
-
for t in store.bootstrap_data.teams
|
|
117
|
-
if params.team_name.lower() in t.name.lower()
|
|
118
|
-
or params.team_name.lower() in t.short_name.lower()
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
if not matching_teams:
|
|
122
|
-
return f"No team found matching '{params.team_name}'. Try using the full team name or abbreviation."
|
|
123
|
-
|
|
124
|
-
if len(matching_teams) > 1:
|
|
125
|
-
team_list = ", ".join([f"{t.name} ({t.short_name})" for t in matching_teams])
|
|
126
|
-
return f"Multiple teams found: {team_list}. Please be more specific."
|
|
127
|
-
|
|
128
|
-
team = matching_teams[0]
|
|
129
|
-
team_dict = store.get_team_by_id(team.id)
|
|
130
|
-
|
|
131
|
-
if params.response_format == ResponseFormat.JSON:
|
|
132
|
-
return format_json_response(team_dict)
|
|
133
|
-
else:
|
|
134
|
-
# Convert Team object to dict for formatter
|
|
135
|
-
team_dict = {
|
|
136
|
-
"name": team.name,
|
|
137
|
-
"short_name": team.short_name,
|
|
138
|
-
"strength": getattr(team, "strength", None),
|
|
139
|
-
"strength_overall_home": getattr(team, "strength_overall_home", None),
|
|
140
|
-
"strength_overall_away": getattr(team, "strength_overall_away", None),
|
|
141
|
-
"strength_attack_home": getattr(team, "strength_attack_home", None),
|
|
142
|
-
"strength_attack_away": getattr(team, "strength_attack_away", None),
|
|
143
|
-
"strength_defence_home": getattr(team, "strength_defence_home", None),
|
|
144
|
-
"strength_defence_away": getattr(team, "strength_defence_away", None),
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
result = format_team_details(team_dict)
|
|
148
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
149
|
-
return truncated
|
|
150
|
-
|
|
151
|
-
except Exception as e:
|
|
152
|
-
return handle_api_error(e)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@mcp.tool(
|
|
156
|
-
name="fpl_list_all_teams",
|
|
157
|
-
annotations={
|
|
158
|
-
"title": "List All FPL Teams",
|
|
159
|
-
"readOnlyHint": True,
|
|
160
|
-
"destructiveHint": False,
|
|
161
|
-
"idempotentHint": True,
|
|
162
|
-
"openWorldHint": True,
|
|
163
|
-
},
|
|
164
|
-
)
|
|
165
|
-
async def fpl_list_all_teams(params: ListAllTeamsInput) -> str:
|
|
166
|
-
"""
|
|
167
|
-
List all Premier League teams with their basic information.
|
|
168
|
-
|
|
169
|
-
Returns all 20 Premier League teams with their names, abbreviations, and average
|
|
170
|
-
strength ratings. Useful for finding exact team names or comparing team strengths.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
params (ListAllTeamsInput): Validated input parameters containing:
|
|
174
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
str: List of all teams with strength ratings
|
|
178
|
-
|
|
179
|
-
Examples:
|
|
180
|
-
- List all teams: response_format="markdown"
|
|
181
|
-
- Get as JSON: response_format="json"
|
|
182
|
-
|
|
183
|
-
Error Handling:
|
|
184
|
-
- Returns error if team data unavailable
|
|
185
|
-
- Returns formatted error message if API fails
|
|
186
|
-
"""
|
|
187
|
-
try:
|
|
188
|
-
await _create_client()
|
|
189
|
-
teams = store.get_all_teams()
|
|
190
|
-
if not teams:
|
|
191
|
-
return "Error: Team data not available. Please try again later."
|
|
192
|
-
|
|
193
|
-
teams_sorted = sorted(teams, key=lambda t: t["name"])
|
|
194
|
-
|
|
195
|
-
if params.response_format == ResponseFormat.JSON:
|
|
196
|
-
return format_json_response({"count": len(teams_sorted), "teams": teams_sorted})
|
|
197
|
-
else:
|
|
198
|
-
output = ["**Premier League Teams:**\n"]
|
|
199
|
-
|
|
200
|
-
for team in teams_sorted:
|
|
201
|
-
strength_info = ""
|
|
202
|
-
if team.get("strength_overall_home") and team.get("strength_overall_away"):
|
|
203
|
-
avg_strength = (
|
|
204
|
-
team["strength_overall_home"] + team["strength_overall_away"]
|
|
205
|
-
) / 2
|
|
206
|
-
strength_info = f" | Strength: {avg_strength:.0f}"
|
|
207
|
-
|
|
208
|
-
output.append(f"{team['name']:20s} ({team['short_name']}){strength_info}")
|
|
209
|
-
|
|
210
|
-
result = "\n".join(output)
|
|
211
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
212
|
-
return truncated
|
|
213
|
-
|
|
214
|
-
except Exception as e:
|
|
215
|
-
return handle_api_error(e)
|
|
216
|
-
|
|
217
|
-
|
|
218
46
|
@mcp.tool(
|
|
219
47
|
name="fpl_analyze_team_fixtures",
|
|
220
48
|
annotations={
|
src/tools/transfers.py
CHANGED
|
@@ -4,31 +4,17 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
4
4
|
|
|
5
5
|
from ..client import FPLClient
|
|
6
6
|
from ..constants import CHARACTER_LIMIT
|
|
7
|
+
from ..formatting import format_player_price
|
|
7
8
|
from ..state import store
|
|
8
9
|
from ..utils import (
|
|
9
10
|
ResponseFormat,
|
|
10
11
|
check_and_truncate,
|
|
11
12
|
format_json_response,
|
|
12
|
-
format_player_price,
|
|
13
13
|
handle_api_error,
|
|
14
14
|
)
|
|
15
15
|
from . import mcp
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class GetPlayerTransfersByGameweekInput(BaseModel):
|
|
19
|
-
"""Input model for getting player transfer statistics."""
|
|
20
|
-
|
|
21
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
22
|
-
|
|
23
|
-
player_name: str = Field(
|
|
24
|
-
...,
|
|
25
|
-
description="Player name (e.g., 'Haaland', 'Salah')",
|
|
26
|
-
min_length=2,
|
|
27
|
-
max_length=100,
|
|
28
|
-
)
|
|
29
|
-
gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
30
|
-
|
|
31
|
-
|
|
32
18
|
class GetTopTransferredPlayersInput(BaseModel):
|
|
33
19
|
"""Input model for getting top transferred players."""
|
|
34
20
|
|
|
@@ -50,6 +36,32 @@ class GetManagerTransfersByGameweekInput(BaseModel):
|
|
|
50
36
|
gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
51
37
|
|
|
52
38
|
|
|
39
|
+
class AnalyzeTransferInput(BaseModel):
|
|
40
|
+
"""Input model for analyzing a potential transfer."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
43
|
+
|
|
44
|
+
player_out: str = Field(
|
|
45
|
+
...,
|
|
46
|
+
description="Name of player to transfer out (e.g., 'Salah')",
|
|
47
|
+
min_length=2,
|
|
48
|
+
max_length=100,
|
|
49
|
+
)
|
|
50
|
+
player_in: str = Field(
|
|
51
|
+
...,
|
|
52
|
+
description="Name of player to transfer in (e.g., 'Palmer')",
|
|
53
|
+
min_length=2,
|
|
54
|
+
max_length=100,
|
|
55
|
+
)
|
|
56
|
+
my_team_id: int | None = Field(
|
|
57
|
+
default=None, description="Your team ID to check budget/value impact (optional)"
|
|
58
|
+
)
|
|
59
|
+
response_format: ResponseFormat = Field(
|
|
60
|
+
default=ResponseFormat.MARKDOWN,
|
|
61
|
+
description="Output format: 'markdown' or 'json'",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
53
65
|
async def _create_client():
|
|
54
66
|
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
55
67
|
client = FPLClient(store=store)
|
|
@@ -58,103 +70,6 @@ async def _create_client():
|
|
|
58
70
|
return client
|
|
59
71
|
|
|
60
72
|
|
|
61
|
-
@mcp.tool(
|
|
62
|
-
name="fpl_get_player_transfers_by_gameweek",
|
|
63
|
-
annotations={
|
|
64
|
-
"title": "Get FPL Player Transfer Statistics",
|
|
65
|
-
"readOnlyHint": True,
|
|
66
|
-
"destructiveHint": False,
|
|
67
|
-
"idempotentHint": True,
|
|
68
|
-
"openWorldHint": True,
|
|
69
|
-
},
|
|
70
|
-
)
|
|
71
|
-
async def fpl_get_player_transfers_by_gameweek(
|
|
72
|
-
params: GetPlayerTransfersByGameweekInput,
|
|
73
|
-
) -> str:
|
|
74
|
-
"""
|
|
75
|
-
Get transfer statistics for a specific player in a specific gameweek.
|
|
76
|
-
|
|
77
|
-
Shows transfers in, transfers out, net transfers, ownership data, and performance.
|
|
78
|
-
Useful for understanding how manager sentiment towards a player changed during a
|
|
79
|
-
specific gameweek and correlation with performance.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
params (GetPlayerTransfersByGameweekInput): Validated input parameters containing:
|
|
83
|
-
- player_name (str): Player name (e.g., 'Haaland', 'Salah')
|
|
84
|
-
- gameweek (int): Gameweek number between 1-38
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
str: Transfer statistics and performance for the gameweek
|
|
88
|
-
|
|
89
|
-
Examples:
|
|
90
|
-
- Check Haaland GW20: player_name="Haaland", gameweek=20
|
|
91
|
-
- Salah transfers: player_name="Salah", gameweek=15
|
|
92
|
-
|
|
93
|
-
Error Handling:
|
|
94
|
-
- Returns error if player not found
|
|
95
|
-
- Suggests using fpl_find_player if name ambiguous
|
|
96
|
-
- Returns error if no data for gameweek
|
|
97
|
-
"""
|
|
98
|
-
try:
|
|
99
|
-
client = await _create_client()
|
|
100
|
-
|
|
101
|
-
# Find player by name
|
|
102
|
-
matches = store.find_players_by_name(params.player_name, fuzzy=True)
|
|
103
|
-
if not matches:
|
|
104
|
-
return f"No player found matching '{params.player_name}'. Use fpl_search_players to find the correct name."
|
|
105
|
-
|
|
106
|
-
if len(matches) > 1 and matches[0][1] < 0.95:
|
|
107
|
-
return f"Ambiguous player name. Use fpl_find_player to see all matches for '{params.player_name}'"
|
|
108
|
-
|
|
109
|
-
player = matches[0][0]
|
|
110
|
-
player_id = player.id
|
|
111
|
-
|
|
112
|
-
# Fetch detailed summary from API
|
|
113
|
-
summary_data = await client.get_element_summary(player_id)
|
|
114
|
-
history = summary_data.get("history", [])
|
|
115
|
-
|
|
116
|
-
# Find the specific gameweek in history
|
|
117
|
-
gw_data = next((gw for gw in history if gw.get("round") == params.gameweek), None)
|
|
118
|
-
|
|
119
|
-
if not gw_data:
|
|
120
|
-
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."
|
|
121
|
-
|
|
122
|
-
# Enrich with team name
|
|
123
|
-
enriched_history = store.enrich_gameweek_history([gw_data])
|
|
124
|
-
if enriched_history:
|
|
125
|
-
gw_data = enriched_history[0]
|
|
126
|
-
|
|
127
|
-
output = [
|
|
128
|
-
f"**{player.web_name}** ({player.first_name} {player.second_name})",
|
|
129
|
-
f"Team: {player.team_name} | Position: {player.position} | Price: {format_player_price(player.now_cost)}",
|
|
130
|
-
"",
|
|
131
|
-
f"**Gameweek {params.gameweek} Transfer Statistics:**",
|
|
132
|
-
"",
|
|
133
|
-
f"├─ Transfers In: {gw_data.get('transfers_in', 0):,}",
|
|
134
|
-
f"├─ Transfers Out: {gw_data.get('transfers_out', 0):,}",
|
|
135
|
-
f"├─ Net Transfers: {gw_data.get('transfers_balance', gw_data.get('transfers_in', 0) - gw_data.get('transfers_out', 0)):+,}",
|
|
136
|
-
f"├─ Ownership at GW: {gw_data.get('selected', 0):,} teams",
|
|
137
|
-
"",
|
|
138
|
-
f"**Performance in GW{params.gameweek}:**",
|
|
139
|
-
f"├─ Points: {gw_data.get('total_points', 0)}",
|
|
140
|
-
f"├─ Minutes: {gw_data.get('minutes', 0)}",
|
|
141
|
-
f"├─ xGoal: {gw_data.get('expected_goals', '0.00')} | Goals: {gw_data.get('goals_scored', 0)}",
|
|
142
|
-
f"├─ xAssist: {gw_data.get('expected_assists', '0.00')} | Assists: {gw_data.get('assists', 0)}",
|
|
143
|
-
f"├─ Clean Sheets: {gw_data.get('clean_sheets', 0)} | Bonus: {gw_data.get('bonus', 0)}",
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
opponent_name = gw_data.get("opponent_team_short", "Unknown")
|
|
147
|
-
home_away = "H" if gw_data.get("was_home") else "A"
|
|
148
|
-
output.append(f"├─ Opponent: vs {opponent_name} ({home_away})")
|
|
149
|
-
|
|
150
|
-
result = "\n".join(output)
|
|
151
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
152
|
-
return truncated
|
|
153
|
-
|
|
154
|
-
except Exception as e:
|
|
155
|
-
return handle_api_error(e)
|
|
156
|
-
|
|
157
|
-
|
|
158
73
|
@mcp.tool(
|
|
159
74
|
name="fpl_get_top_transferred_players",
|
|
160
75
|
annotations={
|
|
@@ -611,3 +526,226 @@ async def fpl_get_manager_chips(params: GetManagerChipsInput) -> str:
|
|
|
611
526
|
|
|
612
527
|
except Exception as e:
|
|
613
528
|
return handle_api_error(e)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@mcp.tool(
|
|
532
|
+
name="fpl_analyze_transfer",
|
|
533
|
+
annotations={
|
|
534
|
+
"title": "Analyze FPL Transfer",
|
|
535
|
+
"readOnlyHint": True,
|
|
536
|
+
"destructiveHint": False,
|
|
537
|
+
"idempotentHint": True,
|
|
538
|
+
"openWorldHint": True,
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
async def fpl_analyze_transfer(params: AnalyzeTransferInput) -> str:
|
|
542
|
+
"""
|
|
543
|
+
Analyze a potential transfer decision between two players.
|
|
544
|
+
|
|
545
|
+
Compares the player being transferred out vs the player being transferred in.
|
|
546
|
+
Analyzes form, upcoming fixtures (next 5), price difference, and overall value.
|
|
547
|
+
Provides a direct recommendation based on the data.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
params (AnalyzeTransferInput): Validated input parameters containing:
|
|
551
|
+
- player_out (str): Name of player to remove
|
|
552
|
+
- player_in (str): Name of player to add
|
|
553
|
+
- my_team_id (int | None): Optional team ID to check budget impact
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
str: Detailed transfer analysis and recommendation
|
|
557
|
+
|
|
558
|
+
Examples:
|
|
559
|
+
- Analyze move: player_out="Salah", player_in="Palmer"
|
|
560
|
+
- Check budget: player_out="Saka", player_in="Foden", my_team_id=123456
|
|
561
|
+
|
|
562
|
+
Error Handling:
|
|
563
|
+
- Returns error if either player not found
|
|
564
|
+
- Returns error if players play different positions (unless specified)
|
|
565
|
+
- Returns formatted error message if API fails
|
|
566
|
+
"""
|
|
567
|
+
try:
|
|
568
|
+
client = await _create_client()
|
|
569
|
+
if not store.bootstrap_data:
|
|
570
|
+
return "Error: Player data not available. Please try again later."
|
|
571
|
+
|
|
572
|
+
# Helper to find player
|
|
573
|
+
def find_player(name):
|
|
574
|
+
matches = store.find_players_by_name(name, fuzzy=True)
|
|
575
|
+
if not matches:
|
|
576
|
+
return None, f"Player '{name}' not found."
|
|
577
|
+
if (
|
|
578
|
+
len(matches) > 1
|
|
579
|
+
and matches[0][1] < 0.95
|
|
580
|
+
and not (matches[0][1] - matches[1][1] > 0.2)
|
|
581
|
+
):
|
|
582
|
+
return (
|
|
583
|
+
None,
|
|
584
|
+
f"Ambiguous name '{name}'. Did you mean: {', '.join(m[0].web_name for m in matches[:3])}?",
|
|
585
|
+
)
|
|
586
|
+
return matches[0][0], None
|
|
587
|
+
|
|
588
|
+
# Find both players
|
|
589
|
+
p_out, err_out = find_player(params.player_out)
|
|
590
|
+
if err_out:
|
|
591
|
+
return f"Error finding player_out: {err_out}"
|
|
592
|
+
|
|
593
|
+
p_in, err_in = find_player(params.player_in)
|
|
594
|
+
if err_in:
|
|
595
|
+
return f"Error finding player_in: {err_in}"
|
|
596
|
+
|
|
597
|
+
# Check positions
|
|
598
|
+
pos_names = {1: "GKP", 2: "DEF", 3: "MID", 4: "FWD"}
|
|
599
|
+
if p_out.element_type != p_in.element_type:
|
|
600
|
+
# element_type 1=GKP, 2=DEF, 3=MID, 4=FWD
|
|
601
|
+
# Get position names
|
|
602
|
+
return (
|
|
603
|
+
f"Invalid transfer: {p_out.web_name} is a {pos_names.get(p_out.element_type)} "
|
|
604
|
+
f"while {p_in.web_name} is a {pos_names.get(p_in.element_type)}. "
|
|
605
|
+
f"Transfers must be between players of the same position."
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Get fixtures for next 5 gameweeks
|
|
609
|
+
current_gw_data = store.get_current_gameweek()
|
|
610
|
+
start_gw = (current_gw_data.id + 1) if current_gw_data else 1
|
|
611
|
+
# Handle case where season is over
|
|
612
|
+
if start_gw > 38:
|
|
613
|
+
start_gw = 38
|
|
614
|
+
|
|
615
|
+
# Fetch detailed summaries for both
|
|
616
|
+
summary_out = await client.get_element_summary(p_out.id)
|
|
617
|
+
summary_in = await client.get_element_summary(p_in.id)
|
|
618
|
+
|
|
619
|
+
# Process Fixtures
|
|
620
|
+
fixtures_out = store.enrich_fixtures(summary_out.get("fixtures", []))
|
|
621
|
+
fixtures_in = store.enrich_fixtures(summary_in.get("fixtures", []))
|
|
622
|
+
|
|
623
|
+
# Filter next 5 fixtures
|
|
624
|
+
next_5_out = [f for f in fixtures_out if f["event"] and f["event"] >= start_gw][:5]
|
|
625
|
+
next_5_in = [f for f in fixtures_in if f["event"] and f["event"] >= start_gw][:5]
|
|
626
|
+
|
|
627
|
+
# Calculate difficulty score (lower is easier)
|
|
628
|
+
def calc_difficulty(fixtures, is_attacker):
|
|
629
|
+
total = 0
|
|
630
|
+
count = 0
|
|
631
|
+
for f in fixtures:
|
|
632
|
+
diff = (
|
|
633
|
+
f.get("team_h_difficulty") if f.get("is_home") else f.get("team_a_difficulty")
|
|
634
|
+
)
|
|
635
|
+
if diff is None:
|
|
636
|
+
diff = 3 # Default to average if missing
|
|
637
|
+
total += int(diff)
|
|
638
|
+
count += 1
|
|
639
|
+
return total / count if count > 0 else 0
|
|
640
|
+
|
|
641
|
+
# Heuristic: MIDs and FWDs are attackers, DEF and GKP defenders
|
|
642
|
+
is_attacker = p_out.element_type in [3, 4]
|
|
643
|
+
diff_out = calc_difficulty(next_5_out, is_attacker)
|
|
644
|
+
diff_in = calc_difficulty(next_5_in, is_attacker)
|
|
645
|
+
|
|
646
|
+
# Calculate budget impact
|
|
647
|
+
price_diff = p_in.now_cost - p_out.now_cost
|
|
648
|
+
|
|
649
|
+
if params.response_format == ResponseFormat.JSON:
|
|
650
|
+
result = {
|
|
651
|
+
"transfer": {
|
|
652
|
+
"out": p_out.web_name,
|
|
653
|
+
"in": p_in.web_name,
|
|
654
|
+
"position": p_out.position,
|
|
655
|
+
},
|
|
656
|
+
"analysis": {
|
|
657
|
+
"price_change": price_diff / 10,
|
|
658
|
+
"fixture_diff_score": {
|
|
659
|
+
"out": round(diff_out, 2),
|
|
660
|
+
"in": round(diff_in, 2),
|
|
661
|
+
"easier_fixtures": "in" if diff_in < diff_out else "out",
|
|
662
|
+
},
|
|
663
|
+
"form": {"out": float(p_out.form), "in": float(p_in.form)},
|
|
664
|
+
"points_per_game": {
|
|
665
|
+
"out": float(p_out.points_per_game),
|
|
666
|
+
"in": float(p_in.points_per_game),
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
"recommendation": "TRANSFER IN"
|
|
670
|
+
if diff_in < diff_out and float(p_in.form) > float(p_out.form)
|
|
671
|
+
else "HOLD",
|
|
672
|
+
}
|
|
673
|
+
return format_json_response(result)
|
|
674
|
+
|
|
675
|
+
# Markdown Output
|
|
676
|
+
output = [
|
|
677
|
+
f"## Transfer Analysis: {p_out.web_name} ➔ {p_in.web_name}",
|
|
678
|
+
f"**Position:** {pos_names.get(p_out.element_type)} | **Budget Impact:** {format_player_price(price_diff)}",
|
|
679
|
+
"",
|
|
680
|
+
"### 📊 Head-to-Head Comparison",
|
|
681
|
+
f"| Metric | ❌ {p_out.web_name} (OUT) | ✅ {p_in.web_name} (IN) | Diff |",
|
|
682
|
+
"| :--- | :--- | :--- | :--- |",
|
|
683
|
+
f"| **Price** | {format_player_price(p_out.now_cost)} | {format_player_price(p_in.now_cost)} | {format_player_price(price_diff)} |",
|
|
684
|
+
f"| **Form** | {p_out.form} | {p_in.form} | {float(p_in.form) - float(p_out.form):.1f} |",
|
|
685
|
+
f"| **PPG** | {p_out.points_per_game} | {p_in.points_per_game} | {float(p_in.points_per_game) - float(p_out.points_per_game):.1f} |",
|
|
686
|
+
f"| **Total Pts** | {getattr(p_out, 'total_points', 0)} | {getattr(p_in, 'total_points', 0)} | {getattr(p_in, 'total_points', 0) - getattr(p_out, 'total_points', 0)} |",
|
|
687
|
+
f"| **Ownership** | {getattr(p_out, 'selected_by_percent', '0.0')}% | {getattr(p_in, 'selected_by_percent', '0.0')}% | {float(getattr(p_in, 'selected_by_percent', '0.0')) - float(getattr(p_out, 'selected_by_percent', '0.0')):+.1f}% |",
|
|
688
|
+
"",
|
|
689
|
+
"### 🗓️ Upcoming Fixtures (Next 5)",
|
|
690
|
+
"Lower difficulty score is better (easier fixtures).",
|
|
691
|
+
"",
|
|
692
|
+
f"**{p_out.web_name}** (Avg Diff: {diff_out:.2f})",
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
# Helper to format fixture string
|
|
696
|
+
def format_fixture(f):
|
|
697
|
+
opp = f.get("team_a_short") if f.get("is_home") else f.get("team_h_short")
|
|
698
|
+
if not opp:
|
|
699
|
+
opp = "UNK"
|
|
700
|
+
diff = f.get("difficulty", "?")
|
|
701
|
+
loc = "H" if f.get("is_home") else "A"
|
|
702
|
+
return f"{opp} ({loc}) [{diff}]"
|
|
703
|
+
|
|
704
|
+
# Format fixtures
|
|
705
|
+
out_fixtures_str = " | ".join([format_fixture(f) for f in next_5_out])
|
|
706
|
+
output.append(f"└─ {out_fixtures_str}")
|
|
707
|
+
|
|
708
|
+
output.append("")
|
|
709
|
+
output.append(f"**{p_in.web_name}** (Avg Diff: {diff_in:.2f})")
|
|
710
|
+
in_fixtures_str = " | ".join([format_fixture(f) for f in next_5_in])
|
|
711
|
+
output.append(f"└─ {in_fixtures_str}")
|
|
712
|
+
|
|
713
|
+
output.append("")
|
|
714
|
+
output.append("### 💡 Recommendation")
|
|
715
|
+
|
|
716
|
+
# Simple Logic
|
|
717
|
+
better_fixtures = diff_in < diff_out
|
|
718
|
+
better_form = float(p_in.form) > float(p_out.form)
|
|
719
|
+
cheaper = price_diff < 0
|
|
720
|
+
|
|
721
|
+
score = 0
|
|
722
|
+
if better_fixtures:
|
|
723
|
+
score += 2
|
|
724
|
+
if better_form:
|
|
725
|
+
score += 1
|
|
726
|
+
if cheaper:
|
|
727
|
+
score += 0.5
|
|
728
|
+
if float(p_in.points_per_game) > float(p_out.points_per_game):
|
|
729
|
+
score += 1
|
|
730
|
+
|
|
731
|
+
if score >= 3:
|
|
732
|
+
output.append(
|
|
733
|
+
f"✅ **Recommended Transfer** - {p_in.web_name} is a strong upgrade with better fixtures and stats."
|
|
734
|
+
)
|
|
735
|
+
elif score >= 1.5:
|
|
736
|
+
output.append(
|
|
737
|
+
f"⚖️ **Consider Transfer** - {p_in.web_name} has some advantages, but it's close."
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
output.append(
|
|
741
|
+
f"🛑 **Hold Transfer** - {p_out.web_name} looks like the better hold right now."
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
# Add availability check
|
|
745
|
+
if p_in.status != "a":
|
|
746
|
+
output.append(f"\n⚠️ **Warning:** {p_in.web_name} is currently flagged: {p_in.news}")
|
|
747
|
+
|
|
748
|
+
return "\n".join(output)
|
|
749
|
+
|
|
750
|
+
except Exception as e:
|
|
751
|
+
return handle_api_error(e)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
src/cache.py,sha256=SeJAmddaY9507Ac5YRnbBBXGOQw_OwpIefB-kn11lDI,4604
|
|
2
|
-
src/client.py,sha256=_Tv7TlXD5d3pvXb7AmMCgy3gbZqjOO9EedMORveRU4s,10493
|
|
3
|
-
src/config.py,sha256=hfjW-W0gdH0PxmC6gEg-o9SqraajJ6gNy1SIlIOG-F4,845
|
|
4
|
-
src/constants.py,sha256=8XkQH1rslnf6VWbJkVY6MmpgRhhS3wjFJhIoZWr91kg,839
|
|
5
|
-
src/exceptions.py,sha256=Q8waMbF8Sr1s6lOoAB8-doX0v6EvqZopwQHGxNQ7m-w,2972
|
|
6
|
-
src/formatting.py,sha256=aLiJWM2hJw68gyGJ1Nc1nPAyfoSIqwyjPE8svr-7ufo,10236
|
|
7
|
-
src/main.py,sha256=C6wX96rm0-b1jSvU2BrTv47hw2FGktkwcqJ5nEM8t5U,977
|
|
8
|
-
src/models.py,sha256=P5rIO-UjVQpLUlDQsDV5hw2Tn3s5Xcj6ye8xJkRizGc,10880
|
|
9
|
-
src/rate_limiter.py,sha256=GLk3ZRFFvEZxkZAQd-pZ7UxQdrAAUVch3pxe_aMU-J8,3450
|
|
10
|
-
src/state.py,sha256=seyygRhlz-K1GtG80os34tnNJ6UkAFA2rVFgupZG2tY,17531
|
|
11
|
-
src/utils.py,sha256=WhcWQIXpc1vIjU8hyrGDJyKJSlcbVoG938k_3UMDlCM,7340
|
|
12
|
-
src/validators.py,sha256=aU36TUNYWb26fvZH27Xnryrp8gve9DM2phvy7vEnAi8,6891
|
|
13
|
-
src/prompts/__init__.py,sha256=Sj7YgIL46wGrmkJq39rpJilPK3blK6oPI-hE2-lBdxY,535
|
|
14
|
-
src/prompts/captain_recommendation.py,sha256=2UK4NQMKL8n1m7gLeebkEDhzndGuJXQBt1FLfS1oo2Y,5850
|
|
15
|
-
src/prompts/chips.py,sha256=zzv5bqr8HuUAkvXenonrTXVhwNYGMwH9OPSC-c-1Dtg,5524
|
|
16
|
-
src/prompts/league_analysis.py,sha256=23rNhCYkU8hSmd5BesXgNgHLFo_B8qgszmw909MPHkA,8095
|
|
17
|
-
src/prompts/player_analysis.py,sha256=SGyd0UYWMF0lgml9idfc853UHgXXBT_qLVLf-8PFePU,5242
|
|
18
|
-
src/prompts/squad_analysis.py,sha256=7ixTIrvTITvLIE-9ATH744ci_pObWgzx3p5yUqVHmEk,5204
|
|
19
|
-
src/prompts/team_analysis.py,sha256=lZZ2R1xlsclwy4UyiokMg41ziuCKAqxgN_CoT1mOvnY,4104
|
|
20
|
-
src/prompts/team_selection.py,sha256=tDOiyQYTp-hyKlKVAdjGxZsr1xPfMgApWREjbMtNpXM,3847
|
|
21
|
-
src/prompts/transfers.py,sha256=B99xjzJDTRRdwMluANjKxr5DPWB6eg69nZqJ5uyTosA,5448
|
|
22
|
-
src/resources/__init__.py,sha256=i7nlLVSLtiIrLtOnyoMiK3KTFGEnct4LXApB4b6URFM,303
|
|
23
|
-
src/resources/bootstrap.py,sha256=ViZsGYtr5YqiTtvM_YTkbCr6R6Z9vUBiVSGGI9wwI3s,6970
|
|
24
|
-
src/tools/__init__.py,sha256=JjoMoMHrhFRMarpgtOS9AoS9604c0p-yFc0PXoITe-E,510
|
|
25
|
-
src/tools/fixtures.py,sha256=rbt565LV4C_gXfM9tTGUKqMRGl-a_jXcOKZ1tVCXkrA,5634
|
|
26
|
-
src/tools/gameweeks.py,sha256=wylGJAXSXhmSy7-PdoXm-w4i4jQIXkSaqM27ctK6w_o,14859
|
|
27
|
-
src/tools/leagues.py,sha256=tW6FDjLf7pSWjGgsxCCMAyOpHvSxpBfYXxyaNtHQiLU,30308
|
|
28
|
-
src/tools/players.py,sha256=9UX1fZJbiUUDBFBMeImcIh8ysIfc1NQV21_298yX1cU,30568
|
|
29
|
-
src/tools/teams.py,sha256=wEbLHKivvGw5YhO0tyvxhUMR9nsYyb4-BQWNBbnzGTw,14183
|
|
30
|
-
src/tools/transfers.py,sha256=kU7xy3d6wDZ4T38gNIg6UBJWkfh9-fYhasY_uXR7qGE,24021
|
|
31
|
-
fpl_mcp_server-0.1.6.dist-info/METADATA,sha256=Pc1pmqRKBJE1ZyRH5IbL_jChwplQ91-hAaFOAwQzgyg,4788
|
|
32
|
-
fpl_mcp_server-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
-
fpl_mcp_server-0.1.6.dist-info/entry_points.txt,sha256=b3R5hBUMTLVnCGl07NfK7kyq9NCKtpn5Q8OsY79pMek,49
|
|
34
|
-
fpl_mcp_server-0.1.6.dist-info/licenses/LICENSE,sha256=HCDOcdX83voRU2Eip214yj6P_tEyjVjCsCW_sixZFPw,1071
|
|
35
|
-
fpl_mcp_server-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|