fpl-mcp-server 0.1.3__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.3.dist-info/METADATA +137 -0
- fpl_mcp_server-0.1.3.dist-info/RECORD +33 -0
- fpl_mcp_server-0.1.3.dist-info/WHEEL +4 -0
- fpl_mcp_server-0.1.3.dist-info/entry_points.txt +2 -0
- fpl_mcp_server-0.1.3.dist-info/licenses/LICENSE +21 -0
- src/cache.py +162 -0
- src/client.py +273 -0
- src/config.py +33 -0
- src/constants.py +118 -0
- src/exceptions.py +114 -0
- src/formatting.py +299 -0
- src/main.py +41 -0
- src/models.py +526 -0
- src/prompts/__init__.py +18 -0
- src/prompts/chips.py +127 -0
- src/prompts/league_analysis.py +250 -0
- src/prompts/player_analysis.py +141 -0
- src/prompts/squad_analysis.py +136 -0
- src/prompts/team_analysis.py +121 -0
- src/prompts/transfers.py +167 -0
- src/rate_limiter.py +101 -0
- src/resources/__init__.py +13 -0
- src/resources/bootstrap.py +183 -0
- src/state.py +443 -0
- src/tools/__init__.py +25 -0
- src/tools/fixtures.py +162 -0
- src/tools/gameweeks.py +392 -0
- src/tools/leagues.py +590 -0
- src/tools/players.py +840 -0
- src/tools/teams.py +397 -0
- src/tools/transfers.py +629 -0
- src/utils.py +226 -0
- src/validators.py +290 -0
src/constants.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants and enums for the FPL MCP Server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
|
|
8
|
+
# =============================================================================
|
|
9
|
+
# HTTP Status Codes
|
|
10
|
+
# =============================================================================
|
|
11
|
+
SUCCESS_STATUS = HTTPStatus.OK
|
|
12
|
+
NOT_FOUND_STATUS = HTTPStatus.NOT_FOUND
|
|
13
|
+
SERVER_ERROR_STATUS = HTTPStatus.INTERNAL_SERVER_ERROR
|
|
14
|
+
RATE_LIMIT_STATUS = HTTPStatus.TOO_MANY_REQUESTS
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Fuzzy Matching Constants
|
|
18
|
+
# =============================================================================
|
|
19
|
+
FUZZY_MATCH_THRESHOLD = 0.6 # Minimum similarity for fuzzy matches
|
|
20
|
+
SUBSTRING_MATCH_PENALTY = 0.9 # Score multiplier for substring matches
|
|
21
|
+
FUZZY_MATCH_PENALTY = 0.8 # Score multiplier for fuzzy matches
|
|
22
|
+
PERFECT_MATCH_SCORE = 1.0 # Score for exact matches
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Cache TTL (Time To Live) in Seconds
|
|
26
|
+
# =============================================================================
|
|
27
|
+
DEFAULT_BOOTSTRAP_TTL = 14400 # 4 hours
|
|
28
|
+
DEFAULT_FIXTURES_TTL = 14400 # 4 hours
|
|
29
|
+
DEFAULT_PLAYER_SUMMARY_TTL = 300 # 5 minutes
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Rate Limiting
|
|
33
|
+
# =============================================================================
|
|
34
|
+
MAX_AUTH_ATTEMPTS = 5 # Maximum login attempts
|
|
35
|
+
AUTH_WINDOW_SECONDS = 300 # Time window for rate limiting (5 minutes)
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# API Timeouts
|
|
39
|
+
# =============================================================================
|
|
40
|
+
DEFAULT_HTTP_TIMEOUT = 30 # Default timeout for HTTP requests
|
|
41
|
+
BROWSER_TIMEOUT = 15 # Browser automation timeout
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Difficulty Ratings
|
|
46
|
+
# =============================================================================
|
|
47
|
+
class FixtureDifficulty(Enum):
|
|
48
|
+
"""FPL fixture difficulty ratings"""
|
|
49
|
+
|
|
50
|
+
VERY_EASY = 1
|
|
51
|
+
EASY = 2
|
|
52
|
+
MODERATE = 3
|
|
53
|
+
HARD = 4
|
|
54
|
+
VERY_HARD = 5
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Difficulty emoji mapping
|
|
58
|
+
DIFFICULTY_EMOJI = {
|
|
59
|
+
FixtureDifficulty.VERY_EASY: "🟢",
|
|
60
|
+
FixtureDifficulty.EASY: "🟢",
|
|
61
|
+
FixtureDifficulty.MODERATE: "🟡",
|
|
62
|
+
FixtureDifficulty.HARD: "🟠",
|
|
63
|
+
FixtureDifficulty.VERY_HARD: "🔴",
|
|
64
|
+
1: "🟢",
|
|
65
|
+
2: "🟢",
|
|
66
|
+
3: "🟡",
|
|
67
|
+
4: "🟠",
|
|
68
|
+
5: "🔴",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# Player Positions
|
|
74
|
+
# =============================================================================
|
|
75
|
+
class PlayerPosition(Enum):
|
|
76
|
+
"""FPL player positions"""
|
|
77
|
+
|
|
78
|
+
GOALKEEPER = "GKP"
|
|
79
|
+
DEFENDER = "DEF"
|
|
80
|
+
MIDFIELDER = "MID"
|
|
81
|
+
FORWARD = "FWD"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Top Players Count
|
|
86
|
+
# =============================================================================
|
|
87
|
+
TOP_GOALKEEPERS_COUNT = 3
|
|
88
|
+
TOP_OUTFIELD_PLAYERS_COUNT = 10
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Pagination
|
|
92
|
+
# =============================================================================
|
|
93
|
+
DEFAULT_PAGE_SIZE = 50 # FPL API default league standings page size
|
|
94
|
+
DEFAULT_PAGINATION_LIMIT = 20 # MCP recommended default limit for tool responses
|
|
95
|
+
MAX_PAGINATION_LIMIT = 100 # MCP recommended maximum limit
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# MCP Response Configuration
|
|
99
|
+
# =============================================================================
|
|
100
|
+
CHARACTER_LIMIT = 25000 # Maximum response size in characters (MCP best practice)
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# FPL URLs
|
|
104
|
+
# =============================================================================
|
|
105
|
+
FPL_BASE_URL = "https://fantasy.premierleague.com"
|
|
106
|
+
FPL_API_BASE = f"{FPL_BASE_URL}/api"
|
|
107
|
+
|
|
108
|
+
# =============================================================================
|
|
109
|
+
# Logging
|
|
110
|
+
# =============================================================================
|
|
111
|
+
DEFAULT_LOG_LEVEL = "INFO"
|
|
112
|
+
WEB_SERVER_LOG_LEVEL = "critical" # Quiet web server logs
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# Server Configuration
|
|
116
|
+
# =============================================================================
|
|
117
|
+
DEFAULT_WEB_SERVER_HOST = "0.0.0.0"
|
|
118
|
+
DEFAULT_WEB_SERVER_PORT = 8000
|
src/exceptions.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the FPL MCP Server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FPLMCPError(Exception):
|
|
7
|
+
"""Base exception for FPL MCP Server."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationError(FPLMCPError):
|
|
13
|
+
"""Raised when authentication fails or is required."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DataFetchError(FPLMCPError):
|
|
19
|
+
"""Raised when data cannot be fetched from the FPL API."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CacheError(FPLMCPError):
|
|
25
|
+
"""Raised when cache operations fail."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlayerNotFoundError(FPLMCPError):
|
|
31
|
+
"""Raised when a player cannot be found."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, player_name: str, suggestions: list[str] = None):
|
|
34
|
+
self.player_name = player_name
|
|
35
|
+
self.suggestions = suggestions or []
|
|
36
|
+
|
|
37
|
+
message = f"No player found matching '{player_name}'."
|
|
38
|
+
if suggestions:
|
|
39
|
+
message += f" Did you mean: {', '.join(suggestions[:3])}?"
|
|
40
|
+
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TeamNotFoundError(FPLMCPError):
|
|
45
|
+
"""Raised when a team cannot be found."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, team_name: str, suggestions: list[str] = None):
|
|
48
|
+
self.team_name = team_name
|
|
49
|
+
self.suggestions = suggestions or []
|
|
50
|
+
|
|
51
|
+
message = f"No team found matching '{team_name}'."
|
|
52
|
+
if suggestions:
|
|
53
|
+
message += f" Did you mean: {', '.join(suggestions[:3])}?"
|
|
54
|
+
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LeagueNotFoundError(FPLMCPError):
|
|
59
|
+
"""Raised when a league cannot be found."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, league_name: str):
|
|
62
|
+
self.league_name = league_name
|
|
63
|
+
message = f"League '{league_name}' not found in your leagues."
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ManagerNotFoundError(FPLMCPError):
|
|
68
|
+
"""Raised when a manager cannot be found in a league."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, manager_name: str, league_name: str):
|
|
71
|
+
self.manager_name = manager_name
|
|
72
|
+
self.league_name = league_name
|
|
73
|
+
message = f"Manager '{manager_name}' not found in league '{league_name}'."
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GameweekNotFoundError(FPLMCPError):
|
|
78
|
+
"""Raised when a gameweek cannot be found."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, gameweek: int):
|
|
81
|
+
self.gameweek = gameweek
|
|
82
|
+
message = f"Gameweek {gameweek} not found or not valid."
|
|
83
|
+
super().__init__(message)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TransferError(FPLMCPError):
|
|
87
|
+
"""Raised when a transfer operation fails."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, message: str, details: dict = None):
|
|
90
|
+
self.details = details or {}
|
|
91
|
+
super().__init__(message)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RateLimitError(FPLMCPError):
|
|
95
|
+
"""Raised when rate limit is exceeded."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, retry_after: int = None):
|
|
98
|
+
self.retry_after = retry_after
|
|
99
|
+
message = "Rate limit exceeded."
|
|
100
|
+
if retry_after:
|
|
101
|
+
message += f" Please try again in {retry_after} seconds."
|
|
102
|
+
super().__init__(message)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ValidationError(FPLMCPError):
|
|
106
|
+
"""Raised when input validation fails."""
|
|
107
|
+
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ScraperError(FPLMCPError):
|
|
112
|
+
"""Raised when web scraping operations fail."""
|
|
113
|
+
|
|
114
|
+
pass
|
src/formatting.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared formatting utilities for FPL MCP tools and resources.
|
|
3
|
+
Centralizes Markdown generation logic to ensure consistency and reduce duplication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .utils import format_player_price
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_player_details(
|
|
12
|
+
player: Any,
|
|
13
|
+
history: list[dict[str, Any]] | None = None,
|
|
14
|
+
fixtures: list[dict[str, Any]] | None = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Format comprehensive player information including fixtures and history.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
player: ElementData object or similar having player attributes
|
|
21
|
+
history: Optional list of past gameweek performance dictionaries
|
|
22
|
+
fixtures: Optional list of upcoming fixture dictionaries
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted markdown string
|
|
26
|
+
"""
|
|
27
|
+
# Handle potentially missing attributes
|
|
28
|
+
team_name = getattr(player, "team_name", "Unknown")
|
|
29
|
+
position = getattr(player, "position", "Unknown")
|
|
30
|
+
|
|
31
|
+
price_val = getattr(player, "now_cost", 0)
|
|
32
|
+
price = format_player_price(price_val)
|
|
33
|
+
|
|
34
|
+
output = [
|
|
35
|
+
f"**{player.web_name}** ({player.first_name} {player.second_name})",
|
|
36
|
+
f"Team: {team_name} | Position: {position} | Price: {price}",
|
|
37
|
+
"",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Upcoming Fixtures
|
|
41
|
+
if fixtures:
|
|
42
|
+
output.append(f"**Upcoming Fixtures ({len(fixtures)}):**")
|
|
43
|
+
for fixture in fixtures[:5]:
|
|
44
|
+
opponent_name = (
|
|
45
|
+
fixture.get("team_h_short")
|
|
46
|
+
if not fixture["is_home"]
|
|
47
|
+
else fixture.get("team_a_short", "Unknown")
|
|
48
|
+
)
|
|
49
|
+
home_away = "H" if fixture["is_home"] else "A"
|
|
50
|
+
difficulty = "●" * fixture["difficulty"]
|
|
51
|
+
|
|
52
|
+
output.append(
|
|
53
|
+
f"├─ GW{fixture['event']}: vs {opponent_name} ({home_away}) | "
|
|
54
|
+
f"Difficulty: {difficulty} ({fixture['difficulty']}/5)"
|
|
55
|
+
)
|
|
56
|
+
output.append("")
|
|
57
|
+
|
|
58
|
+
# Recent Gameweek History
|
|
59
|
+
if history:
|
|
60
|
+
recent_history = history[-5:]
|
|
61
|
+
output.append(f"**Recent Performance (Last {len(recent_history)} GWs):**")
|
|
62
|
+
|
|
63
|
+
for gw in recent_history:
|
|
64
|
+
opponent_name = gw.get("opponent_team_short", "Unknown")
|
|
65
|
+
home_away = "H" if gw["was_home"] else "A"
|
|
66
|
+
xg = gw.get("expected_goals", "0.00")
|
|
67
|
+
xa = gw.get("expected_assists", "0.00")
|
|
68
|
+
|
|
69
|
+
output.append(
|
|
70
|
+
f"├─ GW{gw['round']}: {gw['total_points']}pts vs {opponent_name} ({home_away}) | "
|
|
71
|
+
f"{gw['minutes']}min | xG: {xg} G:{gw['goals_scored']} xA: {xa} A:{gw['assists']} "
|
|
72
|
+
f"CS:{gw['clean_sheets']} | Bonus: {gw['bonus']}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
total_points = sum(gw["total_points"] for gw in recent_history)
|
|
76
|
+
avg_points = total_points / len(recent_history) if recent_history else 0
|
|
77
|
+
total_minutes = sum(gw["minutes"] for gw in recent_history)
|
|
78
|
+
avg_minutes = total_minutes / len(recent_history) if recent_history else 0
|
|
79
|
+
|
|
80
|
+
output.extend(
|
|
81
|
+
[
|
|
82
|
+
"",
|
|
83
|
+
"**Recent Averages:**",
|
|
84
|
+
f"├─ Points per game: {avg_points:.1f}",
|
|
85
|
+
f"├─ Minutes per game: {avg_minutes:.0f}",
|
|
86
|
+
"",
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Performance stats
|
|
91
|
+
output.extend(
|
|
92
|
+
[
|
|
93
|
+
"**Performance:**",
|
|
94
|
+
f"├─ Form: {player.form}",
|
|
95
|
+
f"├─ Points per Game: {player.points_per_game}",
|
|
96
|
+
f"├─ Total Points: {getattr(player, 'total_points', 'N/A')}",
|
|
97
|
+
f"├─ Minutes: {getattr(player, 'minutes', 'N/A')}",
|
|
98
|
+
"",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Popularity
|
|
103
|
+
if hasattr(player, "selected_by_percent"):
|
|
104
|
+
output.extend(
|
|
105
|
+
[
|
|
106
|
+
"**Popularity:**",
|
|
107
|
+
f"├─ Selected by: {getattr(player, 'selected_by_percent', 'N/A')}%",
|
|
108
|
+
f"├─ Transfers in (GW): {getattr(player, 'transfers_in_event', 'N/A')}",
|
|
109
|
+
f"├─ Transfers out (GW): {getattr(player, 'transfers_out_event', 'N/A')}",
|
|
110
|
+
"",
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Season stats
|
|
115
|
+
if hasattr(player, "goals_scored"):
|
|
116
|
+
output.extend(
|
|
117
|
+
[
|
|
118
|
+
"**Season Stats:**",
|
|
119
|
+
f"├─ Goals: {getattr(player, 'goals_scored', 0)}",
|
|
120
|
+
f"├─ Assists: {getattr(player, 'assists', 0)}",
|
|
121
|
+
f"├─ Clean Sheets: {getattr(player, 'clean_sheets', 0)}",
|
|
122
|
+
f"├─ Bonus Points: {getattr(player, 'bonus', 0)}",
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return "\n".join(output)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def format_team_details(team: dict[str, Any]) -> str:
|
|
130
|
+
"""Format team detailed information and strength ratings."""
|
|
131
|
+
name = team.get("name", "Unknown")
|
|
132
|
+
short_name = team.get("short_name", "UNK")
|
|
133
|
+
|
|
134
|
+
output = [f"**{name} ({short_name})**", ""]
|
|
135
|
+
|
|
136
|
+
if team.get("strength"):
|
|
137
|
+
output.append(f"Overall Strength: {team['strength']}")
|
|
138
|
+
|
|
139
|
+
if team.get("strength_overall_home") or team.get("strength_overall_away"):
|
|
140
|
+
output.extend(
|
|
141
|
+
[
|
|
142
|
+
"",
|
|
143
|
+
"**Overall Strength:**",
|
|
144
|
+
f"Home: {team.get('strength_overall_home', 'N/A')}",
|
|
145
|
+
f"Away: {team.get('strength_overall_away', 'N/A')}",
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if team.get("strength_attack_home") or team.get("strength_attack_away"):
|
|
150
|
+
output.extend(
|
|
151
|
+
[
|
|
152
|
+
"",
|
|
153
|
+
"**Attack Strength:**",
|
|
154
|
+
f"Home: {team.get('strength_attack_home', 'N/A')}",
|
|
155
|
+
f"Away: {team.get('strength_attack_away', 'N/A')}",
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if team.get("strength_defence_home") or team.get("strength_defence_away"):
|
|
160
|
+
output.extend(
|
|
161
|
+
[
|
|
162
|
+
"",
|
|
163
|
+
"**Defence Strength:**",
|
|
164
|
+
f"Home: {team.get('strength_defence_home', 'N/A')}",
|
|
165
|
+
f"Away: {team.get('strength_defence_away', 'N/A')}",
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return "\n".join(output)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def format_gameweek_details(event: Any) -> str:
|
|
173
|
+
"""Format detailed gameweek information."""
|
|
174
|
+
# Handle object or dict access if needed, assuming event is an object from bootstrap data
|
|
175
|
+
is_current = getattr(event, "is_current", False)
|
|
176
|
+
is_previous = getattr(event, "is_previous", False)
|
|
177
|
+
is_next = getattr(event, "is_next", False)
|
|
178
|
+
|
|
179
|
+
status_label = "Upcoming"
|
|
180
|
+
if is_current:
|
|
181
|
+
status_label = "Current"
|
|
182
|
+
elif is_previous:
|
|
183
|
+
status_label = "Previous"
|
|
184
|
+
elif is_next:
|
|
185
|
+
status_label = "Next"
|
|
186
|
+
|
|
187
|
+
output = [
|
|
188
|
+
f"**{event.name}**",
|
|
189
|
+
f"Deadline: {event.deadline_time}",
|
|
190
|
+
f"Status: {status_label}",
|
|
191
|
+
f"Finished: {event.finished}",
|
|
192
|
+
f"Released: {getattr(event, 'released', 'N/A')}",
|
|
193
|
+
"",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
if event.finished:
|
|
197
|
+
output.extend(
|
|
198
|
+
[
|
|
199
|
+
"**Statistics:**",
|
|
200
|
+
f"Average Score: {event.average_entry_score}",
|
|
201
|
+
f"Highest Score: {event.highest_score}",
|
|
202
|
+
"",
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Note: top_element_info logic usually requires a separate name lookup
|
|
207
|
+
# so we'll leave that to the caller to append if they have the name
|
|
208
|
+
|
|
209
|
+
return "\n".join(output)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def format_manager_squad(
|
|
213
|
+
team_name: str,
|
|
214
|
+
player_name: str,
|
|
215
|
+
team_id: int,
|
|
216
|
+
gameweek: int,
|
|
217
|
+
entry_history: dict[str, Any],
|
|
218
|
+
picks: list[dict[str, Any]],
|
|
219
|
+
players_info: dict[int, Any],
|
|
220
|
+
active_chip: str | None = None,
|
|
221
|
+
auto_subs: list[dict[str, Any]] = None,
|
|
222
|
+
) -> str:
|
|
223
|
+
"""Format a manager's squad for a specific gameweek."""
|
|
224
|
+
|
|
225
|
+
output = [
|
|
226
|
+
f"**{team_name}** - {player_name}" if player_name else f"**{team_name}**",
|
|
227
|
+
f"Team ID: {team_id}",
|
|
228
|
+
f"Gameweek {gameweek}",
|
|
229
|
+
f"Points: {entry_history.get('points', 0)} | Total: {entry_history.get('total_points', 0)}",
|
|
230
|
+
f"Overall Rank: {entry_history.get('overall_rank', 'N/A'):,}",
|
|
231
|
+
f"Team Value: £{entry_history.get('value', 0) / 10:.1f}m | Bank: £{entry_history.get('bank', 0) / 10:.1f}m",
|
|
232
|
+
f"Transfers: {entry_history.get('event_transfers', 0)} (Cost: {entry_history.get('event_transfers_cost', 0)}pts)",
|
|
233
|
+
f"Points on Bench: {entry_history.get('points_on_bench', 0)}",
|
|
234
|
+
"",
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
if active_chip:
|
|
238
|
+
output.append(f"**Active Chip:** {active_chip}")
|
|
239
|
+
output.append("")
|
|
240
|
+
|
|
241
|
+
starting_xi = [p for p in picks if p["position"] <= 11]
|
|
242
|
+
bench = [p for p in picks if p["position"] > 11]
|
|
243
|
+
|
|
244
|
+
output.append("**Starting XI:**")
|
|
245
|
+
for pick in starting_xi:
|
|
246
|
+
player = players_info.get(pick["element"], {})
|
|
247
|
+
role = " (C)" if pick["is_captain"] else " (VC)" if pick["is_vice_captain"] else ""
|
|
248
|
+
multiplier = f" x{pick['multiplier']}" if pick["multiplier"] > 1 else ""
|
|
249
|
+
|
|
250
|
+
output.append(
|
|
251
|
+
f"{pick['position']:2d}. {player.get('web_name', 'Unknown'):15s} "
|
|
252
|
+
f"({player.get('team', 'UNK'):3s} {player.get('position', 'UNK')}) | "
|
|
253
|
+
f"£{player.get('price', 0):.1f}m{role}{multiplier}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
output.append("\n**Bench:**")
|
|
257
|
+
for pick in bench:
|
|
258
|
+
player = players_info.get(pick["element"], {})
|
|
259
|
+
output.append(
|
|
260
|
+
f"{pick['position']:2d}. {player.get('web_name', 'Unknown'):15s} "
|
|
261
|
+
f"({player.get('team', 'UNK'):3s} {player.get('position', 'UNK')}) | "
|
|
262
|
+
f"£{player.get('price', 0):.1f}m"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# or we handle it if we pass the names.
|
|
266
|
+
# For simplicity, let's assume the caller handles auto_subs appending
|
|
267
|
+
# or we handle it if we pass the names.
|
|
268
|
+
|
|
269
|
+
return "\n".join(output)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def format_difficulty_indicator(difficulty: int) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Format fixture difficulty as visual indicator.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
difficulty: Difficulty rating (1-5)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Visual difficulty indicator
|
|
281
|
+
"""
|
|
282
|
+
return "●" * difficulty + "○" * (5 - difficulty)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def format_markdown_table_row(items: list[str], widths: list[int] | None = None) -> str:
|
|
286
|
+
"""
|
|
287
|
+
Format a markdown table row with proper spacing.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
items: List of cell contents
|
|
291
|
+
widths: Optional list of column widths for padding
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Formatted markdown table row
|
|
295
|
+
"""
|
|
296
|
+
if widths:
|
|
297
|
+
padded = [str(item).ljust(width) for item, width in zip(items, widths, strict=True)]
|
|
298
|
+
return "| " + " | ".join(padded) + " |"
|
|
299
|
+
return "| " + " | ".join(str(item) for item in items) + " |"
|
src/main.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""FPL MCP Server - Main Entry Point"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
sys.stderr.write("Starting FPL MCP Server...\n")
|
|
7
|
+
sys.stderr.flush()
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from .tools import mcp
|
|
11
|
+
|
|
12
|
+
sys.stderr.write("Imports successful. Initializing MCP server...\n")
|
|
13
|
+
sys.stderr.flush()
|
|
14
|
+
except Exception as e:
|
|
15
|
+
sys.stderr.write(f"CRITICAL IMPORT ERROR: {e}\n")
|
|
16
|
+
traceback.print_exc(file=sys.stderr)
|
|
17
|
+
sys.stderr.flush()
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
"""Run the MCP server on stdio transport."""
|
|
23
|
+
try:
|
|
24
|
+
sys.stderr.write("Starting MCP Server (Stdio)...\n")
|
|
25
|
+
sys.stderr.flush()
|
|
26
|
+
|
|
27
|
+
# Run MCP server - blocks waiting for MCP protocol messages
|
|
28
|
+
mcp.run(transport="stdio")
|
|
29
|
+
|
|
30
|
+
sys.stderr.write("MCP Server stopped.\n")
|
|
31
|
+
sys.stderr.flush()
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
sys.stderr.write(f"RUNTIME ERROR: {e}\n")
|
|
35
|
+
traceback.print_exc(file=sys.stderr)
|
|
36
|
+
sys.stderr.flush()
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main()
|