universal-mcp-applications 0.1.1__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.
Files changed (268) hide show
  1. universal_mcp/applications/ahrefs/README.md +51 -0
  2. universal_mcp/applications/ahrefs/__init__.py +1 -0
  3. universal_mcp/applications/ahrefs/app.py +2291 -0
  4. universal_mcp/applications/airtable/README.md +22 -0
  5. universal_mcp/applications/airtable/__init__.py +1 -0
  6. universal_mcp/applications/airtable/app.py +479 -0
  7. universal_mcp/applications/apollo/README.md +44 -0
  8. universal_mcp/applications/apollo/__init__.py +1 -0
  9. universal_mcp/applications/apollo/app.py +1847 -0
  10. universal_mcp/applications/asana/README.md +199 -0
  11. universal_mcp/applications/asana/__init__.py +1 -0
  12. universal_mcp/applications/asana/app.py +9509 -0
  13. universal_mcp/applications/aws-s3/README.md +0 -0
  14. universal_mcp/applications/aws-s3/__init__.py +1 -0
  15. universal_mcp/applications/aws-s3/app.py +552 -0
  16. universal_mcp/applications/bill/README.md +0 -0
  17. universal_mcp/applications/bill/__init__.py +1 -0
  18. universal_mcp/applications/bill/app.py +8705 -0
  19. universal_mcp/applications/box/README.md +307 -0
  20. universal_mcp/applications/box/__init__.py +1 -0
  21. universal_mcp/applications/box/app.py +15987 -0
  22. universal_mcp/applications/braze/README.md +106 -0
  23. universal_mcp/applications/braze/__init__.py +1 -0
  24. universal_mcp/applications/braze/app.py +4754 -0
  25. universal_mcp/applications/cal-com-v2/README.md +150 -0
  26. universal_mcp/applications/cal-com-v2/__init__.py +1 -0
  27. universal_mcp/applications/cal-com-v2/app.py +5541 -0
  28. universal_mcp/applications/calendly/README.md +53 -0
  29. universal_mcp/applications/calendly/__init__.py +1 -0
  30. universal_mcp/applications/calendly/app.py +1436 -0
  31. universal_mcp/applications/canva/README.md +43 -0
  32. universal_mcp/applications/canva/__init__.py +1 -0
  33. universal_mcp/applications/canva/app.py +941 -0
  34. universal_mcp/applications/clickup/README.md +135 -0
  35. universal_mcp/applications/clickup/__init__.py +1 -0
  36. universal_mcp/applications/clickup/app.py +5009 -0
  37. universal_mcp/applications/coda/README.md +108 -0
  38. universal_mcp/applications/coda/__init__.py +1 -0
  39. universal_mcp/applications/coda/app.py +3671 -0
  40. universal_mcp/applications/confluence/README.md +198 -0
  41. universal_mcp/applications/confluence/__init__.py +1 -0
  42. universal_mcp/applications/confluence/app.py +6273 -0
  43. universal_mcp/applications/contentful/README.md +17 -0
  44. universal_mcp/applications/contentful/__init__.py +1 -0
  45. universal_mcp/applications/contentful/app.py +364 -0
  46. universal_mcp/applications/crustdata/README.md +25 -0
  47. universal_mcp/applications/crustdata/__init__.py +1 -0
  48. universal_mcp/applications/crustdata/app.py +586 -0
  49. universal_mcp/applications/dialpad/README.md +202 -0
  50. universal_mcp/applications/dialpad/__init__.py +1 -0
  51. universal_mcp/applications/dialpad/app.py +5949 -0
  52. universal_mcp/applications/digitalocean/README.md +463 -0
  53. universal_mcp/applications/digitalocean/__init__.py +1 -0
  54. universal_mcp/applications/digitalocean/app.py +20835 -0
  55. universal_mcp/applications/domain-checker/README.md +13 -0
  56. universal_mcp/applications/domain-checker/__init__.py +1 -0
  57. universal_mcp/applications/domain-checker/app.py +265 -0
  58. universal_mcp/applications/e2b/README.md +12 -0
  59. universal_mcp/applications/e2b/__init__.py +1 -0
  60. universal_mcp/applications/e2b/app.py +187 -0
  61. universal_mcp/applications/elevenlabs/README.md +88 -0
  62. universal_mcp/applications/elevenlabs/__init__.py +1 -0
  63. universal_mcp/applications/elevenlabs/app.py +3235 -0
  64. universal_mcp/applications/exa/README.md +15 -0
  65. universal_mcp/applications/exa/__init__.py +1 -0
  66. universal_mcp/applications/exa/app.py +221 -0
  67. universal_mcp/applications/falai/README.md +17 -0
  68. universal_mcp/applications/falai/__init__.py +1 -0
  69. universal_mcp/applications/falai/app.py +331 -0
  70. universal_mcp/applications/figma/README.md +49 -0
  71. universal_mcp/applications/figma/__init__.py +1 -0
  72. universal_mcp/applications/figma/app.py +1090 -0
  73. universal_mcp/applications/firecrawl/README.md +20 -0
  74. universal_mcp/applications/firecrawl/__init__.py +1 -0
  75. universal_mcp/applications/firecrawl/app.py +514 -0
  76. universal_mcp/applications/fireflies/README.md +25 -0
  77. universal_mcp/applications/fireflies/__init__.py +1 -0
  78. universal_mcp/applications/fireflies/app.py +506 -0
  79. universal_mcp/applications/fpl/README.md +23 -0
  80. universal_mcp/applications/fpl/__init__.py +1 -0
  81. universal_mcp/applications/fpl/app.py +1327 -0
  82. universal_mcp/applications/fpl/utils/api.py +142 -0
  83. universal_mcp/applications/fpl/utils/fixtures.py +629 -0
  84. universal_mcp/applications/fpl/utils/helper.py +982 -0
  85. universal_mcp/applications/fpl/utils/league_utils.py +546 -0
  86. universal_mcp/applications/fpl/utils/position_utils.py +68 -0
  87. universal_mcp/applications/ghost-content/README.md +25 -0
  88. universal_mcp/applications/ghost-content/__init__.py +1 -0
  89. universal_mcp/applications/ghost-content/app.py +654 -0
  90. universal_mcp/applications/github/README.md +1049 -0
  91. universal_mcp/applications/github/__init__.py +1 -0
  92. universal_mcp/applications/github/app.py +50600 -0
  93. universal_mcp/applications/gong/README.md +63 -0
  94. universal_mcp/applications/gong/__init__.py +1 -0
  95. universal_mcp/applications/gong/app.py +2297 -0
  96. universal_mcp/applications/google-ads/README.md +0 -0
  97. universal_mcp/applications/google-ads/__init__.py +1 -0
  98. universal_mcp/applications/google-ads/app.py +23 -0
  99. universal_mcp/applications/google-calendar/README.md +21 -0
  100. universal_mcp/applications/google-calendar/__init__.py +1 -0
  101. universal_mcp/applications/google-calendar/app.py +574 -0
  102. universal_mcp/applications/google-docs/README.md +25 -0
  103. universal_mcp/applications/google-docs/__init__.py +1 -0
  104. universal_mcp/applications/google-docs/app.py +760 -0
  105. universal_mcp/applications/google-drive/README.md +68 -0
  106. universal_mcp/applications/google-drive/__init__.py +1 -0
  107. universal_mcp/applications/google-drive/app.py +4936 -0
  108. universal_mcp/applications/google-gemini/README.md +25 -0
  109. universal_mcp/applications/google-gemini/__init__.py +1 -0
  110. universal_mcp/applications/google-gemini/app.py +663 -0
  111. universal_mcp/applications/google-mail/README.md +31 -0
  112. universal_mcp/applications/google-mail/__init__.py +1 -0
  113. universal_mcp/applications/google-mail/app.py +1354 -0
  114. universal_mcp/applications/google-searchconsole/README.md +21 -0
  115. universal_mcp/applications/google-searchconsole/__init__.py +1 -0
  116. universal_mcp/applications/google-searchconsole/app.py +320 -0
  117. universal_mcp/applications/google-sheet/README.md +36 -0
  118. universal_mcp/applications/google-sheet/__init__.py +1 -0
  119. universal_mcp/applications/google-sheet/app.py +1941 -0
  120. universal_mcp/applications/hashnode/README.md +20 -0
  121. universal_mcp/applications/hashnode/__init__.py +1 -0
  122. universal_mcp/applications/hashnode/app.py +455 -0
  123. universal_mcp/applications/heygen/README.md +44 -0
  124. universal_mcp/applications/heygen/__init__.py +1 -0
  125. universal_mcp/applications/heygen/app.py +961 -0
  126. universal_mcp/applications/http-tools/README.md +16 -0
  127. universal_mcp/applications/http-tools/__init__.py +1 -0
  128. universal_mcp/applications/http-tools/app.py +153 -0
  129. universal_mcp/applications/hubspot/README.md +239 -0
  130. universal_mcp/applications/hubspot/__init__.py +1 -0
  131. universal_mcp/applications/hubspot/app.py +416 -0
  132. universal_mcp/applications/jira/README.md +600 -0
  133. universal_mcp/applications/jira/__init__.py +1 -0
  134. universal_mcp/applications/jira/app.py +28804 -0
  135. universal_mcp/applications/klaviyo/README.md +313 -0
  136. universal_mcp/applications/klaviyo/__init__.py +1 -0
  137. universal_mcp/applications/klaviyo/app.py +11236 -0
  138. universal_mcp/applications/linkedin/README.md +15 -0
  139. universal_mcp/applications/linkedin/__init__.py +1 -0
  140. universal_mcp/applications/linkedin/app.py +243 -0
  141. universal_mcp/applications/mailchimp/README.md +281 -0
  142. universal_mcp/applications/mailchimp/__init__.py +1 -0
  143. universal_mcp/applications/mailchimp/app.py +10937 -0
  144. universal_mcp/applications/markitdown/README.md +12 -0
  145. universal_mcp/applications/markitdown/__init__.py +1 -0
  146. universal_mcp/applications/markitdown/app.py +63 -0
  147. universal_mcp/applications/miro/README.md +151 -0
  148. universal_mcp/applications/miro/__init__.py +1 -0
  149. universal_mcp/applications/miro/app.py +5429 -0
  150. universal_mcp/applications/ms-teams/README.md +42 -0
  151. universal_mcp/applications/ms-teams/__init__.py +1 -0
  152. universal_mcp/applications/ms-teams/app.py +1823 -0
  153. universal_mcp/applications/neon/README.md +74 -0
  154. universal_mcp/applications/neon/__init__.py +1 -0
  155. universal_mcp/applications/neon/app.py +2018 -0
  156. universal_mcp/applications/notion/README.md +30 -0
  157. universal_mcp/applications/notion/__init__.py +1 -0
  158. universal_mcp/applications/notion/app.py +527 -0
  159. universal_mcp/applications/openai/README.md +22 -0
  160. universal_mcp/applications/openai/__init__.py +1 -0
  161. universal_mcp/applications/openai/app.py +759 -0
  162. universal_mcp/applications/outlook/README.md +20 -0
  163. universal_mcp/applications/outlook/__init__.py +1 -0
  164. universal_mcp/applications/outlook/app.py +444 -0
  165. universal_mcp/applications/perplexity/README.md +12 -0
  166. universal_mcp/applications/perplexity/__init__.py +1 -0
  167. universal_mcp/applications/perplexity/app.py +65 -0
  168. universal_mcp/applications/pipedrive/README.md +284 -0
  169. universal_mcp/applications/pipedrive/__init__.py +1 -0
  170. universal_mcp/applications/pipedrive/app.py +12924 -0
  171. universal_mcp/applications/posthog/README.md +132 -0
  172. universal_mcp/applications/posthog/__init__.py +1 -0
  173. universal_mcp/applications/posthog/app.py +7125 -0
  174. universal_mcp/applications/reddit/README.md +135 -0
  175. universal_mcp/applications/reddit/__init__.py +1 -0
  176. universal_mcp/applications/reddit/app.py +4652 -0
  177. universal_mcp/applications/replicate/README.md +18 -0
  178. universal_mcp/applications/replicate/__init__.py +1 -0
  179. universal_mcp/applications/replicate/app.py +495 -0
  180. universal_mcp/applications/resend/README.md +40 -0
  181. universal_mcp/applications/resend/__init__.py +1 -0
  182. universal_mcp/applications/resend/app.py +881 -0
  183. universal_mcp/applications/retell/README.md +21 -0
  184. universal_mcp/applications/retell/__init__.py +1 -0
  185. universal_mcp/applications/retell/app.py +333 -0
  186. universal_mcp/applications/rocketlane/README.md +70 -0
  187. universal_mcp/applications/rocketlane/__init__.py +1 -0
  188. universal_mcp/applications/rocketlane/app.py +4346 -0
  189. universal_mcp/applications/semanticscholar/README.md +25 -0
  190. universal_mcp/applications/semanticscholar/__init__.py +1 -0
  191. universal_mcp/applications/semanticscholar/app.py +482 -0
  192. universal_mcp/applications/semrush/README.md +44 -0
  193. universal_mcp/applications/semrush/__init__.py +1 -0
  194. universal_mcp/applications/semrush/app.py +2081 -0
  195. universal_mcp/applications/sendgrid/README.md +362 -0
  196. universal_mcp/applications/sendgrid/__init__.py +1 -0
  197. universal_mcp/applications/sendgrid/app.py +9752 -0
  198. universal_mcp/applications/sentry/README.md +186 -0
  199. universal_mcp/applications/sentry/__init__.py +1 -0
  200. universal_mcp/applications/sentry/app.py +7471 -0
  201. universal_mcp/applications/serpapi/README.md +14 -0
  202. universal_mcp/applications/serpapi/__init__.py +1 -0
  203. universal_mcp/applications/serpapi/app.py +293 -0
  204. universal_mcp/applications/sharepoint/README.md +0 -0
  205. universal_mcp/applications/sharepoint/__init__.py +1 -0
  206. universal_mcp/applications/sharepoint/app.py +215 -0
  207. universal_mcp/applications/shopify/README.md +321 -0
  208. universal_mcp/applications/shopify/__init__.py +1 -0
  209. universal_mcp/applications/shopify/app.py +15392 -0
  210. universal_mcp/applications/shortcut/README.md +128 -0
  211. universal_mcp/applications/shortcut/__init__.py +1 -0
  212. universal_mcp/applications/shortcut/app.py +4478 -0
  213. universal_mcp/applications/slack/README.md +0 -0
  214. universal_mcp/applications/slack/__init__.py +1 -0
  215. universal_mcp/applications/slack/app.py +570 -0
  216. universal_mcp/applications/spotify/README.md +91 -0
  217. universal_mcp/applications/spotify/__init__.py +1 -0
  218. universal_mcp/applications/spotify/app.py +2526 -0
  219. universal_mcp/applications/supabase/README.md +87 -0
  220. universal_mcp/applications/supabase/__init__.py +1 -0
  221. universal_mcp/applications/supabase/app.py +2970 -0
  222. universal_mcp/applications/tavily/README.md +12 -0
  223. universal_mcp/applications/tavily/__init__.py +1 -0
  224. universal_mcp/applications/tavily/app.py +51 -0
  225. universal_mcp/applications/trello/README.md +266 -0
  226. universal_mcp/applications/trello/__init__.py +1 -0
  227. universal_mcp/applications/trello/app.py +10875 -0
  228. universal_mcp/applications/twillo/README.md +0 -0
  229. universal_mcp/applications/twillo/__init__.py +1 -0
  230. universal_mcp/applications/twillo/app.py +269 -0
  231. universal_mcp/applications/twitter/README.md +100 -0
  232. universal_mcp/applications/twitter/__init__.py +1 -0
  233. universal_mcp/applications/twitter/api_segments/__init__.py +0 -0
  234. universal_mcp/applications/twitter/api_segments/api_segment_base.py +51 -0
  235. universal_mcp/applications/twitter/api_segments/compliance_api.py +122 -0
  236. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +255 -0
  237. universal_mcp/applications/twitter/api_segments/dm_events_api.py +140 -0
  238. universal_mcp/applications/twitter/api_segments/likes_api.py +159 -0
  239. universal_mcp/applications/twitter/api_segments/lists_api.py +395 -0
  240. universal_mcp/applications/twitter/api_segments/openapi_json_api.py +34 -0
  241. universal_mcp/applications/twitter/api_segments/spaces_api.py +309 -0
  242. universal_mcp/applications/twitter/api_segments/trends_api.py +40 -0
  243. universal_mcp/applications/twitter/api_segments/tweets_api.py +1403 -0
  244. universal_mcp/applications/twitter/api_segments/usage_api.py +40 -0
  245. universal_mcp/applications/twitter/api_segments/users_api.py +1498 -0
  246. universal_mcp/applications/twitter/app.py +46 -0
  247. universal_mcp/applications/unipile/README.md +28 -0
  248. universal_mcp/applications/unipile/__init__.py +1 -0
  249. universal_mcp/applications/unipile/app.py +829 -0
  250. universal_mcp/applications/whatsapp/README.md +23 -0
  251. universal_mcp/applications/whatsapp/__init__.py +1 -0
  252. universal_mcp/applications/whatsapp/app.py +595 -0
  253. universal_mcp/applications/whatsapp-business/README.md +34 -0
  254. universal_mcp/applications/whatsapp-business/__init__.py +1 -0
  255. universal_mcp/applications/whatsapp-business/app.py +1065 -0
  256. universal_mcp/applications/wrike/README.md +46 -0
  257. universal_mcp/applications/wrike/__init__.py +1 -0
  258. universal_mcp/applications/wrike/app.py +1583 -0
  259. universal_mcp/applications/youtube/README.md +57 -0
  260. universal_mcp/applications/youtube/__init__.py +1 -0
  261. universal_mcp/applications/youtube/app.py +1696 -0
  262. universal_mcp/applications/zenquotes/README.md +12 -0
  263. universal_mcp/applications/zenquotes/__init__.py +1 -0
  264. universal_mcp/applications/zenquotes/app.py +31 -0
  265. universal_mcp_applications-0.1.1.dist-info/METADATA +172 -0
  266. universal_mcp_applications-0.1.1.dist-info/RECORD +268 -0
  267. universal_mcp_applications-0.1.1.dist-info/WHEEL +4 -0
  268. universal_mcp_applications-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,982 @@
1
+ import datetime
2
+ import logging
3
+ from typing import Any
4
+
5
+ logger = logging.getLogger("fpl-mcp-server.fixtures")
6
+ from universal_mcp_fpl.utils.api import api
7
+
8
+ # Resources
9
+
10
+
11
+ def get_players_resource(
12
+ name_filter: str | None = None, team_filter: str | None = None
13
+ ) -> list[dict[str, Any]]:
14
+ """
15
+ Format player data for the MCP resource.
16
+
17
+ Args:
18
+ name_filter: Optional filter for player name (case-insensitive partial match)
19
+ team_filter: Optional filter for team name (case-insensitive partial match)
20
+
21
+ Returns:
22
+ Formatted player data
23
+ """
24
+ # Get raw data from API
25
+ data = api.get_bootstrap_static()
26
+
27
+ # Create team and position lookup maps
28
+ team_map = {t["id"]: t for t in data["teams"]}
29
+ position_map = {p["id"]: p for p in data["element_types"]}
30
+ logging.info(f"Team map: {team_map}")
31
+ logging.info(f"Position map: {position_map}")
32
+
33
+ # Format player data
34
+ players = []
35
+ for player in data["elements"]:
36
+ # Extract team and position info
37
+ team = team_map.get(player["team"], {})
38
+ position = position_map.get(player["element_type"], {})
39
+
40
+ player_name = f"{player['first_name']} {player['second_name']}"
41
+ team_name = team.get("name", "Unknown")
42
+
43
+ # Apply filters if specified
44
+ if name_filter and name_filter.lower() not in player_name.lower():
45
+ continue
46
+
47
+ if team_filter and team_filter.lower() not in team_name.lower():
48
+ continue
49
+
50
+ # Build comprehensive player object with all available stats
51
+ player_data = {
52
+ "id": player["id"],
53
+ "name": player_name,
54
+ "web_name": player["web_name"],
55
+ "team": team_name,
56
+ "team_short": team.get("short_name", "UNK"),
57
+ "position": position.get("singular_name_short", "UNK"),
58
+ "price": player["now_cost"] / 10.0,
59
+ "form": player["form"],
60
+ "points": player["total_points"],
61
+ "points_per_game": player["points_per_game"],
62
+ # Playing time
63
+ "minutes": player["minutes"],
64
+ "starts": player["starts"],
65
+ # Key stats
66
+ "goals": player["goals_scored"],
67
+ "assists": player["assists"],
68
+ "clean_sheets": player["clean_sheets"],
69
+ "goals_conceded": player["goals_conceded"],
70
+ "own_goals": player["own_goals"],
71
+ "penalties_saved": player["penalties_saved"],
72
+ "penalties_missed": player["penalties_missed"],
73
+ "yellow_cards": player["yellow_cards"],
74
+ "red_cards": player["red_cards"],
75
+ "saves": player["saves"],
76
+ "bonus": player["bonus"],
77
+ "bps": player["bps"],
78
+ # Advanced metrics
79
+ "influence": player["influence"],
80
+ "creativity": player["creativity"],
81
+ "threat": player["threat"],
82
+ "ict_index": player["ict_index"],
83
+ # Expected stats (if available)
84
+ "expected_goals": player.get("expected_goals", "N/A"),
85
+ "expected_assists": player.get("expected_assists", "N/A"),
86
+ "expected_goal_involvements": player.get(
87
+ "expected_goal_involvements", "N/A"
88
+ ),
89
+ "expected_goals_conceded": player.get("expected_goals_conceded", "N/A"),
90
+ # Ownership & transfers
91
+ "selected_by_percent": player["selected_by_percent"],
92
+ "transfers_in_event": player["transfers_in_event"],
93
+ "transfers_out_event": player["transfers_out_event"],
94
+ # Price changes
95
+ "cost_change_event": player["cost_change_event"] / 10.0,
96
+ "cost_change_start": player["cost_change_start"] / 10.0,
97
+ # Status info
98
+ "status": player["status"],
99
+ "news": player["news"],
100
+ "chance_of_playing_next_round": player["chance_of_playing_next_round"],
101
+ }
102
+
103
+ players.append(player_data)
104
+ logging.info(f"Formatted {len(players)} players")
105
+ return players
106
+
107
+
108
+ def get_team_name_by_id(team_id: int | None) -> str:
109
+ """Get team name from team ID.
110
+
111
+ Args:
112
+ team_id: Team ID
113
+
114
+ Returns:
115
+ Team name or "Unknown team" if not found
116
+ """
117
+ if team_id is None:
118
+ return "Unknown team"
119
+
120
+ teams_data = api.get_teams()
121
+
122
+ for team in teams_data:
123
+ if team.get("id") == team_id:
124
+ return team.get("name", "Unknown team")
125
+
126
+ return "Unknown team"
127
+
128
+
129
+ def get_player_by_id(player_id: int) -> dict[str, Any] | None:
130
+ """
131
+ Get detailed information for a specific player by ID.
132
+
133
+ Args:
134
+ player_id: FPL player ID
135
+
136
+ Returns:
137
+ Player data or None if not found
138
+ """
139
+ # Get all players
140
+ all_players = get_players_resource()
141
+
142
+ # Find player by ID
143
+ for player in all_players:
144
+ if player["id"] == player_id:
145
+ # Get additional detail data
146
+ try:
147
+ summary = api.get_player_summary(player_id)
148
+
149
+ # Add fixture history
150
+ player["history"] = summary.get("history", [])
151
+
152
+ # Add upcoming fixtures
153
+ player["fixtures"] = summary.get("fixtures", [])
154
+
155
+ return player
156
+ except Exception:
157
+ # Return basic player data if detailed data not available
158
+ return player
159
+
160
+ return None
161
+
162
+
163
+ def find_players_by_name(name: str, limit: int = 5) -> list[dict[str, Any]]:
164
+ """
165
+ Find players by partial name match with advanced matching.
166
+
167
+ Args:
168
+ name: Player name to search for (supports partial names, nicknames, and initials)
169
+ limit: Maximum number of results to return
170
+
171
+ Returns:
172
+ List of matching players sorted by relevance and points
173
+ """
174
+ # Get all players
175
+ logger = logging.getLogger(__name__)
176
+ logger.info(f"Finding players by name: {name}")
177
+ all_players = get_players_resource()
178
+ logger.info(f"Found {len(all_players)} players")
179
+
180
+ # Normalize search term
181
+ search_term = name.lower().strip()
182
+ if not search_term:
183
+ return []
184
+
185
+ # Common nickname and abbreviation mapping
186
+ nicknames = {
187
+ "kdb": "kevin de bruyne",
188
+ "vvd": "virgil van dijk",
189
+ "taa": "trent alexander-arnold",
190
+ "cr7": "cristiano ronaldo",
191
+ "bobby": "roberto firmino",
192
+ "mo salah": "mohamed salah",
193
+ "mane": "sadio mane",
194
+ "auba": "aubameyang",
195
+ "lewa": "lewandowski",
196
+ "kane": "harry kane",
197
+ "rashford": "marcus rashford",
198
+ "son": "heung-min son",
199
+ }
200
+
201
+ # Check for nickname match
202
+ if search_term in nicknames:
203
+ search_term = nicknames[search_term]
204
+
205
+ # Split search term into parts for multi-part matching
206
+ search_parts = search_term.split()
207
+
208
+ # Store scored results
209
+ scored_players = []
210
+
211
+ for player in all_players:
212
+ # Extract player name components
213
+ full_name = player["name"].lower()
214
+ web_name = player.get("web_name", "").lower()
215
+
216
+ # Try to extract first and last name
217
+ name_parts = full_name.split()
218
+ first_name = name_parts[0] if name_parts else ""
219
+ last_name = name_parts[-1] if len(name_parts) > 1 else ""
220
+
221
+ # Initialize score and tracking reasons
222
+ score = 0
223
+
224
+ # 1. Exact full name match
225
+ if search_term == full_name:
226
+ score += 100
227
+
228
+ # 2. Exact match on web_name (common name)
229
+ elif search_term == web_name:
230
+ score += 90
231
+
232
+ # 3. Exact match on last name
233
+ elif len(search_parts) == 1 and search_term == last_name:
234
+ score += 80
235
+
236
+ # 4. Exact match on first name
237
+ elif len(search_parts) == 1 and search_term == first_name:
238
+ score += 70
239
+
240
+ # 5. Check for initials match (e.g., "KDB")
241
+ if len(search_term) <= 5 and all(c.isalpha() for c in search_term):
242
+ # Try to match initials
243
+ initials = "".join(part[0] for part in full_name.split() if part)
244
+ if search_term.lower() == initials.lower():
245
+ score += 85
246
+
247
+ # 6. Multi-part name matching (e.g., "Mo Salah")
248
+ if len(search_parts) > 1:
249
+ # Check if first part matches first name and last part matches last name
250
+ if search_parts[0] in first_name and search_parts[-1] in last_name:
251
+ score += 75
252
+
253
+ # Check if parts appear in order in the full name
254
+ search_combined = "".join(search_parts)
255
+ full_combined = "".join(full_name.split())
256
+ if search_combined in full_combined:
257
+ score += 50
258
+
259
+ # 7. Substring matches
260
+ if search_term in full_name:
261
+ score += 40
262
+
263
+ # 8. Partial word matches in full name
264
+ for part in search_parts:
265
+ if part in full_name:
266
+ score += 30
267
+
268
+ # 9. Partial word matches in web name
269
+ for part in search_parts:
270
+ if part in web_name:
271
+ score += 25
272
+
273
+ # 10. Add a bonus score for high-point players (tiebreaker)
274
+ points_score = min(20, float(player["points"]) / 50) # Up to 20 extra points
275
+
276
+ # Total score
277
+ total_score = score + (points_score if score > 0 else 0)
278
+
279
+ # Add to results if there's any match
280
+ if score > 0:
281
+ scored_players.append((total_score, player))
282
+
283
+ # Sort by score (highest first)
284
+ sorted_players = [
285
+ player for _, player in sorted(scored_players, key=lambda x: x[0], reverse=True)
286
+ ]
287
+ # If no matches with good confidence, fall back to simple contains match
288
+ if not sorted_players or (sorted_players and scored_players[0][0] < 30):
289
+ fallback_players = [
290
+ p
291
+ for p in all_players
292
+ if search_term in p["name"].lower()
293
+ or search_term in p.get("web_name", "").lower()
294
+ ]
295
+ # Sort fallback by points
296
+ fallback_players.sort(key=lambda p: float(p["points"]), reverse=True)
297
+
298
+ # Merge results, prioritizing scored results
299
+ merged = []
300
+ seen_ids = set(p["id"] for p in sorted_players)
301
+
302
+ merged.extend(sorted_players)
303
+ for p in fallback_players:
304
+ if p["id"] not in seen_ids:
305
+ merged.append(p)
306
+ seen_ids.add(p["id"])
307
+
308
+ sorted_players = merged
309
+
310
+ # Return limited results
311
+ return sorted_players[:limit]
312
+
313
+
314
+ def get_current_gameweek_resource() -> dict[str, Any]:
315
+ """
316
+ Get current gameweek data with additional details.
317
+
318
+ Returns:
319
+ Current gameweek data with enhanced information
320
+ """
321
+ # Get current gameweek
322
+ current_gw = api.get_current_gameweek()
323
+
324
+ # Get raw data to extract player details
325
+ all_data = api.get_bootstrap_static()
326
+
327
+ # Create enhanced gameweek data
328
+ gw_data = {
329
+ "id": current_gw["id"],
330
+ "name": current_gw["name"],
331
+ "deadline_time": current_gw["deadline_time"],
332
+ "is_current": current_gw["is_current"],
333
+ "is_next": current_gw["is_next"],
334
+ "finished": current_gw["finished"],
335
+ "data_checked": current_gw["data_checked"],
336
+ "status": "Current" if current_gw.get("is_current", False) else "Next",
337
+ }
338
+
339
+ # Format deadline time to be more readable
340
+ try:
341
+ deadline = datetime.datetime.strptime(
342
+ current_gw["deadline_time"], "%Y-%m-%dT%H:%M:%SZ"
343
+ )
344
+ gw_data["deadline_formatted"] = deadline.strftime("%A, %d %B %Y at %H:%M UTC")
345
+
346
+ # Calculate time until deadline
347
+ now = datetime.datetime.utcnow()
348
+ if deadline > now:
349
+ delta = deadline - now
350
+ days = delta.days
351
+ hours = delta.seconds // 3600
352
+ minutes = (delta.seconds % 3600) // 60
353
+
354
+ time_parts = []
355
+ if days > 0:
356
+ time_parts.append(f"{days} day{'s' if days != 1 else ''}")
357
+ if hours > 0:
358
+ time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
359
+ if minutes > 0:
360
+ time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
361
+
362
+ gw_data["time_until_deadline"] = ", ".join(time_parts)
363
+ else:
364
+ gw_data["time_until_deadline"] = "Deadline passed"
365
+ except (ValueError, TypeError):
366
+ gw_data["deadline_formatted"] = current_gw["deadline_time"]
367
+
368
+ # Add stats if available
369
+ if current_gw.get("highest_score") is not None:
370
+ gw_data["stats"] = {
371
+ "highest_score": current_gw["highest_score"],
372
+ "average_score": current_gw.get("average_entry_score", "N/A"),
373
+ "chip_plays": current_gw.get("chip_plays", []),
374
+ }
375
+
376
+ # Add most popular players if available
377
+ popular_players = {}
378
+ player_map = {p["id"]: p for p in all_data.get("elements", [])}
379
+
380
+ popular_fields = [
381
+ ("most_selected", "Most Selected"),
382
+ ("most_transferred_in", "Most Transferred In"),
383
+ ("most_captained", "Most Captained"),
384
+ ("most_vice_captained", "Most Vice Captained"),
385
+ ]
386
+
387
+ for field_key, field_name in popular_fields:
388
+ player_id = current_gw.get(field_key)
389
+ if player_id:
390
+ player = player_map.get(player_id)
391
+ if player:
392
+ popular_players[field_name] = {
393
+ "id": player["id"],
394
+ "name": f"{player['first_name']} {player['second_name']}",
395
+ "web_name": player["web_name"],
396
+ "team": player["team"],
397
+ }
398
+
399
+ if popular_players:
400
+ gw_data["popular_players"] = popular_players
401
+
402
+ # Add fixtures if the API has them
403
+ fixtures = api.get_fixtures()
404
+ if fixtures:
405
+ gw_fixtures = [f for f in fixtures if f.get("event") == current_gw["id"]]
406
+ if gw_fixtures:
407
+ gw_data["fixture_count"] = len(gw_fixtures)
408
+
409
+ return gw_data
410
+
411
+
412
+ def get_player_fixtures(player_id: int, num_fixtures: int = 5) -> list[dict[str, Any]]:
413
+ """Get upcoming fixtures for a specific player
414
+
415
+ Args:
416
+ player_id: FPL ID of the player
417
+ num_fixtures: Number of upcoming fixtures to return
418
+
419
+ Returns:
420
+ List of upcoming fixtures for the player
421
+ """
422
+ logger.info(
423
+ f"Getting player fixtures (player_id={player_id}, num_fixtures={num_fixtures})"
424
+ )
425
+
426
+ # Get player data to find their team
427
+ players_data = api.get_players()
428
+ player = None
429
+ for p in players_data:
430
+ if p.get("id") == player_id:
431
+ player = p
432
+ break
433
+
434
+ if not player:
435
+ logger.warning(f"Player with ID {player_id} not found")
436
+ return []
437
+
438
+ team_id = player.get("team")
439
+ if not team_id:
440
+ logger.warning(f"Team ID not found for player {player_id}")
441
+ return []
442
+
443
+ # Get all fixtures
444
+ all_fixtures = api.get_fixtures()
445
+ if not all_fixtures:
446
+ logger.warning("No fixtures data found")
447
+ return []
448
+
449
+ # Get gameweeks to determine current gameweek
450
+ gameweeks = api.get_gameweeks()
451
+ current_gameweek = None
452
+ for gw in gameweeks:
453
+ if gw.get("is_current"):
454
+ current_gameweek = gw.get("id")
455
+ break
456
+
457
+ if not current_gameweek:
458
+ for gw in gameweeks:
459
+ if gw.get("is_next"):
460
+ gw_id = gw.get("id")
461
+ if gw_id is not None:
462
+ current_gameweek = gw_id - 1
463
+ break
464
+
465
+ if not current_gameweek:
466
+ logger.warning("Could not determine current gameweek")
467
+ return []
468
+
469
+ # Filter upcoming fixtures for player's team
470
+ upcoming_fixtures = []
471
+
472
+ for fixture in all_fixtures:
473
+ # Only include fixtures from current gameweek onwards
474
+ if fixture.get("event") and fixture.get("event") >= current_gameweek:
475
+ # Check if player's team is involved
476
+ if fixture.get("team_h") == team_id or fixture.get("team_a") == team_id:
477
+ upcoming_fixtures.append(fixture)
478
+
479
+ # Sort by gameweek
480
+ upcoming_fixtures.sort(key=lambda x: x.get("event", 0))
481
+
482
+ # Limit to requested number of fixtures
483
+ upcoming_fixtures = upcoming_fixtures[:num_fixtures]
484
+
485
+ # Get teams data for mapping IDs to names
486
+ teams_data = api.get_teams()
487
+ team_map = {t["id"]: t for t in teams_data}
488
+
489
+ # Format fixtures
490
+ formatted_fixtures = []
491
+ for fixture in upcoming_fixtures:
492
+ home_id = fixture.get("team_h", 0)
493
+ away_id = fixture.get("team_a", 0)
494
+
495
+ # Determine if player's team is home or away
496
+ is_home = home_id == team_id
497
+
498
+ # Get opponent team data
499
+ opponent_id = away_id if is_home else home_id
500
+ opponent_team = team_map.get(opponent_id, {})
501
+
502
+ # Determine difficulty - higher is more difficult
503
+ difficulty = fixture.get(
504
+ "team_h_difficulty" if is_home else "team_a_difficulty", 3
505
+ )
506
+
507
+ formatted_fixture = {
508
+ "gameweek": fixture.get("event"),
509
+ "kickoff_time": fixture.get("kickoff_time", ""),
510
+ "location": "home" if is_home else "away",
511
+ "opponent": opponent_team.get("name", f"Team {opponent_id}"),
512
+ "opponent_short": opponent_team.get("short_name", ""),
513
+ "difficulty": difficulty,
514
+ }
515
+
516
+ formatted_fixtures.append(formatted_fixture)
517
+
518
+ return formatted_fixtures
519
+
520
+
521
+ def get_player_gameweek_history(
522
+ player_ids: list[int], num_gameweeks: int = 5
523
+ ) -> dict[str, Any]:
524
+ """Get recent gameweek history for multiple players.
525
+
526
+ Args:
527
+ player_ids: List of player IDs to fetch history for
528
+ num_gameweeks: Number of recent gameweeks to include
529
+
530
+ Returns:
531
+ Dictionary mapping player IDs to their gameweek histories
532
+ """
533
+ logger = logging.getLogger(__name__)
534
+ logger.info(
535
+ f"Getting gameweek history for {len(player_ids)} players, {num_gameweeks} gameweeks"
536
+ )
537
+
538
+ # Get current gameweek to determine range
539
+ gameweeks = api.get_gameweeks()
540
+ current_gameweek = None
541
+
542
+ for gw in gameweeks:
543
+ if gw.get("is_current"):
544
+ current_gameweek = gw.get("id")
545
+ break
546
+
547
+ if current_gameweek is None:
548
+ # If no current gameweek found, try to find next gameweek
549
+ for gw in gameweeks:
550
+ if gw.get("is_next"):
551
+ gw_id = gw.get("id")
552
+ if gw_id is not None:
553
+ current_gameweek = gw_id - 1
554
+ break
555
+
556
+ if current_gameweek is None:
557
+ logger.warning("Could not determine current gameweek")
558
+ return {"error": "Could not determine current gameweek"}
559
+
560
+ # Calculate gameweek range
561
+ start_gameweek = max(1, current_gameweek - num_gameweeks + 1)
562
+ gameweek_range = list(range(start_gameweek, current_gameweek + 1))
563
+ logger.info(f"Analyzing gameweek range: {gameweek_range}")
564
+
565
+ # Fetch history for each player
566
+ result = {}
567
+
568
+ for player_id in player_ids:
569
+ try:
570
+ # Get player summary which includes history
571
+ player_summary = api.get_player_summary(player_id)
572
+
573
+ if not player_summary or "history" not in player_summary:
574
+ logger.warning(f"No history data found for player {player_id}")
575
+ continue
576
+
577
+ # Filter to requested gameweeks and format
578
+ player_history = []
579
+
580
+ for entry in player_summary["history"]:
581
+ round_num = entry.get("round")
582
+ if round_num in gameweek_range:
583
+ player_history.append(
584
+ {
585
+ "gameweek": round_num,
586
+ "minutes": entry.get("minutes", 0),
587
+ "points": entry.get("total_points", 0),
588
+ "goals": entry.get("goals_scored", 0),
589
+ "assists": entry.get("assists", 0),
590
+ "clean_sheets": entry.get("clean_sheets", 0),
591
+ "bonus": entry.get("bonus", 0),
592
+ "opponent": get_team_name_by_id(entry.get("opponent_team")),
593
+ "was_home": entry.get("was_home", False),
594
+ # Added additional stats as requested
595
+ "expected_goals": entry.get("expected_goals", 0),
596
+ "expected_assists": entry.get("expected_assists", 0),
597
+ "expected_goal_involvements": entry.get(
598
+ "expected_goal_involvements", 0
599
+ ),
600
+ "expected_goals_conceded": entry.get(
601
+ "expected_goals_conceded", 0
602
+ ),
603
+ "transfers_in": entry.get("transfers_in", 0),
604
+ "transfers_out": entry.get("transfers_out", 0),
605
+ "selected": entry.get("selected", 0),
606
+ "value": entry.get("value", 0) / 10.0
607
+ if "value" in entry
608
+ else 0,
609
+ "team_score": entry.get(
610
+ "team_h_score"
611
+ if entry.get("was_home")
612
+ else "team_a_score",
613
+ 0,
614
+ ),
615
+ "opponent_score": entry.get(
616
+ "team_a_score"
617
+ if entry.get("was_home")
618
+ else "team_h_score",
619
+ 0,
620
+ ),
621
+ }
622
+ )
623
+
624
+ # Sort by gameweek
625
+ player_history.sort(key=lambda x: x["gameweek"])
626
+ result[player_id] = player_history
627
+
628
+ except Exception as e:
629
+ logger.error(f"Error fetching history for player {player_id}: {e}")
630
+
631
+ return {"players": result, "gameweeks": gameweek_range}
632
+
633
+
634
+ # Tools
635
+ def get_player_info(
636
+ player_id: int | None = None,
637
+ player_name: str | None = None,
638
+ start_gameweek: int | None = None,
639
+ end_gameweek: int | None = None,
640
+ include_history: bool = True,
641
+ include_fixtures: bool = True,
642
+ ) -> dict[str, Any]:
643
+ """
644
+ Get detailed information for a specific player, optionally filtering stats by gameweek range.
645
+
646
+ Args:
647
+ player_id: FPL player ID (if provided, takes precedence over player_name)
648
+ player_name: Player name to search for (used if player_id not provided)
649
+ start_gameweek: Starting gameweek for filtering player history
650
+ end_gameweek: Ending gameweek for filtering player history
651
+ include_history: Whether to include gameweek-by-gameweek history
652
+ include_fixtures: Whether to include upcoming fixtures
653
+
654
+ Returns:
655
+ Detailed player information including stats and history
656
+ """
657
+ logger = logging.getLogger(__name__)
658
+ logger.info(f"Getting player info: ID={player_id}, name={player_name}")
659
+
660
+ # Get current gameweek
661
+ current_gw_info = get_current_gameweek_resource()
662
+ current_gw = current_gw_info.get("id", 1)
663
+
664
+ # Find player by ID or name
665
+ player = None
666
+ if player_id is not None:
667
+ player = get_player_by_id(player_id)
668
+ elif player_name:
669
+ matches = find_players_by_name(player_name)
670
+ if matches:
671
+ player = matches[0]
672
+ player_id = player.get("id")
673
+
674
+ if not player:
675
+ return {"error": f"Player not found: ID={player_id}, name={player_name}"}
676
+
677
+ # Prepare result with basic player info
678
+ result = {
679
+ "player_id": player.get("id"),
680
+ "name": player.get("name"),
681
+ "web_name": player.get("web_name"),
682
+ "team": player.get("team"),
683
+ "team_short": player.get("team_short"),
684
+ "position": player.get("position"),
685
+ "price": player.get("price"),
686
+ "season_stats": {
687
+ "total_points": player.get("points"),
688
+ "points_per_game": player.get("points_per_game"),
689
+ "minutes": player.get("minutes"),
690
+ "goals": player.get("goals"),
691
+ "assists": player.get("assists"),
692
+ "clean_sheets": player.get("clean_sheets"),
693
+ "bonus": player.get("bonus"),
694
+ "form": player.get("form"),
695
+ },
696
+ "ownership": {
697
+ "selected_by_percent": player.get("selected_by_percent"),
698
+ "transfers_in_event": player.get("transfers_in_event"),
699
+ "transfers_out_event": player.get("transfers_out_event"),
700
+ },
701
+ "status": {
702
+ "status": "available" if player.get("status") == "a" else "unavailable",
703
+ "news": player.get("news"),
704
+ "chance_of_playing_next_round": player.get("chance_of_playing_next_round"),
705
+ },
706
+ }
707
+
708
+ # Add expected stats if available
709
+ if "expected_goals" in player:
710
+ result["expected_stats"] = {
711
+ "expected_goals": player.get("expected_goals"),
712
+ "expected_assists": player.get("expected_assists"),
713
+ "expected_goal_involvements": player.get("expected_goal_involvements"),
714
+ "expected_goals_conceded": player.get("expected_goals_conceded"),
715
+ }
716
+
717
+ # Add advanced metrics
718
+ result["advanced_metrics"] = {
719
+ "influence": player.get("influence"),
720
+ "creativity": player.get("creativity"),
721
+ "threat": player.get("threat"),
722
+ "ict_index": player.get("ict_index"),
723
+ "bps": player.get("bps"),
724
+ }
725
+
726
+ # Determine and validate gameweek range
727
+ # Convert Optional[int] to int with defaults
728
+ start_gw: int = 1 if start_gameweek is None else max(1, start_gameweek)
729
+ end_gw: int = current_gw if end_gameweek is None else min(current_gw, end_gameweek)
730
+
731
+ # Ensure start <= end
732
+ start_gw = min(start_gw, end_gw)
733
+
734
+ # Set the validated values as int (not Optional[int])
735
+ start_gameweek = start_gw
736
+ end_gameweek = end_gw
737
+
738
+ # Include gameweek history if requested
739
+ if include_history and "history" in player:
740
+ # Filter history by gameweek range
741
+ filtered_history = [
742
+ gw
743
+ for gw in player.get("history", [])
744
+ if start_gameweek <= gw.get("round", 0) <= end_gameweek
745
+ ]
746
+
747
+ # Get detailed gameweek history
748
+ player_id_value = player.get("id")
749
+ if player_id_value is not None:
750
+ gw_count = max(1, end_gameweek - start_gameweek + 1)
751
+ gameweek_history = get_player_gameweek_history([player_id_value], gw_count)
752
+ else:
753
+ gameweek_history = None
754
+
755
+ # Combine data
756
+ history_data = filtered_history
757
+
758
+ if gameweek_history and "players" in gameweek_history:
759
+ player_id_str = str(player.get("id", ""))
760
+ if player_id_str in gameweek_history["players"]:
761
+ detailed_history = gameweek_history["players"][player_id_str]
762
+
763
+ # Enrich with additional stats if available
764
+ for gw_data in history_data:
765
+ gw_num = gw_data.get("round")
766
+ # Find matching detailed gameweek
767
+ matching_detailed = next(
768
+ (
769
+ gw
770
+ for gw in detailed_history
771
+ if gw.get("round") == gw_num or gw.get("gameweek") == gw_num
772
+ ),
773
+ None,
774
+ )
775
+
776
+ if matching_detailed:
777
+ for key, value in matching_detailed.items():
778
+ # Don't overwrite existing keys
779
+ if key not in gw_data:
780
+ gw_data[key] = value
781
+
782
+ # Add summary stats for the filtered period
783
+ period_stats = {}
784
+ if history_data:
785
+ # Calculate sums
786
+ minutes = sum(gw.get("minutes", 0) for gw in history_data)
787
+ points = sum(gw.get("total_points", 0) for gw in history_data)
788
+ goals = sum(gw.get("goals_scored", 0) for gw in history_data)
789
+ assists = sum(gw.get("assists", 0) for gw in history_data)
790
+ bonus = sum(gw.get("bonus", 0) for gw in history_data)
791
+ clean_sheets = sum(gw.get("clean_sheets", 0) for gw in history_data)
792
+
793
+ # Calculate averages
794
+ games_played = len(history_data)
795
+ games_started = sum(1 for gw in history_data if gw.get("minutes", 0) >= 60)
796
+ points_per_game = points / games_played if games_played > 0 else 0
797
+
798
+ period_stats = {
799
+ "gameweeks_analyzed": games_played,
800
+ "games_started": games_started,
801
+ "minutes": minutes,
802
+ "total_points": points,
803
+ "points_per_game": round(points_per_game, 1),
804
+ "goals": goals,
805
+ "assists": assists,
806
+ "goal_involvements": goals + assists,
807
+ "clean_sheets": clean_sheets,
808
+ "bonus": bonus,
809
+ }
810
+
811
+ result["gameweek_range"] = {
812
+ "start": start_gameweek,
813
+ "end": end_gameweek,
814
+ }
815
+
816
+ result["gameweek_history"] = history_data
817
+ result["period_stats"] = period_stats
818
+
819
+ # Include upcoming fixtures if requested
820
+ if include_fixtures and player_id is not None:
821
+ fixtures_data = get_player_fixtures(player_id, 5) # Next 5 fixtures
822
+
823
+ if fixtures_data:
824
+ result["upcoming_fixtures"] = fixtures_data
825
+
826
+ # Calculate average fixture difficulty
827
+ difficulty_values = [f.get("difficulty", 3) for f in fixtures_data]
828
+ avg_difficulty = (
829
+ sum(difficulty_values) / len(difficulty_values)
830
+ if difficulty_values
831
+ else 3
832
+ )
833
+
834
+ # Convert to a 1-10 scale where 10 is best (easiest fixtures)
835
+ fixture_score = (6 - avg_difficulty) * 2
836
+
837
+ result["fixture_analysis"] = {
838
+ "difficulty_score": round(fixture_score, 1),
839
+ "fixtures_analyzed": len(fixtures_data),
840
+ "home_matches": sum(
841
+ 1 for f in fixtures_data if f.get("location") == "home"
842
+ ),
843
+ "away_matches": sum(
844
+ 1 for f in fixtures_data if f.get("location") == "away"
845
+ ),
846
+ }
847
+
848
+ # Add fixture difficulty assessment
849
+ if "fixture_analysis" in result and isinstance(
850
+ result["fixture_analysis"], dict
851
+ ):
852
+ fixture_analysis = result["fixture_analysis"]
853
+ if fixture_score >= 8:
854
+ fixture_analysis["assessment"] = "Excellent fixtures"
855
+ elif fixture_score >= 6:
856
+ fixture_analysis["assessment"] = "Good fixtures"
857
+ elif fixture_score >= 4:
858
+ fixture_analysis["assessment"] = "Average fixtures"
859
+ else:
860
+ fixture_analysis["assessment"] = "Difficult fixtures"
861
+
862
+ return result
863
+
864
+
865
+ def search_players(
866
+ query: str, position: str | None = None, team: str | None = None, limit: int = 5
867
+ ) -> dict[str, Any]:
868
+ """
869
+ Search for players by name with optional filtering by position and team.
870
+
871
+ Args:
872
+ query: Player name or partial name to search for
873
+ position: Optional position filter (GKP, DEF, MID, FWD)
874
+ team: Optional team name filter
875
+ limit: Maximum number of results to return
876
+
877
+ Returns:
878
+ List of matching players with details
879
+ """
880
+ logger = logging.getLogger(__name__)
881
+ logger.info(f"Searching players: query={query}, position={position}, team={team}")
882
+
883
+ # Find players by name
884
+ matches = find_players_by_name(
885
+ query, limit=limit * 2
886
+ ) # Get more than needed for filtering
887
+
888
+ # Apply position filter if specified
889
+ if position and matches:
890
+ matches = [p for p in matches if p.get("position") == position.upper()]
891
+
892
+ # Apply team filter if specified
893
+ if team and matches:
894
+ matches = [
895
+ p
896
+ for p in matches
897
+ if team.lower() in p.get("team", "").lower()
898
+ or team.lower() in p.get("team_short", "").lower()
899
+ ]
900
+
901
+ # Limit results
902
+ matches = matches[:limit]
903
+
904
+ return {
905
+ "query": query,
906
+ "filters": {
907
+ "position": position,
908
+ "team": team,
909
+ },
910
+ "total_matches": len(matches),
911
+ "players": matches,
912
+ }
913
+
914
+
915
+ def get_teams_resource() -> list[dict[str, Any]]:
916
+ """
917
+ Format teams data for the MCP resource.
918
+
919
+ Returns:
920
+ Formatted teams data
921
+ """
922
+ # Get raw data from API
923
+ data = api.get_bootstrap_static()
924
+
925
+ # Format team data
926
+ teams = []
927
+ for team in data["teams"]:
928
+ team_data = {
929
+ "id": team["id"],
930
+ "name": team["name"],
931
+ "short_name": team["short_name"],
932
+ "code": team["code"],
933
+ # Strength ratings
934
+ "strength": team["strength"],
935
+ "strength_overall_home": team["strength_overall_home"],
936
+ "strength_overall_away": team["strength_overall_away"],
937
+ "strength_attack_home": team["strength_attack_home"],
938
+ "strength_attack_away": team["strength_attack_away"],
939
+ "strength_defence_home": team["strength_defence_home"],
940
+ "strength_defence_away": team["strength_defence_away"],
941
+ # Performance stats
942
+ "position": team["position"],
943
+ }
944
+
945
+ teams.append(team_data)
946
+
947
+ # Sort by position (league standing)
948
+ teams.sort(key=lambda t: t["position"])
949
+
950
+ return teams
951
+
952
+
953
+ def get_team_by_name(name: str) -> dict[str, Any] | None:
954
+ """
955
+ Get team data by name (full or partial match).
956
+
957
+ Args:
958
+ name: Team name to search for
959
+
960
+ Returns:
961
+ Team data or None if not found
962
+ """
963
+ teams = get_teams_resource()
964
+ name_lower = name.lower()
965
+
966
+ # Try exact match first
967
+ for team in teams:
968
+ if (
969
+ team["name"].lower() == name_lower
970
+ or team["short_name"].lower() == name_lower
971
+ ):
972
+ return team
973
+
974
+ # Then try partial match
975
+ for team in teams:
976
+ if (
977
+ name_lower in team["name"].lower()
978
+ or name_lower in team["short_name"].lower()
979
+ ):
980
+ return team
981
+
982
+ return None