universal-mcp 0.1.7rc2__py3-none-any.whl → 0.1.8rc2__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 (46) hide show
  1. universal_mcp/applications/application.py +6 -5
  2. universal_mcp/applications/calendly/README.md +78 -0
  3. universal_mcp/applications/calendly/app.py +954 -0
  4. universal_mcp/applications/e2b/app.py +18 -12
  5. universal_mcp/applications/firecrawl/app.py +28 -1
  6. universal_mcp/applications/github/app.py +150 -107
  7. universal_mcp/applications/google_calendar/app.py +72 -137
  8. universal_mcp/applications/google_docs/app.py +35 -15
  9. universal_mcp/applications/google_drive/app.py +84 -55
  10. universal_mcp/applications/google_mail/app.py +143 -53
  11. universal_mcp/applications/google_sheet/app.py +61 -38
  12. universal_mcp/applications/markitdown/app.py +12 -11
  13. universal_mcp/applications/notion/app.py +199 -89
  14. universal_mcp/applications/perplexity/app.py +17 -15
  15. universal_mcp/applications/reddit/app.py +110 -101
  16. universal_mcp/applications/resend/app.py +14 -7
  17. universal_mcp/applications/{serp → serpapi}/app.py +14 -7
  18. universal_mcp/applications/tavily/app.py +13 -10
  19. universal_mcp/applications/wrike/README.md +71 -0
  20. universal_mcp/applications/wrike/__init__.py +0 -0
  21. universal_mcp/applications/wrike/app.py +1044 -0
  22. universal_mcp/applications/youtube/README.md +82 -0
  23. universal_mcp/applications/youtube/__init__.py +0 -0
  24. universal_mcp/applications/youtube/app.py +986 -0
  25. universal_mcp/applications/zenquotes/app.py +13 -3
  26. universal_mcp/exceptions.py +8 -2
  27. universal_mcp/integrations/__init__.py +15 -1
  28. universal_mcp/integrations/integration.py +132 -27
  29. universal_mcp/servers/__init__.py +6 -15
  30. universal_mcp/servers/server.py +208 -149
  31. universal_mcp/stores/__init__.py +7 -2
  32. universal_mcp/stores/store.py +103 -42
  33. universal_mcp/tools/__init__.py +3 -0
  34. universal_mcp/tools/adapters.py +40 -0
  35. universal_mcp/tools/func_metadata.py +214 -0
  36. universal_mcp/tools/tools.py +285 -0
  37. universal_mcp/utils/docgen.py +277 -123
  38. universal_mcp/utils/docstring_parser.py +156 -0
  39. universal_mcp/utils/openapi.py +149 -40
  40. {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.dist-info}/METADATA +8 -4
  41. universal_mcp-0.1.8rc2.dist-info/RECORD +71 -0
  42. universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
  43. /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
  44. /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
  45. {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.dist-info}/WHEEL +0 -0
  46. {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.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
10
-
9
+ """
10
+ Fetches a random inspirational quote from the Zen Quotes API.
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,12 @@
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
+ class InvalidSignature(Exception):
12
+ """Raised when a signature is invalid."""
@@ -1,8 +1,22 @@
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(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
12
+ if config.type == "api_key":
13
+ return ApiKeyIntegration(config.name, store=store, **kwargs)
14
+ elif config.type == "agentr":
15
+ api_key = kwargs.get("api_key")
16
+ if not api_key:
17
+ raise ValueError("api_key is required for AgentR integration")
18
+ return AgentRIntegration(config.name, api_key=api_key)
19
+ else:
20
+ raise ValueError(f"Unsupported integration type: {config.type}")
21
+
22
+ __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration", "integration_from_config"]
@@ -1,10 +1,12 @@
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:
@@ -30,37 +32,43 @@ class Integration(ABC):
30
32
  store: Store instance for persisting credentials and other data
31
33
  """
32
34
 
33
- def __init__(self, name: str, store: Store = None):
35
+ def __init__(self, name: str, store: BaseStore | None = None):
34
36
  self.name = name
35
37
  self.store = store
36
38
 
37
39
  @abstractmethod
38
- def authorize(self):
40
+ def authorize(self) -> str | dict[str, Any]:
39
41
  """Authorize the integration.
40
42
 
41
43
  Returns:
42
- 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.
43
48
  """
44
49
  pass
45
50
 
46
51
  @abstractmethod
47
- def get_credentials(self):
52
+ def get_credentials(self) -> dict[str, Any]:
48
53
  """Get credentials for the integration.
49
54
 
50
55
  Returns:
51
- dict: Credentials for the integration.
56
+ Dict[str, Any]: Credentials for the integration.
52
57
 
53
58
  Raises:
54
- NotAuthorizedError: If credentials are not found.
59
+ NotAuthorizedError: If credentials are not found or invalid.
55
60
  """
56
61
  pass
57
62
 
58
63
  @abstractmethod
59
- def set_credentials(self, credentials: dict):
64
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
60
65
  """Set credentials for the integration.
61
66
 
62
67
  Args:
63
- 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.
64
72
  """
65
73
  pass
66
74
 
@@ -82,35 +90,85 @@ class ApiKeyIntegration(Integration):
82
90
  store: Store instance for persisting credentials and other data
83
91
  """
84
92
 
85
- def __init__(self, name: str, store: Store = None, **kwargs):
93
+ def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
86
94
  sanitized_name = sanitize_api_key_name(name)
87
95
  super().__init__(sanitized_name, store, **kwargs)
88
96
  logger.info(f"Initializing API Key Integration: {name} with store: {store}")
89
97
 
90
- def get_credentials(self):
91
- credentials = self.store.get(self.name)
92
- if credentials is None:
98
+ def get_credentials(self) -> dict[str, str]:
99
+ """Get API key credentials.
100
+
101
+ Returns:
102
+ Dict[str, str]: Dictionary containing the API key.
103
+
104
+ Raises:
105
+ NotAuthorizedError: If API key is not found.
106
+ """
107
+ try:
108
+ credentials = self.store.get(self.name)
109
+ except KeyNotFoundError:
93
110
  action = self.authorize()
94
111
  raise NotAuthorizedError(action)
95
112
  return {"api_key": credentials}
96
113
 
97
- def set_credentials(self, credentials: dict):
114
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
115
+ """Set API key credentials.
116
+
117
+ Args:
118
+ credentials: Dictionary containing the API key.
119
+
120
+ Raises:
121
+ ValueError: If credentials are invalid or missing API key.
122
+ """
123
+ if not credentials or not isinstance(credentials, dict):
124
+ raise ValueError("Invalid credentials format")
98
125
  self.store.set(self.name, credentials)
99
126
 
100
- def authorize(self):
127
+ def authorize(self) -> str:
128
+ """Get authorization instructions for API key.
129
+
130
+ Returns:
131
+ str: Instructions for setting up API key.
132
+ """
101
133
  return f"Please ask the user for api key and set the API Key for {self.name} in the store"
102
134
 
103
135
 
104
136
  class OAuthIntegration(Integration):
137
+ """Integration class for OAuth based authentication.
138
+
139
+ This class implements the Integration interface for services that use OAuth
140
+ authentication. It handles the OAuth flow including authorization, token exchange,
141
+ and token refresh.
142
+
143
+ Args:
144
+ name: The name identifier for this integration
145
+ store: Optional Store instance for persisting credentials and other data
146
+ client_id: OAuth client ID
147
+ client_secret: OAuth client secret
148
+ auth_url: OAuth authorization URL
149
+ token_url: OAuth token exchange URL
150
+ scope: OAuth scope string
151
+ **kwargs: Additional keyword arguments passed to parent class
152
+
153
+ Attributes:
154
+ name: The name identifier for this integration
155
+ store: Store instance for persisting credentials and other data
156
+ client_id: OAuth client ID
157
+ client_secret: OAuth client secret
158
+ auth_url: OAuth authorization URL
159
+ token_url: OAuth token exchange URL
160
+ scope: OAuth scope string
161
+ """
162
+
105
163
  def __init__(
106
164
  self,
107
165
  name: str,
108
- store: Store = None,
109
- client_id: str = None,
110
- client_secret: str = None,
111
- auth_url: str = None,
112
- token_url: str = None,
113
- scope: str = None,
166
+ store: BaseStore | None = None,
167
+ client_id: str | None = None,
168
+ client_secret: str | None = None,
169
+ auth_url: str | None = None,
170
+ token_url: str | None = None,
171
+ scope: str | None = None,
114
172
  **kwargs,
115
173
  ):
116
174
  super().__init__(name, store, **kwargs)
@@ -120,20 +178,41 @@ class OAuthIntegration(Integration):
120
178
  self.token_url = token_url
121
179
  self.scope = scope
122
180
 
123
- def get_credentials(self):
181
+ def get_credentials(self) -> dict[str, Any] | None:
182
+ """Get OAuth credentials.
183
+
184
+ Returns:
185
+ Optional[Dict[str, Any]]: Dictionary containing OAuth tokens if found, None otherwise.
186
+ """
124
187
  credentials = self.store.get(self.name)
125
188
  if not credentials:
126
189
  return None
127
190
  return credentials
128
191
 
129
- def set_credentials(self, credentials: dict):
192
+ def set_credentials(self, credentials: dict[str, Any]) -> None:
193
+ """Set OAuth credentials.
194
+
195
+ Args:
196
+ credentials: Dictionary containing OAuth tokens.
197
+
198
+ Raises:
199
+ ValueError: If credentials are invalid or missing required tokens.
200
+ """
130
201
  if not credentials or not isinstance(credentials, dict):
131
202
  raise ValueError("Invalid credentials format")
132
203
  if "access_token" not in credentials:
133
204
  raise ValueError("Credentials must contain access_token")
134
205
  self.store.set(self.name, credentials)
135
206
 
136
- def authorize(self):
207
+ def authorize(self) -> dict[str, Any]:
208
+ """Get OAuth authorization parameters.
209
+
210
+ Returns:
211
+ Dict[str, Any]: Dictionary containing OAuth authorization parameters.
212
+
213
+ Raises:
214
+ ValueError: If required OAuth configuration is missing.
215
+ """
137
216
  if not all([self.client_id, self.client_secret, self.auth_url, self.token_url]):
138
217
  raise ValueError("Missing required OAuth configuration")
139
218
 
@@ -150,7 +229,19 @@ class OAuthIntegration(Integration):
150
229
  "token_url": self.token_url,
151
230
  }
152
231
 
153
- def handle_callback(self, code: str):
232
+ def handle_callback(self, code: str) -> dict[str, Any]:
233
+ """Handle OAuth callback and exchange code for tokens.
234
+
235
+ Args:
236
+ code: Authorization code from OAuth callback.
237
+
238
+ Returns:
239
+ Dict[str, Any]: Dictionary containing OAuth tokens.
240
+
241
+ Raises:
242
+ ValueError: If required OAuth configuration is missing.
243
+ httpx.HTTPError: If token exchange request fails.
244
+ """
154
245
  if not all([self.client_id, self.client_secret, self.token_url]):
155
246
  raise ValueError("Missing required OAuth configuration")
156
247
 
@@ -167,15 +258,29 @@ class OAuthIntegration(Integration):
167
258
  self.store.set(self.name, credentials)
168
259
  return credentials
169
260
 
170
- def refresh_token(self):
261
+ def refresh_token(self) -> dict[str, Any]:
262
+ """Refresh OAuth access token using refresh token.
263
+
264
+ Returns:
265
+ Dict[str, Any]: Dictionary containing new OAuth tokens.
266
+
267
+ Raises:
268
+ ValueError: If required OAuth configuration is missing.
269
+ httpx.HTTPError: If token refresh request fails.
270
+ KeyError: If refresh token is not found in current credentials.
271
+ """
171
272
  if not all([self.client_id, self.client_secret, self.token_url]):
172
273
  raise ValueError("Missing required OAuth configuration")
173
274
 
275
+ credentials = self.get_credentials()
276
+ if not credentials or "refresh_token" not in credentials:
277
+ raise KeyError("Refresh token not found in current credentials")
278
+
174
279
  token_params = {
175
280
  "client_id": self.client_id,
176
281
  "client_secret": self.client_secret,
177
282
  "grant_type": "refresh_token",
178
- "refresh_token": self.credentials["refresh_token"],
283
+ "refresh_token": credentials["refresh_token"],
179
284
  }
180
285
 
181
286
  response = httpx.post(self.token_url, data=token_params)
@@ -4,20 +4,11 @@ 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
- )
13
- 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
- )
7
+ return AgentRServer(config=config, api_key=config.api_key)
21
8
 
9
+ elif config.type == "local":
10
+ return LocalServer(config=config)
11
+ else:
12
+ raise ValueError(f"Unsupported server type: {config.type}")
22
13
 
23
- __all__ = [AgentRServer, LocalServer]
14
+ __all__ = [AgentRServer, LocalServer, server_from_config]