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/fixtures.py
CHANGED
|
@@ -22,6 +22,30 @@ class GetFixturesForGameweekInput(BaseModel):
|
|
|
22
22
|
gameweek: int = Field(
|
|
23
23
|
..., description="Gameweek number to get fixtures for (1-38)", ge=1, le=38
|
|
24
24
|
)
|
|
25
|
+
detailed: bool = Field(
|
|
26
|
+
default=False, description="Include detailed match stats (all available metrics)"
|
|
27
|
+
)
|
|
28
|
+
response_format: ResponseFormat = Field(
|
|
29
|
+
default=ResponseFormat.MARKDOWN,
|
|
30
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FindFixtureOpportunitiesInput(BaseModel):
|
|
35
|
+
"""Input model for finding fixture opportunities."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
38
|
+
|
|
39
|
+
num_gameweeks: int = Field(
|
|
40
|
+
default=5, description="Number of future gameweeks to analyze (default: 5)", ge=3, le=10
|
|
41
|
+
)
|
|
42
|
+
max_teams: int = Field(
|
|
43
|
+
default=3, description="Number of top teams to return (default: 3)", ge=1, le=5
|
|
44
|
+
)
|
|
45
|
+
positions: list[str] | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="Filter recommended players by position (e.g. ['Midfielder', 'Forward'])",
|
|
48
|
+
)
|
|
25
49
|
response_format: ResponseFormat = Field(
|
|
26
50
|
default=ResponseFormat.MARKDOWN,
|
|
27
51
|
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
@@ -57,6 +81,7 @@ async def fpl_get_fixtures_for_gameweek(params: GetFixturesForGameweekInput) ->
|
|
|
57
81
|
Args:
|
|
58
82
|
params (GetFixturesForGameweekInput): Validated input parameters containing:
|
|
59
83
|
- gameweek (int): Gameweek number between 1-38
|
|
84
|
+
- detailed (bool): Include detailed stats (default: False)
|
|
60
85
|
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
61
86
|
|
|
62
87
|
Returns:
|
|
@@ -90,26 +115,77 @@ async def fpl_get_fixtures_for_gameweek(params: GetFixturesForGameweekInput) ->
|
|
|
90
115
|
result = {
|
|
91
116
|
"gameweek": params.gameweek,
|
|
92
117
|
"fixture_count": len(gw_fixtures_sorted),
|
|
93
|
-
"fixtures": [
|
|
94
|
-
{
|
|
95
|
-
"home_team": fixture.get("team_h_name"),
|
|
96
|
-
"home_team_short": fixture.get("team_h_short"),
|
|
97
|
-
"away_team": fixture.get("team_a_name"),
|
|
98
|
-
"away_team_short": fixture.get("team_a_short"),
|
|
99
|
-
"kickoff_time": fixture.get("kickoff_time"),
|
|
100
|
-
"finished": fixture.get("finished"),
|
|
101
|
-
"home_score": fixture.get("team_h_score")
|
|
102
|
-
if fixture.get("finished")
|
|
103
|
-
else None,
|
|
104
|
-
"away_score": fixture.get("team_a_score")
|
|
105
|
-
if fixture.get("finished")
|
|
106
|
-
else None,
|
|
107
|
-
"home_difficulty": fixture.get("team_h_difficulty"),
|
|
108
|
-
"away_difficulty": fixture.get("team_a_difficulty"),
|
|
109
|
-
}
|
|
110
|
-
for fixture in gw_fixtures_sorted
|
|
111
|
-
],
|
|
118
|
+
"fixtures": [],
|
|
112
119
|
}
|
|
120
|
+
|
|
121
|
+
for fixture in gw_fixtures_sorted:
|
|
122
|
+
fixture_data = {
|
|
123
|
+
"home_team": fixture.get("team_h_name"),
|
|
124
|
+
"home_team_short": fixture.get("team_h_short"),
|
|
125
|
+
"away_team": fixture.get("team_a_name"),
|
|
126
|
+
"away_team_short": fixture.get("team_a_short"),
|
|
127
|
+
"kickoff_time": fixture.get("kickoff_time"),
|
|
128
|
+
"finished": fixture.get("finished"),
|
|
129
|
+
"home_score": fixture.get("team_h_score") if fixture.get("finished") else None,
|
|
130
|
+
"away_score": fixture.get("team_a_score") if fixture.get("finished") else None,
|
|
131
|
+
"home_difficulty": fixture.get("team_h_difficulty"),
|
|
132
|
+
"away_difficulty": fixture.get("team_a_difficulty"),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if params.detailed and fixture.get("stats"):
|
|
136
|
+
stats = {}
|
|
137
|
+
for stat in fixture.get("stats", []):
|
|
138
|
+
if hasattr(stat, "model_dump"):
|
|
139
|
+
stat_dict = stat.model_dump()
|
|
140
|
+
elif hasattr(stat, "__dict__"):
|
|
141
|
+
stat_dict = stat.__dict__
|
|
142
|
+
else:
|
|
143
|
+
stat_dict = stat
|
|
144
|
+
|
|
145
|
+
identifier = stat_dict.get("identifier")
|
|
146
|
+
if not identifier:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
stats[identifier] = {"h": [], "a": []}
|
|
150
|
+
|
|
151
|
+
# Process home stats
|
|
152
|
+
for item in stat_dict.get("h", []):
|
|
153
|
+
if hasattr(item, "value"):
|
|
154
|
+
value = item.value
|
|
155
|
+
element = item.element
|
|
156
|
+
else:
|
|
157
|
+
value = item.get("value")
|
|
158
|
+
element = item.get("element")
|
|
159
|
+
|
|
160
|
+
stats[identifier]["h"].append(
|
|
161
|
+
{
|
|
162
|
+
"name": store.get_player_name(element),
|
|
163
|
+
"value": value,
|
|
164
|
+
"element": element,
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Process away stats
|
|
169
|
+
for item in stat_dict.get("a", []):
|
|
170
|
+
if hasattr(item, "value"):
|
|
171
|
+
value = item.value
|
|
172
|
+
element = item.element
|
|
173
|
+
else:
|
|
174
|
+
value = item.get("value")
|
|
175
|
+
element = item.get("element")
|
|
176
|
+
|
|
177
|
+
stats[identifier]["a"].append(
|
|
178
|
+
{
|
|
179
|
+
"name": store.get_player_name(element),
|
|
180
|
+
"value": value,
|
|
181
|
+
"element": element,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
fixture_data["stats"] = stats
|
|
186
|
+
|
|
187
|
+
result["fixtures"].append(fixture_data)
|
|
188
|
+
|
|
113
189
|
return format_json_response(result)
|
|
114
190
|
else:
|
|
115
191
|
output = [
|
|
@@ -138,9 +214,269 @@ async def fpl_get_fixtures_for_gameweek(params: GetFixturesForGameweekInput) ->
|
|
|
138
214
|
f"Difficulty: H:{fixture.get('team_h_difficulty')} A:{fixture.get('team_a_difficulty')}"
|
|
139
215
|
)
|
|
140
216
|
|
|
217
|
+
if params.detailed and fixture.get("stats"):
|
|
218
|
+
stats_output = []
|
|
219
|
+
# Filter for interesting stats or show all? Showing all as requested.
|
|
220
|
+
# Common stats order: goals_scored, assists, own_goals, penalties_saved, penalties_missed, yellow_cards, red_cards, bonus, saves, bps
|
|
221
|
+
priority_stats = [
|
|
222
|
+
"goals_scored",
|
|
223
|
+
"assists",
|
|
224
|
+
"bonus",
|
|
225
|
+
"yellow_cards",
|
|
226
|
+
"red_cards",
|
|
227
|
+
"saves",
|
|
228
|
+
]
|
|
229
|
+
other_stats = []
|
|
230
|
+
for s in fixture.get("stats", []):
|
|
231
|
+
ident = s.identifier if hasattr(s, "identifier") else s.get("identifier")
|
|
232
|
+
|
|
233
|
+
if ident and ident not in priority_stats:
|
|
234
|
+
other_stats.append(ident)
|
|
235
|
+
all_stats = priority_stats + other_stats
|
|
236
|
+
|
|
237
|
+
for stat_name in all_stats:
|
|
238
|
+
# Find stat data
|
|
239
|
+
stat_data = None
|
|
240
|
+
for s in fixture.get("stats", []):
|
|
241
|
+
s_ident = (
|
|
242
|
+
s.identifier if hasattr(s, "identifier") else s.get("identifier")
|
|
243
|
+
)
|
|
244
|
+
if s_ident == stat_name:
|
|
245
|
+
stat_data = s
|
|
246
|
+
break
|
|
247
|
+
if not stat_data:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# Convert to dict access if needed
|
|
251
|
+
if hasattr(stat_data, "h"):
|
|
252
|
+
h_items = stat_data.h
|
|
253
|
+
a_items = stat_data.a
|
|
254
|
+
else:
|
|
255
|
+
h_items = stat_data.get("h", [])
|
|
256
|
+
a_items = stat_data.get("a", [])
|
|
257
|
+
|
|
258
|
+
if not h_items and not a_items:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# Format stat line: Stat: Name (val), Name (val) (H) | Name (val) (A)
|
|
262
|
+
h_strs = []
|
|
263
|
+
for item in h_items:
|
|
264
|
+
val = item.value if hasattr(item, "value") else item.get("value")
|
|
265
|
+
elem = item.element if hasattr(item, "element") else item.get("element")
|
|
266
|
+
h_strs.append(f"{store.get_player_name(elem)} ({val})")
|
|
267
|
+
|
|
268
|
+
a_strs = []
|
|
269
|
+
for item in a_items:
|
|
270
|
+
val = item.value if hasattr(item, "value") else item.get("value")
|
|
271
|
+
elem = item.element if hasattr(item, "element") else item.get("element")
|
|
272
|
+
a_strs.append(f"{store.get_player_name(elem)} ({val})")
|
|
273
|
+
|
|
274
|
+
stat_display = stat_name.replace("_", " ").title()
|
|
275
|
+
if stat_name == "goals_scored":
|
|
276
|
+
stat_display = "⚽ Goals"
|
|
277
|
+
elif stat_name == "assists":
|
|
278
|
+
stat_display = "🅰️ Assists"
|
|
279
|
+
elif stat_name == "bonus":
|
|
280
|
+
stat_display = "💎 Bonus"
|
|
281
|
+
elif stat_name == "yellow_cards":
|
|
282
|
+
stat_display = "🟨 Yellows"
|
|
283
|
+
elif stat_name == "red_cards":
|
|
284
|
+
stat_display = "🟥 Reds"
|
|
285
|
+
elif stat_name == "saves":
|
|
286
|
+
stat_display = "🧤 Saves"
|
|
287
|
+
|
|
288
|
+
if h_strs or a_strs:
|
|
289
|
+
line = f" - **{stat_display}**: "
|
|
290
|
+
if h_strs:
|
|
291
|
+
line += f"{', '.join(h_strs)} ({home_name})"
|
|
292
|
+
if h_strs and a_strs:
|
|
293
|
+
line += " | "
|
|
294
|
+
if a_strs:
|
|
295
|
+
line += f"{', '.join(a_strs)} ({away_name})"
|
|
296
|
+
stats_output.append(line)
|
|
297
|
+
|
|
298
|
+
if stats_output:
|
|
299
|
+
output.extend(stats_output)
|
|
300
|
+
output.append("") # Empty line separator
|
|
301
|
+
|
|
141
302
|
result = "\n".join(output)
|
|
142
303
|
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
143
304
|
return truncated
|
|
144
305
|
|
|
145
306
|
except Exception as e:
|
|
146
307
|
return handle_api_error(e)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@mcp.tool(
|
|
311
|
+
name="fpl_find_fixture_opportunities",
|
|
312
|
+
annotations={
|
|
313
|
+
"title": "Find Fixture Opportunities",
|
|
314
|
+
"readOnlyHint": True,
|
|
315
|
+
"destructiveHint": False,
|
|
316
|
+
"idempotentHint": True,
|
|
317
|
+
"openWorldHint": True,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
async def fpl_find_fixture_opportunities(params: FindFixtureOpportunitiesInput) -> str:
|
|
321
|
+
"""
|
|
322
|
+
Find teams with the easiest upcoming fixtures and their best assets.
|
|
323
|
+
|
|
324
|
+
Analyzes fixture difficulty for all 20 teams over the next N gameweeks.
|
|
325
|
+
Identifies teams with the most favorable schedule and recommends their
|
|
326
|
+
top-performing players (filtered by position if requested).
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
params (FindFixtureOpportunitiesInput): Validated input parameters containing:
|
|
330
|
+
- num_gameweeks (int): Number of gameweeks to analyze (3-10)
|
|
331
|
+
- max_teams (int): Number of teams to recommend (1-5)
|
|
332
|
+
- positions (list[str] | None): Optional position filter
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
str: Analysis of best teams to target and their key players
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
- Target next 5 GWs: num_gameweeks=5
|
|
339
|
+
- Find best attackers: positions=['Midfielder', 'Forward']
|
|
340
|
+
|
|
341
|
+
Error Handling:
|
|
342
|
+
- Returns error if data unavailable
|
|
343
|
+
- Returns formatted error message if API fails
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
await _create_client()
|
|
347
|
+
if not store.bootstrap_data:
|
|
348
|
+
return "Error: Player data not available."
|
|
349
|
+
|
|
350
|
+
# Determine current gameweek
|
|
351
|
+
current_gw_data = store.get_current_gameweek()
|
|
352
|
+
current_gw = current_gw_data.id if current_gw_data else 1
|
|
353
|
+
start_gw = current_gw + 1
|
|
354
|
+
|
|
355
|
+
# Calculate average difficulty for each team
|
|
356
|
+
team_difficulties = []
|
|
357
|
+
|
|
358
|
+
# Helper to get difficulty for a team ID in a GW
|
|
359
|
+
def get_team_fixtures(team_id):
|
|
360
|
+
fixtures = []
|
|
361
|
+
for gw in range(start_gw, start_gw + params.num_gameweeks):
|
|
362
|
+
if gw > 38:
|
|
363
|
+
break
|
|
364
|
+
# Find fixture for this team in this GW
|
|
365
|
+
# Use enriched fixtures if pre-calculated, or search raw
|
|
366
|
+
# Searching raw is faster here than full enrich loop
|
|
367
|
+
matches = [
|
|
368
|
+
f
|
|
369
|
+
for f in store.fixtures_data
|
|
370
|
+
if f.event == gw and (f.team_h == team_id or f.team_a == team_id)
|
|
371
|
+
]
|
|
372
|
+
for m in matches:
|
|
373
|
+
is_home = m.team_h == team_id
|
|
374
|
+
diff = m.team_h_difficulty if is_home else m.team_a_difficulty
|
|
375
|
+
opponent_id = m.team_a if is_home else m.team_h
|
|
376
|
+
opponent = next(
|
|
377
|
+
(t for t in store.bootstrap_data.teams if t.id == opponent_id), None
|
|
378
|
+
)
|
|
379
|
+
fixtures.append(
|
|
380
|
+
{
|
|
381
|
+
"gameweek": gw,
|
|
382
|
+
"difficulty": diff,
|
|
383
|
+
"opponent": opponent.short_name if opponent else "UNK",
|
|
384
|
+
"is_home": is_home,
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
return fixtures
|
|
388
|
+
|
|
389
|
+
for team in store.bootstrap_data.teams:
|
|
390
|
+
fixtures = get_team_fixtures(team.id)
|
|
391
|
+
if not fixtures:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
avg_diff = sum(f["difficulty"] for f in fixtures) / len(fixtures)
|
|
395
|
+
team_difficulties.append({"team": team, "avg_diff": avg_diff, "fixtures": fixtures})
|
|
396
|
+
|
|
397
|
+
# Sort by easiest (lowest avg difficulty)
|
|
398
|
+
team_difficulties.sort(key=lambda x: x["avg_diff"])
|
|
399
|
+
top_teams = team_difficulties[: params.max_teams]
|
|
400
|
+
|
|
401
|
+
# Find top players for these teams
|
|
402
|
+
# Map position string to element_type (1=GKP, 2=DEF, 3=MID, 4=FWD)
|
|
403
|
+
pos_map = {"Goalkeeper": 1, "Defender": 2, "Midfielder": 3, "Forward": 4}
|
|
404
|
+
target_types = []
|
|
405
|
+
if params.positions:
|
|
406
|
+
for p in params.positions:
|
|
407
|
+
p_norm = p.capitalize()
|
|
408
|
+
# Handle plurals
|
|
409
|
+
if p_norm.endswith("s"):
|
|
410
|
+
p_norm = p_norm[:-1]
|
|
411
|
+
idx = pos_map.get(p_norm)
|
|
412
|
+
if idx:
|
|
413
|
+
target_types.append(idx)
|
|
414
|
+
|
|
415
|
+
result_teams = []
|
|
416
|
+
|
|
417
|
+
for item in top_teams:
|
|
418
|
+
team = item["team"]
|
|
419
|
+
# Get players for this team
|
|
420
|
+
team_players = [
|
|
421
|
+
p for p in store.bootstrap_data.elements if p.team == team.id and p.status != "u"
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
if target_types:
|
|
425
|
+
team_players = [p for p in team_players if p.element_type in target_types]
|
|
426
|
+
|
|
427
|
+
# Sort by form (best assets)
|
|
428
|
+
top_assets = sorted(team_players, key=lambda x: float(x.form), reverse=True)[:3]
|
|
429
|
+
|
|
430
|
+
result_teams.append(
|
|
431
|
+
{
|
|
432
|
+
"team_name": team.name,
|
|
433
|
+
"avg_diff": item["avg_diff"],
|
|
434
|
+
"fixtures": item["fixtures"],
|
|
435
|
+
"best_players": top_assets,
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if params.response_format == ResponseFormat.JSON:
|
|
440
|
+
json_out = {
|
|
441
|
+
"start_gameweek": start_gw,
|
|
442
|
+
"end_gameweek": start_gw + params.num_gameweeks - 1,
|
|
443
|
+
"opportunities": [],
|
|
444
|
+
}
|
|
445
|
+
for rt in result_teams:
|
|
446
|
+
# Format fixtures string
|
|
447
|
+
fixtures_list = [
|
|
448
|
+
f"{f['opponent']} ({'H' if f['is_home'] else 'A'})" for f in rt["fixtures"]
|
|
449
|
+
]
|
|
450
|
+
json_out["opportunities"].append(
|
|
451
|
+
{
|
|
452
|
+
"team": rt["team_name"],
|
|
453
|
+
"difficulty_score": round(rt["avg_diff"], 2),
|
|
454
|
+
"fixtures": fixtures_list,
|
|
455
|
+
"recommended_players": [p.web_name for p in rt["best_players"]],
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
return format_json_response(json_out)
|
|
459
|
+
|
|
460
|
+
# Markdown Output
|
|
461
|
+
output = [
|
|
462
|
+
f"## 🗓️ Fixture Opportunities (Next {params.num_gameweeks} GWs)",
|
|
463
|
+
"Top teams with the easiest schedules to target:",
|
|
464
|
+
"",
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
for i, rt in enumerate(result_teams, 1):
|
|
468
|
+
fixtures_str = " - ".join(
|
|
469
|
+
[f"**{f['opponent']}** ({'H' if f['is_home'] else 'A'})" for f in rt["fixtures"]]
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
output.append(f"### {i}. {rt['team_name']} (Diff: {rt['avg_diff']:.1f})")
|
|
473
|
+
output.append(f"🗓️ **Schedule:** {fixtures_str}")
|
|
474
|
+
|
|
475
|
+
player_names = [f"{p.web_name} ({p.form} form)" for p in rt["best_players"]]
|
|
476
|
+
output.append(f"🔥 **Targets:** {', '.join(player_names)}")
|
|
477
|
+
output.append("")
|
|
478
|
+
|
|
479
|
+
return "\n".join(output)
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
return handle_api_error(e)
|
src/tools/gameweeks.py
CHANGED
|
@@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
6
6
|
|
|
7
7
|
from ..client import FPLClient
|
|
8
8
|
from ..constants import CHARACTER_LIMIT
|
|
9
|
-
from ..formatting import format_gameweek_details
|
|
10
9
|
from ..state import store
|
|
11
10
|
from ..utils import (
|
|
12
11
|
ResponseFormat,
|
|
@@ -28,29 +27,6 @@ class GetCurrentGameweekInput(BaseModel):
|
|
|
28
27
|
)
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
class GetGameweekInfoInput(BaseModel):
|
|
32
|
-
"""Input model for getting specific gameweek information."""
|
|
33
|
-
|
|
34
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
35
|
-
|
|
36
|
-
gameweek_number: int = Field(..., description="Gameweek number (1-38)", ge=1, le=38)
|
|
37
|
-
response_format: ResponseFormat = Field(
|
|
38
|
-
default=ResponseFormat.MARKDOWN,
|
|
39
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ListAllGameweeksInput(BaseModel):
|
|
44
|
-
"""Input model for listing all gameweeks."""
|
|
45
|
-
|
|
46
|
-
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
47
|
-
|
|
48
|
-
response_format: ResponseFormat = Field(
|
|
49
|
-
default=ResponseFormat.MARKDOWN,
|
|
50
|
-
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
30
|
async def _create_client():
|
|
55
31
|
"""Create an unauthenticated FPL client for public API access and ensure data is loaded."""
|
|
56
32
|
client = FPLClient(store=store)
|
|
@@ -216,161 +192,3 @@ async def fpl_get_current_gameweek(params: GetCurrentGameweekInput) -> str:
|
|
|
216
192
|
|
|
217
193
|
except Exception as e:
|
|
218
194
|
return handle_api_error(e)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@mcp.tool(
|
|
222
|
-
name="fpl_get_gameweek_info",
|
|
223
|
-
annotations={
|
|
224
|
-
"title": "Get FPL Gameweek Information",
|
|
225
|
-
"readOnlyHint": True,
|
|
226
|
-
"destructiveHint": False,
|
|
227
|
-
"idempotentHint": True,
|
|
228
|
-
"openWorldHint": True,
|
|
229
|
-
},
|
|
230
|
-
)
|
|
231
|
-
async def fpl_get_gameweek_info(params: GetGameweekInfoInput) -> str:
|
|
232
|
-
"""
|
|
233
|
-
Get detailed information about a specific Fantasy Premier League gameweek.
|
|
234
|
-
|
|
235
|
-
Returns comprehensive gameweek data including deadline, scores, top players, most
|
|
236
|
-
captained players, and statistics. Useful for analyzing past performance or planning
|
|
237
|
-
for future gameweeks.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
params (GetGameweekInfoInput): Validated input parameters containing:
|
|
241
|
-
- gameweek_number (int): Gameweek number between 1-38
|
|
242
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
str: Detailed gameweek information with statistics and top performers
|
|
246
|
-
|
|
247
|
-
Examples:
|
|
248
|
-
- Check GW1 results: gameweek_number=1
|
|
249
|
-
- Plan for GW10: gameweek_number=10
|
|
250
|
-
- Get as JSON: gameweek_number=5, response_format="json"
|
|
251
|
-
|
|
252
|
-
Error Handling:
|
|
253
|
-
- Returns error if gameweek number invalid (must be 1-38)
|
|
254
|
-
- Returns error if gameweek not found
|
|
255
|
-
- Returns formatted error message if data unavailable
|
|
256
|
-
"""
|
|
257
|
-
try:
|
|
258
|
-
await _create_client()
|
|
259
|
-
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
260
|
-
return "Error: Gameweek data not available. Please try again later."
|
|
261
|
-
|
|
262
|
-
gameweek = next(
|
|
263
|
-
(e for e in store.bootstrap_data.events if e.id == params.gameweek_number),
|
|
264
|
-
None,
|
|
265
|
-
)
|
|
266
|
-
if not gameweek:
|
|
267
|
-
return f"Error: Gameweek {params.gameweek_number} not found. Please use a number between 1-38."
|
|
268
|
-
|
|
269
|
-
if params.response_format == ResponseFormat.JSON:
|
|
270
|
-
return format_json_response(gameweek.model_dump())
|
|
271
|
-
else:
|
|
272
|
-
result = format_gameweek_details(gameweek)
|
|
273
|
-
|
|
274
|
-
# Add top element info manually since it differs from basic details
|
|
275
|
-
if gameweek.top_element_info:
|
|
276
|
-
top_player = store.get_player_by_id(gameweek.top_element_info.id)
|
|
277
|
-
top_player_name = (
|
|
278
|
-
top_player.web_name if top_player else f"ID {gameweek.top_element_info.id}"
|
|
279
|
-
)
|
|
280
|
-
result += (
|
|
281
|
-
f"\n**Top Player:** {top_player_name} ({gameweek.top_element_info.points} pts)"
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
285
|
-
return truncated
|
|
286
|
-
|
|
287
|
-
except Exception as e:
|
|
288
|
-
return handle_api_error(e)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
@mcp.tool(
|
|
292
|
-
name="fpl_list_all_gameweeks",
|
|
293
|
-
annotations={
|
|
294
|
-
"title": "List All FPL Gameweeks",
|
|
295
|
-
"readOnlyHint": True,
|
|
296
|
-
"destructiveHint": False,
|
|
297
|
-
"idempotentHint": True,
|
|
298
|
-
"openWorldHint": True,
|
|
299
|
-
},
|
|
300
|
-
)
|
|
301
|
-
async def fpl_list_all_gameweeks(params: ListAllGameweeksInput) -> str:
|
|
302
|
-
"""
|
|
303
|
-
List all Fantasy Premier League gameweeks with their status.
|
|
304
|
-
|
|
305
|
-
Returns all 38 gameweeks showing which are finished, current, or upcoming. Includes
|
|
306
|
-
deadlines and average scores for finished gameweeks. Useful for getting a season
|
|
307
|
-
overview and planning long-term strategy.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
params (ListAllGameweeksInput): Validated input parameters containing:
|
|
311
|
-
- response_format (ResponseFormat): 'markdown' or 'json' (default: markdown)
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
str: Complete list of all gameweeks with status and scores
|
|
315
|
-
|
|
316
|
-
Examples:
|
|
317
|
-
- View all gameweeks: response_format="markdown"
|
|
318
|
-
- Get season overview: response_format="json"
|
|
319
|
-
|
|
320
|
-
Error Handling:
|
|
321
|
-
- Returns error if gameweek data unavailable
|
|
322
|
-
- Returns formatted error message if API fails
|
|
323
|
-
"""
|
|
324
|
-
try:
|
|
325
|
-
await _create_client()
|
|
326
|
-
if not store.bootstrap_data or not store.bootstrap_data.events:
|
|
327
|
-
return "Error: Gameweek data not available. Please try again later."
|
|
328
|
-
|
|
329
|
-
if params.response_format == ResponseFormat.JSON:
|
|
330
|
-
result = {
|
|
331
|
-
"total_gameweeks": len(store.bootstrap_data.events),
|
|
332
|
-
"gameweeks": [
|
|
333
|
-
{
|
|
334
|
-
"id": event.id,
|
|
335
|
-
"name": event.name,
|
|
336
|
-
"deadline_time": event.deadline_time,
|
|
337
|
-
"is_current": event.is_current,
|
|
338
|
-
"is_previous": event.is_previous,
|
|
339
|
-
"is_next": event.is_next,
|
|
340
|
-
"finished": event.finished,
|
|
341
|
-
"average_entry_score": event.average_entry_score,
|
|
342
|
-
}
|
|
343
|
-
for event in store.bootstrap_data.events
|
|
344
|
-
],
|
|
345
|
-
}
|
|
346
|
-
return format_json_response(result)
|
|
347
|
-
else:
|
|
348
|
-
output = ["**All Gameweeks:**\n"]
|
|
349
|
-
|
|
350
|
-
for event in store.bootstrap_data.events:
|
|
351
|
-
status = []
|
|
352
|
-
if event.is_current:
|
|
353
|
-
status.append("CURRENT")
|
|
354
|
-
if event.is_previous:
|
|
355
|
-
status.append("PREVIOUS")
|
|
356
|
-
if event.is_next:
|
|
357
|
-
status.append("NEXT")
|
|
358
|
-
if event.finished:
|
|
359
|
-
status.append("FINISHED")
|
|
360
|
-
|
|
361
|
-
status_str = f" [{', '.join(status)}]" if status else ""
|
|
362
|
-
avg_score = (
|
|
363
|
-
f" | Avg: {event.average_entry_score}" if event.average_entry_score else ""
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
output.append(
|
|
367
|
-
f"GW{event.id}: {event.name}{status_str} | "
|
|
368
|
-
f"Deadline: {event.deadline_time[:10]}{avg_score}"
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
result = "\n".join(output)
|
|
372
|
-
truncated, _ = check_and_truncate(result, CHARACTER_LIMIT)
|
|
373
|
-
return truncated
|
|
374
|
-
|
|
375
|
-
except Exception as e:
|
|
376
|
-
return handle_api_error(e)
|