universal-mcp-applications 0.1.30rc1__py3-none-any.whl → 0.1.36rc1__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.
Potentially problematic release.
This version of universal-mcp-applications might be problematic. Click here for more details.
- universal_mcp/applications/ahrefs/app.py +52 -198
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +111 -464
- universal_mcp/applications/asana/app.py +417 -1567
- universal_mcp/applications/aws_s3/app.py +33 -100
- universal_mcp/applications/bill/app.py +546 -1957
- universal_mcp/applications/box/app.py +1068 -3981
- universal_mcp/applications/braze/app.py +364 -1430
- universal_mcp/applications/browser_use/app.py +2 -8
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +61 -200
- universal_mcp/applications/canva/app.py +45 -110
- universal_mcp/applications/clickup/app.py +207 -674
- universal_mcp/applications/coda/app.py +146 -426
- universal_mcp/applications/confluence/app.py +310 -1098
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +28 -107
- universal_mcp/applications/dialpad/app.py +283 -756
- universal_mcp/applications/digitalocean/app.py +1766 -5777
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/app.py +6 -17
- universal_mcp/applications/falai/app.py +23 -100
- universal_mcp/applications/figma/app.py +53 -137
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +51 -152
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +91 -528
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +52 -161
- universal_mcp/applications/github/app.py +19 -56
- universal_mcp/applications/gong/app.py +88 -248
- universal_mcp/applications/google_calendar/app.py +16 -68
- universal_mcp/applications/google_docs/app.py +88 -188
- universal_mcp/applications/google_drive/app.py +140 -462
- universal_mcp/applications/google_gemini/app.py +12 -64
- universal_mcp/applications/google_mail/app.py +28 -157
- universal_mcp/applications/google_searchconsole/app.py +15 -48
- universal_mcp/applications/google_sheet/app.py +101 -578
- universal_mcp/applications/google_sheet/helper.py +10 -37
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +44 -122
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
- universal_mcp/applications/hubspot/app.py +23 -87
- universal_mcp/applications/jira/app.py +2071 -7986
- universal_mcp/applications/klaviyo/app.py +494 -1376
- universal_mcp/applications/linkedin/README.md +23 -4
- universal_mcp/applications/linkedin/app.py +392 -212
- universal_mcp/applications/mailchimp/app.py +450 -1605
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +217 -699
- universal_mcp/applications/ms_teams/app.py +64 -186
- universal_mcp/applications/neon/app.py +86 -192
- universal_mcp/applications/notion/app.py +21 -36
- universal_mcp/applications/onedrive/app.py +14 -36
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/app.py +16 -76
- universal_mcp/applications/perplexity/app.py +4 -19
- universal_mcp/applications/pipedrive/app.py +832 -3142
- universal_mcp/applications/posthog/app.py +163 -432
- universal_mcp/applications/reddit/app.py +40 -139
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +14 -41
- universal_mcp/applications/rocketlane/app.py +221 -934
- universal_mcp/applications/scraper/README.md +7 -4
- universal_mcp/applications/scraper/app.py +280 -93
- universal_mcp/applications/semanticscholar/app.py +22 -64
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +512 -1262
- universal_mcp/applications/sentry/app.py +271 -906
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +15 -37
- universal_mcp/applications/shopify/app.py +1551 -4287
- universal_mcp/applications/shortcut/app.py +155 -417
- universal_mcp/applications/slack/app.py +50 -101
- universal_mcp/applications/spotify/app.py +126 -325
- universal_mcp/applications/supabase/app.py +104 -213
- universal_mcp/applications/tavily/app.py +1 -1
- universal_mcp/applications/trello/app.py +693 -2656
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +70 -283
- universal_mcp/applications/wrike/app.py +45 -118
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +75 -261
- universal_mcp/applications/zenquotes/app.py +2 -2
- {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/METADATA +2 -2
- {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/RECORD +105 -106
- universal_mcp/applications/scraper/scraper_testers.py +0 -17
- {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.30rc1.dist-info → universal_mcp_applications-0.1.36rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import Any
|
|
3
|
-
|
|
4
3
|
from loguru import logger
|
|
5
4
|
from universal_mcp.applications.application import GraphQLApplication
|
|
6
5
|
from universal_mcp.exceptions import NotAuthorizedError
|
|
@@ -8,24 +7,14 @@ from universal_mcp.integrations import Integration
|
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class ContentfulApp(GraphQLApplication):
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
integration: Integration | None = None,
|
|
14
|
-
**kwargs: Any,
|
|
15
|
-
) -> None:
|
|
10
|
+
def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
|
|
16
11
|
self.space_id: str | None = None
|
|
17
12
|
self.environment_id: str = "master"
|
|
18
13
|
self._access_token: str | None = None
|
|
19
14
|
self._is_eu_customer: bool = False
|
|
20
15
|
self._credentials_loaded: bool = False
|
|
21
16
|
default_base_url = "https://graphql.contentful.com"
|
|
22
|
-
|
|
23
|
-
super().__init__(
|
|
24
|
-
name="contentful",
|
|
25
|
-
base_url=default_base_url,
|
|
26
|
-
integration=integration,
|
|
27
|
-
**kwargs,
|
|
28
|
-
)
|
|
17
|
+
super().__init__(name="contentful", base_url=default_base_url, integration=integration, **kwargs)
|
|
29
18
|
|
|
30
19
|
def _load_credentials_and_construct_url(self) -> bool:
|
|
31
20
|
"""
|
|
@@ -37,70 +26,39 @@ class ContentfulApp(GraphQLApplication):
|
|
|
37
26
|
"""
|
|
38
27
|
if self._credentials_loaded:
|
|
39
28
|
return True
|
|
40
|
-
|
|
41
29
|
logger.debug("Attempting to load Contentful credentials and construct URL...")
|
|
42
|
-
|
|
43
30
|
if not self.integration:
|
|
44
|
-
logger.error(
|
|
45
|
-
"Contentful integration not configured. Cannot load credentials or construct URL."
|
|
46
|
-
)
|
|
47
|
-
# Mark as 'loaded' to prevent retries, even though it failed.
|
|
31
|
+
logger.error("Contentful integration not configured. Cannot load credentials or construct URL.")
|
|
48
32
|
self._credentials_loaded = True
|
|
49
33
|
return False
|
|
50
|
-
|
|
51
34
|
try:
|
|
52
35
|
credentials = self.integration.get_credentials()
|
|
53
36
|
except NotAuthorizedError as e:
|
|
54
|
-
logger.error(
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
self._credentials_loaded = True # Prevent retries
|
|
37
|
+
logger.error(f"Authorization required or credentials unavailable for Contentful: {e.message}")
|
|
38
|
+
self._credentials_loaded = True
|
|
58
39
|
return False
|
|
59
40
|
except Exception as e:
|
|
60
|
-
logger.error(
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
self._credentials_loaded = True # Prevent retries
|
|
41
|
+
logger.error(f"Failed to get credentials from integration: {e}", exc_info=True)
|
|
42
|
+
self._credentials_loaded = True
|
|
64
43
|
return False
|
|
65
|
-
|
|
66
44
|
self.space_id = credentials.get("space_id")
|
|
67
|
-
self._access_token = credentials.get("access_token") or credentials.get(
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
self.environment_id = credentials.get(
|
|
71
|
-
"environment_id", "master"
|
|
72
|
-
) # Use default if not specified
|
|
73
|
-
self._is_eu_customer = credentials.get(
|
|
74
|
-
"is_eu_customer", False
|
|
75
|
-
) # Use default if not specified
|
|
76
|
-
|
|
45
|
+
self._access_token = credentials.get("access_token") or credentials.get("api_key")
|
|
46
|
+
self.environment_id = credentials.get("environment_id", "master")
|
|
47
|
+
self._is_eu_customer = credentials.get("is_eu_customer", False)
|
|
77
48
|
missing_creds = []
|
|
78
49
|
if not self.space_id:
|
|
79
50
|
missing_creds.append("'space_id'")
|
|
80
51
|
if not self._access_token:
|
|
81
52
|
missing_creds.append("'access_token' or 'api_key'")
|
|
82
|
-
|
|
83
53
|
if missing_creds:
|
|
84
|
-
logger.error(
|
|
85
|
-
|
|
86
|
-
"API calls will fail."
|
|
87
|
-
)
|
|
88
|
-
self._credentials_loaded = True # Prevent retries
|
|
54
|
+
logger.error(f"Missing required Contentful credentials in integration: {', '.join(missing_creds)}. API calls will fail.")
|
|
55
|
+
self._credentials_loaded = True
|
|
89
56
|
return False
|
|
90
|
-
|
|
91
|
-
contentful_api_domain = (
|
|
92
|
-
"graphql.eu.contentful.com"
|
|
93
|
-
if self._is_eu_customer
|
|
94
|
-
else "graphql.contentful.com"
|
|
95
|
-
)
|
|
57
|
+
contentful_api_domain = "graphql.eu.contentful.com" if self._is_eu_customer else "graphql.contentful.com"
|
|
96
58
|
self.base_url = f"https://{contentful_api_domain}/content/v1/spaces/{self.space_id}/environments/{self.environment_id}"
|
|
97
|
-
|
|
98
59
|
self._client = None
|
|
99
|
-
|
|
100
60
|
logger.info(
|
|
101
|
-
f"Contentful credentials loaded and URL constructed successfully. "
|
|
102
|
-
f"Space: '{self.space_id}', Environment: '{self.environment_id}'. "
|
|
103
|
-
f"Base URL: {self.base_url}"
|
|
61
|
+
f"Contentful credentials loaded and URL constructed successfully. Space: '{self.space_id}', Environment: '{self.environment_id}'. Base URL: {self.base_url}"
|
|
104
62
|
)
|
|
105
63
|
self._credentials_loaded = True
|
|
106
64
|
return True
|
|
@@ -114,7 +72,7 @@ class ContentfulApp(GraphQLApplication):
|
|
|
114
72
|
return ""
|
|
115
73
|
if len(parts) == 1 and parts[0] == s and s:
|
|
116
74
|
return s[0].lower() + s[1:] if len(s) > 1 else s.lower()
|
|
117
|
-
return parts[0].lower() + "".join(word.capitalize() for word in parts[1:])
|
|
75
|
+
return parts[0].lower() + "".join((word.capitalize() for word in parts[1:]))
|
|
118
76
|
|
|
119
77
|
@staticmethod
|
|
120
78
|
def _to_pascal_case(s: str) -> str:
|
|
@@ -125,57 +83,36 @@ class ContentfulApp(GraphQLApplication):
|
|
|
125
83
|
return ""
|
|
126
84
|
if len(parts) == 1 and parts[0] == s and s:
|
|
127
85
|
return s[0].upper() + s[1:] if len(s) > 1 else s.upper()
|
|
128
|
-
return "".join(word.capitalize() for word in parts)
|
|
86
|
+
return "".join((word.capitalize() for word in parts))
|
|
129
87
|
|
|
130
88
|
def _ensure_loaded(self) -> bool:
|
|
131
89
|
"""Internal helper to trigger lazy loading and check status."""
|
|
132
90
|
if not self._credentials_loaded:
|
|
133
91
|
return self._load_credentials_and_construct_url()
|
|
134
|
-
# If already marked as loaded, check if essential parts are actually set (robustness check)
|
|
135
92
|
return bool(self.base_url and self.space_id and self._access_token)
|
|
136
93
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def get_entry(
|
|
140
|
-
self,
|
|
141
|
-
content_type_id: str,
|
|
142
|
-
entry_id: str,
|
|
143
|
-
fields_to_select: str,
|
|
144
|
-
locale: str | None = None,
|
|
145
|
-
preview: bool = False,
|
|
94
|
+
async def get_entry(
|
|
95
|
+
self, content_type_id: str, entry_id: str, fields_to_select: str, locale: str | None = None, preview: bool = False
|
|
146
96
|
) -> dict[str, Any]:
|
|
147
97
|
"""
|
|
148
98
|
Fetches a single entry of a specified content type by its ID.
|
|
149
99
|
(See original docstring for details)
|
|
150
100
|
"""
|
|
151
101
|
if not self._ensure_loaded():
|
|
152
|
-
return {
|
|
153
|
-
"error": "Failed to initialize ContentfulApp. Check credentials and configuration."
|
|
154
|
-
}
|
|
155
|
-
|
|
102
|
+
return {"error": "Failed to initialize ContentfulApp. Check credentials and configuration."}
|
|
156
103
|
query_field = self._to_camel_case(content_type_id)
|
|
157
|
-
logger.debug(
|
|
158
|
-
|
|
159
|
-
)
|
|
160
|
-
query_gql = f"""
|
|
161
|
-
query GetEntryById($id: String!, $locale: String, $preview: Boolean) {{
|
|
162
|
-
{query_field}(id: $id, locale: $locale, preview: $preview) {{
|
|
163
|
-
{fields_to_select}
|
|
164
|
-
}}
|
|
165
|
-
}}
|
|
166
|
-
"""
|
|
104
|
+
logger.debug(f"Fetching entry for content_type_id='{content_type_id}' (query field='{query_field}'), entry_id='{entry_id}'")
|
|
105
|
+
query_gql = f"\n query GetEntryById($id: String!, $locale: String, $preview: Boolean) {{\n {query_field}(id: $id, locale: $locale, preview: $preview) {{\n {fields_to_select}\n }}\n }}\n "
|
|
167
106
|
variables: dict[str, Any] = {"id": entry_id, "preview": preview}
|
|
168
107
|
if locale:
|
|
169
108
|
variables["locale"] = locale
|
|
170
|
-
|
|
171
|
-
# Call the base class query method which uses the configured client
|
|
172
109
|
try:
|
|
173
110
|
return self.query(query_gql, variables=variables)
|
|
174
111
|
except Exception as e:
|
|
175
112
|
logger.error(f"Error executing get_entry query: {e}", exc_info=True)
|
|
176
113
|
return {"error": f"Failed to get entry: {e}"}
|
|
177
114
|
|
|
178
|
-
def get_entries_collection(
|
|
115
|
+
async def get_entries_collection(
|
|
179
116
|
self,
|
|
180
117
|
content_type_id: str,
|
|
181
118
|
fields_to_select_for_item: str,
|
|
@@ -191,28 +128,14 @@ class ContentfulApp(GraphQLApplication):
|
|
|
191
128
|
(See original docstring for details)
|
|
192
129
|
"""
|
|
193
130
|
if not self._ensure_loaded():
|
|
194
|
-
return {
|
|
195
|
-
"error": "Failed to initialize ContentfulApp. Check credentials and configuration."
|
|
196
|
-
}
|
|
197
|
-
|
|
131
|
+
return {"error": "Failed to initialize ContentfulApp. Check credentials and configuration."}
|
|
198
132
|
collection_field = self._to_camel_case(content_type_id) + "Collection"
|
|
199
133
|
filter_type = self._to_pascal_case(content_type_id) + "Filter"
|
|
200
134
|
order_enum_type = self._to_pascal_case(content_type_id) + "Order"
|
|
201
135
|
logger.debug(
|
|
202
|
-
f"Fetching collection for content_type_id='{content_type_id}' "
|
|
203
|
-
f"(collection field='{collection_field}', filter type='{filter_type}', order enum='{order_enum_type}')"
|
|
136
|
+
f"Fetching collection for content_type_id='{content_type_id}' (collection field='{collection_field}', filter type='{filter_type}', order enum='{order_enum_type}')"
|
|
204
137
|
)
|
|
205
|
-
query_gql = f""
|
|
206
|
-
query GetEntries(
|
|
207
|
-
$limit: Int, $skip: Int, $where: {filter_type}, $order: [{order_enum_type}!], $locale: String, $preview: Boolean
|
|
208
|
-
) {{
|
|
209
|
-
{collection_field}(
|
|
210
|
-
limit: $limit, skip: $skip, where: $where, order: $order, locale: $locale, preview: $preview
|
|
211
|
-
) {{
|
|
212
|
-
total skip limit items {{ {fields_to_select_for_item} }}
|
|
213
|
-
}}
|
|
214
|
-
}}
|
|
215
|
-
"""
|
|
138
|
+
query_gql = f"\n query GetEntries(\n $limit: Int, $skip: Int, $where: {filter_type}, $order: [{order_enum_type}!], $locale: String, $preview: Boolean\n ) {{\n {collection_field}(\n limit: $limit, skip: $skip, where: $where, order: $order, locale: $locale, preview: $preview\n ) {{\n total skip limit items {{ {fields_to_select_for_item} }}\n }}\n }}\n "
|
|
216
139
|
variables: dict[str, Any] = {"preview": preview}
|
|
217
140
|
if limit is not None:
|
|
218
141
|
variables["limit"] = limit
|
|
@@ -224,16 +147,13 @@ class ContentfulApp(GraphQLApplication):
|
|
|
224
147
|
variables["order"] = order
|
|
225
148
|
if locale:
|
|
226
149
|
variables["locale"] = locale
|
|
227
|
-
|
|
228
150
|
try:
|
|
229
151
|
return self.query(query_gql, variables=variables)
|
|
230
152
|
except Exception as e:
|
|
231
|
-
logger.error(
|
|
232
|
-
f"Error executing get_entries_collection query: {e}", exc_info=True
|
|
233
|
-
)
|
|
153
|
+
logger.error(f"Error executing get_entries_collection query: {e}", exc_info=True)
|
|
234
154
|
return {"error": f"Failed to get entries collection: {e}"}
|
|
235
155
|
|
|
236
|
-
def get_asset(
|
|
156
|
+
async def get_asset(
|
|
237
157
|
self,
|
|
238
158
|
asset_id: str,
|
|
239
159
|
fields_to_select: str = "sys { id } url title description fileName contentType size width height",
|
|
@@ -244,25 +164,17 @@ class ContentfulApp(GraphQLApplication):
|
|
|
244
164
|
(See original docstring for details)
|
|
245
165
|
"""
|
|
246
166
|
if not self._ensure_loaded():
|
|
247
|
-
return {
|
|
248
|
-
"error": "Failed to initialize ContentfulApp. Check credentials and configuration."
|
|
249
|
-
}
|
|
250
|
-
|
|
167
|
+
return {"error": "Failed to initialize ContentfulApp. Check credentials and configuration."}
|
|
251
168
|
logger.debug(f"Fetching asset_id='{asset_id}'")
|
|
252
|
-
query_gql = f""
|
|
253
|
-
query GetAssetById($id: String!, $preview: Boolean) {{
|
|
254
|
-
asset(id: $id, preview: $preview) {{ {fields_to_select} }}
|
|
255
|
-
}}
|
|
256
|
-
"""
|
|
169
|
+
query_gql = f"\n query GetAssetById($id: String!, $preview: Boolean) {{\n asset(id: $id, preview: $preview) {{ {fields_to_select} }}\n }}\n "
|
|
257
170
|
variables: dict[str, Any] = {"id": asset_id, "preview": preview}
|
|
258
|
-
|
|
259
171
|
try:
|
|
260
172
|
return self.query(query_gql, variables=variables)
|
|
261
173
|
except Exception as e:
|
|
262
174
|
logger.error(f"Error executing get_asset query: {e}", exc_info=True)
|
|
263
175
|
return {"error": f"Failed to get asset: {e}"}
|
|
264
176
|
|
|
265
|
-
def get_assets_collection(
|
|
177
|
+
async def get_assets_collection(
|
|
266
178
|
self,
|
|
267
179
|
fields_to_select_for_item: str = "sys { id } url title description fileName contentType size width height",
|
|
268
180
|
limit: int | None = None,
|
|
@@ -277,22 +189,9 @@ class ContentfulApp(GraphQLApplication):
|
|
|
277
189
|
(See original docstring for details)
|
|
278
190
|
"""
|
|
279
191
|
if not self._ensure_loaded():
|
|
280
|
-
return {
|
|
281
|
-
"error": "Failed to initialize ContentfulApp. Check credentials and configuration."
|
|
282
|
-
}
|
|
283
|
-
|
|
192
|
+
return {"error": "Failed to initialize ContentfulApp. Check credentials and configuration."}
|
|
284
193
|
logger.debug("Fetching assets collection")
|
|
285
|
-
query_gql = f""
|
|
286
|
-
query GetAssets(
|
|
287
|
-
$limit: Int, $skip: Int, $where: AssetFilter, $order: [AssetOrder!], $locale: String, $preview: Boolean
|
|
288
|
-
) {{
|
|
289
|
-
assetCollection(
|
|
290
|
-
limit: $limit, skip: $skip, where: $where, order: $order, locale: $locale, preview: $preview
|
|
291
|
-
) {{
|
|
292
|
-
total skip limit items {{ {fields_to_select_for_item} }}
|
|
293
|
-
}}
|
|
294
|
-
}}
|
|
295
|
-
"""
|
|
194
|
+
query_gql = f"\n query GetAssets(\n $limit: Int, $skip: Int, $where: AssetFilter, $order: [AssetOrder!], $locale: String, $preview: Boolean\n ) {{\n assetCollection(\n limit: $limit, skip: $skip, where: $where, order: $order, locale: $locale, preview: $preview\n ) {{\n total skip limit items {{ {fields_to_select_for_item} }}\n }}\n }}\n "
|
|
296
195
|
variables: dict[str, Any] = {"preview": preview}
|
|
297
196
|
if limit is not None:
|
|
298
197
|
variables["limit"] = limit
|
|
@@ -304,18 +203,13 @@ class ContentfulApp(GraphQLApplication):
|
|
|
304
203
|
variables["order"] = order
|
|
305
204
|
if locale:
|
|
306
205
|
variables["locale"] = locale
|
|
307
|
-
|
|
308
206
|
try:
|
|
309
207
|
return self.query(query_gql, variables=variables)
|
|
310
208
|
except Exception as e:
|
|
311
|
-
logger.error(
|
|
312
|
-
f"Error executing get_assets_collection query: {e}", exc_info=True
|
|
313
|
-
)
|
|
209
|
+
logger.error(f"Error executing get_assets_collection query: {e}", exc_info=True)
|
|
314
210
|
return {"error": f"Failed to get assets collection: {e}"}
|
|
315
211
|
|
|
316
|
-
def execute_graphql_query(
|
|
317
|
-
self, query_string: str, variables: dict[str, Any] | None = None
|
|
318
|
-
) -> dict[str, Any]:
|
|
212
|
+
async def execute_graphql_query(self, query_string: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
319
213
|
"""
|
|
320
214
|
Executes an arbitrary GraphQL query against the configured Contentful space/environment.
|
|
321
215
|
|
|
@@ -330,10 +224,7 @@ class ContentfulApp(GraphQLApplication):
|
|
|
330
224
|
important
|
|
331
225
|
"""
|
|
332
226
|
if not self._ensure_loaded():
|
|
333
|
-
return {
|
|
334
|
-
"error": "Failed to initialize ContentfulApp. Check credentials and configuration."
|
|
335
|
-
}
|
|
336
|
-
|
|
227
|
+
return {"error": "Failed to initialize ContentfulApp. Check credentials and configuration."}
|
|
337
228
|
logger.debug(f"Executing custom GraphQL query with variables: {variables}")
|
|
338
229
|
try:
|
|
339
230
|
return self.query(query_string, variables=variables)
|
|
@@ -343,10 +234,4 @@ class ContentfulApp(GraphQLApplication):
|
|
|
343
234
|
|
|
344
235
|
def list_tools(self) -> list[Callable]:
|
|
345
236
|
"""Returns a list of methods exposed as tools."""
|
|
346
|
-
return [
|
|
347
|
-
self.get_entry,
|
|
348
|
-
self.get_entries_collection,
|
|
349
|
-
self.get_asset,
|
|
350
|
-
self.get_assets_collection,
|
|
351
|
-
self.execute_graphql_query,
|
|
352
|
-
]
|
|
237
|
+
return [self.get_entry, self.get_entries_collection, self.get_asset, self.get_assets_collection, self.execute_graphql_query]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
-
|
|
3
2
|
from universal_mcp.applications.application import APIApplication
|
|
4
3
|
from universal_mcp.integrations import Integration
|
|
5
4
|
|
|
@@ -11,15 +10,9 @@ class CrustdataApp(APIApplication):
|
|
|
11
10
|
|
|
12
11
|
def _get_headers(self) -> dict[str, Any]:
|
|
13
12
|
api_key = self.integration.get_credentials().get("api_key")
|
|
14
|
-
return {
|
|
15
|
-
"Authorization": f"Token {api_key}",
|
|
16
|
-
"Content-Type": "application/json",
|
|
17
|
-
"Accept": "application/json",
|
|
18
|
-
}
|
|
13
|
+
return {"Authorization": f"Token {api_key}", "Content-Type": "application/json", "Accept": "application/json"}
|
|
19
14
|
|
|
20
|
-
def screen_companies(
|
|
21
|
-
self, metrics, filters, offset, count, sorts
|
|
22
|
-
) -> dict[str, Any]:
|
|
15
|
+
async def screen_companies(self, metrics, filters, offset, count, sorts) -> dict[str, Any]:
|
|
23
16
|
"""
|
|
24
17
|
Screens companies based on specified metrics, filters, sorting, and pagination parameters, and returns the result as a JSON-compatible dictionary.
|
|
25
18
|
|
|
@@ -50,13 +43,7 @@ class CrustdataApp(APIApplication):
|
|
|
50
43
|
raise ValueError("Missing required parameter 'count'")
|
|
51
44
|
if sorts is None:
|
|
52
45
|
raise ValueError("Missing required parameter 'sorts'")
|
|
53
|
-
request_body = {
|
|
54
|
-
"metrics": metrics,
|
|
55
|
-
"filters": filters,
|
|
56
|
-
"offset": offset,
|
|
57
|
-
"count": count,
|
|
58
|
-
"sorts": sorts,
|
|
59
|
-
}
|
|
46
|
+
request_body = {"metrics": metrics, "filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
60
47
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
61
48
|
url = f"{self.base_url}/screener/screen/"
|
|
62
49
|
query_params = {}
|
|
@@ -64,7 +51,7 @@ class CrustdataApp(APIApplication):
|
|
|
64
51
|
response.raise_for_status()
|
|
65
52
|
return response.json()
|
|
66
53
|
|
|
67
|
-
def get_headcount_timeseries(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
54
|
+
async def get_headcount_timeseries(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
68
55
|
"""
|
|
69
56
|
Retrieve headcount timeseries data from the data lab endpoint using the provided filters, pagination, and sorting options.
|
|
70
57
|
|
|
@@ -92,12 +79,7 @@ class CrustdataApp(APIApplication):
|
|
|
92
79
|
raise ValueError("Missing required parameter 'count'")
|
|
93
80
|
if sorts is None:
|
|
94
81
|
raise ValueError("Missing required parameter 'sorts'")
|
|
95
|
-
request_body = {
|
|
96
|
-
"filters": filters,
|
|
97
|
-
"offset": offset,
|
|
98
|
-
"count": count,
|
|
99
|
-
"sorts": sorts,
|
|
100
|
-
}
|
|
82
|
+
request_body = {"filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
101
83
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
102
84
|
url = f"{self.base_url}/data_lab/headcount_timeseries/"
|
|
103
85
|
query_params = {}
|
|
@@ -105,9 +87,7 @@ class CrustdataApp(APIApplication):
|
|
|
105
87
|
response.raise_for_status()
|
|
106
88
|
return response.json()
|
|
107
89
|
|
|
108
|
-
def get_headcount_by_facet_timeseries(
|
|
109
|
-
self, filters, offset, count, sorts
|
|
110
|
-
) -> dict[str, Any]:
|
|
90
|
+
async def get_headcount_by_facet_timeseries(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
111
91
|
"""
|
|
112
92
|
Retrieves headcount timeseries data aggregated by specified facets using provided filters and sorting options.
|
|
113
93
|
|
|
@@ -135,12 +115,7 @@ class CrustdataApp(APIApplication):
|
|
|
135
115
|
raise ValueError("Missing required parameter 'count'")
|
|
136
116
|
if sorts is None:
|
|
137
117
|
raise ValueError("Missing required parameter 'sorts'")
|
|
138
|
-
request_body = {
|
|
139
|
-
"filters": filters,
|
|
140
|
-
"offset": offset,
|
|
141
|
-
"count": count,
|
|
142
|
-
"sorts": sorts,
|
|
143
|
-
}
|
|
118
|
+
request_body = {"filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
144
119
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
145
120
|
url = f"{self.base_url}/data_lab/headcount_by_facet_timeseries/"
|
|
146
121
|
query_params = {}
|
|
@@ -148,9 +123,7 @@ class CrustdataApp(APIApplication):
|
|
|
148
123
|
response.raise_for_status()
|
|
149
124
|
return response.json()
|
|
150
125
|
|
|
151
|
-
def get_funding_milestone_timeseries(
|
|
152
|
-
self, filters, offset, count, sorts
|
|
153
|
-
) -> dict[str, Any]:
|
|
126
|
+
async def get_funding_milestone_timeseries(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
154
127
|
"""
|
|
155
128
|
Retrieves a time series of funding milestone data based on specified filters, pagination, and sorting options.
|
|
156
129
|
|
|
@@ -178,12 +151,7 @@ class CrustdataApp(APIApplication):
|
|
|
178
151
|
raise ValueError("Missing required parameter 'count'")
|
|
179
152
|
if sorts is None:
|
|
180
153
|
raise ValueError("Missing required parameter 'sorts'")
|
|
181
|
-
request_body = {
|
|
182
|
-
"filters": filters,
|
|
183
|
-
"offset": offset,
|
|
184
|
-
"count": count,
|
|
185
|
-
"sorts": sorts,
|
|
186
|
-
}
|
|
154
|
+
request_body = {"filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
187
155
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
188
156
|
url = f"{self.base_url}/data_lab/funding_milestone_timeseries/"
|
|
189
157
|
query_params = {}
|
|
@@ -191,9 +159,7 @@ class CrustdataApp(APIApplication):
|
|
|
191
159
|
response.raise_for_status()
|
|
192
160
|
return response.json()
|
|
193
161
|
|
|
194
|
-
def get_decision_makers(
|
|
195
|
-
self, filters, offset, count, sorts, decision_maker_titles
|
|
196
|
-
) -> dict[str, Any]:
|
|
162
|
+
async def get_decision_makers(self, filters, offset, count, sorts, decision_maker_titles) -> dict[str, Any]:
|
|
197
163
|
"""
|
|
198
164
|
Retrieves decision makers based on specified filters and parameters.
|
|
199
165
|
|
|
@@ -238,7 +204,7 @@ class CrustdataApp(APIApplication):
|
|
|
238
204
|
response.raise_for_status()
|
|
239
205
|
return response.json()
|
|
240
206
|
|
|
241
|
-
def get_web_traffic(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
207
|
+
async def get_web_traffic(self, filters, offset, count, sorts) -> dict[str, Any]:
|
|
242
208
|
"""
|
|
243
209
|
Retrieves web traffic data based on provided filters, pagination, and sorting criteria.
|
|
244
210
|
|
|
@@ -266,12 +232,7 @@ class CrustdataApp(APIApplication):
|
|
|
266
232
|
raise ValueError("Missing required parameter 'count'")
|
|
267
233
|
if sorts is None:
|
|
268
234
|
raise ValueError("Missing required parameter 'sorts'")
|
|
269
|
-
request_body = {
|
|
270
|
-
"filters": filters,
|
|
271
|
-
"offset": offset,
|
|
272
|
-
"count": count,
|
|
273
|
-
"sorts": sorts,
|
|
274
|
-
}
|
|
235
|
+
request_body = {"filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
275
236
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
276
237
|
url = f"{self.base_url}/data_lab/webtraffic/"
|
|
277
238
|
query_params = {}
|
|
@@ -279,7 +240,7 @@ class CrustdataApp(APIApplication):
|
|
|
279
240
|
response.raise_for_status()
|
|
280
241
|
return response.json()
|
|
281
242
|
|
|
282
|
-
def get_investor_portfolio(self, investor_name) -> dict[str, Any]:
|
|
243
|
+
async def get_investor_portfolio(self, investor_name) -> dict[str, Any]:
|
|
283
244
|
"""
|
|
284
245
|
Retrieves the investment portfolio information for a specified investor.
|
|
285
246
|
|
|
@@ -299,16 +260,12 @@ class CrustdataApp(APIApplication):
|
|
|
299
260
|
if investor_name is None:
|
|
300
261
|
raise ValueError("Missing required parameter 'investor_name'")
|
|
301
262
|
url = f"{self.base_url}/data_lab/investor_portfolio"
|
|
302
|
-
query_params = {
|
|
303
|
-
k: v for k, v in [("investor_name", investor_name)] if v is not None
|
|
304
|
-
}
|
|
263
|
+
query_params = {k: v for k, v in [("investor_name", investor_name)] if v is not None}
|
|
305
264
|
response = self._get(url, params=query_params)
|
|
306
265
|
response.raise_for_status()
|
|
307
266
|
return response.json()
|
|
308
267
|
|
|
309
|
-
def get_job_listings(
|
|
310
|
-
self, tickers, dataset, filters, offset, count, sorts
|
|
311
|
-
) -> dict[str, Any]:
|
|
268
|
+
async def get_job_listings(self, tickers, dataset, filters, offset, count, sorts) -> dict[str, Any]:
|
|
312
269
|
"""
|
|
313
270
|
Retrieves job listings data based on specified parameters.
|
|
314
271
|
|
|
@@ -342,14 +299,7 @@ class CrustdataApp(APIApplication):
|
|
|
342
299
|
raise ValueError("Missing required parameter 'count'")
|
|
343
300
|
if sorts is None:
|
|
344
301
|
raise ValueError("Missing required parameter 'sorts'")
|
|
345
|
-
request_body = {
|
|
346
|
-
"tickers": tickers,
|
|
347
|
-
"dataset": dataset,
|
|
348
|
-
"filters": filters,
|
|
349
|
-
"offset": offset,
|
|
350
|
-
"count": count,
|
|
351
|
-
"sorts": sorts,
|
|
352
|
-
}
|
|
302
|
+
request_body = {"tickers": tickers, "dataset": dataset, "filters": filters, "offset": offset, "count": count, "sorts": sorts}
|
|
353
303
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
354
304
|
url = f"{self.base_url}/data_lab/job_listings/Table/"
|
|
355
305
|
query_params = {}
|
|
@@ -357,7 +307,7 @@ class CrustdataApp(APIApplication):
|
|
|
357
307
|
response.raise_for_status()
|
|
358
308
|
return response.json()
|
|
359
309
|
|
|
360
|
-
def search_persons(self, job_id) -> dict[str, Any]:
|
|
310
|
+
async def search_persons(self, job_id) -> dict[str, Any]:
|
|
361
311
|
"""
|
|
362
312
|
Submits a search request for persons associated with a given asynchronous job and returns the search results as a dictionary.
|
|
363
313
|
|
|
@@ -376,9 +326,7 @@ class CrustdataApp(APIApplication):
|
|
|
376
326
|
"""
|
|
377
327
|
if job_id is None:
|
|
378
328
|
raise ValueError("Missing required parameter 'job_id'")
|
|
379
|
-
request_body = {
|
|
380
|
-
"job_id": job_id,
|
|
381
|
-
}
|
|
329
|
+
request_body = {"job_id": job_id}
|
|
382
330
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
383
331
|
url = f"{self.base_url}/screener/person/search"
|
|
384
332
|
query_params = {}
|
|
@@ -386,7 +334,7 @@ class CrustdataApp(APIApplication):
|
|
|
386
334
|
response.raise_for_status()
|
|
387
335
|
return response.json()
|
|
388
336
|
|
|
389
|
-
def search_companies(self, filters, page) -> dict[str, Any]:
|
|
337
|
+
async def search_companies(self, filters, page) -> dict[str, Any]:
|
|
390
338
|
"""
|
|
391
339
|
Searches for companies using specified filters and pagination parameters.
|
|
392
340
|
|
|
@@ -408,10 +356,7 @@ class CrustdataApp(APIApplication):
|
|
|
408
356
|
raise ValueError("Missing required parameter 'filters'")
|
|
409
357
|
if page is None:
|
|
410
358
|
raise ValueError("Missing required parameter 'page'")
|
|
411
|
-
request_body = {
|
|
412
|
-
"filters": filters,
|
|
413
|
-
"page": page,
|
|
414
|
-
}
|
|
359
|
+
request_body = {"filters": filters, "page": page}
|
|
415
360
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
416
361
|
url = f"{self.base_url}/screener/company/search"
|
|
417
362
|
query_params = {}
|
|
@@ -419,9 +364,7 @@ class CrustdataApp(APIApplication):
|
|
|
419
364
|
response.raise_for_status()
|
|
420
365
|
return response.json()
|
|
421
366
|
|
|
422
|
-
def enrich_person(
|
|
423
|
-
self, linkedin_profile_url, enrich_realtime, fields
|
|
424
|
-
) -> dict[str, Any]:
|
|
367
|
+
async def enrich_person(self, linkedin_profile_url, enrich_realtime, fields) -> dict[str, Any]:
|
|
425
368
|
"""
|
|
426
369
|
Retrieves enriched person data from LinkedIn profile using the provided profile URL, enrichment mode, and requested fields.
|
|
427
370
|
|
|
@@ -449,18 +392,14 @@ class CrustdataApp(APIApplication):
|
|
|
449
392
|
url = f"{self.base_url}/screener/person/enrich"
|
|
450
393
|
query_params = {
|
|
451
394
|
k: v
|
|
452
|
-
for k, v in [
|
|
453
|
-
("linkedin_profile_url", linkedin_profile_url),
|
|
454
|
-
("enrich_realtime", enrich_realtime),
|
|
455
|
-
("fields", fields),
|
|
456
|
-
]
|
|
395
|
+
for k, v in [("linkedin_profile_url", linkedin_profile_url), ("enrich_realtime", enrich_realtime), ("fields", fields)]
|
|
457
396
|
if v is not None
|
|
458
397
|
}
|
|
459
398
|
response = self._get(url, params=query_params)
|
|
460
399
|
response.raise_for_status()
|
|
461
400
|
return response.json()
|
|
462
401
|
|
|
463
|
-
def enrich_company(self, company_domain, enrich_realtime) -> dict[str, Any]:
|
|
402
|
+
async def enrich_company(self, company_domain, enrich_realtime) -> dict[str, Any]:
|
|
464
403
|
"""
|
|
465
404
|
Retrieves enriched company data using the provided company domain and enrichment mode.
|
|
466
405
|
|
|
@@ -483,19 +422,12 @@ class CrustdataApp(APIApplication):
|
|
|
483
422
|
if enrich_realtime is None:
|
|
484
423
|
raise ValueError("Missing required parameter 'enrich_realtime'")
|
|
485
424
|
url = f"{self.base_url}/screener/company"
|
|
486
|
-
query_params = {
|
|
487
|
-
k: v
|
|
488
|
-
for k, v in [
|
|
489
|
-
("company_domain", company_domain),
|
|
490
|
-
("enrich_realtime", enrich_realtime),
|
|
491
|
-
]
|
|
492
|
-
if v is not None
|
|
493
|
-
}
|
|
425
|
+
query_params = {k: v for k, v in [("company_domain", company_domain), ("enrich_realtime", enrich_realtime)] if v is not None}
|
|
494
426
|
response = self._get(url, params=query_params)
|
|
495
427
|
response.raise_for_status()
|
|
496
428
|
return response.json()
|
|
497
429
|
|
|
498
|
-
def get_linked_in_posts(self, company_linkedin_url) -> dict[str, Any]:
|
|
430
|
+
async def get_linked_in_posts(self, company_linkedin_url) -> dict[str, Any]:
|
|
499
431
|
"""
|
|
500
432
|
Fetches LinkedIn posts for a specified company using its LinkedIn URL.
|
|
501
433
|
|
|
@@ -515,18 +447,12 @@ class CrustdataApp(APIApplication):
|
|
|
515
447
|
if company_linkedin_url is None:
|
|
516
448
|
raise ValueError("Missing required parameter 'company_linkedin_url'")
|
|
517
449
|
url = f"{self.base_url}/screener/linkedin_posts"
|
|
518
|
-
query_params = {
|
|
519
|
-
k: v
|
|
520
|
-
for k, v in [("company_linkedin_url", company_linkedin_url)]
|
|
521
|
-
if v is not None
|
|
522
|
-
}
|
|
450
|
+
query_params = {k: v for k, v in [("company_linkedin_url", company_linkedin_url)] if v is not None}
|
|
523
451
|
response = self._get(url, params=query_params)
|
|
524
452
|
response.raise_for_status()
|
|
525
453
|
return response.json()
|
|
526
454
|
|
|
527
|
-
def search_linked_in_posts(
|
|
528
|
-
self, keyword, page, sort_by, date_posted
|
|
529
|
-
) -> dict[str, Any]:
|
|
455
|
+
async def search_linked_in_posts(self, keyword, page, sort_by, date_posted) -> dict[str, Any]:
|
|
530
456
|
"""
|
|
531
457
|
Searches LinkedIn posts using the provided keyword and filters, returning the search results as a dictionary.
|
|
532
458
|
|
|
@@ -554,12 +480,7 @@ class CrustdataApp(APIApplication):
|
|
|
554
480
|
raise ValueError("Missing required parameter 'sort_by'")
|
|
555
481
|
if date_posted is None:
|
|
556
482
|
raise ValueError("Missing required parameter 'date_posted'")
|
|
557
|
-
request_body = {
|
|
558
|
-
"keyword": keyword,
|
|
559
|
-
"page": page,
|
|
560
|
-
"sort_by": sort_by,
|
|
561
|
-
"date_posted": date_posted,
|
|
562
|
-
}
|
|
483
|
+
request_body = {"keyword": keyword, "page": page, "sort_by": sort_by, "date_posted": date_posted}
|
|
563
484
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
564
485
|
url = f"{self.base_url}/screener/linkedin_posts/keyword_search/"
|
|
565
486
|
query_params = {}
|