universal-mcp 0.1.24rc2__tar.gz → 0.1.24rc4__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.24rc2 → universal_mcp-0.1.24rc4}/.gitignore +9 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/PKG-INFO +17 -3
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/pyproject.toml +18 -3
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_api_generator.py +3 -8
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_api_integration.py +5 -7
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_applications.py +5 -3
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_tool_manager.py +13 -13
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/README.md +201 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/__init__.py +6 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/agentr.py +30 -0
- universal_mcp-0.1.24rc2/src/universal_mcp/utils/agentr.py → universal_mcp-0.1.24rc4/src/universal_mcp/agentr/client.py +19 -3
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/integration.py +104 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/registry.py +91 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agentr/server.py +51 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/__init__.py +6 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/auto.py +576 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/base.py +88 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/cli.py +27 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/codeact/__init__.py +243 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/codeact/sandbox.py +27 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/codeact/test.py +15 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/codeact/utils.py +61 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/hil.py +104 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/llm.py +10 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/react.py +58 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/simple.py +40 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/agents/utils.py +111 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/analytics.py +5 -7
- universal_mcp-0.1.24rc4/src/universal_mcp/applications/__init__.py +70 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/applications/application.py +1 -1
- universal_mcp-0.1.24rc4/src/universal_mcp/applications/sample/app.py +245 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/cli.py +10 -3
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/config.py +33 -7
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/exceptions.py +4 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/integrations/__init__.py +11 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/integrations/integration.py +9 -91
- universal_mcp-0.1.24rc4/src/universal_mcp/servers/__init__.py +3 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/servers/server.py +10 -51
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/__init__.py +3 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/adapters.py +20 -11
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/manager.py +29 -56
- universal_mcp-0.1.24rc4/src/universal_mcp/tools/registry.py +41 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/tools.py +22 -1
- universal_mcp-0.1.24rc4/src/universal_mcp/types.py +10 -0
- universal_mcp-0.1.24rc4/src/universal_mcp/utils/common.py +278 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/api_generator.py +46 -18
- universal_mcp-0.1.24rc4/src/universal_mcp/utils/openapi/cli.py +669 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/openapi.py +284 -21
- universal_mcp-0.1.24rc4/src/universal_mcp/utils/openapi/postprocessor.py +275 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/preprocessor.py +1 -1
- universal_mcp-0.1.24rc4/src/universal_mcp/utils/openapi/test_generator.py +287 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/prompts.py +188 -341
- universal_mcp-0.1.24rc4/src/universal_mcp/utils/testing.py +219 -0
- universal_mcp-0.1.24rc2/src/universal_mcp/applications/__init__.py +0 -103
- universal_mcp-0.1.24rc2/src/universal_mcp/applications/sample_tool_app.py +0 -80
- universal_mcp-0.1.24rc2/src/universal_mcp/client/agents/__init__.py +0 -4
- universal_mcp-0.1.24rc2/src/universal_mcp/client/agents/base.py +0 -38
- universal_mcp-0.1.24rc2/src/universal_mcp/client/agents/llm.py +0 -115
- universal_mcp-0.1.24rc2/src/universal_mcp/client/agents/react.py +0 -67
- universal_mcp-0.1.24rc2/src/universal_mcp/client/cli.py +0 -181
- universal_mcp-0.1.24rc2/src/universal_mcp/integrations/__init__.py +0 -26
- universal_mcp-0.1.24rc2/src/universal_mcp/servers/__init__.py +0 -15
- universal_mcp-0.1.24rc2/src/universal_mcp/utils/common.py +0 -33
- universal_mcp-0.1.24rc2/src/universal_mcp/utils/openapi/cli.py +0 -243
- universal_mcp-0.1.24rc2/src/universal_mcp/utils/testing.py +0 -31
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/LICENSE +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/README.md +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/client/oauth.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/client/token_store.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/client/transport.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/docstring_parser.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/docgen.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/filters.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/openapi/readme.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/singleton.py +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.24rc2 → universal_mcp-0.1.24rc4}/src/universal_mcp/utils/templates/api_client.py.j2 +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.24rc4
|
4
4
|
Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
|
5
5
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
6
|
License: MIT
|
@@ -8,17 +8,26 @@ License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
9
9
|
Requires-Dist: black>=25.1.0
|
10
10
|
Requires-Dist: cookiecutter>=2.6.0
|
11
|
-
Requires-Dist: gql[all]
|
11
|
+
Requires-Dist: gql[all]==4.0.0
|
12
12
|
Requires-Dist: jinja2>=3.1.3
|
13
13
|
Requires-Dist: jsonref>=1.1.0
|
14
14
|
Requires-Dist: keyring>=25.6.0
|
15
|
+
Requires-Dist: langchain-mcp-adapters>=0.1.9
|
16
|
+
Requires-Dist: langchain-openai>=0.3.27
|
17
|
+
Requires-Dist: langgraph-cli[inmem]>=0.3.4
|
18
|
+
Requires-Dist: langgraph>=0.5.2
|
19
|
+
Requires-Dist: langsmith>=0.4.5
|
15
20
|
Requires-Dist: loguru>=0.7.3
|
16
|
-
Requires-Dist: mcp>=1.
|
21
|
+
Requires-Dist: mcp>=1.10.0
|
22
|
+
Requires-Dist: mkdocs-material>=9.6.15
|
23
|
+
Requires-Dist: mkdocs>=1.6.1
|
17
24
|
Requires-Dist: posthog>=3.24.0
|
18
25
|
Requires-Dist: pydantic-settings>=2.8.1
|
19
26
|
Requires-Dist: pydantic>=2.11.1
|
20
27
|
Requires-Dist: pyyaml>=6.0.2
|
21
28
|
Requires-Dist: rich>=14.0.0
|
29
|
+
Requires-Dist: streamlit>=1.46.1
|
30
|
+
Requires-Dist: ty>=0.0.1a17
|
22
31
|
Requires-Dist: typer>=0.15.2
|
23
32
|
Provides-Extra: dev
|
24
33
|
Requires-Dist: litellm>=1.30.7; extra == 'dev'
|
@@ -28,6 +37,11 @@ Requires-Dist: pyright>=1.1.398; extra == 'dev'
|
|
28
37
|
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
|
29
38
|
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
30
39
|
Requires-Dist: ruff>=0.11.4; extra == 'dev'
|
40
|
+
Provides-Extra: docs
|
41
|
+
Requires-Dist: mkdocs-glightbox>=0.4.0; extra == 'docs'
|
42
|
+
Requires-Dist: mkdocs-material[imaging]>=9.5.45; extra == 'docs'
|
43
|
+
Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
|
44
|
+
Requires-Dist: mkdocstrings-python>=1.12.2; extra == 'docs'
|
31
45
|
Description-Content-Type: text/markdown
|
32
46
|
|
33
47
|
# Universal MCP
|
@@ -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-rc4"
|
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 = [
|
@@ -16,16 +16,25 @@ dependencies = [
|
|
16
16
|
"Jinja2>=3.1.3",
|
17
17
|
"black>=25.1.0",
|
18
18
|
"cookiecutter>=2.6.0",
|
19
|
-
"gql[all]
|
19
|
+
"gql[all]==4.0.0",
|
20
20
|
"jsonref>=1.1.0",
|
21
21
|
"keyring>=25.6.0",
|
22
|
+
"langchain-mcp-adapters>=0.1.9",
|
23
|
+
"langchain-openai>=0.3.27",
|
24
|
+
"langgraph>=0.5.2",
|
25
|
+
"langgraph-cli[inmem]>=0.3.4",
|
26
|
+
"langsmith>=0.4.5",
|
22
27
|
"loguru>=0.7.3",
|
23
|
-
"mcp>=1.
|
28
|
+
"mcp>=1.10.0",
|
29
|
+
"mkdocs>=1.6.1",
|
30
|
+
"mkdocs-material>=9.6.15",
|
24
31
|
"posthog>=3.24.0",
|
25
32
|
"pydantic>=2.11.1",
|
26
33
|
"pydantic-settings>=2.8.1",
|
27
34
|
"pyyaml>=6.0.2",
|
28
35
|
"rich>=14.0.0",
|
36
|
+
"streamlit>=1.46.1",
|
37
|
+
"ty>=0.0.1a17",
|
29
38
|
"typer>=0.15.2",
|
30
39
|
]
|
31
40
|
|
@@ -39,6 +48,12 @@ dev = [
|
|
39
48
|
"pytest-asyncio>=0.26.0",
|
40
49
|
"ruff>=0.11.4",
|
41
50
|
]
|
51
|
+
docs = [
|
52
|
+
"mkdocs>=1.6.1",
|
53
|
+
"mkdocs-glightbox>=0.4.0",
|
54
|
+
"mkdocs-material[imaging]>=9.5.45",
|
55
|
+
"mkdocstrings-python>=1.12.2",
|
56
|
+
]
|
42
57
|
|
43
58
|
[project.scripts]
|
44
59
|
universal_mcp = "universal_mcp.cli:app"
|
@@ -70,10 +70,7 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
|
|
70
70
|
"""Test API generation with output file."""
|
71
71
|
output_path = temp_dir / "test.py"
|
72
72
|
|
73
|
-
app_file = generate_api_from_schema(schema_path=sample_schema, output_path=output_path)
|
74
|
-
|
75
|
-
assert "app_file" != None
|
76
|
-
assert "readme_file" != None
|
73
|
+
app_file, schemas_file = generate_api_from_schema(schema_path=sample_schema, output_path=output_path)
|
77
74
|
|
78
75
|
assert app_file.exists()
|
79
76
|
content = app_file.read_text()
|
@@ -106,9 +103,8 @@ async def test_generate_api_without_docstrings(sample_schema, temp_dir):
|
|
106
103
|
"""Test API generation without docstring generation."""
|
107
104
|
output_path = temp_dir / "test_without_docs.py"
|
108
105
|
|
109
|
-
app_file = generate_api_from_schema(schema_path=sample_schema, output_path=output_path)
|
106
|
+
app_file, schemas_file = generate_api_from_schema(schema_path=sample_schema, output_path=output_path)
|
110
107
|
|
111
|
-
assert app_file is not None
|
112
108
|
assert app_file.exists()
|
113
109
|
|
114
110
|
# Verify the app was generated
|
@@ -163,9 +159,8 @@ async def test_generate_api_with_complex_schema(temp_dir):
|
|
163
159
|
json.dump(schema, f)
|
164
160
|
|
165
161
|
output_path = temp_dir / "complex.py"
|
166
|
-
app_file = generate_api_from_schema(schema_path=schema_file, output_path=output_path)
|
162
|
+
app_file, schemas_file = generate_api_from_schema(schema_path=schema_file, output_path=output_path)
|
167
163
|
|
168
|
-
assert app_file is not None
|
169
164
|
assert app_file.exists()
|
170
165
|
|
171
166
|
content = app_file.read_text()
|
@@ -1,25 +1,23 @@
|
|
1
1
|
import pytest
|
2
2
|
|
3
|
-
from universal_mcp.applications import
|
3
|
+
from universal_mcp.applications import app_from_config
|
4
|
+
from universal_mcp.config import AppConfig # <-- Import the AppConfig model
|
4
5
|
from universal_mcp.exceptions import NotAuthorizedError
|
5
6
|
from universal_mcp.integrations import ApiKeyIntegration
|
6
7
|
from universal_mcp.stores import MemoryStore
|
7
8
|
|
8
9
|
|
9
10
|
def test_perplexity_api_no_key():
|
10
|
-
# Create a memory store
|
11
11
|
store = MemoryStore()
|
12
12
|
|
13
|
-
# Create API key integration with the store
|
14
13
|
integration = ApiKeyIntegration("PERPLEXITY", store=store)
|
14
|
+
perplexity_app_config = AppConfig(name="perplexity")
|
15
|
+
PerplexityApp = app_from_config(perplexity_app_config)
|
15
16
|
|
16
|
-
|
17
|
-
app = app_from_slug("perplexity")(integration=integration)
|
17
|
+
app = PerplexityApp(integration=integration)
|
18
18
|
|
19
|
-
# Try to make a chat request without setting API key
|
20
19
|
with pytest.raises(NotAuthorizedError) as exc_info:
|
21
20
|
app.chat("Hello, how are you?")
|
22
21
|
|
23
|
-
# Verify the error message suggests setting up API key
|
24
22
|
assert "Please ask the user for api key" in str(exc_info.value)
|
25
23
|
assert "PERPLEXITY_API_KEY" in str(exc_info.value)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import pytest
|
2
2
|
|
3
|
-
from universal_mcp.applications import
|
3
|
+
from universal_mcp.applications import app_from_config
|
4
|
+
from universal_mcp.config import AppConfig
|
4
5
|
from universal_mcp.utils.testing import check_application_instance
|
5
6
|
|
6
7
|
ALL_APPS = [
|
@@ -69,6 +70,7 @@ ALL_APPS = [
|
|
69
70
|
|
70
71
|
@pytest.mark.parametrize("app_name", ALL_APPS)
|
71
72
|
def test_application(app_name):
|
72
|
-
|
73
|
-
|
73
|
+
app_config = AppConfig(name=app_name, source_type="package")
|
74
|
+
app_class = app_from_config(app_config)
|
75
|
+
app_instance = app_class(integration=None)
|
74
76
|
check_application_instance(app_instance, app_name)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import pytest
|
2
2
|
|
3
3
|
from universal_mcp.applications.application import BaseApplication
|
4
|
-
from universal_mcp.exceptions import ToolError
|
4
|
+
from universal_mcp.exceptions import ToolError, ToolNotFoundError
|
5
5
|
from universal_mcp.tools.adapters import ToolFormat
|
6
6
|
from universal_mcp.tools.manager import Tool, ToolManager
|
7
7
|
|
@@ -72,27 +72,27 @@ class ExampleApp(BaseApplication):
|
|
72
72
|
return [dummy_add, dummy_multiply, dummy_error]
|
73
73
|
|
74
74
|
|
75
|
-
def test_add_tool(tool_manager):
|
75
|
+
def test_add_tool(tool_manager: ToolManager):
|
76
76
|
tool = tool_manager.add_tool(dummy_add)
|
77
77
|
assert tool.name == "dummy_add"
|
78
78
|
assert tool.name in [t.name for t in tool_manager.list_tools()]
|
79
79
|
|
80
80
|
|
81
|
-
def test_add_duplicate_tool(tool_manager):
|
81
|
+
def test_add_duplicate_tool(tool_manager: ToolManager):
|
82
82
|
tool1 = tool_manager.add_tool(dummy_add)
|
83
83
|
tool2 = tool_manager.add_tool(dummy_add)
|
84
84
|
assert tool1 is tool2 # Should return existing tool
|
85
85
|
assert len(tool_manager.list_tools()) == 1
|
86
86
|
|
87
87
|
|
88
|
-
def test_remove_tool(tool_manager):
|
88
|
+
def test_remove_tool(tool_manager: ToolManager):
|
89
89
|
tool = tool_manager.add_tool(dummy_add)
|
90
90
|
assert tool_manager.remove_tool(tool.name) is True
|
91
91
|
assert tool_manager.get_tool(tool.name) is None
|
92
92
|
assert tool_manager.remove_tool("nonexistent") is False
|
93
93
|
|
94
94
|
|
95
|
-
def test_clear_tools(tool_manager, dummy_tools):
|
95
|
+
def test_clear_tools(tool_manager: ToolManager, dummy_tools):
|
96
96
|
for tool in dummy_tools:
|
97
97
|
tool_manager.add_tool(tool)
|
98
98
|
assert len(tool_manager.list_tools()) == 3
|
@@ -100,7 +100,7 @@ def test_clear_tools(tool_manager, dummy_tools):
|
|
100
100
|
assert len(tool_manager.list_tools()) == 0
|
101
101
|
|
102
102
|
|
103
|
-
def test_list_tools_format(tool_manager, dummy_tools):
|
103
|
+
def test_list_tools_format(tool_manager: ToolManager, dummy_tools):
|
104
104
|
for tool in dummy_tools:
|
105
105
|
tool_manager.add_tool(tool)
|
106
106
|
|
@@ -117,7 +117,7 @@ def test_list_tools_format(tool_manager, dummy_tools):
|
|
117
117
|
assert len(openai_tools) == 3
|
118
118
|
|
119
119
|
|
120
|
-
def test_filter_tools_by_tags(tool_manager, dummy_tools):
|
120
|
+
def test_filter_tools_by_tags(tool_manager: ToolManager, dummy_tools):
|
121
121
|
for tool in dummy_tools:
|
122
122
|
tool_manager.add_tool(tool)
|
123
123
|
|
@@ -139,20 +139,20 @@ async def test_call_tool_success(tool_manager):
|
|
139
139
|
|
140
140
|
|
141
141
|
@pytest.mark.asyncio
|
142
|
-
async def test_call_tool_error(tool_manager):
|
142
|
+
async def test_call_tool_error(tool_manager: ToolManager):
|
143
143
|
tool_manager.add_tool(dummy_error)
|
144
144
|
with pytest.raises(ToolError):
|
145
145
|
await tool_manager.call_tool("dummy_error", {})
|
146
146
|
|
147
147
|
|
148
148
|
@pytest.mark.asyncio
|
149
|
-
async def test_call_nonexistent_tool(tool_manager):
|
150
|
-
with pytest.raises(
|
149
|
+
async def test_call_nonexistent_tool(tool_manager: ToolManager):
|
150
|
+
with pytest.raises(ToolNotFoundError):
|
151
151
|
await tool_manager.call_tool("nonexistent", {})
|
152
152
|
|
153
153
|
|
154
154
|
@pytest.mark.asyncio
|
155
|
-
async def test_call_tool_from_app(tool_manager):
|
155
|
+
async def test_call_tool_from_app(tool_manager: ToolManager):
|
156
156
|
app = ExampleApp()
|
157
157
|
# Only important are added by default
|
158
158
|
tool_manager.register_tools_from_app(app)
|
@@ -164,7 +164,7 @@ async def test_call_tool_from_app(tool_manager):
|
|
164
164
|
|
165
165
|
|
166
166
|
@pytest.mark.asyncio
|
167
|
-
async def test_call_tool_from_app_with_tags(tool_manager):
|
167
|
+
async def test_call_tool_from_app_with_tags(tool_manager: ToolManager):
|
168
168
|
app = ExampleApp()
|
169
169
|
# Only important are added by default
|
170
170
|
tool_manager.register_tools_from_app(app, tags=["math"])
|
@@ -175,7 +175,7 @@ async def test_call_tool_from_app_with_tags(tool_manager):
|
|
175
175
|
|
176
176
|
|
177
177
|
@pytest.mark.asyncio
|
178
|
-
async def test_load_tool_from_name(tool_manager):
|
178
|
+
async def test_load_tool_from_name(tool_manager: ToolManager):
|
179
179
|
app = ExampleApp()
|
180
180
|
# Only important are added by default
|
181
181
|
tool_manager.register_tools_from_app(app, tool_names=["dummy_multiply", "dummy_add"])
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# AgentR Python SDK
|
2
|
+
|
3
|
+
The official Python SDK for the AgentR platform, a component of the Universal MCP framework.
|
4
|
+
Currently in beta, breaking changes are expected.
|
5
|
+
|
6
|
+
The AgentR Python SDK provides convenient access to the AgentR REST API from any Python 3.10+
|
7
|
+
application, allowing for dynamic loading and management of tools and integrations.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
```bash
|
11
|
+
pip install universal-mcp
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
The AgentR platform is designed to seamlessly integrate a wide array of tools into your agentic applications. The primary entry point for this is the `Agentr` class, which provides a high-level interface for loading and listing tools.
|
16
|
+
|
17
|
+
### High-Level Client (`Agentr`)
|
18
|
+
This is the recommended way to get started. It abstracts away the details of the registry and tool management.
|
19
|
+
|
20
|
+
```python
|
21
|
+
import os
|
22
|
+
from universal_mcp.agentr import Agentr
|
23
|
+
from universal_mcp.tools import ToolFormat
|
24
|
+
|
25
|
+
# Initialize the main client
|
26
|
+
# It reads from environment variables by default (AGENTR_API_KEY, AGENTR_BASE_URL)
|
27
|
+
agentr = Agentr(
|
28
|
+
api_key=os.environ.get("AGENTR_API_KEY")
|
29
|
+
)
|
30
|
+
|
31
|
+
# Load specific tools from the AgentR server into the tool manager
|
32
|
+
agentr.load_tools(["reddit_search_subreddits", "google_drive_list_files"])
|
33
|
+
|
34
|
+
# List the tools that are now loaded and ready to be used
|
35
|
+
# You can specify a format compatible with your LLM (e.g., OPENAI)
|
36
|
+
tools = agentr.list_tools(format=ToolFormat.OPENAI)
|
37
|
+
print(tools)
|
38
|
+
```
|
39
|
+
|
40
|
+
### Low-Level API
|
41
|
+
|
42
|
+
For more granular control over the AgentR platform, you can use the lower-level components directly.
|
43
|
+
|
44
|
+
### AgentrClient
|
45
|
+
The `AgentrClient` provides direct access to the AgentR REST API endpoints.
|
46
|
+
|
47
|
+
#### Methods
|
48
|
+
```python
|
49
|
+
import os
|
50
|
+
from universal_mcp.agentr import AgentrClient
|
51
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
52
|
+
|
53
|
+
# Initialize the low-level client
|
54
|
+
client = AgentrClient(
|
55
|
+
api_key=os.environ.get("AGENTR_API_KEY")
|
56
|
+
)
|
57
|
+
|
58
|
+
# Fetch all available applications from the AgentR server
|
59
|
+
apps = client.fetch_apps()
|
60
|
+
print(apps)
|
61
|
+
|
62
|
+
# Get credentials for a specific integration
|
63
|
+
# This will raise a NotAuthorizedError if the user needs to authenticate
|
64
|
+
try:
|
65
|
+
credentials = client.get_credentials("reddit")
|
66
|
+
print("Reddit credentials found.")
|
67
|
+
except NotAuthorizedError as e:
|
68
|
+
print(e) # "Please ask the user to visit the following url to authorize..."
|
69
|
+
|
70
|
+
# Example of fetching a single app and its actions
|
71
|
+
if apps:
|
72
|
+
app_id = apps[0].id # Assuming AppConfig has an 'id' attribute
|
73
|
+
|
74
|
+
# Fetch a single app's configuration
|
75
|
+
app_config = client.fetch_app(app_id)
|
76
|
+
print(f"Fetched config for app {app_id}:", app_config)
|
77
|
+
|
78
|
+
# List all actions for that app
|
79
|
+
actions = client.list_actions(app_id)
|
80
|
+
print(f"Actions for app {app_id}:", actions)
|
81
|
+
|
82
|
+
# List all apps (returns raw JSON data)
|
83
|
+
all_apps = client.list_all_apps()
|
84
|
+
print("All available apps:", all_apps)
|
85
|
+
```
|
86
|
+
|
87
|
+
### AgentrIntegration
|
88
|
+
This class handles the authentication and authorization flow for a single integration (e.g., "reddit"). It's used under the hood by applications to acquire credentials.
|
89
|
+
|
90
|
+
#### Methods
|
91
|
+
```python
|
92
|
+
from universal_mcp.agentr import AgentrIntegration, AgentrClient
|
93
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
94
|
+
|
95
|
+
client = AgentrClient()
|
96
|
+
|
97
|
+
# Create an integration for a specific service
|
98
|
+
reddit_integration = AgentrIntegration(name="reddit", client=client)
|
99
|
+
|
100
|
+
# If credentials are not present, this will raise NotAuthorizedError
|
101
|
+
try:
|
102
|
+
creds = reddit_integration.credentials
|
103
|
+
print("Successfully retrieved credentials.")
|
104
|
+
except NotAuthorizedError:
|
105
|
+
# Get the URL to send the user to for authentication
|
106
|
+
auth_url = reddit_integration.authorize()
|
107
|
+
print(f"Please authorize here: {auth_url}")
|
108
|
+
|
109
|
+
# You can also use the get_credentials() method
|
110
|
+
try:
|
111
|
+
creds = reddit_integration.get_credentials()
|
112
|
+
print("Successfully retrieved credentials again.")
|
113
|
+
except NotAuthorizedError:
|
114
|
+
print("Still not authorized.")
|
115
|
+
```
|
116
|
+
|
117
|
+
### AgentrRegistry
|
118
|
+
The registry is responsible for discovering which tools are available on the AgentR platform.
|
119
|
+
|
120
|
+
#### Methods
|
121
|
+
```python
|
122
|
+
import asyncio
|
123
|
+
from universal_mcp.agentr import AgentrRegistry, AgentrClient
|
124
|
+
|
125
|
+
client = AgentrClient()
|
126
|
+
registry = AgentrRegistry(client=client)
|
127
|
+
|
128
|
+
async def main():
|
129
|
+
# List all apps available on the AgentR platform
|
130
|
+
available_apps = await registry.list_apps()
|
131
|
+
print(available_apps)
|
132
|
+
|
133
|
+
if available_apps:
|
134
|
+
app_id = available_apps[0]['id']
|
135
|
+
# Get details for a specific app
|
136
|
+
app_details = await registry.get_app_details(app_id)
|
137
|
+
print(f"Details for {app_id}:", app_details)
|
138
|
+
|
139
|
+
# The load_tools method is used internally by the high-level Agentr client
|
140
|
+
# but can be called directly if needed.
|
141
|
+
# from universal_mcp.tools import ToolManager
|
142
|
+
# tool_manager = ToolManager()
|
143
|
+
# registry.load_tools(["reddit_search_subreddits"], tool_manager)
|
144
|
+
# print(tool_manager.list_tools())
|
145
|
+
|
146
|
+
|
147
|
+
if __name__ == "__main__":
|
148
|
+
asyncio.run(main())
|
149
|
+
```
|
150
|
+
|
151
|
+
### AgentrServer
|
152
|
+
For server-side deployments, `AgentrServer` can be used to load all configured applications and their tools from an AgentR instance on startup.
|
153
|
+
|
154
|
+
```python
|
155
|
+
from universal_mcp.config import ServerConfig
|
156
|
+
from universal_mcp.agentr.server import AgentrServer
|
157
|
+
|
158
|
+
# Configuration for the server
|
159
|
+
config = ServerConfig(
|
160
|
+
type="agentr",
|
161
|
+
api_key="your-agentr-api-key"
|
162
|
+
)
|
163
|
+
|
164
|
+
# The server will automatically fetch and register all tools on initialization
|
165
|
+
server = AgentrServer(config=config)
|
166
|
+
|
167
|
+
# The tool manager is now populated with tools from the AgentR instance
|
168
|
+
tool_manager = server.tool_manager
|
169
|
+
print(tool_manager.list_tools())
|
170
|
+
```
|
171
|
+
|
172
|
+
## Executing Tools
|
173
|
+
Once tools are loaded, you can execute them using the `call_tool` method on the `ToolManager` instance, which is available via `agentr.manager`.
|
174
|
+
|
175
|
+
```python
|
176
|
+
import os
|
177
|
+
import asyncio
|
178
|
+
from universal_mcp.agentr import Agentr
|
179
|
+
|
180
|
+
async def main():
|
181
|
+
# 1. Initialize Agentr
|
182
|
+
agentr = Agentr(api_key=os.environ.get("AGENTR_API_KEY"))
|
183
|
+
|
184
|
+
# 2. Load the tool(s) you want to use
|
185
|
+
tool_name = "reddit_search_subreddits"
|
186
|
+
agentr.load_tools([tool_name])
|
187
|
+
|
188
|
+
# 3. Execute the tool using the tool manager
|
189
|
+
try:
|
190
|
+
# Note the 'await' since call_tool is an async method
|
191
|
+
result = await agentr.manager.call_tool(
|
192
|
+
name=tool_name,
|
193
|
+
arguments={"query": "elon musk", "limit": 5, "sort": "relevance"}
|
194
|
+
)
|
195
|
+
print("Execution result:", result)
|
196
|
+
except Exception as e:
|
197
|
+
print(f"An error occurred: {e}")
|
198
|
+
|
199
|
+
if __name__ == "__main__":
|
200
|
+
asyncio.run(main())
|
201
|
+
```
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from universal_mcp.tools import Tool, ToolFormat, ToolManager
|
4
|
+
|
5
|
+
from .client import AgentrClient
|
6
|
+
from .registry import AgentrRegistry
|
7
|
+
|
8
|
+
|
9
|
+
class Agentr:
|
10
|
+
def __init__(
|
11
|
+
self,
|
12
|
+
api_key: str | None = None,
|
13
|
+
base_url: str | None = None,
|
14
|
+
registry: AgentrRegistry | None = None,
|
15
|
+
format: ToolFormat | None = None,
|
16
|
+
manager: ToolManager | None = None,
|
17
|
+
):
|
18
|
+
self.api_key = api_key or os.getenv("AGENTR_API_KEY")
|
19
|
+
self.base_url = base_url or os.getenv("AGENTR_BASE_URL")
|
20
|
+
self.client = AgentrClient(api_key=self.api_key, base_url=self.base_url)
|
21
|
+
self.registry = registry or AgentrRegistry(client=self.client)
|
22
|
+
self.format = format or ToolFormat.NATIVE
|
23
|
+
self.manager = manager or ToolManager()
|
24
|
+
|
25
|
+
def load_tools(self, tool_names: list[str]) -> None:
|
26
|
+
self.registry.load_tools(tool_names, self.manager)
|
27
|
+
return
|
28
|
+
|
29
|
+
def list_tools(self, format: ToolFormat | None = None) -> list[Tool]:
|
30
|
+
return self.manager.list_tools(format=format or self.format)
|
@@ -82,6 +82,22 @@ class AgentrClient:
|
|
82
82
|
data = response.json()
|
83
83
|
return [AppConfig.model_validate(app) for app in data]
|
84
84
|
|
85
|
+
def fetch_app(self, app_id: str) -> dict:
|
86
|
+
"""Fetch a specific app from AgentR API.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
app_id (str): ID of the app to fetch
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
dict: App configuration data
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
httpx.HTTPError: If API request fails
|
96
|
+
"""
|
97
|
+
response = self.client.get(f"/apps/{app_id}/")
|
98
|
+
response.raise_for_status()
|
99
|
+
return response.json()
|
100
|
+
|
85
101
|
def list_all_apps(self) -> list:
|
86
102
|
"""List all apps from AgentR API.
|
87
103
|
|
@@ -92,16 +108,16 @@ class AgentrClient:
|
|
92
108
|
response.raise_for_status()
|
93
109
|
return response.json()
|
94
110
|
|
95
|
-
def list_actions(self,
|
111
|
+
def list_actions(self, app_id: str):
|
96
112
|
"""List actions for an app.
|
97
113
|
|
98
114
|
Args:
|
99
|
-
|
115
|
+
app_id (str): ID of the app to list actions for
|
100
116
|
|
101
117
|
Returns:
|
102
118
|
List of action configurations
|
103
119
|
"""
|
104
120
|
|
105
|
-
response = self.client.get(f"/apps/{
|
121
|
+
response = self.client.get(f"/apps/{app_id}/actions/")
|
106
122
|
response.raise_for_status()
|
107
123
|
return response.json()
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from universal_mcp.integrations.integration import Integration
|
2
|
+
|
3
|
+
from .client import AgentrClient
|
4
|
+
|
5
|
+
|
6
|
+
class AgentrIntegration(Integration):
|
7
|
+
"""Manages authentication and authorization via the AgentR platform.
|
8
|
+
|
9
|
+
This integration uses an `AgentrClient` to interact with the AgentR API
|
10
|
+
for operations like retrieving authorization URLs and fetching stored
|
11
|
+
credentials. It simplifies integration with services supported by AgentR.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
name (str): Name of the integration (e.g., "github", "google").
|
15
|
+
store (BaseStore): Store, typically not used directly by this class
|
16
|
+
as AgentR manages the primary credential storage.
|
17
|
+
client (AgentrClient): Client for communicating with the AgentR API.
|
18
|
+
_credentials (dict | None): Cached credentials.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
name: str,
|
24
|
+
client: AgentrClient | None = None,
|
25
|
+
api_key: str | None = None,
|
26
|
+
base_url: str | None = None,
|
27
|
+
**kwargs,
|
28
|
+
):
|
29
|
+
"""Initializes the AgentRIntegration.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
name (str): The name of the service integration as configured on
|
33
|
+
the AgentR platform (e.g., "github").
|
34
|
+
client (AgentrClient | None, optional): The AgentR client. If not provided,
|
35
|
+
a new `AgentrClient` will be created.
|
36
|
+
api_key (str | None, optional): API key for AgentR. If not provided,
|
37
|
+
will be loaded from environment variables.
|
38
|
+
base_url (str | None, optional): Base URL for AgentR API. If not provided,
|
39
|
+
will be loaded from environment variables.
|
40
|
+
**kwargs: Additional arguments passed to the parent `Integration`.
|
41
|
+
"""
|
42
|
+
super().__init__(name, **kwargs)
|
43
|
+
self.type = "agentr"
|
44
|
+
self.client = client or AgentrClient(api_key=api_key, base_url=base_url)
|
45
|
+
self._credentials = None
|
46
|
+
|
47
|
+
def set_credentials(self, credentials: dict[str, str] | None = None) -> str:
|
48
|
+
"""Not used for direct credential setting; initiates authorization instead.
|
49
|
+
|
50
|
+
For AgentR integrations, credentials are set via the AgentR platform's
|
51
|
+
OAuth flow. This method effectively redirects to the `authorize` flow.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
credentials (dict | None, optional): Not used by this implementation.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
str: The authorization URL or message from the `authorize()` method.
|
58
|
+
"""
|
59
|
+
raise NotImplementedError("AgentR integrations do not support direct credential setting")
|
60
|
+
|
61
|
+
@property
|
62
|
+
def credentials(self):
|
63
|
+
"""Retrieves credentials from the AgentR API, with caching.
|
64
|
+
|
65
|
+
If credentials are not cached locally (in `_credentials`), this property
|
66
|
+
fetches them from the AgentR platform using `self.client.get_credentials`.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
dict: The credentials dictionary obtained from AgentR.
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
NotAuthorizedError: If credentials are not found (e.g., 404 from AgentR).
|
73
|
+
httpx.HTTPStatusError: For other API errors from AgentR.
|
74
|
+
"""
|
75
|
+
if self._credentials is not None:
|
76
|
+
return self._credentials
|
77
|
+
self._credentials = self.client.get_credentials(self.name)
|
78
|
+
return self._credentials
|
79
|
+
|
80
|
+
def get_credentials(self):
|
81
|
+
"""Retrieves credentials from the AgentR API. Alias for `credentials` property.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
dict: The credentials dictionary obtained from AgentR.
|
85
|
+
|
86
|
+
Raises:
|
87
|
+
NotAuthorizedError: If credentials are not found.
|
88
|
+
httpx.HTTPStatusError: For other API errors.
|
89
|
+
"""
|
90
|
+
return self.credentials
|
91
|
+
|
92
|
+
def authorize(self) -> str:
|
93
|
+
"""Retrieves the authorization URL from the AgentR platform.
|
94
|
+
|
95
|
+
This URL should be presented to the user to initiate the OAuth flow
|
96
|
+
managed by AgentR for the service associated with `self.name`.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
str: The authorization URL.
|
100
|
+
|
101
|
+
Raises:
|
102
|
+
httpx.HTTPStatusError: If the API request to AgentR fails.
|
103
|
+
"""
|
104
|
+
return self.client.get_authorization_url(self.name)
|