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.
@@ -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
+ ```
@@ -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.manager import ToolManager, _get_app_and_tool_name
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
- self.tool_manager = ToolManager()
41
- logger.debug("AgentrRegistry initialized successfully")
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
- ) -> str:
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
- String representation of tools in the specified format
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
- logger.info("Loading tools from tool config")
157
- self._load_tools_from_tool_config(tools, self.tool_manager)
164
+ self._load_tools_from_tool_config(tools)
158
165
  else:
159
- logger.info("Loading tools from list")
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
- def _load_tools(self, app_name: str, tool_names: list[str], tool_manager: ToolManager) -> None:
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
- def _load_agentr_tools_from_list(self, tools: list[str], tool_manager: ToolManager) -> None:
176
- """Load tools from AgentR and register them as tools.
170
+ if format != ToolFormat.LANGCHAIN:
171
+ return convert_tools(loaded_tools, format)
177
172
 
178
- Args:
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
- for app_name, tool_names in tools_by_app.items():
189
- self._load_tools(app_name, tool_names, tool_manager)
175
+ langchain_tools = []
176
+ for tool in loaded_tools:
190
177
 
191
- def _load_tools_from_tool_config(self, tool_config: ToolConfig, tool_manager: ToolManager) -> None:
192
- """Load tools from ToolConfig and register them as tools.
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
- Args:
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
- async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> dict[str, Any]:
202
- """Call a tool with the given name and arguments."""
203
- data = await self.tool_manager.call_tool(tool_name, tool_args)
204
- logger.debug(f"Tool {tool_name} called with args {tool_args} and returned {data}")
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()
@@ -1,51 +1,52 @@
1
1
  from loguru import logger
2
2
 
3
- from universal_mcp.applications import app_from_config
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 .integration import AgentrIntegration
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 server."""
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.api_key = config.api_key.get_secret_value() if config.api_key else None
41
- self.base_url = config.base_url
42
- self.client = AgentrClient(api_key=self.api_key, base_url=self.base_url)
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
- if self._tool_manager is None:
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)
@@ -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 ToolFormat, format_to_mcp_result
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
- return self.tool_manager.list_tools(format=ToolFormat.MCP)
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
- result = await self.tool_manager.call_tool(name, arguments)
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
- if self._tool_manager is None:
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):
@@ -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 # noqa: F401
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.")
@@ -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 list_tools(
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
- """List all registered tools in the specified format.
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
- List of tools in the specified format.
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
@@ -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
- self,
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
- self,
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
- self,
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]) -> 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.24rc19
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=t15pVgkCwZM5wzgLgrf0Zv6hVL7dPmKXvAeTf8CiXPQ,6641
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=cWkg9cUEmksWqeuh4xazFf1WtRQ3lEZf1s53hqTlmG4,8657
12
- universal_mcp/agentr/server.py,sha256=bIPmHMiKKwnUYnxmfZVRh1thcn7Rytm_-bNiXTfANzc,2098
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=H-hOoDHqk78A4Fi_TGN7OOFS7PDfqXK_nedH8iSz-6A,16459
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=bJTG86X_kn0R8lWIYvCZw-cp-rVAOKd12-E5WOEsz8Y,5950
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=YJ2oqgc8JgmtsdRRtvO-PO0Q0bKqTJ4Y3J6yxlESoTo,3947
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/manager.py,sha256=T8iSk7Q6s3MFaQcwUapPvcQ6_l7_g4Xt2xXk55lPJ1w,10321
30
- universal_mcp/tools/registry.py,sha256=LD0J_bPsd8PRTObyvXglqTW1jfZX98m7KBdyP8Yn7wA,2585
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=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
50
- universal_mcp-0.1.24rc19.dist-info/METADATA,sha256=zKP0JF9Lp3hnTW-8ZvLutdDpGEcqAishzXtrICY3a4Y,3255
51
- universal_mcp-0.1.24rc19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
- universal_mcp-0.1.24rc19.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
53
- universal_mcp-0.1.24rc19.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
54
- universal_mcp-0.1.24rc19.dist-info/RECORD,,
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,,