universal-mcp 0.1.24rc19__tar.gz → 0.1.24rc21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/PKG-INFO +1 -1
  2. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/pyproject.toml +1 -1
  3. universal_mcp-0.1.24rc21/src/tests/test_apps.py +64 -0
  4. universal_mcp-0.1.24rc21/src/tests/test_local_registry.py +94 -0
  5. universal_mcp-0.1.24rc21/src/tests/test_localserver.py +71 -0
  6. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_tool_manager.py +11 -54
  7. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/README.md +2 -2
  8. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/registry.py +60 -48
  9. universal_mcp-0.1.24rc21/src/universal_mcp/agentr/server.py +52 -0
  10. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/integration.py +17 -0
  11. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/server.py +24 -10
  12. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/adapters.py +15 -1
  13. universal_mcp-0.1.24rc21/src/universal_mcp/tools/local_registry.py +109 -0
  14. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/manager.py +5 -64
  15. universal_mcp-0.1.24rc21/src/universal_mcp/tools/registry.py +97 -0
  16. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/api_client.py.j2 +2 -2
  17. universal_mcp-0.1.24rc19/src/tests/test_api_integration.py +0 -21
  18. universal_mcp-0.1.24rc19/src/tests/test_localserver.py +0 -28
  19. universal_mcp-0.1.24rc19/src/tests/test_zenquotes.py +0 -28
  20. universal_mcp-0.1.24rc19/src/universal_mcp/agentr/server.py +0 -51
  21. universal_mcp-0.1.24rc19/src/universal_mcp/tools/registry.py +0 -88
  22. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/.gitignore +0 -0
  23. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/LICENSE +0 -0
  24. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/README.md +0 -0
  25. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/__init__.py +0 -0
  26. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/conftest.py +0 -0
  27. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_api_generator.py +0 -0
  28. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_stores.py +0 -0
  29. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_tool.py +0 -0
  30. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/__init__.py +0 -0
  31. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/client.py +0 -0
  32. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/integration.py +0 -0
  33. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/application.py +0 -0
  34. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/sample/app.py +0 -0
  35. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/utils.py +0 -0
  36. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/cli.py +0 -0
  37. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/oauth.py +0 -0
  38. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/token_store.py +0 -0
  39. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/transport.py +0 -0
  40. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/config.py +0 -0
  41. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/exceptions.py +0 -0
  42. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/__init__.py +0 -0
  43. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/logger.py +0 -0
  44. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/py.typed +0 -0
  45. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/__init__.py +0 -0
  46. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/__init__.py +0 -0
  47. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/store.py +0 -0
  48. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/__init__.py +0 -0
  49. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/docstring_parser.py +0 -0
  50. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/func_metadata.py +0 -0
  51. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/tools.py +0 -0
  52. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/types.py +0 -0
  53. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/__init__.py +0 -0
  54. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/installation.py +0 -0
  55. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  56. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  57. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
  58. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/cli.py +0 -0
  59. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  60. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/filters.py +0 -0
  61. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/openapi.py +0 -0
  62. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/postprocessor.py +0 -0
  63. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
  64. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/readme.py +0 -0
  65. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/test_generator.py +0 -0
  66. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/prompts.py +0 -0
  67. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/singleton.py +0 -0
  68. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  69. {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/testing.py +0 -0
@@ -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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.24-rc19"
7
+ version = "0.1.24-rc21"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,64 @@
1
+ import pytest
2
+
3
+ from universal_mcp.applications.sample.app import SampleApp
4
+
5
+
6
+ @pytest.mark.asyncio
7
+ async def test_sample_app_initialization():
8
+ """Test that the SampleApp initializes correctly."""
9
+ app = SampleApp()
10
+
11
+ # Check app initialized
12
+ assert app is not None
13
+ assert app.name == "sample"
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_sample_app_tools():
18
+ """Test that the SampleApp provides the expected tools."""
19
+ app = SampleApp()
20
+
21
+ # Get list of tools
22
+ tools = app.list_tools()
23
+
24
+ # Check that tools are available
25
+ assert len(tools) > 0
26
+
27
+ # Check specific tools exist
28
+ tool_names = [tool.__name__ for tool in tools]
29
+ expected_tools = [
30
+ "get_current_time",
31
+ "get_current_date",
32
+ "calculate",
33
+ "read_file",
34
+ "write_file",
35
+ "get_simple_weather",
36
+ "generate_image",
37
+ ]
38
+
39
+ for expected_tool in expected_tools:
40
+ assert expected_tool in tool_names
41
+
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_sample_app_basic_functionality():
45
+ """Test basic functionality of SampleApp tools."""
46
+ app = SampleApp()
47
+
48
+ # Test get_current_time
49
+ current_time = app.get_current_time()
50
+ assert isinstance(current_time, str)
51
+ assert len(current_time) > 0
52
+
53
+ # Test get_current_date
54
+ current_date = app.get_current_date()
55
+ assert isinstance(current_date, str)
56
+ assert len(current_date) > 0
57
+
58
+ # Test calculate
59
+ result = app.calculate("2 + 2")
60
+ assert "Result: 4" in result
61
+
62
+ # Test calculate with error
63
+ error_result = app.calculate("invalid expression")
64
+ assert "Error in calculation" in error_result
@@ -0,0 +1,94 @@
1
+ import os
2
+ import shutil
3
+
4
+ import pytest
5
+ from langchain_core.tools import StructuredTool
6
+ from mcp.server.fastmcp.server import MCPTool
7
+
8
+ from universal_mcp.exceptions import ToolNotFoundError
9
+ from universal_mcp.tools.local_registry import LocalRegistry
10
+ from universal_mcp.types import ToolFormat
11
+
12
+
13
+ @pytest.fixture
14
+ def registry():
15
+ """Provides a LocalRegistry instance and cleans up the output directory."""
16
+ output_dir = "test_output"
17
+ reg = LocalRegistry(output_dir=output_dir)
18
+ yield reg
19
+ # Cleanup the test output directory after tests are done
20
+ if os.path.exists(output_dir):
21
+ shutil.rmtree(output_dir)
22
+
23
+
24
+ @pytest.mark.asyncio
25
+ async def test_export_tools_formats(registry: LocalRegistry):
26
+ """Verify that tools are exported to all supported formats correctly."""
27
+ tool_names = ["sample__get_current_date", "sample__calculate"]
28
+
29
+ # Test MCP Format
30
+ mcp_tools = await registry.export_tools(tool_names, format=ToolFormat.MCP)
31
+ assert len(mcp_tools) == 2
32
+ assert all(isinstance(t, MCPTool) for t in mcp_tools)
33
+
34
+ # Test LangChain Format
35
+ langchain_tools = await registry.export_tools(tool_names, format=ToolFormat.LANGCHAIN)
36
+ assert len(langchain_tools) == 2
37
+ assert all(isinstance(t, StructuredTool) for t in langchain_tools)
38
+
39
+ # Test OpenAI Format
40
+ openai_tools = await registry.export_tools(tool_names, format=ToolFormat.OPENAI)
41
+ assert len(openai_tools) == 2
42
+ assert all(isinstance(t, dict) and t.get("type") == "function" for t in openai_tools)
43
+
44
+ # Test Native Format
45
+ native_tools = await registry.export_tools(tool_names, format=ToolFormat.NATIVE)
46
+ assert len(native_tools) == 2
47
+ assert all(callable(t) for t in native_tools)
48
+
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_call_tool_success(registry: LocalRegistry):
52
+ """Test a successful tool call through the registry."""
53
+ await registry.export_tools(["sample__calculate"], format=ToolFormat.NATIVE) # Load the tool
54
+ result = await registry.call_tool("sample__calculate", {"expression": "10 - 4"})
55
+ assert result == "Result: 6"
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_call_tool_not_found(registry: LocalRegistry):
60
+ """Test that calling a non-existent tool raises the correct exception."""
61
+ with pytest.raises(ToolNotFoundError):
62
+ await registry.call_tool("nonexistent__tool", {})
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_file_output_handling(registry: LocalRegistry):
67
+ """Test that file outputs are correctly handled by writing to a file."""
68
+ tool_name = "sample__generate_image"
69
+ await registry.export_tools([tool_name], format=ToolFormat.NATIVE) # Load the tool
70
+
71
+ result = await registry.call_tool(tool_name, {"prompt": "testing file output"})
72
+ assert isinstance(result, str)
73
+ assert "File saved to:" in result
74
+
75
+ # Verify the file was actually created
76
+ file_path = result.split("File saved to: ")[1]
77
+ assert os.path.exists(file_path)
78
+
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_unimplemented_methods(registry: LocalRegistry):
82
+ """Test that abstract methods raise NotImplementedError."""
83
+ with pytest.raises(NotImplementedError):
84
+ await registry.list_all_apps()
85
+ with pytest.raises(NotImplementedError):
86
+ await registry.get_app_details("some_app")
87
+ with pytest.raises(NotImplementedError):
88
+ await registry.search_apps("query")
89
+ with pytest.raises(NotImplementedError):
90
+ await registry.list_tools("some_app")
91
+ with pytest.raises(NotImplementedError):
92
+ await registry.search_tools("query")
93
+ with pytest.raises(NotImplementedError):
94
+ await registry.list_connected_apps()
@@ -0,0 +1,71 @@
1
+ import pytest
2
+
3
+ from universal_mcp.config import AppConfig, ServerConfig
4
+ from universal_mcp.servers.server import LocalServer
5
+ from universal_mcp.tools.local_registry import LocalRegistry
6
+
7
+
8
+ @pytest.fixture
9
+ def registry():
10
+ """Provides a fresh LocalRegistry for each test."""
11
+ return LocalRegistry()
12
+
13
+
14
+ @pytest.mark.asyncio
15
+ async def test_local_server_initialization(registry: LocalRegistry):
16
+ """Test that the server initializes correctly and loads default tools."""
17
+ apps_list = [AppConfig(name="sample")]
18
+ server_config = ServerConfig(apps=apps_list)
19
+ server = LocalServer(server_config, registry=registry)
20
+
21
+ assert server is not None
22
+ assert server.registry is registry
23
+
24
+ tools = await server.list_tools()
25
+ tool_names = {tool.name for tool in tools}
26
+
27
+ # By default, only "important" tools should be loaded
28
+ assert "sample__get_current_time" in tool_names
29
+ assert "sample__calculate" in tool_names
30
+ assert "sample__generate_image" in tool_names
31
+ # This tool is not tagged as important
32
+ assert "sample__get_simple_weather" not in tool_names
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_local_server_loads_specific_actions(registry: LocalRegistry):
37
+ """Test that the server loads only the actions specified in the config."""
38
+ actions = ["get_current_date", "calculate"]
39
+ apps_list = [AppConfig(name="sample", actions=actions)]
40
+ server_config = ServerConfig(apps=apps_list)
41
+ server = LocalServer(server_config, registry=registry)
42
+
43
+ tools = await server.list_tools()
44
+ assert len(tools) == 2
45
+ tool_names = {tool.name for tool in tools}
46
+ assert tool_names == {"sample__get_current_date", "sample__calculate"}
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_local_server_no_apps(registry: LocalRegistry):
51
+ """Test that the server handles a config with no apps gracefully."""
52
+ server_config = ServerConfig(apps=[])
53
+ server = LocalServer(server_config, registry=registry)
54
+
55
+ tools = await server.list_tools()
56
+ assert len(tools) == 0
57
+
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_local_server_tool_call(registry: LocalRegistry):
61
+ """Test that a tool can be successfully called through the server."""
62
+ apps_list = [AppConfig(name="sample", actions=["get_current_date"])]
63
+ server_config = ServerConfig(apps=apps_list)
64
+ server = LocalServer(server_config, registry=registry)
65
+
66
+ # Test tool call
67
+ result = await server.call_tool("sample__get_current_date", {})
68
+ assert result is not None
69
+ # A basic check on the result format
70
+ assert isinstance(result[0].text, str)
71
+ assert "-" in result[0].text
@@ -1,8 +1,6 @@
1
1
  import pytest
2
2
 
3
3
  from universal_mcp.applications.application import BaseApplication
4
- from universal_mcp.exceptions import ToolError, ToolNotFoundError
5
- from universal_mcp.tools.adapters import ToolFormat
6
4
  from universal_mcp.tools.manager import Tool, ToolManager
7
5
  from universal_mcp.types import TOOL_NAME_SEPARATOR
8
6
 
@@ -76,14 +74,14 @@ class ExampleApp(BaseApplication):
76
74
  def test_add_tool(tool_manager: ToolManager):
77
75
  tool = tool_manager.add_tool(dummy_add)
78
76
  assert tool.name == "dummy_add"
79
- assert tool.name in [t.name for t in tool_manager.list_tools()]
77
+ assert tool.name in [t.name for t in tool_manager.get_tools()]
80
78
 
81
79
 
82
80
  def test_add_duplicate_tool(tool_manager: ToolManager):
83
81
  tool1 = tool_manager.add_tool(dummy_add)
84
82
  tool2 = tool_manager.add_tool(dummy_add)
85
83
  assert tool1 is tool2 # Should return existing tool
86
- assert len(tool_manager.list_tools()) == 1
84
+ assert len(tool_manager.get_tools()) == 1
87
85
 
88
86
 
89
87
  def test_remove_tool(tool_manager: ToolManager):
@@ -96,26 +94,17 @@ def test_remove_tool(tool_manager: ToolManager):
96
94
  def test_clear_tools(tool_manager: ToolManager, dummy_tools):
97
95
  for tool in dummy_tools:
98
96
  tool_manager.add_tool(tool)
99
- assert len(tool_manager.list_tools()) == 3
97
+ assert len(tool_manager.get_tools()) == 3
100
98
  tool_manager.clear_tools()
101
- assert len(tool_manager.list_tools()) == 0
99
+ assert len(tool_manager.get_tools()) == 0
102
100
 
103
101
 
104
- def test_list_tools_format(tool_manager: ToolManager, dummy_tools):
102
+ def test_get_tools_no_filters(tool_manager: ToolManager, dummy_tools):
105
103
  for tool in dummy_tools:
106
104
  tool_manager.add_tool(tool)
107
105
 
108
- # Test MCP format
109
- mcp_tools = tool_manager.list_tools(format=ToolFormat.MCP)
110
- assert len(mcp_tools) == 3
111
-
112
- # Test LangChain format
113
- langchain_tools = tool_manager.list_tools(format=ToolFormat.LANGCHAIN)
114
- assert len(langchain_tools) == 3
115
-
116
- # Test OpenAI format
117
- openai_tools = tool_manager.list_tools(format=ToolFormat.OPENAI)
118
- assert len(openai_tools) == 3
106
+ tools = tool_manager.get_tools()
107
+ assert len(tools) == 3
119
108
 
120
109
 
121
110
  def test_filter_tools_by_tags(tool_manager: ToolManager, dummy_tools):
@@ -123,53 +112,21 @@ def test_filter_tools_by_tags(tool_manager: ToolManager, dummy_tools):
123
112
  tool_manager.add_tool(tool)
124
113
 
125
114
  # Test filtering by important tag
126
- important_tools = tool_manager.list_tools(tags=["important"])
115
+ important_tools = tool_manager.get_tools(tags=["important"])
127
116
  assert len(important_tools) == 1
128
117
  assert important_tools[0].name == "dummy_add"
129
118
 
130
119
  # Test filtering by math tag
131
- math_tools = tool_manager.list_tools(tags=["math"])
120
+ math_tools = tool_manager.get_tools(tags=["math"])
132
121
  assert len(math_tools) == 2
133
122
 
134
123
 
135
- @pytest.mark.asyncio
136
- async def test_call_tool_success(tool_manager):
137
- tool_manager.add_tool(dummy_add)
138
- result = await tool_manager.call_tool("dummy_add", {"a": 2, "b": 3})
139
- assert result == 5
140
-
141
-
142
- @pytest.mark.asyncio
143
- async def test_call_tool_error(tool_manager: ToolManager):
144
- tool_manager.add_tool(dummy_error)
145
- with pytest.raises(ToolError):
146
- await tool_manager.call_tool("dummy_error", {})
147
-
148
-
149
- @pytest.mark.asyncio
150
- async def test_call_nonexistent_tool(tool_manager: ToolManager):
151
- with pytest.raises(ToolNotFoundError):
152
- await tool_manager.call_tool("nonexistent", {})
153
-
154
-
155
- @pytest.mark.asyncio
156
- async def test_call_tool_from_app(tool_manager: ToolManager):
157
- app = ExampleApp()
158
- # Only important are added by default
159
- tool_manager.register_tools_from_app(app)
160
- tools = tool_manager.list_tools()
161
- assert len(tools) == 1
162
- assert f"example_app{TOOL_NAME_SEPARATOR}dummy_add" in [t.name for t in tools]
163
- result = await tool_manager.call_tool(f"example_app{TOOL_NAME_SEPARATOR}dummy_add", {"a": 2, "b": 3})
164
- assert result == 5
165
-
166
-
167
124
  @pytest.mark.asyncio
168
125
  async def test_call_tool_from_app_with_tags(tool_manager: ToolManager):
169
126
  app = ExampleApp()
170
127
  # Only important are added by default
171
128
  tool_manager.register_tools_from_app(app, tags=["math"])
172
- tools = tool_manager.list_tools()
129
+ tools = tool_manager.get_tools()
173
130
  assert len(tools) == 2
174
131
  assert "example_app__dummy_add" in [t.name for t in tools]
175
132
  assert "example_app__dummy_multiply" in [t.name for t in tools]
@@ -180,7 +137,7 @@ async def test_load_tool_from_name(tool_manager: ToolManager):
180
137
  app = ExampleApp()
181
138
  # Only important are added by default
182
139
  tool_manager.register_tools_from_app(app, tool_names=["dummy_multiply", "dummy_add"])
183
- tools = tool_manager.list_tools()
140
+ tools = tool_manager.get_tools()
184
141
  assert len(tools) == 2
185
142
  assert f"example_app{TOOL_NAME_SEPARATOR}dummy_multiply" in [t.name for t in tools]
186
143
  assert f"example_app{TOOL_NAME_SEPARATOR}dummy_add" in [t.name for t in tools]
@@ -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()
@@ -0,0 +1,52 @@
1
+ from loguru import logger
2
+
3
+ from universal_mcp.config import ServerConfig
4
+ from universal_mcp.servers.server import BaseServer
5
+ from universal_mcp.tools import ToolManager
6
+
7
+ from .client import AgentrClient
8
+ from .registry import AgentrRegistry
9
+
10
+
11
+ class AgentrServer(BaseServer):
12
+ """Server that loads apps and tools from an AgentR instance."""
13
+
14
+ def __init__(self, config: ServerConfig, registry: AgentrRegistry | None = None, **kwargs):
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
+
24
+ self._tools_loaded = False
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
49
+
50
+ @property
51
+ def tool_manager(self) -> ToolManager:
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)