universal-mcp 0.1.24rc17__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.24rc17 → universal_mcp-0.1.24rc21}/PKG-INFO +1 -2
  2. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/pyproject.toml +1 -2
  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.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_tool_manager.py +11 -54
  7. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/README.md +2 -2
  8. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/client.py +11 -0
  9. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/registry.py +91 -44
  10. universal_mcp-0.1.24rc21/src/universal_mcp/agentr/server.py +52 -0
  11. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/sample/app.py +79 -20
  12. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/integration.py +17 -0
  13. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/server.py +24 -10
  14. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/adapters.py +15 -1
  15. universal_mcp-0.1.24rc21/src/universal_mcp/tools/local_registry.py +109 -0
  16. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/manager.py +5 -64
  17. universal_mcp-0.1.24rc21/src/universal_mcp/tools/registry.py +97 -0
  18. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/api_client.py.j2 +2 -2
  19. universal_mcp-0.1.24rc17/src/tests/test_api_integration.py +0 -21
  20. universal_mcp-0.1.24rc17/src/tests/test_localserver.py +0 -28
  21. universal_mcp-0.1.24rc17/src/tests/test_zenquotes.py +0 -28
  22. universal_mcp-0.1.24rc17/src/universal_mcp/agentr/server.py +0 -51
  23. universal_mcp-0.1.24rc17/src/universal_mcp/tools/registry.py +0 -88
  24. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/.gitignore +0 -0
  25. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/LICENSE +0 -0
  26. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/README.md +0 -0
  27. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/__init__.py +0 -0
  28. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/conftest.py +0 -0
  29. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_api_generator.py +0 -0
  30. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_stores.py +0 -0
  31. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_tool.py +0 -0
  32. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/__init__.py +0 -0
  33. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/integration.py +0 -0
  34. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/application.py +0 -0
  35. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/utils.py +0 -0
  36. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/cli.py +0 -0
  37. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/oauth.py +0 -0
  38. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/token_store.py +0 -0
  39. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/transport.py +0 -0
  40. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/config.py +0 -0
  41. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/exceptions.py +0 -0
  42. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/__init__.py +0 -0
  43. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/logger.py +0 -0
  44. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/py.typed +0 -0
  45. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/__init__.py +0 -0
  46. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/__init__.py +0 -0
  47. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/store.py +0 -0
  48. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/__init__.py +0 -0
  49. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/docstring_parser.py +0 -0
  50. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/func_metadata.py +0 -0
  51. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/tools.py +0 -0
  52. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/types.py +0 -0
  53. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/__init__.py +0 -0
  54. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/installation.py +0 -0
  55. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  56. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  57. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
  58. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/cli.py +0 -0
  59. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  60. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/filters.py +0 -0
  61. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/openapi.py +0 -0
  62. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/postprocessor.py +0 -0
  63. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
  64. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/readme.py +0 -0
  65. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/test_generator.py +0 -0
  66. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/prompts.py +0 -0
  67. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/singleton.py +0 -0
  68. {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  69. {universal_mcp-0.1.24rc17 → 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.24rc17
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
@@ -32,7 +32,6 @@ Requires-Dist: pyyaml>=6.0.2
32
32
  Requires-Dist: rich>=14.0.0
33
33
  Requires-Dist: streamlit>=1.46.1
34
34
  Requires-Dist: typer>=0.15.2
35
- Requires-Dist: universal-mcp-applications>=0.1.2
36
35
  Provides-Extra: dev
37
36
  Requires-Dist: litellm>=1.30.7; extra == 'dev'
38
37
  Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.24-rc17"
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 = [
@@ -39,7 +39,6 @@ dependencies = [
39
39
  "rich>=14.0.0",
40
40
  "streamlit>=1.46.1",
41
41
  "typer>=0.15.2",
42
- "universal-mcp-applications>=0.1.2",
43
42
  ]
44
43
 
45
44
  [project.optional-dependencies]
@@ -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
+ ```
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import os
2
3
  from typing import Any
3
4
 
@@ -25,6 +26,7 @@ class AgentrClient:
25
26
  base_url = base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
26
27
  self.base_url = f"{base_url.rstrip('/')}/v1"
27
28
  api_key = api_key or os.getenv("AGENTR_API_KEY")
29
+ self.user_id = None
28
30
  if api_key:
29
31
  self.client = httpx.Client(
30
32
  base_url=self.base_url,
@@ -34,6 +36,7 @@ class AgentrClient:
34
36
  verify=False,
35
37
  )
36
38
  me_data = self.me()
39
+ self.user_id = me_data["id"]
37
40
  logger.debug(f"Client initialized with user: {me_data['email']}")
38
41
  elif auth_token:
39
42
  logger.debug("Initializing client with auth token")
@@ -45,6 +48,7 @@ class AgentrClient:
45
48
  verify=False,
46
49
  )
47
50
  me_data = self.me()
51
+ self.user_id = me_data["id"]
48
52
  logger.debug(f"Client initialized with user: {me_data['email']}")
49
53
  else:
50
54
  raise ValueError("No API key or auth token provided")
@@ -207,3 +211,10 @@ class AgentrClient:
207
211
  response = self.client.get("/tools/", params=params)
208
212
  response.raise_for_status()
209
213
  return response.json().get("items", [])
214
+
215
+ def _upload_file(self, file_name: str, mime_type: str, base64_data: str) -> str:
216
+ """Upload a file to the server."""
217
+ files = {"file": (file_name, io.BytesIO(base64_data), mime_type)}
218
+ reponse = self.client.post("/files/upload", files=files)
219
+ reponse.raise_for_status()
220
+ return reponse.json()
@@ -1,25 +1,49 @@
1
+ import base64
1
2
  from typing import Any
2
3
 
3
4
  from loguru import logger
4
5
 
5
6
  from universal_mcp.agentr.client import AgentrClient
7
+ from universal_mcp.applications.application import BaseApplication
6
8
  from universal_mcp.applications.utils import app_from_slug
7
- 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
8
11
  from universal_mcp.tools.registry import ToolRegistry
9
12
  from universal_mcp.types import ToolConfig, ToolFormat
10
13
 
11
14
  from .integration import AgentrIntegration
12
15
 
16
+ MARKDOWN_INSTRUCTIONS = """Always render the URL in markdown format for images and media files. Here are examples:
17
+ The url is provided in the response as "signed_url".
18
+ For images:
19
+ - Use markdown image syntax: ![alt text](url)
20
+ - Example: ![Generated sunset image](https://example.com/image.png)
21
+ - Always include descriptive alt text that explains what the image shows
22
+
23
+ For audio files:
24
+ - Use markdown link syntax with audio description: [🔊 Audio file description](url)
25
+ - Example: [🔊 Generated speech audio](https://example.com/audio.mp3)
26
+
27
+ For other media:
28
+ - Use descriptive link text: [📄 File description](url)
29
+ - Example: [📄 Generated document](https://example.com/document.pdf)
30
+
31
+ Always make the links clickable and include relevant context about what the user will see or hear when they access the URL."""
32
+
13
33
 
14
34
  class AgentrRegistry(ToolRegistry):
15
35
  """Platform manager implementation for AgentR platform."""
16
36
 
17
37
  def __init__(self, client: AgentrClient | None = None, **kwargs):
18
38
  """Initialize the AgentR platform manager."""
19
-
39
+ super().__init__()
20
40
  self.client = client or AgentrClient(**kwargs)
21
- self.tool_manager = ToolManager()
22
- 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)
23
47
 
24
48
  async def list_all_apps(self) -> list[dict[str, Any]]:
25
49
  """Get list of available apps from AgentR.
@@ -120,7 +144,7 @@ class AgentrRegistry(ToolRegistry):
120
144
  self,
121
145
  tools: list[str] | ToolConfig,
122
146
  format: ToolFormat,
123
- ) -> str:
147
+ ) -> list[Any]:
124
148
  """Export given tools to required format.
125
149
 
126
150
  Args:
@@ -128,60 +152,83 @@ class AgentrRegistry(ToolRegistry):
128
152
  format: The format to export tools to (native, mcp, langchain, openai)
129
153
 
130
154
  Returns:
131
- String representation of tools in the specified format
155
+ List of tools in the specified format
132
156
  """
157
+ from langchain_core.tools import StructuredTool
158
+
133
159
  try:
134
160
  # Clear tools from tool manager before loading new tools
135
161
  self.tool_manager.clear_tools()
162
+ logger.info(f"Exporting tools to {format.value} format")
136
163
  if isinstance(tools, dict):
137
- logger.info("Loading tools from tool config")
138
- self._load_tools_from_tool_config(tools, self.tool_manager)
164
+ self._load_tools_from_tool_config(tools)
139
165
  else:
140
- logger.info("Loading tools from list")
141
- self._load_agentr_tools_from_list(tools, self.tool_manager)
142
- loaded_tools = self.tool_manager.list_tools(format=format)
143
- logger.info(f"Exporting {len(loaded_tools)} tools to {format} format")
144
- return loaded_tools
145
- except Exception as e:
146
- logger.error(f"Error exporting tools: {e}")
147
- return ""
166
+ self._load_tools_from_list(tools)
148
167
 
149
- def _load_tools(self, app_name: str, tool_names: list[str], tool_manager: ToolManager) -> None:
150
- """Helper method to load and register tools for an app."""
151
- app = app_from_slug(app_name)
152
- integration = AgentrIntegration(name=app_name, client=self.client)
153
- app_instance = app(integration=integration)
154
- tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
168
+ loaded_tools = self.tool_manager.get_tools()
155
169
 
156
- def _load_agentr_tools_from_list(self, tools: list[str], tool_manager: ToolManager) -> None:
157
- """Load tools from AgentR and register them as tools.
170
+ if format != ToolFormat.LANGCHAIN:
171
+ return convert_tools(loaded_tools, format)
158
172
 
159
- Args:
160
- tools: The list of tools to load (prefixed with app name)
161
- tool_manager: The tool manager to register tools with
162
- """
163
- logger.info(f"Loading all tools: {tools}")
164
- tools_by_app = {}
165
- for tool_name in tools:
166
- app_name, _ = _get_app_and_tool_name(tool_name)
167
- tools_by_app.setdefault(app_name, []).append(tool_name)
173
+ logger.info(f"Exporting {len(loaded_tools)} tools to LangChain format with special handling")
168
174
 
169
- for app_name, tool_names in tools_by_app.items():
170
- self._load_tools(app_name, tool_names, tool_manager)
175
+ langchain_tools = []
176
+ for tool in loaded_tools:
171
177
 
172
- def _load_tools_from_tool_config(self, tool_config: ToolConfig, tool_manager: ToolManager) -> None:
173
- """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)
174
184
 
175
- Args:
176
- tool_config: The tool configuration containing app names and tools
177
- tool_manager: The tool manager to register tools with
178
- """
179
- for app_name, tool_names in tool_config.items():
180
- self._load_tools(app_name, tool_names, tool_manager)
185
+ return call_tool_wrapper
186
+
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:
203
+ if isinstance(data, dict):
204
+ type_ = data.get("type")
205
+ if type_ == "image" or type_ == "audio":
206
+ # Special handling for images and audio
207
+ base64_data = data.get("data")
208
+ mime_type = data.get("mime_type")
209
+ file_name = data.get("file_name")
210
+ if not mime_type or not file_name:
211
+ raise ToolError("Mime type or file name is missing")
212
+ bytes_data = base64.b64decode(base64_data)
213
+ response = self.client._upload_file(file_name, mime_type, bytes_data)
214
+ # Hard code instructions for llm
215
+ response = {**response, "instructions": MARKDOWN_INSTRUCTIONS}
216
+ return response
217
+ return data
181
218
 
182
219
  async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> dict[str, Any]:
183
220
  """Call a tool with the given name and arguments."""
184
- return await self.tool_manager.call_tool(tool_name, tool_args)
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
185
232
 
186
233
  async def list_connected_apps(self) -> list[str]:
187
234
  """List all apps that the user has connected."""