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.
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/PKG-INFO +1 -2
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/pyproject.toml +1 -2
- 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.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_tool_manager.py +11 -54
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/README.md +2 -2
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/client.py +11 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/registry.py +91 -44
- universal_mcp-0.1.24rc21/src/universal_mcp/agentr/server.py +52 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/sample/app.py +79 -20
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/integration.py +17 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/server.py +24 -10
- {universal_mcp-0.1.24rc17 → 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.24rc17 → 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.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/api_client.py.j2 +2 -2
- universal_mcp-0.1.24rc17/src/tests/test_api_integration.py +0 -21
- universal_mcp-0.1.24rc17/src/tests/test_localserver.py +0 -28
- universal_mcp-0.1.24rc17/src/tests/test_zenquotes.py +0 -28
- universal_mcp-0.1.24rc17/src/universal_mcp/agentr/server.py +0 -51
- universal_mcp-0.1.24rc17/src/universal_mcp/tools/registry.py +0 -88
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/.gitignore +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/LICENSE +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/README.md +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_api_generator.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/agentr/integration.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/applications/utils.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/cli.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/oauth.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/token_store.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/client/transport.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/integrations/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/docstring_parser.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/tools/tools.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/types.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/cli.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/docgen.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/filters.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/openapi.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/postprocessor.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/readme.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/openapi/test_generator.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/prompts.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/singleton.py +0 -0
- {universal_mcp-0.1.24rc17 → universal_mcp-0.1.24rc21}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
- {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.
|
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-
|
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.
|
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
|
+
```
|
@@ -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.
|
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: 
|
20
|
+
- Example: 
|
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
|
-
|
22
|
-
|
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
|
-
) ->
|
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
|
-
|
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
|
-
|
138
|
-
self._load_tools_from_tool_config(tools, self.tool_manager)
|
164
|
+
self._load_tools_from_tool_config(tools)
|
139
165
|
else:
|
140
|
-
|
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
|
-
|
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
|
-
|
157
|
-
|
170
|
+
if format != ToolFormat.LANGCHAIN:
|
171
|
+
return convert_tools(loaded_tools, format)
|
158
172
|
|
159
|
-
|
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
|
-
|
170
|
-
|
175
|
+
langchain_tools = []
|
176
|
+
for tool in loaded_tools:
|
171
177
|
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
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."""
|