universal-mcp 0.1.7rc1__py3-none-any.whl → 0.1.8__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 (61) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/ahrefs/README.md +76 -0
  4. universal_mcp/applications/ahrefs/app.py +2291 -0
  5. universal_mcp/applications/application.py +95 -5
  6. universal_mcp/applications/calendly/README.md +78 -0
  7. universal_mcp/applications/calendly/__init__.py +0 -0
  8. universal_mcp/applications/calendly/app.py +1195 -0
  9. universal_mcp/applications/coda/README.md +133 -0
  10. universal_mcp/applications/coda/__init__.py +0 -0
  11. universal_mcp/applications/coda/app.py +3671 -0
  12. universal_mcp/applications/e2b/app.py +14 -28
  13. universal_mcp/applications/figma/README.md +74 -0
  14. universal_mcp/applications/figma/__init__.py +0 -0
  15. universal_mcp/applications/figma/app.py +1261 -0
  16. universal_mcp/applications/firecrawl/app.py +38 -35
  17. universal_mcp/applications/github/app.py +127 -85
  18. universal_mcp/applications/google_calendar/app.py +62 -138
  19. universal_mcp/applications/google_docs/app.py +47 -52
  20. universal_mcp/applications/google_drive/app.py +119 -113
  21. universal_mcp/applications/google_mail/app.py +124 -50
  22. universal_mcp/applications/google_sheet/app.py +89 -91
  23. universal_mcp/applications/markitdown/app.py +9 -8
  24. universal_mcp/applications/notion/app.py +254 -134
  25. universal_mcp/applications/perplexity/app.py +13 -41
  26. universal_mcp/applications/reddit/app.py +94 -85
  27. universal_mcp/applications/resend/app.py +12 -13
  28. universal_mcp/applications/{serp → serpapi}/app.py +14 -25
  29. universal_mcp/applications/tavily/app.py +11 -18
  30. universal_mcp/applications/wrike/README.md +71 -0
  31. universal_mcp/applications/wrike/__init__.py +0 -0
  32. universal_mcp/applications/wrike/app.py +1372 -0
  33. universal_mcp/applications/youtube/README.md +82 -0
  34. universal_mcp/applications/youtube/__init__.py +0 -0
  35. universal_mcp/applications/youtube/app.py +1428 -0
  36. universal_mcp/applications/zenquotes/app.py +12 -2
  37. universal_mcp/exceptions.py +9 -2
  38. universal_mcp/integrations/__init__.py +24 -1
  39. universal_mcp/integrations/agentr.py +27 -4
  40. universal_mcp/integrations/integration.py +146 -32
  41. universal_mcp/logger.py +3 -56
  42. universal_mcp/servers/__init__.py +6 -14
  43. universal_mcp/servers/server.py +201 -146
  44. universal_mcp/stores/__init__.py +7 -2
  45. universal_mcp/stores/store.py +103 -40
  46. universal_mcp/tools/__init__.py +3 -0
  47. universal_mcp/tools/adapters.py +43 -0
  48. universal_mcp/tools/func_metadata.py +213 -0
  49. universal_mcp/tools/tools.py +342 -0
  50. universal_mcp/utils/docgen.py +325 -119
  51. universal_mcp/utils/docstring_parser.py +179 -0
  52. universal_mcp/utils/dump_app_tools.py +33 -23
  53. universal_mcp/utils/installation.py +201 -10
  54. universal_mcp/utils/openapi.py +229 -46
  55. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
  56. universal_mcp-0.1.8.dist-info/RECORD +81 -0
  57. universal_mcp-0.1.7rc1.dist-info/RECORD +0 -58
  58. /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
  59. /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
  60. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
  61. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -6,10 +6,20 @@ class ZenquotesApp(APIApplication):
6
6
  super().__init__(name="zenquote", **kwargs)
7
7
 
8
8
  def get_quote(self) -> str:
9
- """Get an inspirational quote from the Zen Quotes API
9
+ """
10
+ Fetches a random inspirational quote from the Zen Quotes API.
10
11
 
11
12
  Returns:
12
- A random inspirational quote
13
+ A formatted string containing the quote and its author in the format 'quote - author'
14
+
15
+ Raises:
16
+ RequestException: If the HTTP request to the Zen Quotes API fails
17
+ JSONDecodeError: If the API response contains invalid JSON
18
+ IndexError: If the API response doesn't contain any quotes
19
+ KeyError: If the quote data doesn't contain the expected 'q' or 'a' fields
20
+
21
+ Tags:
22
+ fetch, quotes, api, http, important
13
23
  """
14
24
  url = "https://zenquotes.io/api/random"
15
25
  response = self._get(url)
@@ -1,6 +1,13 @@
1
1
  class NotAuthorizedError(Exception):
2
2
  """Raised when a user is not authorized to access a resource or perform an action."""
3
3
 
4
- def __init__(self, message="Not authorized to perform this action"):
4
+ def __init__(self, message: str):
5
5
  self.message = message
6
- super().__init__(self.message)
6
+
7
+
8
+ class ToolError(Exception):
9
+ """Raised when a tool is not found or fails to execute."""
10
+
11
+
12
+ class InvalidSignature(Exception):
13
+ """Raised when a signature is invalid."""
@@ -1,8 +1,31 @@
1
+ from universal_mcp.config import IntegrationConfig
1
2
  from universal_mcp.integrations.agentr import AgentRIntegration
2
3
  from universal_mcp.integrations.integration import (
3
4
  ApiKeyIntegration,
4
5
  Integration,
5
6
  OAuthIntegration,
6
7
  )
8
+ from universal_mcp.stores.store import BaseStore
7
9
 
8
- __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration"]
10
+
11
+ def integration_from_config(
12
+ config: IntegrationConfig, store: BaseStore | None = None, **kwargs
13
+ ) -> Integration:
14
+ if config.type == "api_key":
15
+ return ApiKeyIntegration(config.name, store=store, **kwargs)
16
+ elif config.type == "agentr":
17
+ api_key = kwargs.get("api_key")
18
+ if not api_key:
19
+ raise ValueError("api_key is required for AgentR integration")
20
+ return AgentRIntegration(config.name, api_key=api_key)
21
+ else:
22
+ raise ValueError(f"Unsupported integration type: {config.type}")
23
+
24
+
25
+ __all__ = [
26
+ "AgentRIntegration",
27
+ "Integration",
28
+ "ApiKeyIntegration",
29
+ "OAuthIntegration",
30
+ "integration_from_config",
31
+ ]
@@ -29,7 +29,10 @@ class AgentRIntegration(Integration):
29
29
  "API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
30
30
  )
31
31
  raise ValueError("AgentR API key required - get one at https://agentr.dev")
32
- self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
32
+ self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev").rstrip(
33
+ "/"
34
+ )
35
+ self._credentials = None
33
36
 
34
37
  def set_credentials(self, credentials: dict | None = None):
35
38
  """Set credentials for the integration.
@@ -43,9 +46,9 @@ class AgentRIntegration(Integration):
43
46
  str: Authorization URL from authorize() method
44
47
  """
45
48
  return self.authorize()
46
- # raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
47
49
 
48
- def get_credentials(self):
50
+ @property
51
+ def credentials(self):
49
52
  """Get credentials for the integration from the AgentR API.
50
53
 
51
54
  Makes API request to retrieve stored credentials for this integration.
@@ -57,16 +60,36 @@ class AgentRIntegration(Integration):
57
60
  NotAuthorizedError: If credentials are not found (404 response)
58
61
  HTTPError: For other API errors
59
62
  """
63
+ if self._credentials is not None:
64
+ return self._credentials
60
65
  response = httpx.get(
61
66
  f"{self.base_url}/api/{self.name}/credentials/",
62
67
  headers={"accept": "application/json", "X-API-KEY": self.api_key},
63
68
  )
64
69
  if response.status_code == 404:
70
+ logger.warning(
71
+ f"No credentials found for {self.name}. Requesting authorization..."
72
+ )
65
73
  action = self.authorize()
66
74
  raise NotAuthorizedError(action)
67
75
  response.raise_for_status()
68
76
  data = response.json()
69
- return data
77
+ self._credentials = data
78
+ return self._credentials
79
+
80
+ def get_credentials(self):
81
+ """Get credentials for the integration from the AgentR API.
82
+
83
+ Makes API request to retrieve stored credentials for this integration.
84
+
85
+ Returns:
86
+ dict: Credentials data from API response
87
+
88
+ Raises:
89
+ NotAuthorizedError: If credentials are not found (404 response)
90
+ HTTPError: For other API errors
91
+ """
92
+ return self.credentials
70
93
 
71
94
  def authorize(self):
72
95
  """Get authorization URL for the integration.
@@ -1,19 +1,22 @@
1
1
  from abc import ABC, abstractmethod
2
+ from typing import Any
2
3
 
3
4
  import httpx
4
5
  from loguru import logger
5
6
 
6
7
  from universal_mcp.exceptions import NotAuthorizedError
7
- from universal_mcp.stores.store import Store
8
+ from universal_mcp.stores import BaseStore
9
+ from universal_mcp.stores.store import KeyNotFoundError
8
10
 
9
11
 
10
12
  def sanitize_api_key_name(name: str) -> str:
11
- suffix = "_API_KEY"
13
+ suffix = "_API_KEY"
12
14
  if name.endswith(suffix) or name.endswith(suffix.lower()):
13
15
  return name.upper()
14
16
  else:
15
17
  return f"{name.upper()}{suffix}"
16
-
18
+
19
+
17
20
  class Integration(ABC):
18
21
  """Abstract base class for handling application integrations and authentication.
19
22
 
@@ -29,37 +32,43 @@ class Integration(ABC):
29
32
  store: Store instance for persisting credentials and other data
30
33
  """
31
34
 
32
- def __init__(self, name: str, store: Store = None):
35
+ def __init__(self, name: str, store: BaseStore | None = None):
33
36
  self.name = name
34
37
  self.store = store
35
38
 
36
39
  @abstractmethod
37
- def authorize(self):
40
+ def authorize(self) -> str | dict[str, Any]:
38
41
  """Authorize the integration.
39
42
 
40
43
  Returns:
41
- str: Authorization URL.
44
+ Union[str, Dict[str, Any]]: Authorization URL or parameters needed for authorization.
45
+
46
+ Raises:
47
+ ValueError: If required configuration is missing.
42
48
  """
43
49
  pass
44
50
 
45
51
  @abstractmethod
46
- def get_credentials(self):
52
+ def get_credentials(self) -> dict[str, Any]:
47
53
  """Get credentials for the integration.
48
54
 
49
55
  Returns:
50
- dict: Credentials for the integration.
56
+ Dict[str, Any]: Credentials for the integration.
51
57
 
52
58
  Raises:
53
- NotAuthorizedError: If credentials are not found.
59
+ NotAuthorizedError: If credentials are not found or invalid.
54
60
  """
55
61
  pass
56
62
 
57
63
  @abstractmethod
58
- def set_credentials(self, credentials: dict):
64
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
59
65
  """Set credentials for the integration.
60
66
 
61
67
  Args:
62
- credentials: Credentials for the integration.
68
+ credentials: Dictionary containing credentials for the integration.
69
+
70
+ Raises:
71
+ ValueError: If credentials are invalid or missing required fields.
63
72
  """
64
73
  pass
65
74
 
@@ -81,35 +90,93 @@ class ApiKeyIntegration(Integration):
81
90
  store: Store instance for persisting credentials and other data
82
91
  """
83
92
 
84
- def __init__(self, name: str, store: Store = None, **kwargs):
93
+ def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
94
+ self.type = "api_key"
85
95
  sanitized_name = sanitize_api_key_name(name)
86
96
  super().__init__(sanitized_name, store, **kwargs)
87
97
  logger.info(f"Initializing API Key Integration: {name} with store: {store}")
98
+ self._api_key: str | None = None
99
+
100
+ @property
101
+ def api_key(self) -> str | None:
102
+ if not self._api_key:
103
+ try:
104
+ credentials = self.store.get(self.name)
105
+ self.api_key = credentials
106
+ except KeyNotFoundError as e:
107
+ action = self.authorize()
108
+ raise NotAuthorizedError(action) from e
109
+ return self._api_key
110
+
111
+ def get_credentials(self) -> dict[str, str]:
112
+ """Get API key credentials.
88
113
 
89
- def get_credentials(self):
90
- credentials = self.store.get(self.name)
91
- if credentials is None:
92
- action = self.authorize()
93
- raise NotAuthorizedError(action)
94
- return credentials
114
+ Returns:
115
+ Dict[str, str]: Dictionary containing the API key.
116
+
117
+ Raises:
118
+ NotAuthorizedError: If API key is not found.
119
+ """
120
+ return {"api_key": self.api_key}
121
+
122
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
123
+ """Set API key credentials.
124
+
125
+ Args:
126
+ credentials: Dictionary containing the API key.
95
127
 
96
- def set_credentials(self, credentials: dict):
128
+ Raises:
129
+ ValueError: If credentials are invalid or missing API key.
130
+ """
131
+ if not credentials or not isinstance(credentials, dict):
132
+ raise ValueError("Invalid credentials format")
97
133
  self.store.set(self.name, credentials)
98
134
 
99
- def authorize(self):
135
+ def authorize(self) -> str:
136
+ """Get authorization instructions for API key.
137
+
138
+ Returns:
139
+ str: Instructions for setting up API key.
140
+ """
100
141
  return f"Please ask the user for api key and set the API Key for {self.name} in the store"
101
142
 
102
143
 
103
144
  class OAuthIntegration(Integration):
145
+ """Integration class for OAuth based authentication.
146
+
147
+ This class implements the Integration interface for services that use OAuth
148
+ authentication. It handles the OAuth flow including authorization, token exchange,
149
+ and token refresh.
150
+
151
+ Args:
152
+ name: The name identifier for this integration
153
+ store: Optional Store instance for persisting credentials and other data
154
+ client_id: OAuth client ID
155
+ client_secret: OAuth client secret
156
+ auth_url: OAuth authorization URL
157
+ token_url: OAuth token exchange URL
158
+ scope: OAuth scope string
159
+ **kwargs: Additional keyword arguments passed to parent class
160
+
161
+ Attributes:
162
+ name: The name identifier for this integration
163
+ store: Store instance for persisting credentials and other data
164
+ client_id: OAuth client ID
165
+ client_secret: OAuth client secret
166
+ auth_url: OAuth authorization URL
167
+ token_url: OAuth token exchange URL
168
+ scope: OAuth scope string
169
+ """
170
+
104
171
  def __init__(
105
172
  self,
106
173
  name: str,
107
- store: Store = None,
108
- client_id: str = None,
109
- client_secret: str = None,
110
- auth_url: str = None,
111
- token_url: str = None,
112
- scope: str = None,
174
+ store: BaseStore | None = None,
175
+ client_id: str | None = None,
176
+ client_secret: str | None = None,
177
+ auth_url: str | None = None,
178
+ token_url: str | None = None,
179
+ scope: str | None = None,
113
180
  **kwargs,
114
181
  ):
115
182
  super().__init__(name, store, **kwargs)
@@ -119,20 +186,41 @@ class OAuthIntegration(Integration):
119
186
  self.token_url = token_url
120
187
  self.scope = scope
121
188
 
122
- def get_credentials(self):
189
+ def get_credentials(self) -> dict[str, Any] | None:
190
+ """Get OAuth credentials.
191
+
192
+ Returns:
193
+ Optional[Dict[str, Any]]: Dictionary containing OAuth tokens if found, None otherwise.
194
+ """
123
195
  credentials = self.store.get(self.name)
124
196
  if not credentials:
125
197
  return None
126
198
  return credentials
127
199
 
128
- def set_credentials(self, credentials: dict):
200
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
201
+ """Set OAuth credentials.
202
+
203
+ Args:
204
+ credentials: Dictionary containing OAuth tokens.
205
+
206
+ Raises:
207
+ ValueError: If credentials are invalid or missing required tokens.
208
+ """
129
209
  if not credentials or not isinstance(credentials, dict):
130
210
  raise ValueError("Invalid credentials format")
131
211
  if "access_token" not in credentials:
132
212
  raise ValueError("Credentials must contain access_token")
133
213
  self.store.set(self.name, credentials)
134
214
 
135
- def authorize(self):
215
+ def authorize(self) -> dict[str, Any]:
216
+ """Get OAuth authorization parameters.
217
+
218
+ Returns:
219
+ Dict[str, Any]: Dictionary containing OAuth authorization parameters.
220
+
221
+ Raises:
222
+ ValueError: If required OAuth configuration is missing.
223
+ """
136
224
  if not all([self.client_id, self.client_secret, self.auth_url, self.token_url]):
137
225
  raise ValueError("Missing required OAuth configuration")
138
226
 
@@ -149,7 +237,19 @@ class OAuthIntegration(Integration):
149
237
  "token_url": self.token_url,
150
238
  }
151
239
 
152
- def handle_callback(self, code: str):
240
+ def handle_callback(self, code: str) -> dict[str, Any]:
241
+ """Handle OAuth callback and exchange code for tokens.
242
+
243
+ Args:
244
+ code: Authorization code from OAuth callback.
245
+
246
+ Returns:
247
+ Dict[str, Any]: Dictionary containing OAuth tokens.
248
+
249
+ Raises:
250
+ ValueError: If required OAuth configuration is missing.
251
+ httpx.HTTPError: If token exchange request fails.
252
+ """
153
253
  if not all([self.client_id, self.client_secret, self.token_url]):
154
254
  raise ValueError("Missing required OAuth configuration")
155
255
 
@@ -166,15 +266,29 @@ class OAuthIntegration(Integration):
166
266
  self.store.set(self.name, credentials)
167
267
  return credentials
168
268
 
169
- def refresh_token(self):
269
+ def refresh_token(self) -> dict[str, Any]:
270
+ """Refresh OAuth access token using refresh token.
271
+
272
+ Returns:
273
+ Dict[str, Any]: Dictionary containing new OAuth tokens.
274
+
275
+ Raises:
276
+ ValueError: If required OAuth configuration is missing.
277
+ httpx.HTTPError: If token refresh request fails.
278
+ KeyError: If refresh token is not found in current credentials.
279
+ """
170
280
  if not all([self.client_id, self.client_secret, self.token_url]):
171
281
  raise ValueError("Missing required OAuth configuration")
172
282
 
283
+ credentials = self.get_credentials()
284
+ if not credentials or "refresh_token" not in credentials:
285
+ raise KeyError("Refresh token not found in current credentials")
286
+
173
287
  token_params = {
174
288
  "client_id": self.client_id,
175
289
  "client_secret": self.client_secret,
176
290
  "grant_type": "refresh_token",
177
- "refresh_token": self.credentials["refresh_token"],
291
+ "refresh_token": credentials["refresh_token"],
178
292
  }
179
293
 
180
294
  response = httpx.post(self.token_url, data=token_params)
universal_mcp/logger.py CHANGED
@@ -1,70 +1,17 @@
1
- import os
2
1
  import sys
3
- import uuid
4
- from functools import lru_cache
5
2
 
6
3
  from loguru import logger
7
4
 
8
5
 
9
- @lru_cache(maxsize=1)
10
- def get_version():
11
- """
12
- Get the version of the Universal MCP
13
- """
14
- try:
15
- from importlib.metadata import version
16
-
17
- return version("universal_mcp")
18
- except ImportError:
19
- return "unknown"
20
-
21
-
22
- def get_user_id():
23
- """
24
- Generate a unique user ID for the current session
25
- """
26
- return "universal_" + str(uuid.uuid4())[:8]
27
-
28
-
29
- def posthog_sink(message, user_id=get_user_id()):
30
- """
31
- Custom sink for sending logs to PostHog
32
- """
33
- try:
34
- import posthog
35
-
36
- posthog.host = "https://us.i.posthog.com"
37
- posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
38
-
39
- record = message.record
40
- properties = {
41
- "level": record["level"].name,
42
- "module": record["name"],
43
- "function": record["function"],
44
- "line": record["line"],
45
- "message": record["message"],
46
- "version": get_version(),
47
- }
48
- posthog.capture(user_id, "universal_mcp", properties)
49
- except Exception:
50
- # Silently fail if PostHog capture fails - don't want logging to break the app
51
- pass
52
-
53
-
54
6
  def setup_logger():
55
7
  logger.remove()
8
+ # STDOUT cant be used as a sink because it will break the stream
56
9
  # logger.add(
57
10
  # sink=sys.stdout,
58
- # format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
59
11
  # level="INFO",
60
- # colorize=True,
61
12
  # )
13
+ # STDERR
62
14
  logger.add(
63
15
  sink=sys.stderr,
64
- format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
65
- level="WARNING",
66
- colorize=True,
16
+ level="INFO",
67
17
  )
68
- telemetry_enabled = os.getenv("TELEMETRY_ENABLED", "true").lower() == "true"
69
- if telemetry_enabled:
70
- logger.add(posthog_sink, level="INFO") # PostHog telemetry
@@ -4,20 +4,12 @@ from universal_mcp.servers.server import AgentRServer, LocalServer
4
4
 
5
5
  def server_from_config(config: ServerConfig):
6
6
  if config.type == "agentr":
7
- return AgentRServer(
8
- name=config.name,
9
- description=config.description,
10
- api_key=config.api_key,
11
- port=config.port,
12
- )
7
+ return AgentRServer(config=config, api_key=config.api_key)
8
+
13
9
  elif config.type == "local":
14
- return LocalServer(
15
- name=config.name,
16
- description=config.description,
17
- store=config.store,
18
- apps_list=config.apps,
19
- port=config.port,
20
- )
10
+ return LocalServer(config=config)
11
+ else:
12
+ raise ValueError(f"Unsupported server type: {config.type}")
21
13
 
22
14
 
23
- __all__ = [AgentRServer, LocalServer]
15
+ __all__ = [AgentRServer, LocalServer, server_from_config]