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.
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, format_team_details
7
+ from ..formatting import format_difficulty_indicator
8
8
  from ..state import store
9
9
  from ..utils import (
10
10
  ResponseFormat,
@@ -12,42 +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
-
24
- class GetTeamInfoInput(BaseModel):
25
- """Input model for getting team information."""
26
-
27
- model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
28
-
29
- team_name: str = Field(
30
- ...,
31
- description="Team name or abbreviation (e.g., 'Arsenal', 'MCI', 'Liverpool')",
32
- min_length=2,
33
- max_length=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 ListAllTeamsInput(BaseModel):
42
- """Input model for listing all teams."""
43
-
44
- model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
45
-
46
- response_format: ResponseFormat = Field(
47
- default=ResponseFormat.MARKDOWN,
48
- description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
49
- )
50
-
51
17
 
52
18
  class AnalyzeTeamFixturesInput(BaseModel):
53
19
  """Input model for analyzing team fixtures."""
@@ -69,11 +35,6 @@ class AnalyzeTeamFixturesInput(BaseModel):
69
35
  )
70
36
 
71
37
 
72
- # =============================================================================
73
- # Helper Functions
74
- # =============================================================================
75
-
76
-
77
38
  async def _create_client():
78
39
  """Create an unauthenticated FPL client for public API access and ensure data is loaded."""
79
40
  client = FPLClient(store=store)
@@ -82,155 +43,6 @@ async def _create_client():
82
43
  return client
83
44
 
84
45
 
85
- # =============================================================================
86
- # MCP Tools
87
- # =============================================================================
88
-
89
-
90
- @mcp.tool(
91
- name="fpl_get_team_info",
92
- annotations={
93
- "title": "Get FPL Team Information",
94
- "readOnlyHint": True,
95
- "destructiveHint": False,
96
- "idempotentHint": True,
97
- "openWorldHint": True,
98
- },
99
- )
100
- async def fpl_get_team_info(params: GetTeamInfoInput) -> str:
101
- """
102
- Get detailed information about a specific Premier League team.
103
-
104
- Returns team strength ratings for home/away attack/defence, useful for assessing
105
- which teams have strong defensive or attacking potential.
106
-
107
- Args:
108
- params (GetTeamInfoInput): Validated input parameters containing:
109
- - team_name (str): Team name or abbreviation (e.g., 'Arsenal', 'MCI')
110
- - response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
111
-
112
- Returns:
113
- str: Detailed team information with strength ratings
114
-
115
- Examples:
116
- - Get Arsenal info: team_name="Arsenal"
117
- - Use abbreviation: team_name="LIV"
118
- - Get JSON format: team_name="Man City", response_format="json"
119
-
120
- Error Handling:
121
- - Returns error if no team found
122
- - Returns error if multiple teams match (asks user to be more specific)
123
- - Returns formatted error message if data unavailable
124
- """
125
- try:
126
- await _create_client()
127
- if not store.bootstrap_data:
128
- return "Error: Team data not available. Please try again later."
129
-
130
- matching_teams = [
131
- t
132
- for t in store.bootstrap_data.teams
133
- if params.team_name.lower() in t.name.lower()
134
- or params.team_name.lower() in t.short_name.lower()
135
- ]
136
-
137
- if not matching_teams:
138
- return f"No team found matching '{params.team_name}'. Try using the full team name or abbreviation."
139
-
140
- if len(matching_teams) > 1:
141
- team_list = ", ".join([f"{t.name} ({t.short_name})" for t in matching_teams])
142
- return f"Multiple teams found: {team_list}. Please be more specific."
143
-
144
- team = matching_teams[0]
145
- team_dict = store.get_team_by_id(team.id)
146
-
147
- if params.response_format == ResponseFormat.JSON:
148
- return format_json_response(team_dict)
149
- else:
150
- # Convert Team object to dict for formatter
151
- team_dict = {
152
- "name": team.name,
153
- "short_name": team.short_name,
154
- "strength": getattr(team, "strength", None),
155
- "strength_overall_home": getattr(team, "strength_overall_home", None),
156
- "strength_overall_away": getattr(team, "strength_overall_away", None),
157
- "strength_attack_home": getattr(team, "strength_attack_home", None),
158
- "strength_attack_away": getattr(team, "strength_attack_away", None),
159
- "strength_defence_home": getattr(team, "strength_defence_home", None),
160
- "strength_defence_away": getattr(team, "strength_defence_away", None),
161
- }
162
-
163
- result = format_team_details(team_dict)
164
- truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
165
- return truncated
166
-
167
- except Exception as e:
168
- return handle_api_error(e)
169
-
170
-
171
- @mcp.tool(
172
- name="fpl_list_all_teams",
173
- annotations={
174
- "title": "List All FPL Teams",
175
- "readOnlyHint": True,
176
- "destructiveHint": False,
177
- "idempotentHint": True,
178
- "openWorldHint": True,
179
- },
180
- )
181
- async def fpl_list_all_teams(params: ListAllTeamsInput) -> str:
182
- """
183
- List all Premier League teams with their basic information.
184
-
185
- Returns all 20 Premier League teams with their names, abbreviations, and average
186
- strength ratings. Useful for finding exact team names or comparing team strengths.
187
-
188
- Args:
189
- params (ListAllTeamsInput): Validated input parameters containing:
190
- - response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
191
-
192
- Returns:
193
- str: List of all teams with strength ratings
194
-
195
- Examples:
196
- - List all teams: response_format="markdown"
197
- - Get as JSON: response_format="json"
198
-
199
- Error Handling:
200
- - Returns error if team data unavailable
201
- - Returns formatted error message if API fails
202
- """
203
- try:
204
- await _create_client()
205
- teams = store.get_all_teams()
206
- if not teams:
207
- return "Error: Team data not available. Please try again later."
208
-
209
- teams_sorted = sorted(teams, key=lambda t: t["name"])
210
-
211
- if params.response_format == ResponseFormat.JSON:
212
- return format_json_response({"count": len(teams_sorted), "teams": teams_sorted})
213
- else:
214
- output = ["**Premier League Teams:**\n"]
215
-
216
- for team in teams_sorted:
217
- strength_info = ""
218
- if team.get("strength_overall_home") and team.get("strength_overall_away"):
219
- avg_strength = (
220
- team["strength_overall_home"] + team["strength_overall_away"]
221
- ) / 2
222
- strength_info = f" | Strength: {avg_strength:.0f}"
223
-
224
- output.append(f"{team['name']:20s} ({team['short_name']}){strength_info}")
225
-
226
- result = "\n".join(output)
227
- truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
228
- return truncated
229
-
230
- except Exception as e:
231
- return handle_api_error(e)
232
-
233
-
234
46
  @mcp.tool(
235
47
  name="fpl_analyze_team_fixtures",
236
48
  annotations={
src/tools/transfers.py CHANGED
@@ -4,36 +4,16 @@ 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
-
16
- # Import shared mcp instance
17
15
  from . import mcp
18
16
 
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
17
 
38
18
  class GetTopTransferredPlayersInput(BaseModel):
39
19
  """Input model for getting top transferred players."""
@@ -56,9 +36,30 @@ class GetManagerTransfersByGameweekInput(BaseModel):
56
36
  gameweek: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
57
37
 
58
38
 
59
- # =============================================================================
60
- # Helper Functions
61
- # =============================================================================
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
+ )
62
63
 
63
64
 
64
65
  async def _create_client():
@@ -69,108 +70,6 @@ async def _create_client():
69
70
  return client
70
71
 
71
72
 
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
73
  @mcp.tool(
175
74
  name="fpl_get_top_transferred_players",
176
75
  annotations={
@@ -627,3 +526,226 @@ async def fpl_get_manager_chips(params: GetManagerChipsInput) -> str:
627
526
 
628
527
  except Exception as e:
629
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,33 +0,0 @@
1
- src/cache.py,sha256=SeJAmddaY9507Ac5YRnbBBXGOQw_OwpIefB-kn11lDI,4604
2
- src/client.py,sha256=9c7jViZx-YavjDeNNeBt43cAqxVW_4NK08ztUbhYvZA,9737
3
- src/config.py,sha256=hfjW-W0gdH0PxmC6gEg-o9SqraajJ6gNy1SIlIOG-F4,845
4
- src/constants.py,sha256=kzcVmX__3miaHvs976H_zF2uBG9l-O3EzsLmwSsLJRE,4466
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=0W6tZ6ZXxJTZrdLda3QGDQ-53XKeJ37GGkekR2w3E7Q,11725
9
- src/rate_limiter.py,sha256=GLk3ZRFFvEZxkZAQd-pZ7UxQdrAAUVch3pxe_aMU-J8,3450
10
- src/state.py,sha256=aZwZw9nuI3Ipf2MYVI_IXaLNGdbfUbst5uUZtyLLTLA,17412
11
- src/utils.py,sha256=WhcWQIXpc1vIjU8hyrGDJyKJSlcbVoG938k_3UMDlCM,7340
12
- src/validators.py,sha256=aU36TUNYWb26fvZH27Xnryrp8gve9DM2phvy7vEnAi8,6891
13
- src/prompts/__init__.py,sha256=ArMCl0rgPRwWHgrsHau8Uf1zoPD_HbLniSzfzuEEADU,459
14
- src/prompts/chips.py,sha256=zzv5bqr8HuUAkvXenonrTXVhwNYGMwH9OPSC-c-1Dtg,5524
15
- src/prompts/league_analysis.py,sha256=23rNhCYkU8hSmd5BesXgNgHLFo_B8qgszmw909MPHkA,8095
16
- src/prompts/player_analysis.py,sha256=SGyd0UYWMF0lgml9idfc853UHgXXBT_qLVLf-8PFePU,5242
17
- src/prompts/squad_analysis.py,sha256=7ixTIrvTITvLIE-9ATH744ci_pObWgzx3p5yUqVHmEk,5204
18
- src/prompts/team_analysis.py,sha256=lZZ2R1xlsclwy4UyiokMg41ziuCKAqxgN_CoT1mOvnY,4104
19
- src/prompts/transfers.py,sha256=B99xjzJDTRRdwMluANjKxr5DPWB6eg69nZqJ5uyTosA,5448
20
- src/resources/__init__.py,sha256=i7nlLVSLtiIrLtOnyoMiK3KTFGEnct4LXApB4b6URFM,303
21
- src/resources/bootstrap.py,sha256=H6s1vubqNm9I3hcc6U5fdQbEPM_TJNweQvlhKVYCc9Y,6773
22
- src/tools/__init__.py,sha256=mKHfS7-KsOcMOChZ-xfWTNpShJdKTi61ClnDHiToQQw,644
23
- src/tools/fixtures.py,sha256=C2d06MX7sbVgX25oHixDorkQyJEbD0DvPdpsrytXMS4,6204
24
- src/tools/gameweeks.py,sha256=qBsMjdQajiNvt6-DZm8YDdBOYi7ZQY46wST1MuPoK8I,15429
25
- src/tools/leagues.py,sha256=XRF8h6Dt70wFtGPiU8UD6Rgpe2SAdIM9cgnDOh2geT8,23285
26
- src/tools/players.py,sha256=Jd4FUzU_qvkc6UE8rMYxZ_EM_nBWnSgIUX4MBz8WoQA,30964
27
- src/tools/teams.py,sha256=kqths5I7K_1rtsf4HyIzXpn9g8i7uMuCWbJ7YNy_tfQ,14753
28
- src/tools/transfers.py,sha256=Hy8JXjtbzRjUrtX7kg6IaDZ0IqbstISmt2Ben2Di2XE,24591
29
- fpl_mcp_server-0.1.5.dist-info/METADATA,sha256=-Ogr03lFL2lqx7YPor-ZXAHZpJk00Fdmb932FtRdfVw,4787
30
- fpl_mcp_server-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- fpl_mcp_server-0.1.5.dist-info/entry_points.txt,sha256=b3R5hBUMTLVnCGl07NfK7kyq9NCKtpn5Q8OsY79pMek,49
32
- fpl_mcp_server-0.1.5.dist-info/licenses/LICENSE,sha256=HCDOcdX83voRU2Eip214yj6P_tEyjVjCsCW_sixZFPw,1071
33
- fpl_mcp_server-0.1.5.dist-info/RECORD,,