universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc3__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 (69) hide show
  1. universal_mcp/agentr/__init__.py +6 -0
  2. universal_mcp/agentr/agentr.py +30 -0
  3. universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
  4. universal_mcp/agentr/integration.py +104 -0
  5. universal_mcp/agentr/registry.py +91 -0
  6. universal_mcp/agentr/server.py +51 -0
  7. universal_mcp/agents/__init__.py +6 -0
  8. universal_mcp/agents/auto.py +576 -0
  9. universal_mcp/agents/base.py +88 -0
  10. universal_mcp/agents/cli.py +27 -0
  11. universal_mcp/agents/codeact/__init__.py +243 -0
  12. universal_mcp/agents/codeact/sandbox.py +27 -0
  13. universal_mcp/agents/codeact/test.py +15 -0
  14. universal_mcp/agents/codeact/utils.py +61 -0
  15. universal_mcp/agents/hil.py +104 -0
  16. universal_mcp/agents/llm.py +10 -0
  17. universal_mcp/agents/react.py +58 -0
  18. universal_mcp/agents/simple.py +40 -0
  19. universal_mcp/agents/utils.py +111 -0
  20. universal_mcp/analytics.py +44 -14
  21. universal_mcp/applications/__init__.py +42 -75
  22. universal_mcp/applications/application.py +187 -133
  23. universal_mcp/applications/sample/app.py +245 -0
  24. universal_mcp/cli.py +14 -231
  25. universal_mcp/client/oauth.py +122 -18
  26. universal_mcp/client/token_store.py +62 -3
  27. universal_mcp/client/{client.py → transport.py} +127 -48
  28. universal_mcp/config.py +189 -49
  29. universal_mcp/exceptions.py +54 -6
  30. universal_mcp/integrations/__init__.py +0 -18
  31. universal_mcp/integrations/integration.py +185 -168
  32. universal_mcp/servers/__init__.py +2 -14
  33. universal_mcp/servers/server.py +84 -258
  34. universal_mcp/stores/store.py +126 -93
  35. universal_mcp/tools/__init__.py +3 -0
  36. universal_mcp/tools/adapters.py +20 -11
  37. universal_mcp/tools/func_metadata.py +1 -1
  38. universal_mcp/tools/manager.py +38 -53
  39. universal_mcp/tools/registry.py +41 -0
  40. universal_mcp/tools/tools.py +24 -3
  41. universal_mcp/types.py +10 -0
  42. universal_mcp/utils/common.py +245 -0
  43. universal_mcp/utils/installation.py +3 -4
  44. universal_mcp/utils/openapi/api_generator.py +71 -17
  45. universal_mcp/utils/openapi/api_splitter.py +0 -1
  46. universal_mcp/utils/openapi/cli.py +669 -0
  47. universal_mcp/utils/openapi/filters.py +114 -0
  48. universal_mcp/utils/openapi/openapi.py +315 -23
  49. universal_mcp/utils/openapi/postprocessor.py +275 -0
  50. universal_mcp/utils/openapi/preprocessor.py +63 -8
  51. universal_mcp/utils/openapi/test_generator.py +287 -0
  52. universal_mcp/utils/prompts.py +634 -0
  53. universal_mcp/utils/singleton.py +4 -1
  54. universal_mcp/utils/testing.py +196 -8
  55. universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
  56. universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
  57. universal_mcp/applications/README.md +0 -122
  58. universal_mcp/client/__main__.py +0 -30
  59. universal_mcp/client/agent.py +0 -96
  60. universal_mcp/integrations/README.md +0 -25
  61. universal_mcp/servers/README.md +0 -79
  62. universal_mcp/stores/README.md +0 -74
  63. universal_mcp/tools/README.md +0 -86
  64. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  65. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  66. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  67. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
  68. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
  69. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/licenses/LICENSE +0 -0
@@ -1,87 +1,97 @@
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
- from universal_mcp.applications import BaseApplication, app_from_slug
11
- from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
8
+ from universal_mcp.applications import BaseApplication, app_from_config
9
+ from universal_mcp.config import ServerConfig
12
10
  from universal_mcp.exceptions import ConfigurationError, ToolError
13
- from universal_mcp.integrations import AgentRIntegration, integration_from_config
14
- from universal_mcp.stores import BaseStore, store_from_config
11
+ from universal_mcp.integrations.integration import ApiKeyIntegration, OAuthIntegration
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
- from universal_mcp.utils.agentr import AgentrClient
18
15
 
16
+ # --- Loader Implementations ---
19
17
 
20
- class BaseServer(FastMCP):
21
- """Base server class with common functionality.
22
18
 
23
- This class provides core server functionality including store setup,
24
- tool management, and application loading.
19
+ def load_from_local_config(config: ServerConfig, tool_manager: ToolManager) -> None:
20
+ """Load apps and store from local config, register their tools."""
21
+ # Setup store if present
22
+ if config.store:
23
+ try:
24
+ store = store_from_config(config.store)
25
+ tool_manager.add_tool(store.set)
26
+ tool_manager.add_tool(store.delete)
27
+ logger.info(f"Store loaded: {config.store.type}")
28
+ except Exception as e:
29
+ logger.error(f"Failed to setup store: {e}", exc_info=True)
30
+ raise ConfigurationError(f"Store setup failed: {str(e)}") from e
25
31
 
26
- Args:
27
- config: Server configuration
28
- **kwargs: Additional keyword arguments passed to FastMCP
29
- """
32
+ # Load apps
33
+ if not config.apps:
34
+ logger.warning("No applications configured in local config")
35
+ return
36
+
37
+ for app_config in config.apps:
38
+ try:
39
+ integration = None
40
+ if app_config.integration:
41
+ if app_config.integration.type == "api_key":
42
+ integration = ApiKeyIntegration(config.name, store=store, **app_config.integration.credentials)
43
+ elif app_config.integration.type == "oauth":
44
+ integration = OAuthIntegration(config.name, store=store, **app_config.integration.credentials)
45
+ else:
46
+ raise ValueError(f"Unsupported integration type: {app_config.integration.type}")
47
+ app = app_from_config(app_config)(integration=integration)
48
+ tool_manager.register_tools_from_app(app, app_config.actions)
49
+ logger.info(f"Loaded app: {app_config.name}")
50
+ except Exception as e:
51
+ logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
52
+
53
+
54
+ def load_from_application(app_instance: BaseApplication, tool_manager: ToolManager) -> None:
55
+ """Register all tools from a single application instance."""
56
+ tool_manager.register_tools_from_app(app_instance, tags=["all"])
57
+ logger.info(f"Loaded tools from application: {app_instance.name}")
58
+
59
+
60
+ # --- Server Implementations ---
61
+
62
+
63
+ class BaseServer(FastMCP):
64
+ """Base server for Universal MCP, manages ToolManager and tool invocation."""
30
65
 
31
66
  def __init__(self, config: ServerConfig, tool_manager: ToolManager | None = None, **kwargs):
32
67
  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}")
68
+ super().__init__(config.name, config.description, port=config.port, **kwargs) # type: ignore
35
69
  self.config = config
36
- self._tool_manager = tool_manager or ToolManager(warn_on_duplicate_tools=True)
37
- # Validate config after setting attributes to ensure proper initialization
70
+ self._tool_manager = tool_manager
38
71
  ServerConfig.model_validate(config)
39
72
  except Exception as e:
40
73
  logger.error(f"Failed to initialize server: {e}", exc_info=True)
41
74
  raise ConfigurationError(f"Server initialization failed: {str(e)}") from e
42
75
 
43
- def add_tool(self, tool: Callable) -> None:
44
- """Add a tool to the server.
45
-
46
- Args:
47
- tool: Tool to add
76
+ @property
77
+ def tool_manager(self) -> ToolManager:
78
+ if self._tool_manager is None:
79
+ self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
80
+ return self._tool_manager
48
81
 
49
- Raises:
50
- ValueError: If tool is invalid
51
- """
52
- self._tool_manager.add_tool(tool)
82
+ def add_tool(self, fn: Callable, name: str | None = None, description: str | None = None) -> None:
83
+ self.tool_manager.add_tool(fn, name)
53
84
 
54
- async def list_tools(self) -> list:
55
- """List all available tools in MCP format.
56
-
57
- Returns:
58
- List of tool definitions
59
- """
60
- return self._tool_manager.list_tools(format=ToolFormat.MCP)
85
+ async def list_tools(self) -> list: # type: ignore
86
+ return self.tool_manager.list_tools(format=ToolFormat.MCP)
61
87
 
62
88
  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
89
  if not name:
77
90
  raise ValueError("Tool name is required")
78
91
  if not isinstance(arguments, dict):
79
92
  raise ValueError("Arguments must be a dictionary")
80
-
81
- logger.info(f"Calling tool: {name} with arguments: {arguments}")
82
93
  try:
83
- result = await self._tool_manager.call_tool(name, arguments)
84
- logger.info(f"Tool '{name}' completed successfully")
94
+ result = await self.tool_manager.call_tool(name, arguments)
85
95
  return format_to_mcp_result(result)
86
96
  except Exception as e:
87
97
  logger.error(f"Tool '{name}' failed: {e}", exc_info=True)
@@ -89,215 +99,24 @@ class BaseServer(FastMCP):
89
99
 
90
100
 
91
101
  class LocalServer(BaseServer):
92
- """Local development server implementation.
93
-
94
- Args:
95
- config: Server configuration
96
- **kwargs: Additional keyword arguments passed to FastMCP
97
- """
102
+ """Server that loads apps and store from local config."""
98
103
 
99
104
  def __init__(self, config: ServerConfig, **kwargs):
100
105
  super().__init__(config, **kwargs)
101
- self.store = self._setup_store(config.store)
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
106
+ self._tools_loaded = False
112
107
 
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
159
-
160
- def _load_apps(self) -> None:
161
- """Load all configured applications with graceful degradation."""
162
- if not self.config.apps:
163
- logger.warning("No applications configured")
164
- return
165
-
166
- logger.info(f"Loading {len(self.config.apps)} apps")
167
- loaded_apps = 0
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")
190
-
191
-
192
- class AgentRServer(BaseServer):
193
- """AgentR API-connected server implementation.
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
- """
203
-
204
- def __init__(self, config: ServerConfig, **kwargs):
205
- super().__init__(config, **kwargs)
206
- self.api_key = config.api_key.get_secret_value() if config.api_key else None
207
- if not self.api_key:
208
- raise ValueError("API key is required for AgentR server")
209
- logger.info(f"Initializing AgentR server with API key: {self.api_key} and base URL: {config.base_url}")
210
- self.client = AgentrClient(api_key=self.api_key, base_url=config.base_url)
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, base_url=self.config.base_url)
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
258
-
259
- def _load_apps(self) -> None:
260
- """Load all apps available from AgentR with graceful degradation."""
261
- try:
262
- app_configs = self._fetch_apps()
263
- if not app_configs:
264
- logger.warning("No apps found from AgentR API")
265
- return
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 as e:
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(f"Server will start with limited functionality due to app loading failures: {e}")
108
+ @property
109
+ def tool_manager(self) -> ToolManager:
110
+ if self._tool_manager is None:
111
+ self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
112
+ if not getattr(self, "_tools_loaded", False):
113
+ load_from_local_config(self.config, self._tool_manager)
114
+ self._tools_loaded = True
115
+ return self._tool_manager
283
116
 
284
117
 
285
118
  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
- """
119
+ """Server for a single, pre-configured application."""
301
120
 
302
121
  def __init__(
303
122
  self,
@@ -305,13 +124,20 @@ class SingleMCPServer(BaseServer):
305
124
  config: ServerConfig | None = None,
306
125
  **kwargs,
307
126
  ):
308
- if not app_instance:
309
- raise ValueError("app_instance is required for SingleMCPServer")
310
-
311
127
  config = config or ServerConfig(
312
128
  type="local",
313
129
  name=f"{app_instance.name.title()} MCP Server for Local Development",
314
130
  description=f"Minimal MCP server for the local {app_instance.name} application.",
315
131
  )
316
132
  super().__init__(config, **kwargs)
317
- self._tool_manager.register_tools_from_app(app_instance, tags="all")
133
+ self.app_instance = app_instance
134
+ self._tools_loaded = False
135
+
136
+ @property
137
+ def tool_manager(self) -> ToolManager:
138
+ if self._tool_manager is None:
139
+ self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
140
+ if not self._tools_loaded:
141
+ load_from_application(self.app_instance, self._tool_manager)
142
+ self._tools_loaded = True
143
+ return self._tool_manager