universal-mcp 0.1.24rc26__py3-none-any.whl → 0.1.24rc27__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.
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import inspect
3
2
  from typing import Any
4
3
 
5
4
  from loguru import logger
@@ -8,9 +7,7 @@ from universal_mcp.agentr.client import AgentrClient
8
7
  from universal_mcp.applications.application import BaseApplication
9
8
  from universal_mcp.applications.utils import app_from_slug
10
9
  from universal_mcp.exceptions import ToolError, ToolNotFoundError
11
- from universal_mcp.tools.adapters import convert_tools
12
10
  from universal_mcp.tools.registry import ToolRegistry
13
- from universal_mcp.types import ToolConfig, ToolFormat
14
11
 
15
12
  from .integration import AgentrIntegration
16
13
 
@@ -145,64 +142,6 @@ class AgentrRegistry(ToolRegistry):
145
142
  logger.error(f"Error searching tools from AgentR: {e}")
146
143
  return []
147
144
 
148
- async def export_tools(
149
- self,
150
- tools: list[str] | ToolConfig,
151
- format: ToolFormat,
152
- ) -> list[Any]:
153
- """Export given tools to required format.
154
-
155
- Args:
156
- tools: List of tool identifiers to export
157
- format: The format to export tools to (native, mcp, langchain, openai)
158
-
159
- Returns:
160
- List of tools in the specified format
161
- """
162
- from langchain_core.tools import StructuredTool
163
-
164
- try:
165
- logger.info(f"Exporting tools to {format.value} format")
166
- if isinstance(tools, dict):
167
- self._load_tools_from_tool_config(tools)
168
- else:
169
- self._load_tools_from_list(tools)
170
-
171
- loaded_tools = self.tool_manager.get_tools()
172
-
173
- if format != ToolFormat.LANGCHAIN:
174
- return convert_tools(loaded_tools, format)
175
-
176
- logger.info(f"Exporting {len(loaded_tools)} tools to LangChain format with special handling")
177
-
178
- langchain_tools = []
179
- for tool in loaded_tools:
180
- full_docstring = inspect.getdoc(tool.fn)
181
-
182
- def create_coroutine(t):
183
- async def call_tool_wrapper(**arguments: dict[str, Any]):
184
- logger.debug(
185
- f"Executing registry-wrapped LangChain tool '{t.name}' with arguments: {arguments}"
186
- )
187
- return await self.call_tool(t.name, arguments)
188
-
189
- return call_tool_wrapper
190
-
191
- langchain_tool = StructuredTool(
192
- name=tool.name,
193
- description=full_docstring or tool.description or "",
194
- coroutine=create_coroutine(tool),
195
- response_format="content",
196
- args_schema=tool.parameters,
197
- )
198
- langchain_tools.append(langchain_tool)
199
-
200
- return langchain_tools
201
-
202
- except Exception as e:
203
- logger.error(f"Error exporting tools: {e}")
204
- return []
205
-
206
145
  def _handle_special_output(self, data: Any) -> Any:
207
146
  if isinstance(data, dict):
208
147
  type_ = data.get("type")
@@ -1,4 +1,6 @@
1
1
  import inspect
2
+ from collections.abc import Callable
3
+ from functools import wraps
2
4
  from typing import Any
3
5
 
4
6
  from loguru import logger
@@ -12,7 +14,7 @@ def convert_tools(tools: list[Tool], format: ToolFormat) -> list[Any]:
12
14
  """Convert a list of Tool objects to a specified format."""
13
15
  logger.debug(f"Converting {len(tools)} tools to {format.value} format.")
14
16
  if format == ToolFormat.NATIVE:
15
- return [tool.fn for tool in tools]
17
+ return [convert_to_native_tool(tool) for tool in tools]
16
18
  if format == ToolFormat.MCP:
17
19
  return [convert_tool_to_mcp_tool(tool) for tool in tools]
18
20
  if format == ToolFormat.LANGCHAIN:
@@ -22,6 +24,21 @@ def convert_tools(tools: list[Tool], format: ToolFormat) -> list[Any]:
22
24
  raise ValueError(f"Invalid format: {format}")
23
25
 
24
26
 
27
+ def convert_to_native_tool(tool: Tool) -> Callable[..., Any]:
28
+ """Decorator to convert a Tool object to a native tool."""
29
+
30
+ def decorator(func):
31
+ @wraps(func)
32
+ def wrapper(*args, **kwargs):
33
+ return func(*args, **kwargs)
34
+
35
+ wrapper.__name__ = tool.name
36
+ wrapper.__doc__ = tool.fn.__doc__
37
+ return wrapper
38
+
39
+ return decorator(tool.fn)
40
+
41
+
25
42
  def convert_tool_to_mcp_tool(
26
43
  tool: Tool,
27
44
  ):
@@ -81,8 +98,6 @@ def convert_tool_to_langchain_tool(
81
98
 
82
99
  """Convert an tool to a LangChain tool.
83
100
 
84
- NOTE: this tool can be executed only in a context of an active MCP client session.
85
-
86
101
  Args:
87
102
  tool: Tool to convert
88
103
 
@@ -5,23 +5,13 @@ from loguru import logger
5
5
 
6
6
  from universal_mcp.applications.application import BaseApplication
7
7
  from universal_mcp.tools.tools import Tool
8
- from universal_mcp.types import DEFAULT_APP_NAME, DEFAULT_IMPORTANT_TAG, TOOL_NAME_SEPARATOR, ToolFormat
9
-
10
-
11
- def _get_app_and_tool_name(tool_name: str) -> tuple[str, str]:
12
- """Get the app name from a tool name."""
13
- if TOOL_NAME_SEPARATOR in tool_name:
14
- app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[0]
15
- tool_name_without_app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[1]
16
- else:
17
- app_name = DEFAULT_APP_NAME
18
- tool_name_without_app_name = tool_name
19
- return app_name, tool_name_without_app_name
8
+ from universal_mcp.tools.utils import get_app_and_tool_name
9
+ from universal_mcp.types import DEFAULT_IMPORTANT_TAG, ToolFormat
20
10
 
21
11
 
22
12
  def _sanitize_tool_names(tool_names: list[str]) -> list[str]:
23
13
  """Sanitize tool names by removing empty strings and converting to lowercase."""
24
- return [_get_app_and_tool_name(name)[1].lower() for name in tool_names if name]
14
+ return [get_app_and_tool_name(name)[1].lower() for name in tool_names if name]
25
15
 
26
16
 
27
17
  def _filter_by_name(tools: list[Tool], tool_names: list[str] | None) -> list[Tool]:
@@ -114,8 +104,8 @@ class ToolManager:
114
104
 
115
105
  def get_tools(
116
106
  self,
117
- tags: list[str] | None = None,
118
107
  tool_names: list[str] | None = None,
108
+ tags: list[str] | None = None,
119
109
  ) -> list[Tool]:
120
110
  """Get a filtered list of registered tools.
121
111
 
@@ -151,10 +141,10 @@ class ToolManager:
151
141
  if self.warn_on_duplicate_tools:
152
142
  if existing.fn is not tool.fn:
153
143
  logger.warning(
154
- f"Tool name '{tool.name}' conflicts with an existing tool. Skipping addition of new function."
144
+ f"Tool '{tool.name}' already exists with a different function. Skipping addition of new function."
155
145
  )
156
146
  else:
157
- logger.debug(f"Tool '{tool.name}' with the same function already exists.")
147
+ logger.debug(f"Tool '{tool.name}' already exists with the same function.")
158
148
  return existing
159
149
 
160
150
  logger.debug(f"Adding tool: {tool.name}")
@@ -4,7 +4,9 @@ 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.tools.manager import ToolManager, _get_app_and_tool_name
7
+ from universal_mcp.tools.adapters import convert_tools
8
+ from universal_mcp.tools.manager import ToolManager
9
+ from universal_mcp.tools.utils import list_to_tool_config, tool_config_to_list
8
10
  from universal_mcp.types import ToolConfig, ToolFormat
9
11
 
10
12
 
@@ -20,6 +22,36 @@ class ToolRegistry(ABC):
20
22
  self.tool_manager = ToolManager()
21
23
  logger.debug(f"{self.__class__.__name__} initialized.")
22
24
 
25
+ def _load_tools_from_app(self, app_name: str, tool_names: list[str] | None) -> None:
26
+ """Helper method to load and register tools for an app."""
27
+ logger.info(f"Loading tools for app '{app_name}' (tools: {tool_names or 'default'})")
28
+ try:
29
+ if app_name not in self._app_instances:
30
+ self._app_instances[app_name] = self._create_app_instance(app_name)
31
+ app_instance = self._app_instances[app_name]
32
+ self.tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
33
+ logger.info(f"Successfully registered tools for app: {app_name}")
34
+ except Exception as e:
35
+ logger.error(f"Failed to load tools for app {app_name}: {e}", exc_info=True)
36
+
37
+ def _load_tools_from_list(self, tools: list[str]) -> None:
38
+ """Load tools from a list of full tool names (e.g., 'app__tool')."""
39
+ tool_config = list_to_tool_config(tools)
40
+ for app_name, tool_names in tool_config.items():
41
+ self._load_tools_from_app(app_name, tool_names or None)
42
+
43
+ def _load_tools_from_tool_config(self, tool_config: ToolConfig) -> None:
44
+ """Load tools from a ToolConfig dictionary."""
45
+ logger.debug(f"Loading tools from tool_config: {tool_config}")
46
+ for app_name, tool_names in tool_config.items():
47
+ self._load_tools_from_app(app_name, tool_names or None)
48
+
49
+ # --- Abstract method for subclass implementation ---
50
+
51
+ def _create_app_instance(self, app_name: str) -> BaseApplication:
52
+ """Create an application instance for a given app name."""
53
+ raise NotImplementedError("Subclasses must implement this method")
54
+
23
55
  # --- Abstract methods for the public interface ---
24
56
 
25
57
  @abstractmethod
@@ -49,54 +81,35 @@ class ToolRegistry(ABC):
49
81
  """Search for tools by a query, optionally filtered by an app."""
50
82
  pass
51
83
 
52
- @abstractmethod
53
- async def export_tools(self, tools: list[str] | ToolConfig, format: ToolFormat) -> list[Any]:
54
- """Export a selection of tools to a specified format."""
55
- pass
84
+ async def load_tools(self, tools: list[str] | ToolConfig | None = None):
85
+ """Load the tools to be used"""
86
+ if isinstance(tools, list):
87
+ self._load_tools_from_list(tools)
88
+ elif isinstance(tools, dict):
89
+ self._load_tools_from_tool_config(tools)
90
+ else:
91
+ raise ValueError(f"Invalid tools type: {type(tools)}. Expected list or ToolConfig.")
92
+ return self.tool_manager.get_tools()
93
+
94
+ async def export_tools(
95
+ self, tools: list[str] | ToolConfig | None = None, format: ToolFormat = ToolFormat.NATIVE
96
+ ) -> list[Any]:
97
+ """Export the loaded tools as in required format"""
98
+ if tools is not None:
99
+ # Load the tools if they are not already loaded
100
+ await self.load_tools(tools)
101
+ tools_list = tool_config_to_list(tools) if isinstance(tools, dict) else tools
102
+ loaded_tools = self.tool_manager.get_tools(tool_names=tools_list)
103
+ exported_tools = convert_tools(loaded_tools, format)
104
+ logger.info(f"Exported {len(exported_tools)} tools to {format.value} format")
105
+ return exported_tools if isinstance(exported_tools, list) else [exported_tools]
56
106
 
57
- @abstractmethod
58
107
  async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> Any:
59
108
  """Call a tool with the given name and arguments."""
60
- pass
109
+ result = await self.tool_manager.get_tool(tool_name)
110
+ return await result.run(tool_args)
61
111
 
62
112
  @abstractmethod
63
113
  async def list_connected_apps(self) -> list[dict[str, Any]]:
64
114
  """List all apps that the user has connected."""
65
115
  pass
66
-
67
- # --- Abstract method for subclass implementation ---
68
-
69
- def _create_app_instance(self, app_name: str) -> BaseApplication:
70
- """Create an application instance for a given app name."""
71
- raise NotImplementedError("Subclasses must implement this method")
72
-
73
- # --- Concrete methods for shared tool loading ---
74
-
75
- def _load_tools(self, app_name: str, tool_names: list[str] | None) -> None:
76
- """Helper method to load and register tools for an app."""
77
- logger.info(f"Loading tools for app '{app_name}' (tools: {tool_names or 'default'})")
78
- try:
79
- if app_name not in self._app_instances:
80
- self._app_instances[app_name] = self._create_app_instance(app_name)
81
- app_instance = self._app_instances[app_name]
82
- self.tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
83
- logger.info(f"Successfully registered tools for app: {app_name}")
84
- except Exception as e:
85
- logger.error(f"Failed to load tools for app {app_name}: {e}", exc_info=True)
86
-
87
- def _load_tools_from_list(self, tools: list[str]) -> None:
88
- """Load tools from a list of full tool names (e.g., 'app__tool')."""
89
- logger.debug(f"Loading tools from list: {tools}")
90
- tools_by_app: dict[str, list[str]] = {}
91
- for tool_name in tools:
92
- app_name, _ = _get_app_and_tool_name(tool_name)
93
- tools_by_app.setdefault(app_name, []).append(tool_name)
94
-
95
- for app_name, tool_names in tools_by_app.items():
96
- self._load_tools(app_name, tool_names)
97
-
98
- def _load_tools_from_tool_config(self, tool_config: ToolConfig) -> None:
99
- """Load tools from a ToolConfig dictionary."""
100
- logger.debug(f"Loading tools from tool_config: {tool_config}")
101
- for app_name, tool_names in tool_config.items():
102
- self._load_tools(app_name, tool_names or None)
@@ -0,0 +1,30 @@
1
+ from universal_mcp.types import DEFAULT_APP_NAME, TOOL_NAME_SEPARATOR, ToolConfig
2
+
3
+
4
+ def get_app_and_tool_name(tool_name: str) -> tuple[str, str]:
5
+ """Get the app and tool name from a tool name."""
6
+ if TOOL_NAME_SEPARATOR in tool_name:
7
+ app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[0]
8
+ tool_name_without_app_name = tool_name.split(TOOL_NAME_SEPARATOR, 1)[1]
9
+ else:
10
+ app_name = DEFAULT_APP_NAME
11
+ tool_name_without_app_name = tool_name
12
+ return app_name, tool_name_without_app_name
13
+
14
+
15
+ def tool_config_to_list(tools: ToolConfig) -> list[str]:
16
+ """Convert a ToolConfig dictionary to a list of tool names."""
17
+ return [
18
+ f"{app_name}{TOOL_NAME_SEPARATOR}{tool_name}"
19
+ for app_name, tool_names in tools.items()
20
+ for tool_name in tool_names
21
+ ]
22
+
23
+
24
+ def list_to_tool_config(tools: list[str]) -> ToolConfig:
25
+ """Convert a list of tool names to a ToolConfig dictionary."""
26
+ tool_config = {}
27
+ for tool_name in tools:
28
+ app_name, tool_name_without_app_name = get_app_and_tool_name(tool_name)
29
+ tool_config.setdefault(app_name, []).append(tool_name_without_app_name)
30
+ return tool_config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.24rc26
3
+ Version: 0.1.24rc27
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
@@ -8,7 +8,7 @@ universal_mcp/agentr/README.md,sha256=ypsA_jD4Fgdg0wQ8bLFIFPmpFwYCCCv-29pNizJDCk
8
8
  universal_mcp/agentr/__init__.py,sha256=fv1ZnOCduIUiJ9oN4e6Ya_hA2oWQvcEuDU3Ek1vEufI,180
9
9
  universal_mcp/agentr/client.py,sha256=MnHYwD5V9UgmqcmTcOcOIZoYZ289cPkWBGRBAB4GJCw,8092
10
10
  universal_mcp/agentr/integration.py,sha256=V5GjqocqS02tRoI8MeV9PL6m-BzejwBzgJhOHo4MxAE,4212
11
- universal_mcp/agentr/registry.py,sha256=nmymeBEY9obRiVvvk-ASRdZiIkgDh4u3ql1r_h7mMRU,9282
11
+ universal_mcp/agentr/registry.py,sha256=Oz35KNeaUWuWxGCkpEU_1Zyg0q-1Qh66rUorZP_GySM,7098
12
12
  universal_mcp/agentr/server.py,sha256=d_UaxCGTOzdOsIMiHiILgAjeZLbUASCyQkUqhd1x9wA,1911
13
13
  universal_mcp/applications/application.py,sha256=Zxgrr0LVNUTnKxqnX11ScTd5IWEUPCsNYZO2URL6mbQ,23838
14
14
  universal_mcp/applications/utils.py,sha256=8Pp9lZU6IPt9z9BnuJ-vpv-NGuzryt1c4e4-ShDd2XI,1450
@@ -23,13 +23,14 @@ universal_mcp/servers/server.py,sha256=OUze-imwZxfQdqaRtsoT4DOAbjRK42qfO7k13TpiK
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=lea_qVULaUlzBtTo2hxn45BaD8hliIntGmKkt-tTg6c,4669
26
+ universal_mcp/tools/adapters.py,sha256=JffIZlIQijeU1dZb2VH0Aov4i0OSHMl706HK5JBKPLg,5054
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
29
  universal_mcp/tools/local_registry.py,sha256=Gp3bXwLwJ1SBzS1ZSDXa5pHU6kapwAYx5lU41NfRKx4,4247
30
- universal_mcp/tools/manager.py,sha256=73x5RB5Sgew1hNqCOMmoxHZN2DYjiE7pjsytfi0Gj0g,8213
31
- universal_mcp/tools/registry.py,sha256=XDdkuYAE5LnUQvPVNOhFvWo_hsgyONjJNK9ldnQvev4,4122
30
+ universal_mcp/tools/manager.py,sha256=BfJgql6zbmfPJ67P5bBPcv5vO3rt7PUdoPqKjV7TXss,7803
31
+ universal_mcp/tools/registry.py,sha256=Fh7iM9qLLulIBkiO48FshP-haGsFvCgh405kXKDuJcI,5046
32
32
  universal_mcp/tools/tools.py,sha256=Lk-wUO3rfhwdxaRANtC7lQr5fXi7nclf0oHzxNAb79Q,4927
33
+ universal_mcp/tools/utils.py,sha256=cV0Au1-XmR_X4eyqpDy7pxPCgfqEcqVBpzPM9G6QKF4,1167
33
34
  universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
34
35
  universal_mcp/utils/installation.py,sha256=PU_GfHPqzkumKk-xG4L9CkBzSmABxmchwblZkx-zY-I,7204
35
36
  universal_mcp/utils/prompts.py,sha256=yaZ8lj5GooA8bbT42lcWR2r35HAnOalCvyWS1I2ltGM,27439
@@ -48,8 +49,8 @@ universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vH
48
49
  universal_mcp/utils/openapi/test_generator.py,sha256=vucBh9klWmQOUA740TFwfM9ry2nkwKWQiNRcsiZ9HbY,12229
49
50
  universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
50
51
  universal_mcp/utils/templates/api_client.py.j2,sha256=DS1nczOOD8YkMexVSGpUGeyc0nYGKKTPadL_x1_if7k,900
51
- universal_mcp-0.1.24rc26.dist-info/METADATA,sha256=IhuO1V1mYnvTmTKzB7hL5v2l1MZQ-HtWcVgxUvKS-zA,3255
52
- universal_mcp-0.1.24rc26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- universal_mcp-0.1.24rc26.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
54
- universal_mcp-0.1.24rc26.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
55
- universal_mcp-0.1.24rc26.dist-info/RECORD,,
52
+ universal_mcp-0.1.24rc27.dist-info/METADATA,sha256=vtEpLsfmhnWdbwr35e1cJ0Gw1XgIavr-a3CUdcdw1dE,3255
53
+ universal_mcp-0.1.24rc27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
54
+ universal_mcp-0.1.24rc27.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
55
+ universal_mcp-0.1.24rc27.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
56
+ universal_mcp-0.1.24rc27.dist-info/RECORD,,