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/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
|
|
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
|
|
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)
|