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.
- universal_mcp/applications/application.py +6 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/app.py +954 -0
- universal_mcp/applications/e2b/app.py +18 -12
- universal_mcp/applications/firecrawl/app.py +28 -1
- universal_mcp/applications/github/app.py +150 -107
- universal_mcp/applications/google_calendar/app.py +72 -137
- universal_mcp/applications/google_docs/app.py +35 -15
- universal_mcp/applications/google_drive/app.py +84 -55
- universal_mcp/applications/google_mail/app.py +143 -53
- universal_mcp/applications/google_sheet/app.py +61 -38
- universal_mcp/applications/markitdown/app.py +12 -11
- universal_mcp/applications/notion/app.py +199 -89
- universal_mcp/applications/perplexity/app.py +17 -15
- universal_mcp/applications/reddit/app.py +110 -101
- universal_mcp/applications/resend/app.py +14 -7
- universal_mcp/applications/{serp → serpapi}/app.py +14 -7
- universal_mcp/applications/tavily/app.py +13 -10
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1044 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +986 -0
- universal_mcp/applications/zenquotes/app.py +13 -3
- universal_mcp/exceptions.py +8 -2
- universal_mcp/integrations/__init__.py +15 -1
- universal_mcp/integrations/integration.py +132 -27
- universal_mcp/servers/__init__.py +6 -15
- universal_mcp/servers/server.py +208 -149
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -42
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +40 -0
- universal_mcp/tools/func_metadata.py +214 -0
- universal_mcp/tools/tools.py +285 -0
- universal_mcp/utils/docgen.py +277 -123
- universal_mcp/utils/docstring_parser.py +156 -0
- universal_mcp/utils/openapi.py +149 -40
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.dist-info}/METADATA +8 -4
- universal_mcp-0.1.8rc2.dist-info/RECORD +71 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8rc2.dist-info}/entry_points.txt +0 -0
universal_mcp/servers/server.py
CHANGED
@@ -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,
|
13
|
-
from universal_mcp.exceptions import NotAuthorizedError
|
14
|
-
from universal_mcp.integrations import
|
15
|
-
from universal_mcp.stores import store_from_config
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
self.
|
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
|
-
|
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
|
38
|
-
return
|
39
|
-
|
40
|
-
|
41
|
-
self.add_tool(
|
42
|
-
|
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
|
-
|
49
|
-
"""
|
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
|
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
|
-
|
57
|
-
|
58
|
-
logger.warning(
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
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
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
universal_mcp/stores/__init__.py
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
from universal_mcp.config import StoreConfig
|
2
|
-
from universal_mcp.stores.store import
|
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]
|