universal-mcp 0.1.24rc19__py3-none-any.whl → 0.1.24rc21__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 +2 -2
- universal_mcp/agentr/registry.py +60 -48
- universal_mcp/agentr/server.py +37 -36
- universal_mcp/integrations/integration.py +17 -0
- universal_mcp/servers/server.py +24 -10
- universal_mcp/tools/adapters.py +15 -1
- universal_mcp/tools/local_registry.py +109 -0
- universal_mcp/tools/manager.py +5 -64
- universal_mcp/tools/registry.py +59 -50
- universal_mcp/utils/templates/api_client.py.j2 +2 -2
- {universal_mcp-0.1.24rc19.dist-info → universal_mcp-0.1.24rc21.dist-info}/METADATA +1 -1
- {universal_mcp-0.1.24rc19.dist-info → universal_mcp-0.1.24rc21.dist-info}/RECORD +15 -14
- {universal_mcp-0.1.24rc19.dist-info → universal_mcp-0.1.24rc21.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.24rc19.dist-info → universal_mcp-0.1.24rc21.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.24rc19.dist-info → universal_mcp-0.1.24rc21.dist-info}/licenses/LICENSE +0 -0
universal_mcp/agentr/README.md
CHANGED
@@ -85,7 +85,7 @@ if apps:
|
|
85
85
|
|
86
86
|
if all_tools:
|
87
87
|
tool_id = all_tools[0]['id']
|
88
|
-
|
88
|
+
|
89
89
|
# Fetch a single tool's details
|
90
90
|
tool_details = client.get_tool(tool_id)
|
91
91
|
print(f"Fetched details for tool '{tool_id}':", tool_details)
|
@@ -207,4 +207,4 @@ async def main():
|
|
207
207
|
|
208
208
|
if __name__ == "__main__":
|
209
209
|
asyncio.run(main())
|
210
|
-
```
|
210
|
+
```
|
universal_mcp/agentr/registry.py
CHANGED
@@ -4,9 +4,10 @@ from typing import Any
|
|
4
4
|
from loguru import logger
|
5
5
|
|
6
6
|
from universal_mcp.agentr.client import AgentrClient
|
7
|
+
from universal_mcp.applications.application import BaseApplication
|
7
8
|
from universal_mcp.applications.utils import app_from_slug
|
8
|
-
from universal_mcp.exceptions import ToolError
|
9
|
-
from universal_mcp.tools.
|
9
|
+
from universal_mcp.exceptions import ToolError, ToolNotFoundError
|
10
|
+
from universal_mcp.tools.adapters import convert_tools
|
10
11
|
from universal_mcp.tools.registry import ToolRegistry
|
11
12
|
from universal_mcp.types import ToolConfig, ToolFormat
|
12
13
|
|
@@ -35,10 +36,14 @@ class AgentrRegistry(ToolRegistry):
|
|
35
36
|
|
36
37
|
def __init__(self, client: AgentrClient | None = None, **kwargs):
|
37
38
|
"""Initialize the AgentR platform manager."""
|
38
|
-
|
39
|
+
super().__init__()
|
39
40
|
self.client = client or AgentrClient(**kwargs)
|
40
|
-
|
41
|
-
|
41
|
+
|
42
|
+
def _create_app_instance(self, app_name: str) -> BaseApplication:
|
43
|
+
"""Create an app instance with an AgentrIntegration."""
|
44
|
+
app = app_from_slug(app_name)
|
45
|
+
integration = AgentrIntegration(name=app_name, client=self.client)
|
46
|
+
return app(integration=integration)
|
42
47
|
|
43
48
|
async def list_all_apps(self) -> list[dict[str, Any]]:
|
44
49
|
"""Get list of available apps from AgentR.
|
@@ -139,7 +144,7 @@ class AgentrRegistry(ToolRegistry):
|
|
139
144
|
self,
|
140
145
|
tools: list[str] | ToolConfig,
|
141
146
|
format: ToolFormat,
|
142
|
-
) ->
|
147
|
+
) -> list[Any]:
|
143
148
|
"""Export given tools to required format.
|
144
149
|
|
145
150
|
Args:
|
@@ -147,61 +152,54 @@ class AgentrRegistry(ToolRegistry):
|
|
147
152
|
format: The format to export tools to (native, mcp, langchain, openai)
|
148
153
|
|
149
154
|
Returns:
|
150
|
-
|
155
|
+
List of tools in the specified format
|
151
156
|
"""
|
157
|
+
from langchain_core.tools import StructuredTool
|
158
|
+
|
152
159
|
try:
|
153
160
|
# Clear tools from tool manager before loading new tools
|
154
161
|
self.tool_manager.clear_tools()
|
162
|
+
logger.info(f"Exporting tools to {format.value} format")
|
155
163
|
if isinstance(tools, dict):
|
156
|
-
|
157
|
-
self._load_tools_from_tool_config(tools, self.tool_manager)
|
164
|
+
self._load_tools_from_tool_config(tools)
|
158
165
|
else:
|
159
|
-
|
160
|
-
self._load_agentr_tools_from_list(tools, self.tool_manager)
|
161
|
-
loaded_tools = self.tool_manager.list_tools(format=format)
|
162
|
-
logger.info(f"Exporting {len(loaded_tools)} tools to {format} format")
|
163
|
-
return loaded_tools
|
164
|
-
except Exception as e:
|
165
|
-
logger.error(f"Error exporting tools: {e}")
|
166
|
-
return ""
|
166
|
+
self._load_tools_from_list(tools)
|
167
167
|
|
168
|
-
|
169
|
-
"""Helper method to load and register tools for an app."""
|
170
|
-
app = app_from_slug(app_name)
|
171
|
-
integration = AgentrIntegration(name=app_name, client=self.client)
|
172
|
-
app_instance = app(integration=integration)
|
173
|
-
tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
|
168
|
+
loaded_tools = self.tool_manager.get_tools()
|
174
169
|
|
175
|
-
|
176
|
-
|
170
|
+
if format != ToolFormat.LANGCHAIN:
|
171
|
+
return convert_tools(loaded_tools, format)
|
177
172
|
|
178
|
-
|
179
|
-
tools: The list of tools to load (prefixed with app name)
|
180
|
-
tool_manager: The tool manager to register tools with
|
181
|
-
"""
|
182
|
-
logger.info(f"Loading all tools: {tools}")
|
183
|
-
tools_by_app = {}
|
184
|
-
for tool_name in tools:
|
185
|
-
app_name, _ = _get_app_and_tool_name(tool_name)
|
186
|
-
tools_by_app.setdefault(app_name, []).append(tool_name)
|
173
|
+
logger.info(f"Exporting {len(loaded_tools)} tools to LangChain format with special handling")
|
187
174
|
|
188
|
-
|
189
|
-
|
175
|
+
langchain_tools = []
|
176
|
+
for tool in loaded_tools:
|
190
177
|
|
191
|
-
|
192
|
-
|
178
|
+
def create_coroutine(t):
|
179
|
+
async def call_tool_wrapper(**arguments: dict[str, Any]):
|
180
|
+
logger.debug(
|
181
|
+
f"Executing registry-wrapped LangChain tool '{t.name}' with arguments: {arguments}"
|
182
|
+
)
|
183
|
+
return await self.call_tool(t.name, arguments)
|
193
184
|
|
194
|
-
|
195
|
-
tool_config: The tool configuration containing app names and tools
|
196
|
-
tool_manager: The tool manager to register tools with
|
197
|
-
"""
|
198
|
-
for app_name, tool_names in tool_config.items():
|
199
|
-
self._load_tools(app_name, tool_names, tool_manager)
|
185
|
+
return call_tool_wrapper
|
200
186
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
187
|
+
langchain_tool = StructuredTool(
|
188
|
+
name=tool.name,
|
189
|
+
description=tool.description or "",
|
190
|
+
coroutine=create_coroutine(tool),
|
191
|
+
response_format="content",
|
192
|
+
args_schema=tool.parameters,
|
193
|
+
)
|
194
|
+
langchain_tools.append(langchain_tool)
|
195
|
+
|
196
|
+
return langchain_tools
|
197
|
+
|
198
|
+
except Exception as e:
|
199
|
+
logger.error(f"Error exporting tools: {e}")
|
200
|
+
return []
|
201
|
+
|
202
|
+
def _handle_special_output(self, data: Any) -> Any:
|
205
203
|
if isinstance(data, dict):
|
206
204
|
type_ = data.get("type")
|
207
205
|
if type_ == "image" or type_ == "audio":
|
@@ -218,6 +216,20 @@ class AgentrRegistry(ToolRegistry):
|
|
218
216
|
return response
|
219
217
|
return data
|
220
218
|
|
219
|
+
async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> dict[str, Any]:
|
220
|
+
"""Call a tool with the given name and arguments."""
|
221
|
+
logger.debug(f"Calling tool: {tool_name} with arguments: {tool_args}")
|
222
|
+
tool = self.tool_manager.get_tool(tool_name)
|
223
|
+
if not tool:
|
224
|
+
logger.error(f"Unknown tool: {tool_name}")
|
225
|
+
raise ToolNotFoundError(f"Unknown tool: {tool_name}")
|
226
|
+
try:
|
227
|
+
data = await tool.run(tool_args)
|
228
|
+
logger.debug(f"Tool {tool_name} called with args {tool_args} and returned {data}")
|
229
|
+
return self._handle_special_output(data)
|
230
|
+
except Exception as e:
|
231
|
+
raise e
|
232
|
+
|
221
233
|
async def list_connected_apps(self) -> list[str]:
|
222
234
|
"""List all apps that the user has connected."""
|
223
235
|
return self.client.list_my_connections()
|
universal_mcp/agentr/server.py
CHANGED
@@ -1,51 +1,52 @@
|
|
1
1
|
from loguru import logger
|
2
2
|
|
3
|
-
from universal_mcp.
|
4
|
-
from universal_mcp.config import AppConfig, ServerConfig
|
3
|
+
from universal_mcp.config import ServerConfig
|
5
4
|
from universal_mcp.servers.server import BaseServer
|
6
5
|
from universal_mcp.tools import ToolManager
|
7
6
|
|
8
7
|
from .client import AgentrClient
|
9
|
-
from .
|
10
|
-
|
11
|
-
|
12
|
-
def load_from_agentr_server(client: AgentrClient, tool_manager: ToolManager) -> None:
|
13
|
-
"""Load apps from AgentR server and register their tools."""
|
14
|
-
try:
|
15
|
-
apps = client.fetch_apps()
|
16
|
-
for app in apps:
|
17
|
-
try:
|
18
|
-
app_config = AppConfig.model_validate(app)
|
19
|
-
integration = (
|
20
|
-
AgentrIntegration(name=app_config.integration.name, client=client) # type: ignore
|
21
|
-
if app_config.integration
|
22
|
-
else None
|
23
|
-
)
|
24
|
-
app_instance = app_from_config(app_config)(integration=integration)
|
25
|
-
tool_manager.register_tools_from_app(app_instance, app_config.actions)
|
26
|
-
logger.info(f"Loaded app from AgentR: {app_config.name}")
|
27
|
-
except Exception as e:
|
28
|
-
logger.error(f"Failed to load app from AgentR: {e}", exc_info=True)
|
29
|
-
except Exception as e:
|
30
|
-
logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
|
31
|
-
raise
|
8
|
+
from .registry import AgentrRegistry
|
32
9
|
|
33
10
|
|
34
11
|
class AgentrServer(BaseServer):
|
35
|
-
"""Server that loads apps from AgentR
|
12
|
+
"""Server that loads apps and tools from an AgentR instance."""
|
36
13
|
|
37
|
-
def __init__(self, config: ServerConfig, **kwargs):
|
14
|
+
def __init__(self, config: ServerConfig, registry: AgentrRegistry | None = None, **kwargs):
|
38
15
|
super().__init__(config, **kwargs)
|
16
|
+
|
17
|
+
if registry:
|
18
|
+
self.registry = registry
|
19
|
+
else:
|
20
|
+
api_key = config.api_key.get_secret_value() if config.api_key else None
|
21
|
+
client = AgentrClient(api_key=api_key, base_url=config.base_url)
|
22
|
+
self.registry = AgentrRegistry(client=client)
|
23
|
+
|
39
24
|
self._tools_loaded = False
|
40
|
-
self.
|
41
|
-
|
42
|
-
|
25
|
+
self._load_all_tools_from_remote()
|
26
|
+
|
27
|
+
def _load_all_tools_from_remote(self):
|
28
|
+
"""Load all available tools from the remote AgentR server."""
|
29
|
+
if self._tools_loaded:
|
30
|
+
return
|
31
|
+
|
32
|
+
logger.info("Loading all available tools from AgentR server...")
|
33
|
+
try:
|
34
|
+
all_apps = self.registry.client.list_all_apps()
|
35
|
+
if not all_apps:
|
36
|
+
logger.warning("No apps found on AgentR server.")
|
37
|
+
self._tools_loaded = True
|
38
|
+
return
|
39
|
+
|
40
|
+
# Create a tool config to load all default (important) tools for each app
|
41
|
+
tool_config = {app["name"]: [] for app in all_apps}
|
42
|
+
|
43
|
+
self.registry._load_tools_from_tool_config(tool_config)
|
44
|
+
self._tools_loaded = True
|
45
|
+
logger.info(f"Finished loading tools for {len(all_apps)} app(s) from AgentR server.")
|
46
|
+
except Exception as e:
|
47
|
+
logger.error(f"Failed to load tools from AgentR: {e}", exc_info=True)
|
48
|
+
raise
|
43
49
|
|
44
50
|
@property
|
45
51
|
def tool_manager(self) -> ToolManager:
|
46
|
-
|
47
|
-
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
48
|
-
if not self._tools_loaded:
|
49
|
-
load_from_agentr_server(self.client, self._tool_manager)
|
50
|
-
self._tools_loaded = True
|
51
|
-
return self._tool_manager
|
52
|
+
return self.registry.tool_manager
|
@@ -404,3 +404,20 @@ class OAuthIntegration(Integration):
|
|
404
404
|
credentials = response.json()
|
405
405
|
self.store.set(self.name, credentials)
|
406
406
|
return credentials
|
407
|
+
|
408
|
+
|
409
|
+
class IntegrationFactory:
|
410
|
+
"""A factory for creating integration instances."""
|
411
|
+
|
412
|
+
@staticmethod
|
413
|
+
def create(app_name: str, integration_type: str = "api_key", **kwargs) -> "Integration":
|
414
|
+
"""Create an integration instance."""
|
415
|
+
if integration_type == "api_key":
|
416
|
+
return ApiKeyIntegration(app_name, **kwargs)
|
417
|
+
elif integration_type == "oauth":
|
418
|
+
return OAuthIntegration(app_name, **kwargs)
|
419
|
+
# Add other integration types here
|
420
|
+
else:
|
421
|
+
# Return a default or generic integration if type is unknown
|
422
|
+
logger.warning(f"Unknown integration type '{integration_type}'. Using a default integration.")
|
423
|
+
return Integration(app_name, **kwargs)
|
universal_mcp/servers/server.py
CHANGED
@@ -12,7 +12,8 @@ from universal_mcp.exceptions import ConfigurationError, ToolError
|
|
12
12
|
from universal_mcp.integrations.integration import ApiKeyIntegration, OAuthIntegration
|
13
13
|
from universal_mcp.stores import store_from_config
|
14
14
|
from universal_mcp.tools import ToolManager
|
15
|
-
from universal_mcp.tools.adapters import
|
15
|
+
from universal_mcp.tools.adapters import convert_tool_to_mcp_tool, format_to_mcp_result
|
16
|
+
from universal_mcp.tools.local_registry import LocalRegistry
|
16
17
|
|
17
18
|
# --- Loader Implementations ---
|
18
19
|
|
@@ -69,6 +70,7 @@ class BaseServer(FastMCP):
|
|
69
70
|
super().__init__(config.name, config.description, port=config.port, **kwargs) # type: ignore
|
70
71
|
self.config = config
|
71
72
|
self._tool_manager = tool_manager
|
73
|
+
self.registry: Any = None
|
72
74
|
ServerConfig.model_validate(config)
|
73
75
|
except Exception as e:
|
74
76
|
logger.error(f"Failed to initialize server: {e}", exc_info=True)
|
@@ -84,7 +86,8 @@ class BaseServer(FastMCP):
|
|
84
86
|
self.tool_manager.add_tool(fn, name)
|
85
87
|
|
86
88
|
async def list_tools(self) -> list: # type: ignore
|
87
|
-
|
89
|
+
tools = self.tool_manager.get_tools()
|
90
|
+
return [convert_tool_to_mcp_tool(tool) for tool in tools]
|
88
91
|
|
89
92
|
async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
90
93
|
if not name:
|
@@ -92,7 +95,8 @@ class BaseServer(FastMCP):
|
|
92
95
|
if not isinstance(arguments, dict):
|
93
96
|
raise ValueError("Arguments must be a dictionary")
|
94
97
|
try:
|
95
|
-
|
98
|
+
# Delegate the call to the registry
|
99
|
+
result = await self.registry.call_tool(name, arguments)
|
96
100
|
return format_to_mcp_result(result)
|
97
101
|
except Exception as e:
|
98
102
|
logger.error(f"Tool '{name}' failed: {e}", exc_info=True)
|
@@ -102,18 +106,28 @@ class BaseServer(FastMCP):
|
|
102
106
|
class LocalServer(BaseServer):
|
103
107
|
"""Server that loads apps and store from local config."""
|
104
108
|
|
105
|
-
def __init__(self, config: ServerConfig, **kwargs):
|
109
|
+
def __init__(self, config: ServerConfig, registry: LocalRegistry | None = None, **kwargs):
|
106
110
|
super().__init__(config, **kwargs)
|
111
|
+
self.registry = registry or LocalRegistry()
|
107
112
|
self._tools_loaded = False
|
113
|
+
self._load_tools_from_config()
|
114
|
+
|
115
|
+
def _load_tools_from_config(self):
|
116
|
+
"""Load tools from the server configuration into the registry."""
|
117
|
+
if not self.config.apps:
|
118
|
+
logger.warning("No applications configured in server config; no tools to load.")
|
119
|
+
return
|
120
|
+
|
121
|
+
logger.info(f"Loading tools from {len(self.config.apps)} app(s) specified in server config...")
|
122
|
+
# Create a tool config dictionary from the server config
|
123
|
+
tool_config = {app.name: app.actions for app in self.config.apps}
|
124
|
+
self.registry._load_tools_from_tool_config(tool_config)
|
125
|
+
self._tools_loaded = True
|
126
|
+
logger.info("Finished loading tools from server config.")
|
108
127
|
|
109
128
|
@property
|
110
129
|
def tool_manager(self) -> ToolManager:
|
111
|
-
|
112
|
-
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
113
|
-
if not getattr(self, "_tools_loaded", False):
|
114
|
-
load_from_local_config(self.config, self._tool_manager)
|
115
|
-
self._tools_loaded = True
|
116
|
-
return self._tool_manager
|
130
|
+
return self.registry.tool_manager
|
117
131
|
|
118
132
|
|
119
133
|
class SingleMCPServer(BaseServer):
|
universal_mcp/tools/adapters.py
CHANGED
@@ -4,7 +4,21 @@ from loguru import logger
|
|
4
4
|
from mcp.types import TextContent
|
5
5
|
|
6
6
|
from universal_mcp.tools.tools import Tool
|
7
|
-
from universal_mcp.types import ToolFormat
|
7
|
+
from universal_mcp.types import ToolFormat
|
8
|
+
|
9
|
+
|
10
|
+
def convert_tools(tools: list[Tool], format: ToolFormat) -> list[Any]:
|
11
|
+
"""Convert a list of Tool objects to a specified format."""
|
12
|
+
logger.debug(f"Converting {len(tools)} tools to {format.value} format.")
|
13
|
+
if format == ToolFormat.NATIVE:
|
14
|
+
return [tool.fn for tool in tools]
|
15
|
+
if format == ToolFormat.MCP:
|
16
|
+
return [convert_tool_to_mcp_tool(tool) for tool in tools]
|
17
|
+
if format == ToolFormat.LANGCHAIN:
|
18
|
+
return [convert_tool_to_langchain_tool(tool) for tool in tools]
|
19
|
+
if format == ToolFormat.OPENAI:
|
20
|
+
return [convert_tool_to_openai_tool(tool) for tool in tools]
|
21
|
+
raise ValueError(f"Invalid format: {format}")
|
8
22
|
|
9
23
|
|
10
24
|
def convert_tool_to_mcp_tool(
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import base64
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
from universal_mcp.applications.application import BaseApplication
|
8
|
+
from universal_mcp.applications.utils import app_from_slug
|
9
|
+
from universal_mcp.exceptions import ToolError, ToolNotFoundError
|
10
|
+
from universal_mcp.integrations.integration import IntegrationFactory
|
11
|
+
from universal_mcp.tools.adapters import convert_tools
|
12
|
+
from universal_mcp.tools.registry import ToolRegistry
|
13
|
+
from universal_mcp.types import ToolConfig, ToolFormat
|
14
|
+
|
15
|
+
|
16
|
+
class LocalRegistry(ToolRegistry):
|
17
|
+
"""A local implementation of the tool registry."""
|
18
|
+
|
19
|
+
def __init__(self, output_dir: str = "output"):
|
20
|
+
"""Initialize the LocalRegistry."""
|
21
|
+
super().__init__()
|
22
|
+
self.output_dir = output_dir
|
23
|
+
if not os.path.exists(self.output_dir):
|
24
|
+
os.makedirs(self.output_dir)
|
25
|
+
logger.debug(f"Local output directory set to: {self.output_dir}")
|
26
|
+
|
27
|
+
def _create_app_instance(self, app_name: str) -> BaseApplication:
|
28
|
+
"""Create a local app instance with a default integration."""
|
29
|
+
app = app_from_slug(app_name)
|
30
|
+
integration = IntegrationFactory.create(app_name)
|
31
|
+
return app(integration=integration)
|
32
|
+
|
33
|
+
async def list_all_apps(self) -> list[dict[str, Any]]:
|
34
|
+
"""Not implemented for LocalRegistry."""
|
35
|
+
raise NotImplementedError("LocalRegistry does not support listing all apps.")
|
36
|
+
|
37
|
+
async def get_app_details(self, app_id: str) -> dict[str, Any]:
|
38
|
+
"""Not implemented for LocalRegistry."""
|
39
|
+
raise NotImplementedError("LocalRegistry does not support getting app details.")
|
40
|
+
|
41
|
+
async def search_apps(
|
42
|
+
self,
|
43
|
+
query: str,
|
44
|
+
limit: int = 2,
|
45
|
+
) -> list[dict[str, Any]]:
|
46
|
+
"""Not implemented for LocalRegistry."""
|
47
|
+
raise NotImplementedError("LocalRegistry does not support searching apps.")
|
48
|
+
|
49
|
+
async def list_tools(
|
50
|
+
self,
|
51
|
+
app_id: str,
|
52
|
+
) -> list[dict[str, Any]]:
|
53
|
+
"""Not implemented for LocalRegistry."""
|
54
|
+
raise NotImplementedError("LocalRegistry does not support listing tools.")
|
55
|
+
|
56
|
+
async def search_tools(
|
57
|
+
self,
|
58
|
+
query: str,
|
59
|
+
limit: int = 2,
|
60
|
+
app_id: str | None = None,
|
61
|
+
) -> list[dict[str, Any]]:
|
62
|
+
"""Not implemented for LocalRegistry."""
|
63
|
+
raise NotImplementedError("LocalRegistry does not support searching tools.")
|
64
|
+
|
65
|
+
async def export_tools(
|
66
|
+
self,
|
67
|
+
tools: list[str] | ToolConfig,
|
68
|
+
format: ToolFormat,
|
69
|
+
) -> list[Any]:
|
70
|
+
"""Export given tools to the required format."""
|
71
|
+
self.tool_manager.clear_tools()
|
72
|
+
logger.info(f"Exporting tools to {format.value} format")
|
73
|
+
if isinstance(tools, dict):
|
74
|
+
self._load_tools_from_tool_config(tools)
|
75
|
+
else:
|
76
|
+
self._load_tools_from_list(tools)
|
77
|
+
|
78
|
+
loaded_tools = self.tool_manager.get_tools()
|
79
|
+
exported = convert_tools(loaded_tools, format)
|
80
|
+
logger.info(f"Exported {len(exported)} tools")
|
81
|
+
return exported
|
82
|
+
|
83
|
+
def _handle_file_output(self, data: Any) -> Any:
|
84
|
+
"""Handle special file outputs by writing them to the filesystem."""
|
85
|
+
if isinstance(data, dict) and data.get("type") in ["image", "audio"]:
|
86
|
+
base64_data = data.get("data")
|
87
|
+
file_name = data.get("file_name")
|
88
|
+
if not base64_data or not file_name:
|
89
|
+
raise ToolError("File data or name is missing")
|
90
|
+
|
91
|
+
bytes_data = base64.b64decode(base64_data)
|
92
|
+
file_path = os.path.join(self.output_dir, file_name)
|
93
|
+
with open(file_path, "wb") as f:
|
94
|
+
f.write(bytes_data)
|
95
|
+
return f"File saved to: {file_path}"
|
96
|
+
return data
|
97
|
+
|
98
|
+
async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> Any:
|
99
|
+
"""Call a tool and handle its output."""
|
100
|
+
tool = self.tool_manager.get_tool(tool_name)
|
101
|
+
if not tool:
|
102
|
+
raise ToolNotFoundError(f"Tool '{tool_name}' not found.")
|
103
|
+
|
104
|
+
result = await tool.run(tool_args)
|
105
|
+
return self._handle_file_output(result)
|
106
|
+
|
107
|
+
async def list_connected_apps(self) -> list[str]:
|
108
|
+
"""Not implemented for LocalRegistry."""
|
109
|
+
raise NotImplementedError("LocalRegistry does not support listing connected apps.")
|
universal_mcp/tools/manager.py
CHANGED
@@ -4,12 +4,6 @@ from typing import Any
|
|
4
4
|
from loguru import logger
|
5
5
|
|
6
6
|
from universal_mcp.applications.application import BaseApplication
|
7
|
-
from universal_mcp.exceptions import ToolNotFoundError
|
8
|
-
from universal_mcp.tools.adapters import (
|
9
|
-
convert_tool_to_langchain_tool,
|
10
|
-
convert_tool_to_mcp_tool,
|
11
|
-
convert_tool_to_openai_tool,
|
12
|
-
)
|
13
7
|
from universal_mcp.tools.tools import Tool
|
14
8
|
from universal_mcp.types import DEFAULT_APP_NAME, DEFAULT_IMPORTANT_TAG, TOOL_NAME_SEPARATOR, ToolFormat
|
15
9
|
|
@@ -118,46 +112,24 @@ class ToolManager:
|
|
118
112
|
"""
|
119
113
|
return self._all_tools.get(name)
|
120
114
|
|
121
|
-
def
|
115
|
+
def get_tools(
|
122
116
|
self,
|
123
|
-
format: ToolFormat | None = None,
|
124
117
|
tags: list[str] | None = None,
|
125
118
|
tool_names: list[str] | None = None,
|
126
|
-
) -> list:
|
127
|
-
"""
|
119
|
+
) -> list[Tool]:
|
120
|
+
"""Get a filtered list of registered tools.
|
128
121
|
|
129
122
|
Args:
|
130
|
-
format: The format to convert tools to.
|
131
123
|
tags: Optional list of tags to filter tools by.
|
132
|
-
app_name: Optional app name to filter tools by.
|
133
124
|
tool_names: Optional list of tool names to filter by.
|
134
125
|
|
135
126
|
Returns:
|
136
|
-
|
137
|
-
|
138
|
-
Raises:
|
139
|
-
ValueError: If an invalid format is provided.
|
127
|
+
A list of Tool instances.
|
140
128
|
"""
|
141
|
-
if format is None:
|
142
|
-
format = self.default_format
|
143
|
-
|
144
|
-
# Start with app-specific tools or all tools
|
145
129
|
tools = list(self._all_tools.values())
|
146
|
-
# Apply filters
|
147
130
|
tools = _filter_by_tags(tools, tags)
|
148
131
|
tools = _filter_by_name(tools, tool_names)
|
149
|
-
|
150
|
-
# Convert to requested format
|
151
|
-
if format == ToolFormat.NATIVE:
|
152
|
-
return [tool.fn for tool in tools]
|
153
|
-
if format == ToolFormat.MCP:
|
154
|
-
return [convert_tool_to_mcp_tool(tool) for tool in tools]
|
155
|
-
elif format == ToolFormat.LANGCHAIN:
|
156
|
-
return [convert_tool_to_langchain_tool(tool) for tool in tools]
|
157
|
-
elif format == ToolFormat.OPENAI:
|
158
|
-
return [convert_tool_to_openai_tool(tool) for tool in tools]
|
159
|
-
else:
|
160
|
-
raise ValueError(f"Invalid format: {format}")
|
132
|
+
return tools
|
161
133
|
|
162
134
|
def add_tool(self, fn: Callable[..., Any] | Tool, name: str | None = None) -> Tool:
|
163
135
|
"""Add a tool to the manager.
|
@@ -269,34 +241,3 @@ class ToolManager:
|
|
269
241
|
tools = _filter_by_tags(tools, [DEFAULT_IMPORTANT_TAG])
|
270
242
|
|
271
243
|
self.register_tools(tools)
|
272
|
-
|
273
|
-
async def call_tool(
|
274
|
-
self,
|
275
|
-
name: str,
|
276
|
-
arguments: dict[str, Any],
|
277
|
-
context: dict[str, Any] | None = None,
|
278
|
-
) -> Any:
|
279
|
-
"""Call a tool by name with arguments.
|
280
|
-
|
281
|
-
Args:
|
282
|
-
name: The name of the tool to call.
|
283
|
-
arguments: The arguments to pass to the tool.
|
284
|
-
context: Optional context information for the tool execution.
|
285
|
-
|
286
|
-
Returns:
|
287
|
-
The result of the tool execution.
|
288
|
-
|
289
|
-
Raises:
|
290
|
-
ToolError: If the tool is not found or execution fails.
|
291
|
-
"""
|
292
|
-
logger.debug(f"Calling tool: {name} with arguments: {arguments}")
|
293
|
-
app_name, _ = _get_app_and_tool_name(name)
|
294
|
-
tool = self.get_tool(name)
|
295
|
-
if not tool:
|
296
|
-
logger.error(f"Unknown tool: {name}")
|
297
|
-
raise ToolNotFoundError(f"Unknown tool: {name}")
|
298
|
-
try:
|
299
|
-
result = await tool.run(arguments, context)
|
300
|
-
return result
|
301
|
-
except Exception as e:
|
302
|
-
raise e
|
universal_mcp/tools/registry.py
CHANGED
@@ -1,84 +1,58 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from universal_mcp.applications.application import BaseApplication
|
7
|
+
from universal_mcp.tools.manager import ToolManager, _get_app_and_tool_name
|
4
8
|
from universal_mcp.types import ToolConfig, ToolFormat
|
5
9
|
|
6
10
|
|
7
11
|
class ToolRegistry(ABC):
|
8
|
-
"""Abstract base class for platform-specific functionality.
|
9
|
-
|
10
|
-
This class abstracts away platform-specific operations like fetching apps,
|
11
|
-
loading actions, and managing integrations. This allows the agents to
|
12
|
-
work with different platforms without being tightly coupled to any specific one.
|
13
|
-
|
14
|
-
The following methods are abstract and must be implemented by the subclass:
|
15
|
-
- list_all_apps: Get list of available apps from the platform.
|
16
|
-
- get_app_details: Get details of a specific app.
|
17
|
-
- search_apps: Search for apps by a query.
|
18
|
-
- list_tools: List all tools available on the platform, filter by app_id.
|
19
|
-
- search_tools: Search for tools by a query.
|
20
|
-
- export_tools: Export tools to required format.
|
21
12
|
"""
|
13
|
+
Abstract base class for tool registries, defining a common interface and providing
|
14
|
+
shared tool loading functionality.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self):
|
18
|
+
"""Initializes the registry and its internal tool manager."""
|
19
|
+
self.tool_manager = ToolManager()
|
20
|
+
logger.debug(f"{self.__class__.__name__} initialized.")
|
21
|
+
|
22
|
+
# --- Abstract methods for the public interface ---
|
22
23
|
|
23
24
|
@abstractmethod
|
24
25
|
async def list_all_apps(self) -> list[dict[str, Any]]:
|
25
|
-
"""Get list of available apps from the platform.
|
26
|
-
|
27
|
-
Returns:
|
28
|
-
Return a list of apps with their details
|
29
|
-
"""
|
26
|
+
"""Get a list of all available apps from the platform."""
|
30
27
|
pass
|
31
28
|
|
32
29
|
@abstractmethod
|
33
30
|
async def get_app_details(self, app_id: str) -> dict[str, Any]:
|
34
|
-
"""Get detailed information about a specific app.
|
35
|
-
|
36
|
-
Args:
|
37
|
-
app_id: The ID of the app to get details for
|
38
|
-
|
39
|
-
Returns:
|
40
|
-
Dictionary containing app details
|
41
|
-
"""
|
31
|
+
"""Get detailed information about a specific app."""
|
42
32
|
pass
|
43
33
|
|
44
34
|
@abstractmethod
|
45
|
-
async def search_apps(
|
46
|
-
self,
|
47
|
-
query: str,
|
48
|
-
limit: int = 2,
|
49
|
-
) -> list[dict[str, Any]]:
|
35
|
+
async def search_apps(self, query: str, limit: int = 2) -> list[dict[str, Any]]:
|
50
36
|
"""Search for apps by a query."""
|
51
37
|
pass
|
52
38
|
|
53
39
|
@abstractmethod
|
54
|
-
async def list_tools(
|
55
|
-
|
56
|
-
app_id: str,
|
57
|
-
) -> list[dict[str, Any]]:
|
58
|
-
"""List all tools available on the platform, filter by app_id."""
|
40
|
+
async def list_tools(self, app_id: str) -> list[dict[str, Any]]:
|
41
|
+
"""List all tools available for a specific app."""
|
59
42
|
pass
|
60
43
|
|
61
44
|
@abstractmethod
|
62
|
-
async def search_tools(
|
63
|
-
|
64
|
-
query: str,
|
65
|
-
limit: int = 2,
|
66
|
-
app_id: str | None = None,
|
67
|
-
) -> list[dict[str, Any]]:
|
68
|
-
"""Search for tools by a query."""
|
45
|
+
async def search_tools(self, query: str, limit: int = 2, app_id: str | None = None) -> list[dict[str, Any]]:
|
46
|
+
"""Search for tools by a query, optionally filtered by an app."""
|
69
47
|
pass
|
70
48
|
|
71
49
|
@abstractmethod
|
72
|
-
async def export_tools(
|
73
|
-
|
74
|
-
tools: list[str] | ToolConfig,
|
75
|
-
format: ToolFormat,
|
76
|
-
) -> str:
|
77
|
-
"""Export giventools to required format."""
|
50
|
+
async def export_tools(self, tools: list[str] | ToolConfig, format: ToolFormat) -> list[Any]:
|
51
|
+
"""Export a selection of tools to a specified format."""
|
78
52
|
pass
|
79
53
|
|
80
54
|
@abstractmethod
|
81
|
-
async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) ->
|
55
|
+
async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> Any:
|
82
56
|
"""Call a tool with the given name and arguments."""
|
83
57
|
pass
|
84
58
|
|
@@ -86,3 +60,38 @@ class ToolRegistry(ABC):
|
|
86
60
|
async def list_connected_apps(self) -> list[str]:
|
87
61
|
"""List all apps that the user has connected."""
|
88
62
|
pass
|
63
|
+
|
64
|
+
# --- Abstract method for subclass implementation ---
|
65
|
+
|
66
|
+
def _create_app_instance(self, app_name: str) -> BaseApplication:
|
67
|
+
"""Create an application instance for a given app name."""
|
68
|
+
raise NotImplementedError("Subclasses must implement this method")
|
69
|
+
|
70
|
+
# --- Concrete methods for shared tool loading ---
|
71
|
+
|
72
|
+
def _load_tools(self, app_name: str, tool_names: list[str] | None) -> None:
|
73
|
+
"""Helper method to load and register tools for an app."""
|
74
|
+
logger.info(f"Loading tools for app '{app_name}' (tools: {tool_names or 'default'})")
|
75
|
+
try:
|
76
|
+
app_instance = self._create_app_instance(app_name)
|
77
|
+
self.tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
|
78
|
+
logger.info(f"Successfully registered tools for app: {app_name}")
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"Failed to load tools for app {app_name}: {e}", exc_info=True)
|
81
|
+
|
82
|
+
def _load_tools_from_list(self, tools: list[str]) -> None:
|
83
|
+
"""Load tools from a list of full tool names (e.g., 'app__tool')."""
|
84
|
+
logger.debug(f"Loading tools from list: {tools}")
|
85
|
+
tools_by_app: dict[str, list[str]] = {}
|
86
|
+
for tool_name in tools:
|
87
|
+
app_name, _ = _get_app_and_tool_name(tool_name)
|
88
|
+
tools_by_app.setdefault(app_name, []).append(tool_name)
|
89
|
+
|
90
|
+
for app_name, tool_names in tools_by_app.items():
|
91
|
+
self._load_tools(app_name, tool_names)
|
92
|
+
|
93
|
+
def _load_tools_from_tool_config(self, tool_config: ToolConfig) -> None:
|
94
|
+
"""Load tools from a ToolConfig dictionary."""
|
95
|
+
logger.debug(f"Loading tools from tool_config: {tool_config}")
|
96
|
+
for app_name, tool_names in tool_config.items():
|
97
|
+
self._load_tools(app_name, tool_names or None)
|
@@ -15,7 +15,7 @@ class {{ class_name }}(APIApplication):
|
|
15
15
|
Tags: {{ method.tags|join(', ') }}
|
16
16
|
{% endif %}
|
17
17
|
"""
|
18
|
-
|
18
|
+
|
19
19
|
{{ method.implementation|indent(8) }}
|
20
20
|
{% endfor %}
|
21
21
|
|
@@ -24,4 +24,4 @@ class {{ class_name }}(APIApplication):
|
|
24
24
|
{% for method in methods %}
|
25
25
|
self.{{ method.name }}{% if not loop.last %},{% endif %}
|
26
26
|
{%- endfor %}
|
27
|
-
]
|
27
|
+
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.24rc21
|
4
4
|
Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
|
5
5
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
6
|
License: MIT
|
@@ -4,12 +4,12 @@ universal_mcp/exceptions.py,sha256=Uen8UFgLHGlSwXgRUyF-nhqTwdiBuL3okgBVRV2AgtA,2
|
|
4
4
|
universal_mcp/logger.py,sha256=VmH_83efpErLEDTJqz55Dp0dioTXfGvMBLZUx5smOLc,2116
|
5
5
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
universal_mcp/types.py,sha256=DAzYyS7Zqdi38SpnhdOm08_JeN2CJiC0hDIpLLIhlSw,316
|
7
|
-
universal_mcp/agentr/README.md,sha256=
|
7
|
+
universal_mcp/agentr/README.md,sha256=oF2Ou3czOCLZnRrfcko8xL7zt89j3Im3_wxwpSikz0A,6638
|
8
8
|
universal_mcp/agentr/__init__.py,sha256=fv1ZnOCduIUiJ9oN4e6Ya_hA2oWQvcEuDU3Ek1vEufI,180
|
9
9
|
universal_mcp/agentr/client.py,sha256=CwfmOAgWsApm0buxZPni3vW6MFgrwdddXsXmQA5UYrA,7882
|
10
10
|
universal_mcp/agentr/integration.py,sha256=V5GjqocqS02tRoI8MeV9PL6m-BzejwBzgJhOHo4MxAE,4212
|
11
|
-
universal_mcp/agentr/registry.py,sha256=
|
12
|
-
universal_mcp/agentr/server.py,sha256=
|
11
|
+
universal_mcp/agentr/registry.py,sha256=VrB39dq3HMOrunPa9ruB8YbNyu1-18BtLaBUio_gCGk,8722
|
12
|
+
universal_mcp/agentr/server.py,sha256=d_UaxCGTOzdOsIMiHiILgAjeZLbUASCyQkUqhd1x9wA,1911
|
13
13
|
universal_mcp/applications/application.py,sha256=do45GC0jnNepZHL8US2yDNq9s0ZnE6bII4Xbw90GRTc,23727
|
14
14
|
universal_mcp/applications/utils.py,sha256=8Pp9lZU6IPt9z9BnuJ-vpv-NGuzryt1c4e4-ShDd2XI,1450
|
15
15
|
universal_mcp/applications/sample/app.py,sha256=D9zPezC13xXVMlfO2A0fHgJQD_I-bnpf9UOOHveHMek,10537
|
@@ -17,17 +17,18 @@ universal_mcp/client/oauth.py,sha256=O00zOUfQxINaruFU2zt-64DIR1_mAqrY8ykLQo-teJU
|
|
17
17
|
universal_mcp/client/token_store.py,sha256=6VAzjzJG49wYvmEDqksFvb-fVqdjHIKWv7yYyh_AuF8,3912
|
18
18
|
universal_mcp/client/transport.py,sha256=qqtb0ky6yvLBxsaA9-oFU0v9MwfcQb4eomq-O2fmwtQ,11850
|
19
19
|
universal_mcp/integrations/__init__.py,sha256=tfzLyPEPly5tfIcT8K6-oKCr_MEFGxOROHy_NeVy0KM,200
|
20
|
-
universal_mcp/integrations/integration.py,sha256=
|
20
|
+
universal_mcp/integrations/integration.py,sha256=JAf6_Oy4f3gNI94xS3-yGPwEswAah7hre1en7AwVLdY,17186
|
21
21
|
universal_mcp/servers/__init__.py,sha256=speBb_E94UJa4A6Fv8RHFeoJ7cR-q2bCMtKV7R21P5w,142
|
22
|
-
universal_mcp/servers/server.py,sha256=
|
22
|
+
universal_mcp/servers/server.py,sha256=OUze-imwZxfQdqaRtsoT4DOAbjRK42qfO7k13TpiKMk,6683
|
23
23
|
universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
|
24
24
|
universal_mcp/stores/store.py,sha256=yWbEGZb53z3fpVyqGWbes63z1CtIzC_IuM49OXy__UY,10137
|
25
25
|
universal_mcp/tools/__init__.py,sha256=jC8hsqfTdtn32yU57AVFUXiU3ZmUOCfCERSCaNEIH7E,395
|
26
|
-
universal_mcp/tools/adapters.py,sha256=
|
26
|
+
universal_mcp/tools/adapters.py,sha256=LmI5J6e-3TPVuyeDki7uHeZVeCES52y7TRcLpUurzNQ,4591
|
27
27
|
universal_mcp/tools/docstring_parser.py,sha256=efEOE-ME7G5Jbbzpn7pN2xNuyu2M5zfZ1Tqu1lRB0Gk,8392
|
28
28
|
universal_mcp/tools/func_metadata.py,sha256=F4jd--hoZWKPBbZihVtluYKUsIdXdq4a0VWRgMl5k-Q,10838
|
29
|
-
universal_mcp/tools/
|
30
|
-
universal_mcp/tools/
|
29
|
+
universal_mcp/tools/local_registry.py,sha256=lvvpW_reF2lHSr3_s3txRU9b5Ps8rUwH-cZSApw46Ew,4236
|
30
|
+
universal_mcp/tools/manager.py,sha256=73x5RB5Sgew1hNqCOMmoxHZN2DYjiE7pjsytfi0Gj0g,8213
|
31
|
+
universal_mcp/tools/registry.py,sha256=PTVDeVIdHDxMHlf5cNuBmvbmSTFBt_YencA6e79DH4s,3868
|
31
32
|
universal_mcp/tools/tools.py,sha256=Lk-wUO3rfhwdxaRANtC7lQr5fXi7nclf0oHzxNAb79Q,4927
|
32
33
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
33
34
|
universal_mcp/utils/installation.py,sha256=PU_GfHPqzkumKk-xG4L9CkBzSmABxmchwblZkx-zY-I,7204
|
@@ -46,9 +47,9 @@ universal_mcp/utils/openapi/preprocessor.py,sha256=r4n0WQI__OzPL8FTza7jxiM4EYeZw
|
|
46
47
|
universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vHh4O_nB4c,2974
|
47
48
|
universal_mcp/utils/openapi/test_generator.py,sha256=vucBh9klWmQOUA740TFwfM9ry2nkwKWQiNRcsiZ9HbY,12229
|
48
49
|
universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
|
49
|
-
universal_mcp/utils/templates/api_client.py.j2,sha256=
|
50
|
-
universal_mcp-0.1.
|
51
|
-
universal_mcp-0.1.
|
52
|
-
universal_mcp-0.1.
|
53
|
-
universal_mcp-0.1.
|
54
|
-
universal_mcp-0.1.
|
50
|
+
universal_mcp/utils/templates/api_client.py.j2,sha256=DS1nczOOD8YkMexVSGpUGeyc0nYGKKTPadL_x1_if7k,900
|
51
|
+
universal_mcp-0.1.24rc21.dist-info/METADATA,sha256=uMU2UHEiXuJZSI4-3DhheHEjz_IImx2YTN13upi82d8,3255
|
52
|
+
universal_mcp-0.1.24rc21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
53
|
+
universal_mcp-0.1.24rc21.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
54
|
+
universal_mcp-0.1.24rc21.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
|
55
|
+
universal_mcp-0.1.24rc21.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|