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.
- universal_mcp/agentr/README.md +201 -0
- universal_mcp/agentr/__init__.py +6 -0
- universal_mcp/agentr/agentr.py +30 -0
- universal_mcp/{utils/agentr.py → agentr/client.py} +19 -3
- universal_mcp/agentr/integration.py +104 -0
- universal_mcp/agentr/registry.py +91 -0
- universal_mcp/agentr/server.py +51 -0
- universal_mcp/agents/__init__.py +6 -0
- universal_mcp/agents/auto.py +576 -0
- universal_mcp/agents/base.py +88 -0
- universal_mcp/agents/cli.py +27 -0
- universal_mcp/agents/codeact/__init__.py +243 -0
- universal_mcp/agents/codeact/sandbox.py +27 -0
- universal_mcp/agents/codeact/test.py +15 -0
- universal_mcp/agents/codeact/utils.py +61 -0
- universal_mcp/agents/hil.py +104 -0
- universal_mcp/agents/llm.py +10 -0
- universal_mcp/agents/react.py +58 -0
- universal_mcp/agents/simple.py +40 -0
- universal_mcp/agents/utils.py +111 -0
- universal_mcp/analytics.py +5 -7
- universal_mcp/applications/__init__.py +42 -75
- universal_mcp/applications/application.py +1 -1
- universal_mcp/applications/sample/app.py +245 -0
- universal_mcp/cli.py +10 -3
- universal_mcp/config.py +33 -7
- universal_mcp/exceptions.py +4 -0
- universal_mcp/integrations/__init__.py +0 -15
- universal_mcp/integrations/integration.py +9 -91
- universal_mcp/servers/__init__.py +2 -14
- universal_mcp/servers/server.py +10 -51
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +20 -11
- universal_mcp/tools/manager.py +29 -56
- universal_mcp/tools/registry.py +41 -0
- universal_mcp/tools/tools.py +22 -1
- universal_mcp/types.py +10 -0
- universal_mcp/utils/common.py +245 -0
- universal_mcp/utils/openapi/api_generator.py +46 -18
- universal_mcp/utils/openapi/cli.py +445 -19
- universal_mcp/utils/openapi/openapi.py +284 -21
- universal_mcp/utils/openapi/postprocessor.py +275 -0
- universal_mcp/utils/openapi/preprocessor.py +1 -1
- universal_mcp/utils/openapi/test_generator.py +287 -0
- universal_mcp/utils/prompts.py +188 -341
- universal_mcp/utils/testing.py +190 -2
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/METADATA +17 -3
- universal_mcp-0.1.24rc4.dist-info/RECORD +71 -0
- universal_mcp/applications/sample_tool_app.py +0 -80
- universal_mcp/client/agents/__init__.py +0 -4
- universal_mcp/client/agents/base.py +0 -38
- universal_mcp/client/agents/llm.py +0 -115
- universal_mcp/client/agents/react.py +0 -67
- universal_mcp/client/cli.py +0 -181
- universal_mcp-0.1.24rc2.dist-info/RECORD +0 -53
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/entry_points.txt +0 -0
- {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.
|
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"]
|
universal_mcp/servers/server.py
CHANGED
@@ -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,
|
9
|
-
from universal_mcp.config import
|
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
|
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
|
-
|
43
|
-
integration =
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
|
universal_mcp/tools/__init__.py
CHANGED
@@ -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",
|
universal_mcp/tools/adapters.py
CHANGED
@@ -1,35 +1,44 @@
|
|
1
|
-
from
|
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:
|
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,
|
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)
|
universal_mcp/tools/manager.py
CHANGED
@@ -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
|
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
|
-
"""
|
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.
|
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}
|
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]
|
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
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
self.add_tool(tool
|
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
|
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
|
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
|
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
|
universal_mcp/tools/tools.py
CHANGED
@@ -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
|
)
|