universal-mcp 0.1.8rc1__py3-none-any.whl → 0.1.8rc3__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 (52) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/application.py +28 -5
  4. universal_mcp/applications/calendly/README.md +78 -0
  5. universal_mcp/applications/calendly/app.py +1207 -0
  6. universal_mcp/applications/coda/README.md +133 -0
  7. universal_mcp/applications/coda/__init__.py +0 -0
  8. universal_mcp/applications/coda/app.py +3704 -0
  9. universal_mcp/applications/e2b/app.py +12 -7
  10. universal_mcp/applications/firecrawl/app.py +27 -0
  11. universal_mcp/applications/github/app.py +127 -85
  12. universal_mcp/applications/google_calendar/app.py +62 -127
  13. universal_mcp/applications/google_docs/app.py +48 -35
  14. universal_mcp/applications/google_drive/app.py +119 -96
  15. universal_mcp/applications/google_mail/app.py +124 -34
  16. universal_mcp/applications/google_sheet/app.py +90 -74
  17. universal_mcp/applications/markitdown/app.py +9 -8
  18. universal_mcp/applications/notion/app.py +254 -134
  19. universal_mcp/applications/perplexity/app.py +16 -14
  20. universal_mcp/applications/reddit/app.py +94 -85
  21. universal_mcp/applications/resend/app.py +12 -5
  22. universal_mcp/applications/serpapi/app.py +11 -4
  23. universal_mcp/applications/tavily/app.py +11 -8
  24. universal_mcp/applications/wrike/README.md +71 -0
  25. universal_mcp/applications/wrike/__init__.py +0 -0
  26. universal_mcp/applications/wrike/app.py +1384 -0
  27. universal_mcp/applications/youtube/README.md +82 -0
  28. universal_mcp/applications/youtube/__init__.py +0 -0
  29. universal_mcp/applications/youtube/app.py +1446 -0
  30. universal_mcp/applications/zenquotes/app.py +12 -2
  31. universal_mcp/exceptions.py +9 -2
  32. universal_mcp/integrations/__init__.py +24 -1
  33. universal_mcp/integrations/integration.py +133 -28
  34. universal_mcp/logger.py +3 -56
  35. universal_mcp/servers/__init__.py +6 -14
  36. universal_mcp/servers/server.py +205 -150
  37. universal_mcp/stores/__init__.py +7 -2
  38. universal_mcp/stores/store.py +103 -40
  39. universal_mcp/tools/__init__.py +3 -0
  40. universal_mcp/tools/adapters.py +43 -0
  41. universal_mcp/tools/func_metadata.py +213 -0
  42. universal_mcp/tools/tools.py +342 -0
  43. universal_mcp/utils/docgen.py +325 -119
  44. universal_mcp/utils/docstring_parser.py +179 -0
  45. universal_mcp/utils/dump_app_tools.py +33 -23
  46. universal_mcp/utils/openapi.py +229 -46
  47. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/METADATA +8 -4
  48. universal_mcp-0.1.8rc3.dist-info/RECORD +75 -0
  49. universal_mcp-0.1.8rc1.dist-info/RECORD +0 -58
  50. /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
  51. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/WHEEL +0 -0
  52. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/entry_points.txt +0 -0
@@ -1,184 +1,239 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
+ from collections.abc import Callable
3
4
  from typing import Any
5
+ from urllib.parse import urlparse
4
6
 
5
7
  import httpx
6
8
  from loguru import logger
7
9
  from mcp.server.fastmcp import FastMCP
8
- from mcp.server.fastmcp.exceptions import ToolError
9
10
  from mcp.types import TextContent
10
11
 
11
- from universal_mcp.applications import app_from_slug
12
- from universal_mcp.config import AppConfig, IntegrationConfig, StoreConfig
13
- from universal_mcp.exceptions import NotAuthorizedError
14
- from universal_mcp.integrations import AgentRIntegration, ApiKeyIntegration
15
- from universal_mcp.stores import store_from_config
12
+ from universal_mcp.analytics import analytics
13
+ from universal_mcp.applications import Application, app_from_slug
14
+ from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
15
+ from universal_mcp.integrations import AgentRIntegration, integration_from_config
16
+ from universal_mcp.stores import BaseStore, store_from_config
17
+ from universal_mcp.tools.tools import ToolManager
16
18
 
17
19
 
18
- class Server(FastMCP, ABC):
19
- """
20
- Server is responsible for managing the applications and the store
21
- It also acts as a router for the applications, and exposed to the client
20
+ class BaseServer(FastMCP, ABC):
21
+ """Base server class with common functionality.
22
+
23
+ This class provides core server functionality including store setup,
24
+ tool management, and application loading.
25
+
26
+ Args:
27
+ config: Server configuration
28
+ **kwargs: Additional keyword arguments passed to FastMCP
22
29
  """
23
30
 
24
- def __init__(
25
- self, name: str, description: str, store: StoreConfig | None = None, **kwargs
26
- ):
27
- super().__init__(name, description, **kwargs)
28
- logger.info(f"Initializing server: {name} with store: {store}")
29
- self.store = store_from_config(store) if store else None
30
- self._setup_store(store)
31
- self._load_apps()
31
+ def __init__(self, config: ServerConfig, **kwargs):
32
+ super().__init__(config.name, config.description, **kwargs)
33
+ logger.info(
34
+ f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
35
+ )
32
36
 
33
- def _setup_store(self, store_config: StoreConfig | None):
34
- """
35
- Setup the store for the server.
36
- """
37
- if store_config is None:
38
- return
39
- self.store = store_from_config(store_config)
40
- self.add_tool(self.store.set)
41
- self.add_tool(self.store.delete)
42
- # self.add_tool(self.store.get)
37
+ self.config = config # Store config at base level for consistency
38
+ self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
43
39
 
44
40
  @abstractmethod
45
- def _load_apps(self):
41
+ def _load_apps(self) -> None:
42
+ """Load and register applications."""
46
43
  pass
47
44
 
48
- async def call_tool(self, name: str, arguments: dict[str, Any]):
49
- """Call a tool by name with arguments."""
50
- logger.info(f"Calling tool: {name} with arguments: {arguments}")
51
- try:
52
- result = await super().call_tool(name, arguments)
53
- logger.info(f"Tool {name} completed successfully")
45
+ def add_tool(self, tool: Callable) -> None:
46
+ """Add a tool to the server.
47
+
48
+ Args:
49
+ tool: Tool to add
50
+ """
51
+ self._tool_manager.add_tool(tool)
52
+
53
+ async def list_tools(self) -> list[dict]:
54
+ """List all available tools in MCP format.
55
+
56
+ Returns:
57
+ List of tool definitions
58
+ """
59
+ return self._tool_manager.list_tools(format="mcp")
60
+
61
+ def _format_tool_result(self, result: Any) -> list[TextContent]:
62
+ """Format tool result into TextContent list.
63
+
64
+ Args:
65
+ result: Raw tool result
66
+
67
+ Returns:
68
+ List of TextContent objects
69
+ """
70
+ if isinstance(result, str):
71
+ return [TextContent(type="text", text=result)]
72
+ elif isinstance(result, list) and all(
73
+ isinstance(item, TextContent) for item in result
74
+ ):
54
75
  return result
55
- except ToolError as e:
56
- raised_error = e.__cause__
57
- if isinstance(raised_error, NotAuthorizedError):
58
- logger.warning(
59
- f"Not authorized to call tool {name}: {raised_error.message}"
60
- )
61
- return [TextContent(type="text", text=raised_error.message)]
62
- else:
63
- logger.error(f"Error calling tool {name}: {str(e)}")
64
- raise e
76
+ else:
77
+ logger.warning(
78
+ f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
79
+ )
80
+ return [TextContent(type="text", text=str(result))]
65
81
 
82
+ async def call_tool(
83
+ self, name: str, arguments: dict[str, Any]
84
+ ) -> list[TextContent]:
85
+ """Call a tool with comprehensive error handling.
66
86
 
67
- class LocalServer(Server):
68
- """
69
- Local server for development purposes
87
+ Args:
88
+ name: Tool name
89
+ arguments: Tool arguments
90
+
91
+ Returns:
92
+ List of TextContent results
93
+
94
+ Raises:
95
+ ToolError: If tool execution fails
96
+ """
97
+ logger.info(f"Calling tool: {name} with arguments: {arguments}")
98
+ result = await self._tool_manager.call_tool(name, arguments)
99
+ logger.info(f"Tool '{name}' completed successfully")
100
+ return self._format_tool_result(result)
101
+
102
+
103
+ class LocalServer(BaseServer):
104
+ """Local development server implementation.
105
+
106
+ Args:
107
+ config: Server configuration
108
+ **kwargs: Additional keyword arguments passed to FastMCP
70
109
  """
71
110
 
72
- def __init__(
73
- self,
74
- apps_list: list[AppConfig] = None,
75
- **kwargs,
76
- ):
77
- if not apps_list:
78
- self.apps_list = []
79
- else:
80
- self.apps_list = apps_list
81
- super().__init__(**kwargs)
82
-
83
- def _get_store(self, store_config: StoreConfig | None):
84
- logger.info(f"Getting store: {store_config}")
85
- # No store override, use the one from the server
86
- if store_config is None:
87
- return self.store
88
- return store_from_config(store_config)
89
-
90
- def _get_integration(self, integration_config: IntegrationConfig | None):
91
- if not integration_config:
111
+ def __init__(self, config: ServerConfig, **kwargs):
112
+ super().__init__(config, **kwargs)
113
+ self.store = self._setup_store(config.store)
114
+ self._load_apps()
115
+
116
+ def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
117
+ """Setup and configure the store.
118
+
119
+ Args:
120
+ store_config: Store configuration
121
+
122
+ Returns:
123
+ Configured store instance or None if no config provided
124
+ """
125
+ if not store_config:
92
126
  return None
93
- if integration_config.type == "api_key":
94
- store = self._get_store(integration_config.store)
95
- integration = ApiKeyIntegration(integration_config.name, store=store)
96
- if integration_config.credentials:
97
- integration.set_credentials(integration_config.credentials)
98
- return integration
99
- return None
100
-
101
- def _load_app(self, app_config: AppConfig):
102
- name = app_config.name
103
- integration = self._get_integration(app_config.integration)
104
- app = app_from_slug(name)(integration=integration)
105
- return app
106
-
107
- def _load_apps(self):
108
- logger.info(f"Loading apps: {self.apps_list}")
109
- for app_config in self.apps_list:
110
- try:
111
- app = self._load_app(app_config)
112
- if app:
113
- tools = app.list_tools()
114
- for tool in tools:
115
- tool_name = tool.__name__
116
- name = app.name + "_" + tool_name
117
- description = tool.__doc__
118
- if (
119
- app_config.actions is None
120
- or len(app_config.actions) == 0
121
- or name in app_config.actions
122
- ):
123
- self.add_tool(tool, name=name, description=description)
124
- except Exception as e:
125
- logger.error(f"Error loading app {app_config.name}: {e}")
126
-
127
-
128
- class AgentRServer(Server):
129
- """
130
- AgentR server. Connects to the AgentR API to get the apps and tools. Only supports agentr integrations.
127
+
128
+ store = store_from_config(store_config)
129
+ self.add_tool(store.set)
130
+ self.add_tool(store.delete)
131
+ return store
132
+
133
+ def _load_app(self, app_config: AppConfig) -> Application | None:
134
+ """Load a single application with its integration.
135
+
136
+ Args:
137
+ app_config: Application configuration
138
+
139
+ Returns:
140
+ Configured application instance or None if loading fails
141
+ """
142
+ try:
143
+ integration = (
144
+ integration_from_config(app_config.integration, store=self.store)
145
+ if app_config.integration
146
+ else None
147
+ )
148
+ analytics.track_app_loaded(app_config.name) # Track app loading
149
+ return app_from_slug(app_config.name)(integration=integration)
150
+ except Exception as e:
151
+ logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
152
+ return None
153
+
154
+ def _load_apps(self) -> None:
155
+ """Load all configured applications."""
156
+ logger.info(f"Loading apps: {self.config.apps}")
157
+ for app_config in self.config.apps:
158
+ app = self._load_app(app_config)
159
+ if app:
160
+ self._tool_manager.register_tools_from_app(app, app_config.actions)
161
+
162
+
163
+ class AgentRServer(BaseServer):
164
+ """AgentR API-connected server implementation.
165
+
166
+ Args:
167
+ config: Server configuration
168
+ api_key: Optional API key for AgentR authentication. If not provided,
169
+ will attempt to read from AGENTR_API_KEY environment variable.
170
+ **kwargs: Additional keyword arguments passed to FastMCP
131
171
  """
132
172
 
133
- def __init__(
134
- self, name: str, description: str, api_key: str | None = None, **kwargs
135
- ):
173
+ def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
136
174
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
137
175
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
176
+
138
177
  if not self.api_key:
139
178
  raise ValueError("API key required - get one at https://agentr.dev")
140
- super().__init__(name, description=description, **kwargs)
141
-
142
- def _load_app(self, app_config: AppConfig):
143
- logger.info(f"Loading app: {app_config}")
144
- name = app_config.name
145
- if app_config.integration:
146
- integration_name = app_config.integration.name
147
- integration = AgentRIntegration(integration_name, api_key=self.api_key)
148
- else:
149
- integration = None
150
- app = app_from_slug(name)(integration=integration)
151
- return app
152
-
153
- def _list_apps_with_integrations(self) -> list[AppConfig]:
154
- # TODO: get this from the API
155
- response = httpx.get(
156
- f"{self.base_url}/api/apps/",
157
- headers={"X-API-KEY": self.api_key},
158
- timeout=10,
159
- )
160
- response.raise_for_status()
161
- apps = response.json()
179
+ parsed = urlparse(self.base_url)
180
+ if not all([parsed.scheme, parsed.netloc]):
181
+ raise ValueError(f"Invalid base URL format: {self.base_url}")
182
+ super().__init__(config, **kwargs)
183
+ self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
184
+ self._load_apps()
162
185
 
163
- logger.info(f"Loaded apps: {apps}")
164
- return [AppConfig.model_validate(app) for app in apps]
186
+ def _fetch_apps(self) -> list[AppConfig]:
187
+ """Fetch available apps from AgentR API.
165
188
 
166
- def _load_apps(self):
167
- apps = self._list_apps_with_integrations()
168
- for app_config in apps:
169
- try:
189
+ Returns:
190
+ List of application configurations
191
+
192
+ Raises:
193
+ httpx.HTTPError: If API request fails
194
+ """
195
+ try:
196
+ response = httpx.get(
197
+ f"{self.base_url}/api/apps/",
198
+ headers={"X-API-KEY": self.api_key},
199
+ timeout=10,
200
+ )
201
+ response.raise_for_status()
202
+ return [AppConfig.model_validate(app) for app in response.json()]
203
+ except httpx.HTTPError as e:
204
+ logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
205
+ raise
206
+
207
+ def _load_app(self, app_config: AppConfig) -> Application | None:
208
+ """Load a single application with AgentR integration.
209
+
210
+ Args:
211
+ app_config: Application configuration
212
+
213
+ Returns:
214
+ Configured application instance or None if loading fails
215
+ """
216
+ try:
217
+ integration = (
218
+ AgentRIntegration(
219
+ name=app_config.integration.name, api_key=self.api_key
220
+ )
221
+ if app_config.integration
222
+ else None
223
+ )
224
+ analytics.track_app_loaded(app_config.name) # Track app loading
225
+ return app_from_slug(app_config.name)(integration=integration)
226
+ except Exception as e:
227
+ logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
228
+ return None
229
+
230
+ def _load_apps(self) -> None:
231
+ """Load all apps available from AgentR."""
232
+ try:
233
+ for app_config in self._fetch_apps():
170
234
  app = self._load_app(app_config)
171
235
  if app:
172
- tools = app.list_tools()
173
- for tool in tools:
174
- tool_name = tool.__name__
175
- name = app.name + "_" + tool_name
176
- description = tool.__doc__
177
- if (
178
- app_config.actions is None
179
- or len(app_config.actions) == 0
180
- or name in app_config.actions
181
- ):
182
- self.add_tool(tool, name=name, description=description)
183
- except Exception as e:
184
- logger.error(f"Error loading app {app_config.name}: {e}")
236
+ self._tool_manager.register_tools_from_app(app, app_config.actions)
237
+ except Exception:
238
+ logger.error("Failed to load apps", exc_info=True)
239
+ raise
@@ -1,5 +1,10 @@
1
1
  from universal_mcp.config import StoreConfig
2
- from universal_mcp.stores.store import EnvironmentStore, KeyringStore, MemoryStore
2
+ from universal_mcp.stores.store import (
3
+ BaseStore,
4
+ EnvironmentStore,
5
+ KeyringStore,
6
+ MemoryStore,
7
+ )
3
8
 
4
9
 
5
10
  def store_from_config(store_config: StoreConfig):
@@ -13,4 +18,4 @@ def store_from_config(store_config: StoreConfig):
13
18
  raise ValueError(f"Invalid store type: {store_config.type}")
14
19
 
15
20
 
16
- __all__ = [MemoryStore, EnvironmentStore, KeyringStore]
21
+ __all__ = [BaseStore, MemoryStore, EnvironmentStore, KeyringStore]