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.
Files changed (47) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -239
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -228
  5. universal_mcp/client/agents/__init__.py +4 -0
  6. universal_mcp/client/agents/base.py +38 -0
  7. universal_mcp/client/agents/llm.py +115 -0
  8. universal_mcp/client/agents/react.py +67 -0
  9. universal_mcp/client/cli.py +181 -0
  10. universal_mcp/client/oauth.py +218 -0
  11. universal_mcp/client/token_store.py +91 -0
  12. universal_mcp/client/transport.py +277 -0
  13. universal_mcp/config.py +201 -28
  14. universal_mcp/exceptions.py +50 -6
  15. universal_mcp/integrations/__init__.py +1 -4
  16. universal_mcp/integrations/integration.py +220 -121
  17. universal_mcp/servers/__init__.py +1 -1
  18. universal_mcp/servers/server.py +114 -247
  19. universal_mcp/stores/store.py +126 -93
  20. universal_mcp/tools/adapters.py +16 -0
  21. universal_mcp/tools/func_metadata.py +1 -1
  22. universal_mcp/tools/manager.py +15 -3
  23. universal_mcp/tools/tools.py +2 -2
  24. universal_mcp/utils/agentr.py +3 -4
  25. universal_mcp/utils/installation.py +3 -4
  26. universal_mcp/utils/openapi/api_generator.py +28 -2
  27. universal_mcp/utils/openapi/api_splitter.py +8 -19
  28. universal_mcp/utils/openapi/cli.py +243 -0
  29. universal_mcp/utils/openapi/filters.py +114 -0
  30. universal_mcp/utils/openapi/openapi.py +45 -12
  31. universal_mcp/utils/openapi/preprocessor.py +62 -7
  32. universal_mcp/utils/prompts.py +787 -0
  33. universal_mcp/utils/singleton.py +4 -1
  34. universal_mcp/utils/testing.py +6 -6
  35. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  36. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  37. universal_mcp/applications/README.md +0 -122
  38. universal_mcp/integrations/README.md +0 -25
  39. universal_mcp/servers/README.md +0 -79
  40. universal_mcp/stores/README.md +0 -74
  41. universal_mcp/tools/README.md +0 -86
  42. universal_mcp-0.1.23rc1.dist-info/METADATA +0 -283
  43. universal_mcp-0.1.23rc1.dist-info/RECORD +0 -46
  44. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  45. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  46. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  47. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -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, StoreConfig
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 BaseStore, store_from_config
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
- This class provides core server functionality including store setup,
24
- tool management, and application loading.
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
- Args:
27
- config: Server configuration
28
- **kwargs: Additional keyword arguments passed to FastMCP
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 or ToolManager(warn_on_duplicate_tools=True)
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
- def add_tool(self, tool: Callable) -> None:
44
- """Add a tool to the server.
45
-
46
- Args:
47
- tool: Tool to add
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
- Raises:
50
- ValueError: If tool is invalid
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
- """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)
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._tool_manager.call_tool(name, arguments)
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
- """Local development server implementation.
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.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
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
- 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")
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 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
- """
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
- 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}")
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
- 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:
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._tool_manager.register_tools_from_app(app_instance, tags="all")
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