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