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.
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/PKG-INFO +1 -1
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/pyproject.toml +1 -1
- universal_mcp-0.1.24rc21/src/tests/test_apps.py +64 -0
- universal_mcp-0.1.24rc21/src/tests/test_local_registry.py +94 -0
- universal_mcp-0.1.24rc21/src/tests/test_localserver.py +71 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_tool_manager.py +11 -54
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/README.md +2 -2
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/registry.py +60 -48
- universal_mcp-0.1.24rc21/src/universal_mcp/agentr/server.py +52 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/integration.py +17 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/server.py +24 -10
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/adapters.py +15 -1
- universal_mcp-0.1.24rc21/src/universal_mcp/tools/local_registry.py +109 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/manager.py +5 -64
- universal_mcp-0.1.24rc21/src/universal_mcp/tools/registry.py +97 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/api_client.py.j2 +2 -2
- universal_mcp-0.1.24rc19/src/tests/test_api_integration.py +0 -21
- universal_mcp-0.1.24rc19/src/tests/test_localserver.py +0 -28
- universal_mcp-0.1.24rc19/src/tests/test_zenquotes.py +0 -28
- universal_mcp-0.1.24rc19/src/universal_mcp/agentr/server.py +0 -51
- universal_mcp-0.1.24rc19/src/universal_mcp/tools/registry.py +0 -88
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/.gitignore +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/LICENSE +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/README.md +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_api_generator.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/client.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/integration.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/sample/app.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/utils.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/cli.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/oauth.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/token_store.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/transport.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/docstring_parser.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/tools.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/types.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/cli.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/docgen.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/filters.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/openapi.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/postprocessor.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/readme.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/test_generator.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/prompts.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/singleton.py +0 -0
- {universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
- {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.
|
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-
|
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.
|
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.
|
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.
|
97
|
+
assert len(tool_manager.get_tools()) == 3
|
100
98
|
tool_manager.clear_tools()
|
101
|
-
assert len(tool_manager.
|
99
|
+
assert len(tool_manager.get_tools()) == 0
|
102
100
|
|
103
101
|
|
104
|
-
def
|
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
|
-
|
109
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
9
|
+
from universal_mcp.exceptions import ToolError, ToolNotFoundError
|
10
|
+
from universal_mcp.tools.adapters import convert_tools
|
10
11
|
from universal_mcp.tools.registry import ToolRegistry
|
11
12
|
from universal_mcp.types import ToolConfig, ToolFormat
|
12
13
|
|
@@ -35,10 +36,14 @@ class AgentrRegistry(ToolRegistry):
|
|
35
36
|
|
36
37
|
def __init__(self, client: AgentrClient | None = None, **kwargs):
|
37
38
|
"""Initialize the AgentR platform manager."""
|
38
|
-
|
39
|
+
super().__init__()
|
39
40
|
self.client = client or AgentrClient(**kwargs)
|
40
|
-
|
41
|
-
|
41
|
+
|
42
|
+
def _create_app_instance(self, app_name: str) -> BaseApplication:
|
43
|
+
"""Create an app instance with an AgentrIntegration."""
|
44
|
+
app = app_from_slug(app_name)
|
45
|
+
integration = AgentrIntegration(name=app_name, client=self.client)
|
46
|
+
return app(integration=integration)
|
42
47
|
|
43
48
|
async def list_all_apps(self) -> list[dict[str, Any]]:
|
44
49
|
"""Get list of available apps from AgentR.
|
@@ -139,7 +144,7 @@ class AgentrRegistry(ToolRegistry):
|
|
139
144
|
self,
|
140
145
|
tools: list[str] | ToolConfig,
|
141
146
|
format: ToolFormat,
|
142
|
-
) ->
|
147
|
+
) -> list[Any]:
|
143
148
|
"""Export given tools to required format.
|
144
149
|
|
145
150
|
Args:
|
@@ -147,61 +152,54 @@ class AgentrRegistry(ToolRegistry):
|
|
147
152
|
format: The format to export tools to (native, mcp, langchain, openai)
|
148
153
|
|
149
154
|
Returns:
|
150
|
-
|
155
|
+
List of tools in the specified format
|
151
156
|
"""
|
157
|
+
from langchain_core.tools import StructuredTool
|
158
|
+
|
152
159
|
try:
|
153
160
|
# Clear tools from tool manager before loading new tools
|
154
161
|
self.tool_manager.clear_tools()
|
162
|
+
logger.info(f"Exporting tools to {format.value} format")
|
155
163
|
if isinstance(tools, dict):
|
156
|
-
|
157
|
-
self._load_tools_from_tool_config(tools, self.tool_manager)
|
164
|
+
self._load_tools_from_tool_config(tools)
|
158
165
|
else:
|
159
|
-
|
160
|
-
self._load_agentr_tools_from_list(tools, self.tool_manager)
|
161
|
-
loaded_tools = self.tool_manager.list_tools(format=format)
|
162
|
-
logger.info(f"Exporting {len(loaded_tools)} tools to {format} format")
|
163
|
-
return loaded_tools
|
164
|
-
except Exception as e:
|
165
|
-
logger.error(f"Error exporting tools: {e}")
|
166
|
-
return ""
|
166
|
+
self._load_tools_from_list(tools)
|
167
167
|
|
168
|
-
|
169
|
-
"""Helper method to load and register tools for an app."""
|
170
|
-
app = app_from_slug(app_name)
|
171
|
-
integration = AgentrIntegration(name=app_name, client=self.client)
|
172
|
-
app_instance = app(integration=integration)
|
173
|
-
tool_manager.register_tools_from_app(app_instance, tool_names=tool_names)
|
168
|
+
loaded_tools = self.tool_manager.get_tools()
|
174
169
|
|
175
|
-
|
176
|
-
|
170
|
+
if format != ToolFormat.LANGCHAIN:
|
171
|
+
return convert_tools(loaded_tools, format)
|
177
172
|
|
178
|
-
|
179
|
-
tools: The list of tools to load (prefixed with app name)
|
180
|
-
tool_manager: The tool manager to register tools with
|
181
|
-
"""
|
182
|
-
logger.info(f"Loading all tools: {tools}")
|
183
|
-
tools_by_app = {}
|
184
|
-
for tool_name in tools:
|
185
|
-
app_name, _ = _get_app_and_tool_name(tool_name)
|
186
|
-
tools_by_app.setdefault(app_name, []).append(tool_name)
|
173
|
+
logger.info(f"Exporting {len(loaded_tools)} tools to LangChain format with special handling")
|
187
174
|
|
188
|
-
|
189
|
-
|
175
|
+
langchain_tools = []
|
176
|
+
for tool in loaded_tools:
|
190
177
|
|
191
|
-
|
192
|
-
|
178
|
+
def create_coroutine(t):
|
179
|
+
async def call_tool_wrapper(**arguments: dict[str, Any]):
|
180
|
+
logger.debug(
|
181
|
+
f"Executing registry-wrapped LangChain tool '{t.name}' with arguments: {arguments}"
|
182
|
+
)
|
183
|
+
return await self.call_tool(t.name, arguments)
|
193
184
|
|
194
|
-
|
195
|
-
tool_config: The tool configuration containing app names and tools
|
196
|
-
tool_manager: The tool manager to register tools with
|
197
|
-
"""
|
198
|
-
for app_name, tool_names in tool_config.items():
|
199
|
-
self._load_tools(app_name, tool_names, tool_manager)
|
185
|
+
return call_tool_wrapper
|
200
186
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
187
|
+
langchain_tool = StructuredTool(
|
188
|
+
name=tool.name,
|
189
|
+
description=tool.description or "",
|
190
|
+
coroutine=create_coroutine(tool),
|
191
|
+
response_format="content",
|
192
|
+
args_schema=tool.parameters,
|
193
|
+
)
|
194
|
+
langchain_tools.append(langchain_tool)
|
195
|
+
|
196
|
+
return langchain_tools
|
197
|
+
|
198
|
+
except Exception as e:
|
199
|
+
logger.error(f"Error exporting tools: {e}")
|
200
|
+
return []
|
201
|
+
|
202
|
+
def _handle_special_output(self, data: Any) -> Any:
|
205
203
|
if isinstance(data, dict):
|
206
204
|
type_ = data.get("type")
|
207
205
|
if type_ == "image" or type_ == "audio":
|
@@ -218,6 +216,20 @@ class AgentrRegistry(ToolRegistry):
|
|
218
216
|
return response
|
219
217
|
return data
|
220
218
|
|
219
|
+
async def call_tool(self, tool_name: str, tool_args: dict[str, Any]) -> dict[str, Any]:
|
220
|
+
"""Call a tool with the given name and arguments."""
|
221
|
+
logger.debug(f"Calling tool: {tool_name} with arguments: {tool_args}")
|
222
|
+
tool = self.tool_manager.get_tool(tool_name)
|
223
|
+
if not tool:
|
224
|
+
logger.error(f"Unknown tool: {tool_name}")
|
225
|
+
raise ToolNotFoundError(f"Unknown tool: {tool_name}")
|
226
|
+
try:
|
227
|
+
data = await tool.run(tool_args)
|
228
|
+
logger.debug(f"Tool {tool_name} called with args {tool_args} and returned {data}")
|
229
|
+
return self._handle_special_output(data)
|
230
|
+
except Exception as e:
|
231
|
+
raise e
|
232
|
+
|
221
233
|
async def list_connected_apps(self) -> list[str]:
|
222
234
|
"""List all apps that the user has connected."""
|
223
235
|
return self.client.list_my_connections()
|
@@ -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
|
{universal_mcp-0.1.24rc19 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/integration.py
RENAMED
@@ -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)
|