universal-mcp 0.1.24rc2__py3-none-any.whl → 0.1.24rc4__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 (58) hide show
  1. universal_mcp/agentr/README.md +201 -0
  2. universal_mcp/agentr/__init__.py +6 -0
  3. universal_mcp/agentr/agentr.py +30 -0
  4. universal_mcp/{utils/agentr.py → agentr/client.py} +19 -3
  5. universal_mcp/agentr/integration.py +104 -0
  6. universal_mcp/agentr/registry.py +91 -0
  7. universal_mcp/agentr/server.py +51 -0
  8. universal_mcp/agents/__init__.py +6 -0
  9. universal_mcp/agents/auto.py +576 -0
  10. universal_mcp/agents/base.py +88 -0
  11. universal_mcp/agents/cli.py +27 -0
  12. universal_mcp/agents/codeact/__init__.py +243 -0
  13. universal_mcp/agents/codeact/sandbox.py +27 -0
  14. universal_mcp/agents/codeact/test.py +15 -0
  15. universal_mcp/agents/codeact/utils.py +61 -0
  16. universal_mcp/agents/hil.py +104 -0
  17. universal_mcp/agents/llm.py +10 -0
  18. universal_mcp/agents/react.py +58 -0
  19. universal_mcp/agents/simple.py +40 -0
  20. universal_mcp/agents/utils.py +111 -0
  21. universal_mcp/analytics.py +5 -7
  22. universal_mcp/applications/__init__.py +42 -75
  23. universal_mcp/applications/application.py +1 -1
  24. universal_mcp/applications/sample/app.py +245 -0
  25. universal_mcp/cli.py +10 -3
  26. universal_mcp/config.py +33 -7
  27. universal_mcp/exceptions.py +4 -0
  28. universal_mcp/integrations/__init__.py +0 -15
  29. universal_mcp/integrations/integration.py +9 -91
  30. universal_mcp/servers/__init__.py +2 -14
  31. universal_mcp/servers/server.py +10 -51
  32. universal_mcp/tools/__init__.py +3 -0
  33. universal_mcp/tools/adapters.py +20 -11
  34. universal_mcp/tools/manager.py +29 -56
  35. universal_mcp/tools/registry.py +41 -0
  36. universal_mcp/tools/tools.py +22 -1
  37. universal_mcp/types.py +10 -0
  38. universal_mcp/utils/common.py +245 -0
  39. universal_mcp/utils/openapi/api_generator.py +46 -18
  40. universal_mcp/utils/openapi/cli.py +445 -19
  41. universal_mcp/utils/openapi/openapi.py +284 -21
  42. universal_mcp/utils/openapi/postprocessor.py +275 -0
  43. universal_mcp/utils/openapi/preprocessor.py +1 -1
  44. universal_mcp/utils/openapi/test_generator.py +287 -0
  45. universal_mcp/utils/prompts.py +188 -341
  46. universal_mcp/utils/testing.py +190 -2
  47. {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/METADATA +17 -3
  48. universal_mcp-0.1.24rc4.dist-info/RECORD +71 -0
  49. universal_mcp/applications/sample_tool_app.py +0 -80
  50. universal_mcp/client/agents/__init__.py +0 -4
  51. universal_mcp/client/agents/base.py +0 -38
  52. universal_mcp/client/agents/llm.py +0 -115
  53. universal_mcp/client/agents/react.py +0 -67
  54. universal_mcp/client/cli.py +0 -181
  55. universal_mcp-0.1.24rc2.dist-info/RECORD +0 -53
  56. {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/WHEEL +0 -0
  57. {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/entry_points.txt +0 -0
  58. {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,6 @@ from loguru import logger
5
5
 
6
6
  from universal_mcp.exceptions import KeyNotFoundError, NotAuthorizedError
7
7
  from universal_mcp.stores import BaseStore, MemoryStore
8
- from universal_mcp.utils.agentr import AgentrClient
9
8
 
10
9
 
11
10
  def sanitize_api_key_name(name: str) -> str:
@@ -39,12 +38,13 @@ class Integration:
39
38
  """Initializes the Integration.
40
39
 
41
40
  Args:
42
- name (str): The unique name for this integration instance.
41
+ name (str): The unique name/identifier for this integration instance.
43
42
  store (BaseStore | None, optional): A store instance for
44
43
  persisting credentials. Defaults to `MemoryStore()`.
45
44
  """
46
45
  self.name = name
47
46
  self.store = store or MemoryStore()
47
+ self.type = ""
48
48
 
49
49
  def authorize(self) -> str | dict[str, Any]:
50
50
  """Initiates or provides details for the authorization process.
@@ -103,6 +103,12 @@ class Integration:
103
103
  """
104
104
  self.store.set(self.name, credentials)
105
105
 
106
+ def __str__(self) -> str:
107
+ return f"Integration(name={self.name}, type={self.type})"
108
+
109
+ def __repr__(self) -> str:
110
+ return self.__str__()
111
+
106
112
 
107
113
  class ApiKeyIntegration(Integration):
108
114
  """Handles integrations that use a simple API key for authentication.
@@ -262,6 +268,7 @@ class OAuthIntegration(Integration):
262
268
  **kwargs: Additional arguments passed to the parent `Integration`.
263
269
  """
264
270
  super().__init__(name, store, **kwargs)
271
+ self.type = "oauth"
265
272
  self.client_id = client_id
266
273
  self.client_secret = client_secret
267
274
  self.auth_url = auth_url
@@ -397,92 +404,3 @@ class OAuthIntegration(Integration):
397
404
  credentials = response.json()
398
405
  self.store.set(self.name, credentials)
399
406
  return credentials
400
-
401
-
402
- class AgentRIntegration(Integration):
403
- """Manages authentication and authorization via the AgentR platform.
404
-
405
- This integration uses an `AgentrClient` to interact with the AgentR API
406
- for operations like retrieving authorization URLs and fetching stored
407
- credentials. It simplifies integration with services supported by AgentR.
408
-
409
- Attributes:
410
- name (str): Name of the integration (e.g., "github", "google").
411
- store (BaseStore): Store, typically not used directly by this class
412
- as AgentR manages the primary credential storage.
413
- client (AgentrClient): Client for communicating with the AgentR API.
414
- _credentials (dict | None): Cached credentials.
415
- """
416
-
417
- def __init__(self, name: str, client: AgentrClient | None = None, **kwargs):
418
- """Initializes the AgentRIntegration.
419
-
420
- Args:
421
- name (str): The name of the service integration as configured on
422
- the AgentR platform (e.g., "github").
423
- client (AgentrClient | None, optional): The AgentR client. If not provided,
424
- a new `AgentrClient` will be created.
425
- **kwargs: Additional arguments passed to the parent `Integration`.
426
- """
427
- super().__init__(name, **kwargs)
428
- self.client = client or AgentrClient()
429
- self._credentials = None
430
-
431
- def set_credentials(self, credentials: dict[str, Any] | None = None) -> str:
432
- """Not used for direct credential setting; initiates authorization instead.
433
-
434
- For AgentR integrations, credentials are set via the AgentR platform's
435
- OAuth flow. This method effectively redirects to the `authorize` flow.
436
-
437
- Args:
438
- credentials (dict | None, optional): Not used by this implementation.
439
-
440
- Returns:
441
- str: The authorization URL or message from the `authorize()` method.
442
- """
443
- return self.authorize()
444
-
445
- @property
446
- def credentials(self):
447
- """Retrieves credentials from the AgentR API, with caching.
448
-
449
- If credentials are not cached locally (in `_credentials`), this property
450
- fetches them from the AgentR platform using `self.client.get_credentials`.
451
-
452
- Returns:
453
- dict: The credentials dictionary obtained from AgentR.
454
-
455
- Raises:
456
- NotAuthorizedError: If credentials are not found (e.g., 404 from AgentR).
457
- httpx.HTTPStatusError: For other API errors from AgentR.
458
- """
459
- if self._credentials is not None:
460
- return self._credentials
461
- self._credentials = self.client.get_credentials(self.name)
462
- return self._credentials
463
-
464
- def get_credentials(self):
465
- """Retrieves credentials from the AgentR API. Alias for `credentials` property.
466
-
467
- Returns:
468
- dict: The credentials dictionary obtained from AgentR.
469
-
470
- Raises:
471
- NotAuthorizedError: If credentials are not found.
472
- httpx.HTTPStatusError: For other API errors.
473
- """
474
- return self.credentials
475
-
476
- def authorize(self) -> str:
477
- """Retrieves the authorization URL from the AgentR platform.
478
-
479
- This URL should be presented to the user to initiate the OAuth flow
480
- managed by AgentR for the service associated with `self.name`.
481
-
482
- Returns:
483
- str: The authorization URL.
484
-
485
- Raises:
486
- httpx.HTTPStatusError: If the API request to AgentR fails.
487
- """
488
- return self.client.get_authorization_url(self.name)
@@ -1,15 +1,3 @@
1
- from universal_mcp.config import ServerConfig
2
- from universal_mcp.servers.server import AgentRServer, BaseServer, LocalServer, SingleMCPServer
1
+ from universal_mcp.servers.server import BaseServer, LocalServer, SingleMCPServer
3
2
 
4
-
5
- def server_from_config(config: ServerConfig):
6
- if config.type == "agentr":
7
- return AgentRServer(config=config, api_key=config.api_key)
8
-
9
- elif config.type == "local":
10
- return LocalServer(config=config)
11
- else:
12
- raise ValueError(f"Unsupported server type: {config.type}")
13
-
14
-
15
- __all__ = ["AgentRServer", "LocalServer", "SingleMCPServer", "BaseServer", "server_from_config"]
3
+ __all__ = ["LocalServer", "SingleMCPServer", "BaseServer"]
@@ -5,14 +5,13 @@ from loguru import logger
5
5
  from mcp.server.fastmcp import FastMCP
6
6
  from mcp.types import TextContent
7
7
 
8
- from universal_mcp.applications import BaseApplication, app_from_slug
9
- from universal_mcp.config import AppConfig, ServerConfig
8
+ from universal_mcp.applications import BaseApplication, app_from_config
9
+ from universal_mcp.config import ServerConfig
10
10
  from universal_mcp.exceptions import ConfigurationError, ToolError
11
- from universal_mcp.integrations import AgentRIntegration, integration_from_config
11
+ from universal_mcp.integrations.integration import ApiKeyIntegration, OAuthIntegration
12
12
  from universal_mcp.stores import store_from_config
13
13
  from universal_mcp.tools import ToolManager
14
14
  from universal_mcp.tools.adapters import ToolFormat, format_to_mcp_result
15
- from universal_mcp.utils.agentr import AgentrClient
16
15
 
17
16
  # --- Loader Implementations ---
18
17
 
@@ -39,39 +38,19 @@ def load_from_local_config(config: ServerConfig, tool_manager: ToolManager) -> N
39
38
  try:
40
39
  integration = None
41
40
  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)
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)
47
48
  tool_manager.register_tools_from_app(app, app_config.actions)
48
49
  logger.info(f"Loaded app: {app_config.name}")
49
50
  except Exception as e:
50
51
  logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
51
52
 
52
53
 
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
73
-
74
-
75
54
  def load_from_application(app_instance: BaseApplication, tool_manager: ToolManager) -> None:
76
55
  """Register all tools from a single application instance."""
77
56
  tool_manager.register_tools_from_app(app_instance, tags=["all"])
@@ -136,26 +115,6 @@ class LocalServer(BaseServer):
136
115
  return self._tool_manager
137
116
 
138
117
 
139
- class AgentRServer(BaseServer):
140
- """Server that loads apps from AgentR server."""
141
-
142
- def __init__(self, config: ServerConfig, **kwargs):
143
- super().__init__(config, **kwargs)
144
- self._tools_loaded = False
145
- self.api_key = config.api_key.get_secret_value() if config.api_key else None
146
- self.base_url = config.base_url
147
- self.client = AgentrClient(api_key=self.api_key, base_url=self.base_url)
148
-
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
157
-
158
-
159
118
  class SingleMCPServer(BaseServer):
160
119
  """Server for a single, pre-configured application."""
161
120
 
@@ -1,3 +1,5 @@
1
+ from universal_mcp.types import ToolFormat
2
+
1
3
  from .adapters import (
2
4
  convert_tool_to_langchain_tool,
3
5
  convert_tool_to_mcp_tool,
@@ -9,6 +11,7 @@ from .tools import Tool
9
11
  __all__ = [
10
12
  "Tool",
11
13
  "ToolManager",
14
+ "ToolFormat",
12
15
  "convert_tool_to_langchain_tool",
13
16
  "convert_tool_to_openai_tool",
14
17
  "convert_tool_to_mcp_tool",
@@ -1,35 +1,44 @@
1
- from enum import Enum
1
+ from typing import Any
2
2
 
3
3
  from loguru import logger
4
4
  from mcp.types import TextContent
5
5
 
6
6
  from universal_mcp.tools.tools import Tool
7
-
8
-
9
- class ToolFormat(str, Enum):
10
- """Supported tool formats."""
11
-
12
- MCP = "mcp"
13
- LANGCHAIN = "langchain"
14
- OPENAI = "openai"
7
+ from universal_mcp.types import ToolFormat # noqa: F401
15
8
 
16
9
 
17
10
  def convert_tool_to_mcp_tool(
18
11
  tool: Tool,
19
12
  ):
20
13
  from mcp.server.fastmcp.server import MCPTool
14
+ from mcp.types import ToolAnnotations
21
15
 
22
16
  logger.debug(f"Converting tool '{tool.name}' to MCP format")
17
+
18
+ annotations = None
19
+ annotations = None
20
+ if tool.tags:
21
+ # Only set annotation hints if present in tags
22
+ annotation_hints = ["readOnlyHint", "destructiveHint", "openWorldHint"]
23
+ annotation_kwargs = {}
24
+ for hint in annotation_hints:
25
+ if hint in tool.tags:
26
+ annotation_kwargs[hint] = True
27
+ if annotation_kwargs:
28
+ annotations = ToolAnnotations(**annotation_kwargs)
29
+
23
30
  mcp_tool = MCPTool(
24
31
  name=tool.name[:63],
25
32
  description=tool.description or "",
26
33
  inputSchema=tool.parameters,
34
+ outputSchema=tool.output_schema,
35
+ annotations=annotations,
27
36
  )
28
37
  logger.debug(f"Successfully converted tool '{tool.name}' to MCP format")
29
38
  return mcp_tool
30
39
 
31
40
 
32
- def format_to_mcp_result(result: any) -> list[TextContent]:
41
+ def format_to_mcp_result(result: Any) -> list[TextContent]:
33
42
  """Format tool result into TextContent list.
34
43
 
35
44
  Args:
@@ -69,7 +78,7 @@ def convert_tool_to_langchain_tool(
69
78
  logger.debug(f"Converting tool '{tool.name}' to LangChain format")
70
79
 
71
80
  async def call_tool(
72
- **arguments: dict[str, any],
81
+ **arguments: dict[str, Any],
73
82
  ):
74
83
  logger.debug(f"Executing LangChain tool '{tool.name}' with arguments: {arguments}")
75
84
  call_tool_result = await tool.run(arguments)
@@ -5,14 +5,14 @@ from loguru import logger
5
5
 
6
6
  from universal_mcp.analytics import analytics
7
7
  from universal_mcp.applications.application import BaseApplication
8
- from universal_mcp.exceptions import ToolError
8
+ from universal_mcp.exceptions import ToolNotFoundError
9
9
  from universal_mcp.tools.adapters import (
10
- ToolFormat,
11
10
  convert_tool_to_langchain_tool,
12
11
  convert_tool_to_mcp_tool,
13
12
  convert_tool_to_openai_tool,
14
13
  )
15
14
  from universal_mcp.tools.tools import Tool
15
+ from universal_mcp.types import ToolFormat
16
16
 
17
17
  # Constants
18
18
  DEFAULT_IMPORTANT_TAG = "important"
@@ -20,6 +20,17 @@ TOOL_NAME_SEPARATOR = "_"
20
20
  DEFAULT_APP_NAME = "common"
21
21
 
22
22
 
23
+ def _get_app_and_tool_name(tool_name: str) -> tuple[str, str]:
24
+ """Get the app name from a tool name."""
25
+ if TOOL_NAME_SEPARATOR in tool_name:
26
+ app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[0]
27
+ tool_name_without_app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[1]
28
+ else:
29
+ app_name = DEFAULT_APP_NAME
30
+ tool_name_without_app_name = tool_name
31
+ return app_name, tool_name_without_app_name
32
+
33
+
23
34
  def _filter_by_name(tools: list[Tool], tool_names: list[str] | None) -> list[Tool]:
24
35
  """Filter tools by name using simple string matching.
25
36
 
@@ -81,7 +92,8 @@ def _filter_by_tags(tools: list[Tool], tags: list[str] | None) -> list[Tool]:
81
92
 
82
93
 
83
94
  class ToolManager:
84
- """Manages FastMCP tools.
95
+ """
96
+ Manages tools
85
97
 
86
98
  This class provides functionality for registering, managing, and executing tools.
87
99
  It supports multiple tool formats and provides filtering capabilities based on names and tags.
@@ -94,7 +106,6 @@ class ToolManager:
94
106
  Args:
95
107
  warn_on_duplicate_tools: Whether to warn when duplicate tool names are detected.
96
108
  """
97
- self._tools_by_app: dict[str, dict[str, Tool]] = {}
98
109
  self._all_tools: dict[str, Tool] = {}
99
110
  self.warn_on_duplicate_tools = warn_on_duplicate_tools
100
111
  self.default_format = default_format
@@ -110,25 +121,10 @@ class ToolManager:
110
121
  """
111
122
  return self._all_tools.get(name)
112
123
 
113
- def get_tools_by_app(self, app_name: str | None = None) -> list[Tool]:
114
- """Get all tools from a specific application.
115
-
116
- Args:
117
- app_name: The name of the application to get tools from.
118
-
119
- Returns:
120
- List of tools from the specified application.
121
- """
122
- if app_name:
123
- return list(self._tools_by_app.get(app_name, {}).values())
124
- else:
125
- return list(self._all_tools.values())
126
-
127
124
  def list_tools(
128
125
  self,
129
126
  format: ToolFormat | None = None,
130
127
  tags: list[str] | None = None,
131
- app_name: str | None = None,
132
128
  tool_names: list[str] | None = None,
133
129
  ) -> list:
134
130
  """List all registered tools in the specified format.
@@ -149,12 +145,14 @@ class ToolManager:
149
145
  format = self.default_format
150
146
 
151
147
  # Start with app-specific tools or all tools
152
- tools = self.get_tools_by_app(app_name)
148
+ tools = list(self._all_tools.values())
153
149
  # Apply filters
154
150
  tools = _filter_by_tags(tools, tags)
155
151
  tools = _filter_by_name(tools, tool_names)
156
152
 
157
153
  # Convert to requested format
154
+ if format == ToolFormat.NATIVE:
155
+ return [tool.fn for tool in tools]
158
156
  if format == ToolFormat.MCP:
159
157
  return [convert_tool_to_mcp_tool(tool) for tool in tools]
160
158
  elif format == ToolFormat.LANGCHAIN:
@@ -164,15 +162,12 @@ class ToolManager:
164
162
  else:
165
163
  raise ValueError(f"Invalid format: {format}")
166
164
 
167
- def add_tool(
168
- self, fn: Callable[..., Any] | Tool, name: str | None = None, app_name: str = DEFAULT_APP_NAME
169
- ) -> Tool:
165
+ def add_tool(self, fn: Callable[..., Any] | Tool, name: str | None = None) -> Tool:
170
166
  """Add a tool to the manager.
171
167
 
172
168
  Args:
173
169
  fn: The tool function or Tool instance to add.
174
170
  name: Optional name override for the tool.
175
- app_name: Application name to group the tool under.
176
171
 
177
172
  Returns:
178
173
  The registered Tool instance.
@@ -193,17 +188,11 @@ class ToolManager:
193
188
  logger.debug(f"Tool '{tool.name}' with the same function already exists.")
194
189
  return existing
195
190
 
196
- logger.debug(f"Adding tool: {tool.name} to app: {app_name}")
191
+ logger.debug(f"Adding tool: {tool.name}")
197
192
  self._all_tools[tool.name] = tool
198
-
199
- # Group tool by application
200
- if app_name not in self._tools_by_app:
201
- self._tools_by_app[app_name] = {}
202
- self._tools_by_app[app_name][tool.name] = tool
203
-
204
193
  return tool
205
194
 
206
- def register_tools(self, tools: list[Tool], app_name: str = DEFAULT_APP_NAME) -> None:
195
+ def register_tools(self, tools: list[Tool]) -> None:
207
196
  """Register a list of tools.
208
197
 
209
198
  Args:
@@ -211,14 +200,12 @@ class ToolManager:
211
200
  app_name: Application name to group the tools under.
212
201
  """
213
202
  for tool in tools:
214
- # Add app name to tool name if not already present
215
- if app_name not in tool.name:
216
- tool.name = f"{app_name}{TOOL_NAME_SEPARATOR}{tool.name}"
203
+ app_name, tool_name = _get_app_and_tool_name(tool.name)
217
204
 
218
- if tool.name in self._all_tools:
219
- logger.warning(f"Tool '{tool.name}' already exists. Skipping registration.")
220
- continue
221
- self.add_tool(tool, app_name=app_name)
205
+ # Add prefix to tool name, if not already present
206
+ tool.name = f"{app_name}{TOOL_NAME_SEPARATOR}{tool_name}"
207
+ tool.tags.append(app_name)
208
+ self.add_tool(tool)
222
209
 
223
210
  def remove_tool(self, name: str) -> bool:
224
211
  """Remove a tool by name.
@@ -230,23 +217,13 @@ class ToolManager:
230
217
  True if the tool was removed, False if it didn't exist.
231
218
  """
232
219
  if name in self._all_tools:
233
- self._all_tools[name]
234
220
  del self._all_tools[name]
235
-
236
- # Remove from app-specific grouping if present
237
- for app_tools in self._tools_by_app.values():
238
- if name in app_tools:
239
- del app_tools[name]
240
- # PERFORMANCE: Break after finding and removing to avoid unnecessary iterations
241
- break
242
-
243
221
  return True
244
222
  return False
245
223
 
246
224
  def clear_tools(self) -> None:
247
225
  """Remove all registered tools."""
248
226
  self._all_tools.clear()
249
- self._tools_by_app.clear()
250
227
 
251
228
  def register_tools_from_app(
252
229
  self,
@@ -283,7 +260,6 @@ class ToolManager:
283
260
  try:
284
261
  tool_instance = Tool.from_function(function)
285
262
  tool_instance.name = f"{app.name}{TOOL_NAME_SEPARATOR}{tool_instance.name}"
286
- # BUG FIX: Avoid duplicate tags - check if app.name is already in tags before adding
287
263
  if app.name not in tool_instance.tags:
288
264
  tool_instance.tags.append(app.name)
289
265
  tools.append(tool_instance)
@@ -291,19 +267,16 @@ class ToolManager:
291
267
  tool_name = getattr(function, "__name__", "unknown")
292
268
  logger.error(f"Failed to create Tool from '{tool_name}' in {app.name}: {e}")
293
269
 
294
- # BUG FIX: Apply filtering logic correctly - if both tool_names and tags are provided,
295
- # we should filter by both, not use default important tag
296
270
  if tags:
297
271
  tools = _filter_by_tags(tools, tags)
298
272
 
299
273
  if tool_names:
300
274
  tools = _filter_by_name(tools, tool_names)
301
275
 
302
- # BUG FIX: Only use default important tag if NO filters are provided at all
303
276
  if not tool_names and not tags:
304
277
  tools = _filter_by_tags(tools, [DEFAULT_IMPORTANT_TAG])
305
278
 
306
- self.register_tools(tools, app_name=app.name)
279
+ self.register_tools(tools)
307
280
 
308
281
  async def call_tool(
309
282
  self,
@@ -325,11 +298,11 @@ class ToolManager:
325
298
  ToolError: If the tool is not found or execution fails.
326
299
  """
327
300
  logger.debug(f"Calling tool: {name} with arguments: {arguments}")
328
- app_name = name.split(TOOL_NAME_SEPARATOR, 1)[0] if TOOL_NAME_SEPARATOR in name else DEFAULT_APP_NAME
301
+ app_name, _ = _get_app_and_tool_name(name)
329
302
  tool = self.get_tool(name)
330
303
  if not tool:
331
304
  logger.error(f"Unknown tool: {name}")
332
- raise ToolError(f"Unknown tool: {name}")
305
+ raise ToolNotFoundError(f"Unknown tool: {name}")
333
306
  try:
334
307
  result = await tool.run(arguments, context)
335
308
  analytics.track_tool_called(name, app_name, "success")
@@ -0,0 +1,41 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class ToolRegistry(ABC):
6
+ """Abstract base class for platform-specific functionality.
7
+
8
+ This class abstracts away platform-specific operations like fetching apps,
9
+ loading actions, and managing integrations. This allows the AutoAgent to
10
+ work with different platforms without being tightly coupled to any specific one.
11
+ """
12
+
13
+ @abstractmethod
14
+ async def list_apps(self) -> list[dict[str, Any]]:
15
+ """Get list of available apps from the platform.
16
+
17
+ Returns:
18
+ Return a list of apps with their details
19
+ """
20
+ pass
21
+
22
+ @abstractmethod
23
+ async def get_app_details(self, app_id: str) -> dict[str, Any]:
24
+ """Get detailed information about a specific app.
25
+
26
+ Args:
27
+ app_id: The ID of the app to get details for
28
+
29
+ Returns:
30
+ Dictionary containing app details
31
+ """
32
+ pass
33
+
34
+ @abstractmethod
35
+ async def load_tools(self, tools: list[str]) -> None:
36
+ """Load tools from the platform and register them as tools.
37
+
38
+ Args:
39
+ tools: The list of tools to load
40
+ """
41
+ pass
@@ -3,7 +3,7 @@ from collections.abc import Callable
3
3
  from typing import Any
4
4
 
5
5
  import httpx
6
- from pydantic import BaseModel, Field
6
+ from pydantic import BaseModel, Field, create_model
7
7
 
8
8
  from universal_mcp.exceptions import NotAuthorizedError, ToolError
9
9
  from universal_mcp.tools.docstring_parser import parse_docstring
@@ -11,6 +11,22 @@ from universal_mcp.tools.docstring_parser import parse_docstring
11
11
  from .func_metadata import FuncMetadata
12
12
 
13
13
 
14
+ def _get_return_type_schema(return_annotation: Any) -> dict[str, Any] | None:
15
+ """Convert return type annotation to JSON schema using Pydantic."""
16
+ if return_annotation == inspect.Signature.empty or return_annotation == Any:
17
+ return None
18
+
19
+ try:
20
+ temp_model = create_model("ReturnTypeModel", return_value=(return_annotation, ...))
21
+
22
+ full_schema = temp_model.model_json_schema()
23
+ return_field_schema = full_schema.get("properties", {}).get("return_value")
24
+
25
+ return return_field_schema
26
+ except Exception:
27
+ return None
28
+
29
+
14
30
  class Tool(BaseModel):
15
31
  """Internal tool registration info."""
16
32
 
@@ -27,6 +43,7 @@ class Tool(BaseModel):
27
43
  )
28
44
  tags: list[str] = Field(default_factory=list, description="Tags for categorizing the tool")
29
45
  parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
46
+ output_schema: dict[str, Any] | None = Field(default=None, description="JSON schema for tool output")
30
47
  fn_metadata: FuncMetadata = Field(
31
48
  description="Metadata about the function including a pydantic model for tool arguments"
32
49
  )
@@ -53,6 +70,9 @@ class Tool(BaseModel):
53
70
  func_arg_metadata = FuncMetadata.func_metadata(fn, arg_description=parsed_doc["args"])
54
71
  parameters = func_arg_metadata.arg_model.model_json_schema()
55
72
 
73
+ sig = inspect.signature(fn)
74
+ output_schema = _get_return_type_schema(sig.return_annotation)
75
+
56
76
  simple_args_descriptions: dict[str, str] = {}
57
77
  if parsed_doc.get("args"):
58
78
  for arg_name, arg_details in parsed_doc["args"].items():
@@ -68,6 +88,7 @@ class Tool(BaseModel):
68
88
  raises_description=parsed_doc["raises"],
69
89
  tags=parsed_doc["tags"],
70
90
  parameters=parameters,
91
+ output_schema=output_schema,
71
92
  fn_metadata=func_arg_metadata,
72
93
  is_async=is_async,
73
94
  )
universal_mcp/types.py ADDED
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ToolFormat(str, Enum):
5
+ """Supported tool formats."""
6
+
7
+ NATIVE = "native"
8
+ MCP = "mcp"
9
+ LANGCHAIN = "langchain"
10
+ OPENAI = "openai"