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,546 @@
1
+ from typing import Any
2
+
3
+
4
+ def parse_league_standings(data: dict[str, Any]) -> dict[str, Any]:
5
+ """
6
+ Parse league standings data into a more usable format
7
+
8
+ Args:
9
+ data: Raw league data from the API
10
+
11
+ Returns:
12
+ Parsed league data
13
+ """
14
+ # Handle error responses
15
+ if "error" in data:
16
+ return data
17
+
18
+ # Parse league info
19
+ league_info = {
20
+ "id": data.get("league", {}).get("id"),
21
+ "name": data.get("league", {}).get("name"),
22
+ "created": data.get("league", {}).get("created"),
23
+ "type": "Public"
24
+ if data.get("league", {}).get("league_type") == "s"
25
+ else "Private",
26
+ "scoring": "Classic"
27
+ if data.get("league", {}).get("scoring") == "c"
28
+ else "Head-to-Head",
29
+ "admin_entry": data.get("league", {}).get("admin_entry"),
30
+ "start_event": data.get("league", {}).get("start_event"),
31
+ }
32
+
33
+ # Parse standings
34
+ standings = data.get("standings", {}).get("results", [])
35
+
36
+ # Get total count
37
+ total_count = len(standings)
38
+
39
+ # Format standings
40
+ formatted_standings = []
41
+ for standing in standings:
42
+ team = {
43
+ "id": standing.get("id"),
44
+ "team_id": standing.get("entry"),
45
+ "team_name": standing.get("entry_name"),
46
+ "manager_name": standing.get("player_name"),
47
+ "rank": standing.get("rank"),
48
+ "last_rank": standing.get("last_rank"),
49
+ "rank_change": standing.get("last_rank", 0) - standing.get("rank", 0)
50
+ if standing.get("last_rank") and standing.get("rank")
51
+ else 0,
52
+ "total_points": standing.get("total"),
53
+ "event_total": standing.get("event_total"),
54
+ }
55
+ formatted_standings.append(team)
56
+
57
+ response = {
58
+ "league_info": league_info,
59
+ "standings": formatted_standings[:25], # Limit to top 25 teams
60
+ "total_teams": total_count,
61
+ }
62
+
63
+ if len(formatted_standings) > 25:
64
+ response["disclaimers"] = ["Limited to top 25 teams"]
65
+
66
+ return response
67
+
68
+
69
+ def get_teams_historical_data(
70
+ team_ids: list[int], api, start_gw: int | None = None, end_gw: int | None = None
71
+ ) -> dict[str, Any]:
72
+ """Get historical data for multiple teams"""
73
+ results = {}
74
+ errors = {}
75
+
76
+ # Validate and process gameweek range
77
+ try:
78
+ if end_gw is None or end_gw == "current":
79
+ current_gw_data = api.get_current_gameweek()
80
+ current_gw = current_gw_data.get("id", 38)
81
+ end_gw = current_gw
82
+ elif isinstance(end_gw, str) and end_gw.startswith("current-"):
83
+ current_gw_data = api.get_current_gameweek()
84
+ current_gw = current_gw_data.get("id", 38)
85
+ offset = int(end_gw.split("-")[1])
86
+ end_gw = max(1, current_gw - offset)
87
+
88
+ if start_gw is None:
89
+ start_gw = 1
90
+ elif isinstance(start_gw, str) and start_gw.startswith("current-"):
91
+ current_gw_data = api.get_current_gameweek()
92
+ current_gw = current_gw_data.get("id", 38)
93
+ offset = int(start_gw.split("-")[1])
94
+ start_gw = max(1, current_gw - offset)
95
+
96
+ start_gw = int(start_gw) if start_gw is not None else 1
97
+ end_gw = int(end_gw) if end_gw is not None else 38
98
+
99
+ start_gw = max(start_gw, 1)
100
+ end_gw = min(end_gw, 38)
101
+ if start_gw > end_gw:
102
+ start_gw, end_gw = end_gw, start_gw
103
+
104
+ except Exception as e:
105
+ return {
106
+ "error": f"Invalid gameweek range: {str(e)}",
107
+ "suggestion": "Use numeric values or 'current'/'current-N' format",
108
+ }
109
+
110
+ # Get history data for each team
111
+ for team_id in team_ids:
112
+ try:
113
+ # Use the API class to make the request
114
+ history_data = api._make_request(f"entry/{team_id}/history/")
115
+
116
+ if "current" in history_data:
117
+ current = [
118
+ gw
119
+ for gw in history_data["current"]
120
+ if start_gw <= gw.get("event", 0) <= end_gw
121
+ ]
122
+
123
+ filtered_data = {
124
+ "current": current,
125
+ "past": history_data.get("past", []),
126
+ "chips": history_data.get("chips", []),
127
+ }
128
+
129
+ results[team_id] = filtered_data
130
+ else:
131
+ errors[team_id] = "No historical data found"
132
+
133
+ except Exception as e:
134
+ errors[team_id] = str(e)
135
+
136
+ return {
137
+ "teams_data": results,
138
+ "errors": errors,
139
+ "gameweek_range": {"start": start_gw, "end": end_gw},
140
+ "success_rate": len(results) / len(team_ids) if team_ids else 0,
141
+ }
142
+
143
+
144
+ def _get_league_standings(league_id: int, api) -> dict[str, Any]:
145
+ """Get standings for a specified FPL league"""
146
+ try:
147
+ # Use the API class to make the request
148
+ data = api._make_request(f"leagues-classic/{league_id}/standings/")
149
+ except Exception as e:
150
+ return {"error": f"API request failed: {str(e)}"}
151
+
152
+ # Import parse_league_standings from the user's api.py or define a simple version here
153
+ # For now, we'll define a simple version
154
+ if "error" in data:
155
+ return data
156
+
157
+ league_info = {
158
+ "id": data.get("league", {}).get("id"),
159
+ "name": data.get("league", {}).get("name"),
160
+ "created": data.get("league", {}).get("created"),
161
+ "type": "Public"
162
+ if data.get("league", {}).get("league_type") == "s"
163
+ else "Private",
164
+ "scoring": "Classic"
165
+ if data.get("league", {}).get("scoring") == "c"
166
+ else "Head-to-Head",
167
+ "admin_entry": data.get("league", {}).get("admin_entry"),
168
+ "start_event": data.get("league", {}).get("start_event"),
169
+ }
170
+
171
+ standings = data.get("standings", {}).get("results", [])
172
+ total_count = len(standings)
173
+
174
+ formatted_standings = []
175
+ for standing in standings:
176
+ team = {
177
+ "id": standing.get("id"),
178
+ "team_id": standing.get("entry"),
179
+ "team_name": standing.get("entry_name"),
180
+ "manager_name": standing.get("player_name"),
181
+ "rank": standing.get("rank"),
182
+ "last_rank": standing.get("last_rank"),
183
+ "rank_change": standing.get("last_rank", 0) - standing.get("rank", 0)
184
+ if standing.get("last_rank") and standing.get("rank")
185
+ else 0,
186
+ "total_points": standing.get("total"),
187
+ "event_total": standing.get("event_total"),
188
+ }
189
+ formatted_standings.append(team)
190
+
191
+ response_data = {
192
+ "league_info": league_info,
193
+ "standings": formatted_standings[:25],
194
+ "total_teams": total_count,
195
+ }
196
+
197
+ if len(formatted_standings) > 25:
198
+ response_data["disclaimers"] = ["Limited to top 25 teams"]
199
+
200
+ return response_data
201
+
202
+
203
+ def _get_league_historical_performance(
204
+ league_id: int,
205
+ api, # noqa: F821
206
+ start_gw: int | None = None,
207
+ end_gw: int | None = None,
208
+ ) -> dict[str, Any]:
209
+ """Get historical performance data for teams in a league"""
210
+ league_data = _get_league_standings(league_id, api)
211
+
212
+ if "error" in league_data:
213
+ return league_data
214
+
215
+ if "standings" in league_data and len(league_data["standings"]) > 25:
216
+ league_data["standings"] = league_data["standings"][:25]
217
+ league_data["limited_to_top"] = 25
218
+
219
+ team_ids = [team["team_id"] for team in league_data["standings"]]
220
+
221
+ historical_data = get_teams_historical_data(team_ids, api, start_gw, end_gw)
222
+
223
+ if "error" in historical_data:
224
+ return historical_data
225
+
226
+ teams_data = historical_data["teams_data"]
227
+ gameweek_range = historical_data["gameweek_range"]
228
+
229
+ gameweeks = list(range(gameweek_range["start"], gameweek_range["end"] + 1))
230
+
231
+ series = []
232
+ for team in league_data["standings"]:
233
+ team_id = team["team_id"]
234
+
235
+ if team_id not in teams_data:
236
+ continue
237
+
238
+ current = teams_data[team_id].get("current", [])
239
+
240
+ points_series = []
241
+ rank_series = []
242
+ value_series = []
243
+
244
+ for gw in gameweeks:
245
+ gw_data = next((g for g in current if g.get("event") == gw), None)
246
+
247
+ if gw_data:
248
+ points_series.append(gw_data.get("points", 0))
249
+ rank_series.append(gw_data.get("overall_rank", 0))
250
+ value_series.append(
251
+ gw_data.get("value", 0) / 10.0 if gw_data.get("value") else 0
252
+ )
253
+ else:
254
+ points_series.append(0)
255
+ rank_series.append(0)
256
+ value_series.append(0)
257
+
258
+ series.append(
259
+ {
260
+ "team_id": team_id,
261
+ "name": team["team_name"],
262
+ "manager": team["manager_name"],
263
+ "points_series": points_series,
264
+ "rank_series": rank_series,
265
+ "value_series": value_series,
266
+ "current_rank": team["rank"],
267
+ "total_points": team["total_points"],
268
+ }
269
+ )
270
+
271
+ gameweek_winners = {}
272
+ for gw_index, gw in enumerate(gameweeks):
273
+ max_points = 0
274
+ winner = None
275
+
276
+ for team in series:
277
+ points = team["points_series"][gw_index]
278
+ if points > max_points:
279
+ max_points = points
280
+ winner = {
281
+ "team_id": team["team_id"],
282
+ "name": team["name"],
283
+ "points": points,
284
+ }
285
+
286
+ if winner:
287
+ gameweek_winners[str(gw)] = winner
288
+
289
+ for team in series:
290
+ rank_variance = 0
291
+ valid_ranks = [r for r in team["rank_series"] if r > 0]
292
+
293
+ if valid_ranks:
294
+ mean_rank = sum(valid_ranks) / len(valid_ranks)
295
+ rank_variance = sum((r - mean_rank) ** 2 for r in valid_ranks) / len(
296
+ valid_ranks
297
+ )
298
+ consistency_score = 10.0 - min(10.0, (rank_variance / 1000000) * 10)
299
+ team["consistency_score"] = round(consistency_score, 1)
300
+ else:
301
+ team["consistency_score"] = 0
302
+
303
+ return {
304
+ "league_info": league_data["league_info"],
305
+ "gameweeks": gameweeks,
306
+ "teams": series,
307
+ "gameweek_winners": gameweek_winners,
308
+ "errors": historical_data["errors"],
309
+ "success_rate": historical_data["success_rate"],
310
+ }
311
+
312
+
313
+ def _get_league_team_composition(
314
+ league_id: int,
315
+ api, # noqa: F821
316
+ gameweek: int | None = None,
317
+ ) -> dict[str, Any]:
318
+ """Get team composition analysis for a league"""
319
+ league_data = _get_league_standings(league_id, api)
320
+
321
+ if "error" in league_data:
322
+ return league_data
323
+
324
+ if "standings" in league_data and len(league_data["standings"]) > 25:
325
+ league_data["standings"] = league_data["standings"][:25]
326
+ league_data["limited_to_top"] = 25
327
+
328
+ team_ids = [team["team_id"] for team in league_data["standings"]]
329
+
330
+ if gameweek is None:
331
+ current_gw_data = api.get_current_gameweek()
332
+ gameweek = current_gw_data.get("id", 1)
333
+
334
+ try:
335
+ gameweek = int(gameweek) if gameweek is not None else 1
336
+ except (ValueError, TypeError):
337
+ return {"error": f"Invalid gameweek value: {gameweek}"}
338
+
339
+ static_data = api.get_bootstrap_static()
340
+ all_players = static_data.get("elements", [])
341
+
342
+ teams_map = {t["id"]: t for t in static_data.get("teams", [])}
343
+ positions_map = {p["id"]: p for p in static_data.get("element_types", [])}
344
+
345
+ players_map = {}
346
+ for p in all_players:
347
+ player = dict(p)
348
+
349
+ team_id = player.get("team")
350
+ if team_id and team_id in teams_map:
351
+ player["team_short"] = teams_map[team_id].get("short_name")
352
+
353
+ position_id = player.get("element_type")
354
+ if position_id and position_id in positions_map:
355
+ player["position"] = positions_map[position_id].get("singular_name_short")
356
+
357
+ players_map[player["id"]] = player
358
+
359
+ teams_data = {}
360
+ errors = {}
361
+
362
+ for team_id in team_ids:
363
+ try:
364
+ # Use the API class to make the request
365
+ picks_data = api._make_request(f"entry/{team_id}/event/{gameweek}/picks/")
366
+ teams_data[team_id] = picks_data
367
+
368
+ except Exception as e:
369
+ errors[team_id] = str(e)
370
+
371
+ if not teams_data:
372
+ return {
373
+ "error": "Failed to retrieve team data for any teams in the league",
374
+ "errors": errors,
375
+ }
376
+
377
+ player_ownership = {}
378
+ captain_picks = {}
379
+ vice_captain_picks = {}
380
+ position_distribution = {"GKP": {}, "DEF": {}, "MID": {}, "FWD": {}}
381
+ team_values = {}
382
+
383
+ for team_id, team_data in teams_data.items():
384
+ team_info = next(
385
+ (t for t in league_data["standings"] if t["team_id"] == team_id), None
386
+ )
387
+ if not team_info:
388
+ continue
389
+
390
+ picks = team_data.get("picks", [])
391
+ entry_history = team_data.get("entry_history", {})
392
+
393
+ team_values[team_id] = {
394
+ "team_name": team_info["team_name"],
395
+ "manager_name": team_info["manager_name"],
396
+ "bank": entry_history.get("bank", 0) / 10.0 if entry_history else 0,
397
+ "value": entry_history.get("value", 0) / 10.0 if entry_history else 0,
398
+ }
399
+
400
+ for pick in picks:
401
+ player_id = pick.get("element")
402
+ if not player_id:
403
+ continue
404
+
405
+ player_data = players_map.get(player_id, {})
406
+ if not player_data:
407
+ continue
408
+
409
+ if player_id not in player_ownership:
410
+ position = player_data.get("position", "UNK")
411
+ full_name = f"{player_data.get('first_name', '')} {player_data.get('second_name', '')}"
412
+ player_ownership[player_id] = {
413
+ "id": player_id,
414
+ "name": player_data.get("web_name", "Unknown"),
415
+ "full_name": full_name.strip() or "Unknown",
416
+ "position": position,
417
+ "team": player_data.get("team_short", "UNK"),
418
+ "price": player_data.get("now_cost", 0) / 10.0
419
+ if player_data.get("now_cost")
420
+ else 0,
421
+ "form": float(player_data.get("form", "0.0"))
422
+ if player_data.get("form")
423
+ else 0,
424
+ "total_points": player_data.get("total_points", 0),
425
+ "points_per_game": float(player_data.get("points_per_game", "0.0"))
426
+ if player_data.get("points_per_game")
427
+ else 0,
428
+ "ownership_count": 0,
429
+ "ownership_percent": 0,
430
+ "captain_count": 0,
431
+ "vice_captain_count": 0,
432
+ "teams": [],
433
+ }
434
+
435
+ player_ownership[player_id]["ownership_count"] += 1
436
+ player_ownership[player_id]["teams"].append(
437
+ {
438
+ "team_id": team_id,
439
+ "team_name": team_info["team_name"],
440
+ "manager_name": team_info["manager_name"],
441
+ "is_captain": pick.get("is_captain", False),
442
+ "is_vice_captain": pick.get("is_vice_captain", False),
443
+ "multiplier": pick.get("multiplier", 0),
444
+ "position": pick.get("position", 0),
445
+ }
446
+ )
447
+
448
+ if pick.get("is_captain", False):
449
+ if player_id not in captain_picks:
450
+ captain_picks[player_id] = 0
451
+ captain_picks[player_id] += 1
452
+ player_ownership[player_id]["captain_count"] += 1
453
+
454
+ if pick.get("is_vice_captain", False):
455
+ if player_id not in vice_captain_picks:
456
+ vice_captain_picks[player_id] = 0
457
+ vice_captain_picks[player_id] += 1
458
+ player_ownership[player_id]["vice_captain_count"] += 1
459
+
460
+ position = player_data.get("position", "UNK")
461
+ if position in position_distribution:
462
+ if player_id not in position_distribution[position]:
463
+ position_distribution[position][player_id] = 0
464
+ position_distribution[position][player_id] += 1
465
+
466
+ team_count = len(teams_data)
467
+ for player_id, player in player_ownership.items():
468
+ player["ownership_percent"] = round(
469
+ (player["ownership_count"] / team_count) * 100, 1
470
+ )
471
+
472
+ players_by_ownership = sorted(
473
+ player_ownership.values(),
474
+ key=lambda p: (p["ownership_percent"], p["total_points"]),
475
+ reverse=True,
476
+ )
477
+
478
+ template_threshold = 30.0
479
+ differential_threshold = 10.0
480
+
481
+ template_players = [
482
+ p for p in players_by_ownership if p["ownership_percent"] > template_threshold
483
+ ]
484
+ differential_players = [
485
+ p
486
+ for p in players_by_ownership
487
+ if 0 < p["ownership_percent"] < differential_threshold
488
+ ]
489
+
490
+ position_order = {"GKP": 1, "DEF": 2, "MID": 3, "FWD": 4, "UNK": 5}
491
+ template_players.sort(
492
+ key=lambda p: (position_order.get(p["position"], 5), -p["ownership_percent"])
493
+ )
494
+ differential_players.sort(
495
+ key=lambda p: (position_order.get(p["position"], 5), -p["total_points"])
496
+ )
497
+
498
+ captain_data = []
499
+ for player_id, count in sorted(
500
+ captain_picks.items(), key=lambda x: x[1], reverse=True
501
+ ):
502
+ if player_id in player_ownership:
503
+ player = player_ownership[player_id]
504
+ captain_data.append(
505
+ {
506
+ "id": player_id,
507
+ "name": player["name"],
508
+ "count": count,
509
+ "percent": round((count / team_count) * 100, 1),
510
+ "position": player["position"],
511
+ "team": player["team"],
512
+ "points": player["total_points"],
513
+ }
514
+ )
515
+
516
+ value_stats = {}
517
+ for team_id, value_data in team_values.items():
518
+ value_stats[team_id] = {
519
+ "team_name": value_data["team_name"],
520
+ "manager_name": value_data["manager_name"],
521
+ "bank": value_data["bank"],
522
+ "team_value": value_data["value"],
523
+ }
524
+
525
+ sorted_value_stats = sorted(
526
+ value_stats.values(), key=lambda v: v["team_value"], reverse=True
527
+ )
528
+
529
+ PLAYER_LIMIT = 25
530
+
531
+ return {
532
+ "league_info": league_data["league_info"],
533
+ "gameweek": gameweek,
534
+ "teams_analyzed": team_count,
535
+ "player_ownership": {
536
+ "all_players": players_by_ownership[:PLAYER_LIMIT],
537
+ "template_players": template_players[:PLAYER_LIMIT],
538
+ "differential_players": differential_players[:PLAYER_LIMIT],
539
+ "captain_picks": captain_data[:PLAYER_LIMIT]
540
+ if len(captain_data) > PLAYER_LIMIT
541
+ else captain_data,
542
+ },
543
+ "team_values": sorted_value_stats,
544
+ "errors": errors,
545
+ "success_rate": len(teams_data) / len(team_ids) if team_ids else 0,
546
+ }
@@ -0,0 +1,68 @@
1
+ """Utilities for normalizing position terms in FPL context."""
2
+
3
+
4
+ # Comprehensive position mapping dictionary
5
+ POSITION_MAPPINGS = {
6
+ # Standard FPL codes
7
+ "GKP": "GKP",
8
+ "DEF": "DEF",
9
+ "MID": "MID",
10
+ "FWD": "FWD",
11
+ # Common variations - singular
12
+ "goalkeeper": "GKP",
13
+ "goalie": "GKP",
14
+ "keeper": "GKP",
15
+ "defender": "DEF",
16
+ "fullback": "DEF",
17
+ "center-back": "DEF",
18
+ "cb": "DEF",
19
+ "midfielder": "MID",
20
+ "mid": "MID",
21
+ "winger": "MID",
22
+ "forward": "FWD",
23
+ "striker": "FWD",
24
+ "attacker": "FWD",
25
+ "st": "FWD",
26
+ # Common variations - plural
27
+ "goalkeepers": "GKP",
28
+ "goalies": "GKP",
29
+ "keepers": "GKP",
30
+ "defenders": "DEF",
31
+ "fullbacks": "DEF",
32
+ "center-backs": "DEF",
33
+ "midfielders": "MID",
34
+ "mids": "MID",
35
+ "wingers": "MID",
36
+ "forwards": "FWD",
37
+ "strikers": "FWD",
38
+ "attackers": "FWD",
39
+ }
40
+
41
+
42
+ def normalize_position(position_term: str | None) -> str | None:
43
+ """Convert various position terms to standard FPL position codes.
44
+
45
+ Args:
46
+ position_term: Position term to normalize (can be None)
47
+
48
+ Returns:
49
+ Normalized FPL position code or None if input is None
50
+ """
51
+ if not position_term:
52
+ return None
53
+
54
+ # Convert to lowercase for case-insensitive matching
55
+ normalized = position_term.lower().strip()
56
+
57
+ # Try direct match in mapping (case insensitive)
58
+ for term, code in POSITION_MAPPINGS.items():
59
+ if normalized == term.lower():
60
+ return code
61
+
62
+ # Try partial matches
63
+ for term, code in POSITION_MAPPINGS.items():
64
+ if normalized in term.lower() or term.lower() in normalized:
65
+ return code
66
+
67
+ # No match found, return original
68
+ return position_term
@@ -0,0 +1,25 @@
1
+ # GhostContentApp MCP Server
2
+
3
+ An MCP Server for the GhostContentApp API.
4
+
5
+ ## 🛠️ Tool List
6
+
7
+ This is automatically generated from OpenAPI schema for the GhostContentApp API.
8
+
9
+
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `browse_posts` | Retrieves and browses posts from a data source based on provided parameters. |
13
+ | `read_post_by_id` | Retrieves a post by its ID, optionally including additional data or specific fields. |
14
+ | `read_post_by_slug` | Retrieves a post by its slug, with optional parameters to specify included data, select specific fields, or request particular data formats. |
15
+ | `browse_authors` | Browse authors using various filtering and pagination options. |
16
+ | `read_author_by_id` | Read an author from the database by their unique ID. |
17
+ | `read_author_by_slug` | Retrieve an author's information by their slug. |
18
+ | `browse_tags` | Browse and retrieve tags based on specified parameters. |
19
+ | `read_tag_by_id` | Retrieves a tag's details by its unique identifier, optionally filtering by included and field sets. |
20
+ | `read_tag_by_slug` | Retrieve tag information identified by a unique slug, with optional inclusion of related data and selective fields. |
21
+ | `browse_pages` | Retrieves a list of pages using optional filtering, pagination, and formatting parameters. |
22
+ | `read_page_by_id` | Read a page by ID, allowing for optional inclusion of additional data, specific fields, and formats. |
23
+ | `read_page_by_slug` | Retrieve a page's content and metadata by its slug identifier, optionally including related data, specific fields, and content formats. |
24
+ | `browse_tiers` | Browse tiers based on optional filters and pagination. |
25
+ | `browse_settings` | Fetches site settings by making a GET request to the settings endpoint. |
@@ -0,0 +1 @@
1
+ from .app import GhostContentApp