universal-mcp 0.1.23rc1__py3-none-any.whl → 0.1.24rc2__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/analytics.py +43 -11
- universal_mcp/applications/application.py +186 -239
- universal_mcp/applications/sample_tool_app.py +80 -0
- universal_mcp/cli.py +5 -228
- universal_mcp/client/agents/__init__.py +4 -0
- universal_mcp/client/agents/base.py +38 -0
- universal_mcp/client/agents/llm.py +115 -0
- universal_mcp/client/agents/react.py +67 -0
- universal_mcp/client/cli.py +181 -0
- universal_mcp/client/oauth.py +218 -0
- universal_mcp/client/token_store.py +91 -0
- universal_mcp/client/transport.py +277 -0
- universal_mcp/config.py +201 -28
- universal_mcp/exceptions.py +50 -6
- universal_mcp/integrations/__init__.py +1 -4
- universal_mcp/integrations/integration.py +220 -121
- universal_mcp/servers/__init__.py +1 -1
- universal_mcp/servers/server.py +114 -247
- universal_mcp/stores/store.py +126 -93
- universal_mcp/tools/adapters.py +16 -0
- universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp/tools/manager.py +15 -3
- universal_mcp/tools/tools.py +2 -2
- universal_mcp/utils/agentr.py +3 -4
- universal_mcp/utils/installation.py +3 -4
- universal_mcp/utils/openapi/api_generator.py +28 -2
- universal_mcp/utils/openapi/api_splitter.py +8 -19
- universal_mcp/utils/openapi/cli.py +243 -0
- universal_mcp/utils/openapi/filters.py +114 -0
- universal_mcp/utils/openapi/openapi.py +45 -12
- universal_mcp/utils/openapi/preprocessor.py +62 -7
- universal_mcp/utils/prompts.py +787 -0
- universal_mcp/utils/singleton.py +4 -1
- universal_mcp/utils/testing.py +6 -6
- universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
- universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
- universal_mcp/applications/README.md +0 -122
- universal_mcp/integrations/README.md +0 -25
- universal_mcp/servers/README.md +0 -79
- universal_mcp/stores/README.md +0 -74
- universal_mcp/tools/README.md +0 -86
- universal_mcp-0.1.23rc1.dist-info/METADATA +0 -283
- universal_mcp-0.1.23rc1.dist-info/RECORD +0 -46
- /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
universal_mcp/servers/server.py
CHANGED
@@ -1,87 +1,118 @@
|
|
1
1
|
from collections.abc import Callable
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
import httpx
|
5
4
|
from loguru import logger
|
6
5
|
from mcp.server.fastmcp import FastMCP
|
7
6
|
from mcp.types import TextContent
|
8
|
-
from pydantic import ValidationError
|
9
7
|
|
10
8
|
from universal_mcp.applications import BaseApplication, app_from_slug
|
11
|
-
from universal_mcp.config import AppConfig, ServerConfig
|
9
|
+
from universal_mcp.config import AppConfig, ServerConfig
|
12
10
|
from universal_mcp.exceptions import ConfigurationError, ToolError
|
13
11
|
from universal_mcp.integrations import AgentRIntegration, integration_from_config
|
14
|
-
from universal_mcp.stores import
|
12
|
+
from universal_mcp.stores import store_from_config
|
15
13
|
from universal_mcp.tools import ToolManager
|
16
14
|
from universal_mcp.tools.adapters import ToolFormat, format_to_mcp_result
|
17
15
|
from universal_mcp.utils.agentr import AgentrClient
|
18
16
|
|
17
|
+
# --- Loader Implementations ---
|
18
|
+
|
19
|
+
|
20
|
+
def load_from_local_config(config: ServerConfig, tool_manager: ToolManager) -> None:
|
21
|
+
"""Load apps and store from local config, register their tools."""
|
22
|
+
# Setup store if present
|
23
|
+
if config.store:
|
24
|
+
try:
|
25
|
+
store = store_from_config(config.store)
|
26
|
+
tool_manager.add_tool(store.set)
|
27
|
+
tool_manager.add_tool(store.delete)
|
28
|
+
logger.info(f"Store loaded: {config.store.type}")
|
29
|
+
except Exception as e:
|
30
|
+
logger.error(f"Failed to setup store: {e}", exc_info=True)
|
31
|
+
raise ConfigurationError(f"Store setup failed: {str(e)}") from e
|
32
|
+
|
33
|
+
# Load apps
|
34
|
+
if not config.apps:
|
35
|
+
logger.warning("No applications configured in local config")
|
36
|
+
return
|
37
|
+
|
38
|
+
for app_config in config.apps:
|
39
|
+
try:
|
40
|
+
integration = None
|
41
|
+
if app_config.integration:
|
42
|
+
try:
|
43
|
+
integration = integration_from_config(app_config.integration, store=store if config.store else None)
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"Failed to setup integration for {app_config.name}: {e}", exc_info=True)
|
46
|
+
app = app_from_slug(app_config.name)(integration=integration)
|
47
|
+
tool_manager.register_tools_from_app(app, app_config.actions)
|
48
|
+
logger.info(f"Loaded app: {app_config.name}")
|
49
|
+
except Exception as e:
|
50
|
+
logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
|
19
51
|
|
20
|
-
class BaseServer(FastMCP):
|
21
|
-
"""Base server class with common functionality.
|
22
52
|
|
23
|
-
|
24
|
-
|
53
|
+
def load_from_agentr_server(client: AgentrClient, tool_manager: ToolManager) -> None:
|
54
|
+
"""Load apps from AgentR server and register their tools."""
|
55
|
+
try:
|
56
|
+
apps = client.fetch_apps()
|
57
|
+
for app in apps:
|
58
|
+
try:
|
59
|
+
app_config = AppConfig.model_validate(app)
|
60
|
+
integration = (
|
61
|
+
AgentRIntegration(name=app_config.integration.name, client=client) # type: ignore
|
62
|
+
if app_config.integration
|
63
|
+
else None
|
64
|
+
)
|
65
|
+
app_instance = app_from_slug(app_config.name)(integration=integration)
|
66
|
+
tool_manager.register_tools_from_app(app_instance, app_config.actions)
|
67
|
+
logger.info(f"Loaded app from AgentR: {app_config.name}")
|
68
|
+
except Exception as e:
|
69
|
+
logger.error(f"Failed to load app from AgentR: {e}", exc_info=True)
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
|
72
|
+
raise
|
25
73
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
""
|
74
|
+
|
75
|
+
def load_from_application(app_instance: BaseApplication, tool_manager: ToolManager) -> None:
|
76
|
+
"""Register all tools from a single application instance."""
|
77
|
+
tool_manager.register_tools_from_app(app_instance, tags=["all"])
|
78
|
+
logger.info(f"Loaded tools from application: {app_instance.name}")
|
79
|
+
|
80
|
+
|
81
|
+
# --- Server Implementations ---
|
82
|
+
|
83
|
+
|
84
|
+
class BaseServer(FastMCP):
|
85
|
+
"""Base server for Universal MCP, manages ToolManager and tool invocation."""
|
30
86
|
|
31
87
|
def __init__(self, config: ServerConfig, tool_manager: ToolManager | None = None, **kwargs):
|
32
88
|
try:
|
33
|
-
super().__init__(config.name, config.description, port=config.port, **kwargs)
|
34
|
-
logger.info(f"Initializing server: {config.name} ({config.type}) with store: {config.store}")
|
89
|
+
super().__init__(config.name, config.description, port=config.port, **kwargs) # type: ignore
|
35
90
|
self.config = config
|
36
|
-
self._tool_manager = tool_manager
|
37
|
-
# Validate config after setting attributes to ensure proper initialization
|
91
|
+
self._tool_manager = tool_manager
|
38
92
|
ServerConfig.model_validate(config)
|
39
93
|
except Exception as e:
|
40
94
|
logger.error(f"Failed to initialize server: {e}", exc_info=True)
|
41
95
|
raise ConfigurationError(f"Server initialization failed: {str(e)}") from e
|
42
96
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
97
|
+
@property
|
98
|
+
def tool_manager(self) -> ToolManager:
|
99
|
+
if self._tool_manager is None:
|
100
|
+
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
101
|
+
return self._tool_manager
|
48
102
|
|
49
|
-
|
50
|
-
|
51
|
-
"""
|
52
|
-
self._tool_manager.add_tool(tool)
|
103
|
+
def add_tool(self, fn: Callable, name: str | None = None, description: str | None = None) -> None:
|
104
|
+
self.tool_manager.add_tool(fn, name)
|
53
105
|
|
54
|
-
async def list_tools(self) -> list:
|
55
|
-
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
List of tool definitions
|
59
|
-
"""
|
60
|
-
return self._tool_manager.list_tools(format=ToolFormat.MCP)
|
106
|
+
async def list_tools(self) -> list: # type: ignore
|
107
|
+
return self.tool_manager.list_tools(format=ToolFormat.MCP)
|
61
108
|
|
62
109
|
async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
63
|
-
"""Call a tool with comprehensive error handling.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
name: Tool name
|
67
|
-
arguments: Tool arguments
|
68
|
-
|
69
|
-
Returns:
|
70
|
-
List of TextContent results
|
71
|
-
|
72
|
-
Raises:
|
73
|
-
ToolError: If tool execution fails
|
74
|
-
ValueError: If tool name is invalid or arguments are malformed
|
75
|
-
"""
|
76
110
|
if not name:
|
77
111
|
raise ValueError("Tool name is required")
|
78
112
|
if not isinstance(arguments, dict):
|
79
113
|
raise ValueError("Arguments must be a dictionary")
|
80
|
-
|
81
|
-
logger.info(f"Calling tool: {name} with arguments: {arguments}")
|
82
114
|
try:
|
83
|
-
result = await self.
|
84
|
-
logger.info(f"Tool '{name}' completed successfully")
|
115
|
+
result = await self.tool_manager.call_tool(name, arguments)
|
85
116
|
return format_to_mcp_result(result)
|
86
117
|
except Exception as e:
|
87
118
|
logger.error(f"Tool '{name}' failed: {e}", exc_info=True)
|
@@ -89,215 +120,44 @@ class BaseServer(FastMCP):
|
|
89
120
|
|
90
121
|
|
91
122
|
class LocalServer(BaseServer):
|
92
|
-
"""
|
93
|
-
|
94
|
-
Args:
|
95
|
-
config: Server configuration
|
96
|
-
**kwargs: Additional keyword arguments passed to FastMCP
|
97
|
-
"""
|
123
|
+
"""Server that loads apps and store from local config."""
|
98
124
|
|
99
125
|
def __init__(self, config: ServerConfig, **kwargs):
|
100
126
|
super().__init__(config, **kwargs)
|
101
|
-
self.
|
102
|
-
self._load_apps()
|
103
|
-
|
104
|
-
def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
|
105
|
-
"""Setup and configure the store.
|
106
|
-
|
107
|
-
Args:
|
108
|
-
store_config: Store configuration
|
109
|
-
|
110
|
-
Returns:
|
111
|
-
Configured store instance or None if no config provided
|
112
|
-
|
113
|
-
Raises:
|
114
|
-
ConfigurationError: If store configuration is invalid
|
115
|
-
"""
|
116
|
-
if not store_config:
|
117
|
-
logger.info("No store configuration provided")
|
118
|
-
return None
|
119
|
-
|
120
|
-
try:
|
121
|
-
store = store_from_config(store_config)
|
122
|
-
self.add_tool(store.set)
|
123
|
-
self.add_tool(store.delete)
|
124
|
-
logger.info(f"Successfully configured store: {store_config.type}")
|
125
|
-
return store
|
126
|
-
except Exception as e:
|
127
|
-
logger.error(f"Failed to setup store: {e}", exc_info=True)
|
128
|
-
raise ConfigurationError(f"Store setup failed: {str(e)}") from e
|
129
|
-
|
130
|
-
def _load_app(self, app_config: AppConfig) -> BaseApplication | None:
|
131
|
-
"""Load a single application with its integration.
|
132
|
-
|
133
|
-
Args:
|
134
|
-
app_config: Application configuration
|
135
|
-
|
136
|
-
Returns:
|
137
|
-
Configured application instance or None if loading fails
|
138
|
-
"""
|
139
|
-
if not app_config.name:
|
140
|
-
logger.error("App configuration missing name")
|
141
|
-
return None
|
142
|
-
|
143
|
-
try:
|
144
|
-
integration = None
|
145
|
-
if app_config.integration:
|
146
|
-
try:
|
147
|
-
integration = integration_from_config(app_config.integration, store=self.store)
|
148
|
-
logger.debug(f"Successfully configured integration for {app_config.name}")
|
149
|
-
except Exception as e:
|
150
|
-
logger.error(f"Failed to setup integration for {app_config.name}: {e}", exc_info=True)
|
151
|
-
# Continue without integration if it fails
|
152
|
-
|
153
|
-
app = app_from_slug(app_config.name)(integration=integration)
|
154
|
-
logger.info(f"Successfully loaded app: {app_config.name}")
|
155
|
-
return app
|
156
|
-
except Exception as e:
|
157
|
-
logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
|
158
|
-
return None
|
127
|
+
self._tools_loaded = False
|
159
128
|
|
160
|
-
|
161
|
-
|
162
|
-
if
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
failed_apps = []
|
169
|
-
|
170
|
-
for app_config in self.config.apps:
|
171
|
-
app = self._load_app(app_config)
|
172
|
-
if app:
|
173
|
-
try:
|
174
|
-
self._tool_manager.register_tools_from_app(app, app_config.actions)
|
175
|
-
loaded_apps += 1
|
176
|
-
logger.info(f"Successfully registered tools for {app_config.name}")
|
177
|
-
except Exception as e:
|
178
|
-
logger.error(f"Failed to register tools for {app_config.name}: {e}", exc_info=True)
|
179
|
-
failed_apps.append(app_config.name)
|
180
|
-
else:
|
181
|
-
failed_apps.append(app_config.name)
|
182
|
-
|
183
|
-
if failed_apps:
|
184
|
-
logger.warning(f"Failed to load {len(failed_apps)} apps: {', '.join(failed_apps)}")
|
185
|
-
|
186
|
-
if loaded_apps == 0:
|
187
|
-
logger.error("No apps were successfully loaded")
|
188
|
-
else:
|
189
|
-
logger.info(f"Successfully loaded {loaded_apps}/{len(self.config.apps)} apps")
|
129
|
+
@property
|
130
|
+
def tool_manager(self) -> ToolManager:
|
131
|
+
if self._tool_manager is None:
|
132
|
+
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
133
|
+
if not getattr(self, "_tools_loaded", False):
|
134
|
+
load_from_local_config(self.config, self._tool_manager)
|
135
|
+
self._tools_loaded = True
|
136
|
+
return self._tool_manager
|
190
137
|
|
191
138
|
|
192
139
|
class AgentRServer(BaseServer):
|
193
|
-
"""AgentR
|
194
|
-
|
195
|
-
Args:
|
196
|
-
config: Server configuration
|
197
|
-
api_key: Optional API key for AgentR authentication. If not provided,
|
198
|
-
will attempt to read from AGENTR_API_KEY environment variable.
|
199
|
-
max_retries: Maximum number of retries for API calls (default: 3)
|
200
|
-
retry_delay: Delay between retries in seconds (default: 1)
|
201
|
-
**kwargs: Additional keyword arguments passed to FastMCP
|
202
|
-
"""
|
140
|
+
"""Server that loads apps from AgentR server."""
|
203
141
|
|
204
142
|
def __init__(self, config: ServerConfig, **kwargs):
|
205
143
|
super().__init__(config, **kwargs)
|
144
|
+
self._tools_loaded = False
|
206
145
|
self.api_key = config.api_key.get_secret_value() if config.api_key else None
|
207
|
-
|
208
|
-
|
209
|
-
logger.info(f"Initializing AgentR server with API key: {self.api_key}")
|
210
|
-
self.client = AgentrClient(api_key=self.api_key)
|
211
|
-
self._load_apps()
|
212
|
-
|
213
|
-
def _fetch_apps(self) -> list[AppConfig]:
|
214
|
-
"""Fetch available apps from AgentR API with retry logic.
|
215
|
-
|
216
|
-
Returns:
|
217
|
-
List of application configurations
|
218
|
-
|
219
|
-
Raises:
|
220
|
-
httpx.HTTPError: If API request fails after all retries
|
221
|
-
ValidationError: If app configuration validation fails
|
222
|
-
"""
|
223
|
-
try:
|
224
|
-
apps = self.client.fetch_apps()
|
225
|
-
validated_apps = []
|
226
|
-
for app in apps:
|
227
|
-
try:
|
228
|
-
validated_apps.append(AppConfig.model_validate(app))
|
229
|
-
except ValidationError as e:
|
230
|
-
logger.error(f"Failed to validate app config: {e}", exc_info=True)
|
231
|
-
continue
|
232
|
-
return validated_apps
|
233
|
-
except httpx.HTTPError as e:
|
234
|
-
logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
|
235
|
-
raise
|
236
|
-
|
237
|
-
def _load_app(self, app_config: AppConfig) -> BaseApplication | None:
|
238
|
-
"""Load a single application with AgentR integration.
|
239
|
-
|
240
|
-
Args:
|
241
|
-
app_config: Application configuration
|
242
|
-
|
243
|
-
Returns:
|
244
|
-
Configured application instance or None if loading fails
|
245
|
-
"""
|
246
|
-
try:
|
247
|
-
integration = (
|
248
|
-
AgentRIntegration(name=app_config.integration.name, api_key=self.api_key)
|
249
|
-
if app_config.integration
|
250
|
-
else None
|
251
|
-
)
|
252
|
-
app = app_from_slug(app_config.name)(integration=integration)
|
253
|
-
logger.info(f"Successfully loaded app: {app_config.name}")
|
254
|
-
return app
|
255
|
-
except Exception as e:
|
256
|
-
logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
|
257
|
-
return None
|
146
|
+
self.base_url = config.base_url
|
147
|
+
self.client = AgentrClient(api_key=self.api_key, base_url=self.base_url)
|
258
148
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
loaded_apps = 0
|
268
|
-
for app_config in app_configs:
|
269
|
-
app = self._load_app(app_config)
|
270
|
-
if app:
|
271
|
-
self._tool_manager.register_tools_from_app(app, app_config.actions)
|
272
|
-
loaded_apps += 1
|
273
|
-
|
274
|
-
if loaded_apps == 0:
|
275
|
-
logger.error("Failed to load any apps from AgentR")
|
276
|
-
else:
|
277
|
-
logger.info(f"Successfully loaded {loaded_apps}/{len(app_configs)} apps from AgentR")
|
278
|
-
|
279
|
-
except Exception:
|
280
|
-
logger.error("Failed to load apps", exc_info=True)
|
281
|
-
# Don't raise the exception to allow server to start with partial functionality
|
282
|
-
logger.warning("Server will start with limited functionality due to app loading failures")
|
149
|
+
@property
|
150
|
+
def tool_manager(self) -> ToolManager:
|
151
|
+
if self._tool_manager is None:
|
152
|
+
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
153
|
+
if not self._tools_loaded:
|
154
|
+
load_from_agentr_server(self.client, self._tool_manager)
|
155
|
+
self._tools_loaded = True
|
156
|
+
return self._tool_manager
|
283
157
|
|
284
158
|
|
285
159
|
class SingleMCPServer(BaseServer):
|
286
|
-
"""
|
287
|
-
Minimal server implementation hosting a single BaseApplication instance.
|
288
|
-
|
289
|
-
This server type is intended for development and testing of a single
|
290
|
-
application's tools. It does not manage integrations or stores internally
|
291
|
-
beyond initializing the ToolManager and exposing the provided application's tools.
|
292
|
-
The application instance passed to the constructor should already be
|
293
|
-
configured with its appropriate integration (if required).
|
294
|
-
|
295
|
-
Args:
|
296
|
-
config: Server configuration (used for name, description, etc. but ignores 'apps')
|
297
|
-
app_instance: The single BaseApplication instance to host and expose its tools.
|
298
|
-
Can be None, in which case no tools will be registered.
|
299
|
-
**kwargs: Additional keyword arguments passed to FastMCP parent class.
|
300
|
-
"""
|
160
|
+
"""Server for a single, pre-configured application."""
|
301
161
|
|
302
162
|
def __init__(
|
303
163
|
self,
|
@@ -305,13 +165,20 @@ class SingleMCPServer(BaseServer):
|
|
305
165
|
config: ServerConfig | None = None,
|
306
166
|
**kwargs,
|
307
167
|
):
|
308
|
-
if not app_instance:
|
309
|
-
raise ValueError("app_instance is required for SingleMCPServer")
|
310
|
-
|
311
168
|
config = config or ServerConfig(
|
312
169
|
type="local",
|
313
170
|
name=f"{app_instance.name.title()} MCP Server for Local Development",
|
314
171
|
description=f"Minimal MCP server for the local {app_instance.name} application.",
|
315
172
|
)
|
316
173
|
super().__init__(config, **kwargs)
|
317
|
-
self.
|
174
|
+
self.app_instance = app_instance
|
175
|
+
self._tools_loaded = False
|
176
|
+
|
177
|
+
@property
|
178
|
+
def tool_manager(self) -> ToolManager:
|
179
|
+
if self._tool_manager is None:
|
180
|
+
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
181
|
+
if not self._tools_loaded:
|
182
|
+
load_from_application(self.app_instance, self._tool_manager)
|
183
|
+
self._tools_loaded = True
|
184
|
+
return self._tool_manager
|