fpl-mcp-server 0.1.6__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/leagues.py CHANGED
@@ -115,6 +115,25 @@ class GetManagerByTeamIdInput(BaseModel):
115
115
  )
116
116
 
117
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
+ )
135
+
136
+
118
137
  async def _create_client():
119
138
  """Create an unauthenticated FPL client for public API access and ensure data is loaded."""
120
139
  client = FPLClient(store=store)
@@ -641,7 +660,9 @@ async def fpl_get_manager_by_team_id(params: GetManagerByTeamIdInput) -> str:
641
660
  try:
642
661
  entry_data = await client.get_manager_entry(params.team_id)
643
662
  except Exception:
644
- return f"Manager with team ID {params.team_id} not found. Verify the team ID is correct."
663
+ return (
664
+ f"Manager with team ID {params.team_id} not found. Verify the team ID is correct."
665
+ )
645
666
 
646
667
  team_name = entry_data.get("name", "Unknown Team")
647
668
  player_name = f"{entry_data.get('player_first_name', '')} {entry_data.get('player_last_name', '')}".strip()
@@ -651,7 +672,9 @@ async def fpl_get_manager_by_team_id(params: GetManagerByTeamIdInput) -> str:
651
672
  if gameweek is None:
652
673
  current_gw = store.get_current_gameweek()
653
674
  if not current_gw:
654
- return "Error: Could not determine current gameweek. Please specify a gameweek number."
675
+ return (
676
+ "Error: Could not determine current gameweek. Please specify a gameweek number."
677
+ )
655
678
  gameweek = current_gw.id
656
679
 
657
680
  # Fetch gameweek picks from API
@@ -752,3 +775,167 @@ async def fpl_get_manager_by_team_id(params: GetManagerByTeamIdInput) -> str:
752
775
 
753
776
  except Exception as e:
754
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)