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.
- universal_mcp/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/application.py +28 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/app.py +1207 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3704 -0
- universal_mcp/applications/e2b/app.py +12 -7
- universal_mcp/applications/firecrawl/app.py +27 -0
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -127
- universal_mcp/applications/google_docs/app.py +48 -35
- universal_mcp/applications/google_drive/app.py +119 -96
- universal_mcp/applications/google_mail/app.py +124 -34
- universal_mcp/applications/google_sheet/app.py +90 -74
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +16 -14
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -5
- universal_mcp/applications/serpapi/app.py +11 -4
- universal_mcp/applications/tavily/app.py +11 -8
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1384 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1446 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/integration.py +133 -28
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +205 -150
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/METADATA +8 -4
- universal_mcp-0.1.8rc3.dist-info/RECORD +75 -0
- universal_mcp-0.1.8rc1.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/entry_points.txt +0 -0
universal_mcp/servers/server.py
CHANGED
@@ -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.
|
12
|
-
from universal_mcp.
|
13
|
-
from universal_mcp.
|
14
|
-
from universal_mcp.integrations import AgentRIntegration,
|
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
|
19
|
-
"""
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
49
|
-
"""
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if store_config
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
AgentR
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
name =
|
145
|
-
|
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
|
-
|
164
|
-
|
186
|
+
def _fetch_apps(self) -> list[AppConfig]:
|
187
|
+
"""Fetch available apps from AgentR API.
|
165
188
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
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]
|