universal-mcp 0.1.23rc2__tar.gz → 0.1.24rc3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/.gitignore +9 -0
  2. universal_mcp-0.1.24rc3/PKG-INFO +68 -0
  3. universal_mcp-0.1.24rc3/README.md +22 -0
  4. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/pyproject.toml +22 -40
  5. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_api_generator.py +3 -8
  6. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_api_integration.py +5 -7
  7. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_applications.py +5 -3
  8. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_tool.py +1 -1
  9. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_tool_manager.py +13 -13
  10. universal_mcp-0.1.24rc3/src/universal_mcp/agentr/__init__.py +6 -0
  11. universal_mcp-0.1.24rc3/src/universal_mcp/agentr/agentr.py +30 -0
  12. universal_mcp-0.1.23rc2/src/universal_mcp/utils/agentr.py → universal_mcp-0.1.24rc3/src/universal_mcp/agentr/client.py +22 -7
  13. universal_mcp-0.1.24rc3/src/universal_mcp/agentr/integration.py +104 -0
  14. universal_mcp-0.1.24rc3/src/universal_mcp/agentr/registry.py +91 -0
  15. universal_mcp-0.1.24rc3/src/universal_mcp/agentr/server.py +51 -0
  16. universal_mcp-0.1.24rc3/src/universal_mcp/agents/__init__.py +6 -0
  17. universal_mcp-0.1.24rc3/src/universal_mcp/agents/auto.py +576 -0
  18. universal_mcp-0.1.24rc3/src/universal_mcp/agents/base.py +88 -0
  19. universal_mcp-0.1.24rc3/src/universal_mcp/agents/cli.py +27 -0
  20. universal_mcp-0.1.24rc3/src/universal_mcp/agents/codeact/__init__.py +243 -0
  21. universal_mcp-0.1.24rc3/src/universal_mcp/agents/codeact/sandbox.py +27 -0
  22. universal_mcp-0.1.24rc3/src/universal_mcp/agents/codeact/test.py +15 -0
  23. universal_mcp-0.1.24rc3/src/universal_mcp/agents/codeact/utils.py +61 -0
  24. universal_mcp-0.1.24rc3/src/universal_mcp/agents/hil.py +104 -0
  25. universal_mcp-0.1.24rc3/src/universal_mcp/agents/llm.py +10 -0
  26. universal_mcp-0.1.24rc3/src/universal_mcp/agents/react.py +58 -0
  27. universal_mcp-0.1.24rc3/src/universal_mcp/agents/simple.py +40 -0
  28. universal_mcp-0.1.24rc3/src/universal_mcp/agents/utils.py +111 -0
  29. universal_mcp-0.1.24rc3/src/universal_mcp/analytics.py +111 -0
  30. universal_mcp-0.1.24rc3/src/universal_mcp/applications/__init__.py +70 -0
  31. universal_mcp-0.1.24rc3/src/universal_mcp/applications/application.py +569 -0
  32. universal_mcp-0.1.24rc3/src/universal_mcp/applications/sample/app.py +245 -0
  33. universal_mcp-0.1.24rc3/src/universal_mcp/cli.py +82 -0
  34. universal_mcp-0.1.24rc3/src/universal_mcp/client/oauth.py +218 -0
  35. universal_mcp-0.1.24rc3/src/universal_mcp/client/token_store.py +91 -0
  36. universal_mcp-0.1.23rc2/src/universal_mcp/client/client.py → universal_mcp-0.1.24rc3/src/universal_mcp/client/transport.py +127 -48
  37. universal_mcp-0.1.24rc3/src/universal_mcp/config.py +271 -0
  38. universal_mcp-0.1.24rc3/src/universal_mcp/exceptions.py +73 -0
  39. universal_mcp-0.1.24rc3/src/universal_mcp/integrations/__init__.py +11 -0
  40. universal_mcp-0.1.24rc3/src/universal_mcp/integrations/integration.py +406 -0
  41. universal_mcp-0.1.24rc3/src/universal_mcp/servers/__init__.py +3 -0
  42. universal_mcp-0.1.24rc3/src/universal_mcp/servers/server.py +143 -0
  43. universal_mcp-0.1.24rc3/src/universal_mcp/stores/store.py +273 -0
  44. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/tools/__init__.py +3 -0
  45. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/tools/adapters.py +20 -11
  46. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/tools/func_metadata.py +1 -1
  47. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/tools/manager.py +38 -53
  48. universal_mcp-0.1.24rc3/src/universal_mcp/tools/registry.py +41 -0
  49. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/tools/tools.py +24 -3
  50. universal_mcp-0.1.24rc3/src/universal_mcp/types.py +10 -0
  51. universal_mcp-0.1.24rc3/src/universal_mcp/utils/common.py +278 -0
  52. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/installation.py +3 -4
  53. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/api_generator.py +71 -17
  54. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/api_splitter.py +0 -1
  55. universal_mcp-0.1.24rc3/src/universal_mcp/utils/openapi/cli.py +669 -0
  56. universal_mcp-0.1.24rc3/src/universal_mcp/utils/openapi/filters.py +114 -0
  57. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/openapi.py +315 -23
  58. universal_mcp-0.1.24rc3/src/universal_mcp/utils/openapi/postprocessor.py +275 -0
  59. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/preprocessor.py +63 -8
  60. universal_mcp-0.1.24rc3/src/universal_mcp/utils/openapi/test_generator.py +287 -0
  61. universal_mcp-0.1.24rc3/src/universal_mcp/utils/prompts.py +634 -0
  62. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/singleton.py +4 -1
  63. universal_mcp-0.1.24rc3/src/universal_mcp/utils/testing.py +219 -0
  64. universal_mcp-0.1.23rc2/PKG-INFO +0 -283
  65. universal_mcp-0.1.23rc2/README.md +0 -242
  66. universal_mcp-0.1.23rc2/src/universal_mcp/analytics.py +0 -81
  67. universal_mcp-0.1.23rc2/src/universal_mcp/applications/README.md +0 -122
  68. universal_mcp-0.1.23rc2/src/universal_mcp/applications/__init__.py +0 -103
  69. universal_mcp-0.1.23rc2/src/universal_mcp/applications/application.py +0 -515
  70. universal_mcp-0.1.23rc2/src/universal_mcp/cli.py +0 -299
  71. universal_mcp-0.1.23rc2/src/universal_mcp/client/__main__.py +0 -30
  72. universal_mcp-0.1.23rc2/src/universal_mcp/client/agent.py +0 -96
  73. universal_mcp-0.1.23rc2/src/universal_mcp/client/oauth.py +0 -114
  74. universal_mcp-0.1.23rc2/src/universal_mcp/client/token_store.py +0 -32
  75. universal_mcp-0.1.23rc2/src/universal_mcp/config.py +0 -131
  76. universal_mcp-0.1.23rc2/src/universal_mcp/exceptions.py +0 -25
  77. universal_mcp-0.1.23rc2/src/universal_mcp/integrations/README.md +0 -25
  78. universal_mcp-0.1.23rc2/src/universal_mcp/integrations/__init__.py +0 -29
  79. universal_mcp-0.1.23rc2/src/universal_mcp/integrations/integration.py +0 -389
  80. universal_mcp-0.1.23rc2/src/universal_mcp/servers/README.md +0 -79
  81. universal_mcp-0.1.23rc2/src/universal_mcp/servers/__init__.py +0 -15
  82. universal_mcp-0.1.23rc2/src/universal_mcp/servers/server.py +0 -317
  83. universal_mcp-0.1.23rc2/src/universal_mcp/stores/README.md +0 -74
  84. universal_mcp-0.1.23rc2/src/universal_mcp/stores/store.py +0 -240
  85. universal_mcp-0.1.23rc2/src/universal_mcp/tools/README.md +0 -86
  86. universal_mcp-0.1.23rc2/src/universal_mcp/utils/common.py +0 -33
  87. universal_mcp-0.1.23rc2/src/universal_mcp/utils/testing.py +0 -31
  88. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/LICENSE +0 -0
  89. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/__init__.py +0 -0
  90. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/conftest.py +0 -0
  91. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_localserver.py +0 -0
  92. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_stores.py +0 -0
  93. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/tests/test_zenquotes.py +0 -0
  94. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/__init__.py +0 -0
  95. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/logger.py +0 -0
  96. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/py.typed +0 -0
  97. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/stores/__init__.py +0 -0
  98. {universal_mcp-0.1.23rc2/src/universal_mcp/utils → universal_mcp-0.1.24rc3/src/universal_mcp/tools}/docstring_parser.py +0 -0
  99. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/__init__.py +0 -0
  100. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  101. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  102. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/openapi/readme.py +0 -0
  103. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  104. {universal_mcp-0.1.23rc2 → universal_mcp-0.1.24rc3}/src/universal_mcp/utils/templates/api_client.py.j2 +0 -0
@@ -48,3 +48,12 @@ src/universal_mcp/applications/test_without_docs/
48
48
 
49
49
  # Temporary files
50
50
  tmp/
51
+
52
+
53
+ # Docs
54
+ site/
55
+ .cache/
56
+
57
+ # langgraph temporary files
58
+ .langgraph_api/
59
+ langgraph.json
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: universal-mcp
3
+ Version: 0.1.24rc3
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
+ Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: black>=25.1.0
10
+ Requires-Dist: cookiecutter>=2.6.0
11
+ Requires-Dist: gql[all]>=3.5.2
12
+ Requires-Dist: jinja2>=3.1.3
13
+ Requires-Dist: jsonref>=1.1.0
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
20
+ Requires-Dist: loguru>=0.7.3
21
+ Requires-Dist: mcp>=1.10.0
22
+ Requires-Dist: mkdocs-material>=9.6.15
23
+ Requires-Dist: mkdocs>=1.6.1
24
+ Requires-Dist: posthog>=3.24.0
25
+ Requires-Dist: pydantic-settings>=2.8.1
26
+ Requires-Dist: pydantic>=2.11.1
27
+ Requires-Dist: pyyaml>=6.0.2
28
+ Requires-Dist: rich>=14.0.0
29
+ Requires-Dist: streamlit>=1.46.1
30
+ Requires-Dist: ty>=0.0.1a17
31
+ Requires-Dist: typer>=0.15.2
32
+ Provides-Extra: dev
33
+ Requires-Dist: litellm>=1.30.7; extra == 'dev'
34
+ Requires-Dist: mypy>=1.16.0; extra == 'dev'
35
+ Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
36
+ Requires-Dist: pyright>=1.1.398; extra == 'dev'
37
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
38
+ Requires-Dist: pytest>=8.3.5; extra == 'dev'
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'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # Universal MCP
48
+
49
+ Universal MCP acts as a middleware layer for your API applications, enabling seamless integration with various services through the Model Control Protocol (MCP). It simplifies credential management, authorization, dynamic app enablement, and provides a robust framework for building and managing AI-powered tools.
50
+
51
+ ## Documentation
52
+
53
+ The primary documentation for Universal MCP is available in `/docs` folder in this repository.
54
+
55
+ Please refer to the following for more detailed information:
56
+
57
+ * **[Main Documentation](docs/index.md)**: For an overview of the project, features, quick start, and installation.
58
+ * **[Playground Usage](docs/playground.md)**: For instructions on using the interactive playground.
59
+ * **[Applications Framework](docs/applications.md)**: For details on the applications module.
60
+ * **[Integrations & Authentication](docs/integrations.md)**: For information on integration types.
61
+ * **[Server Implementations](docs/servers.md)**: For details on server types.
62
+ * **[Credential Stores](docs/stores.md)**: For information on credential stores.
63
+ * **[Tools Framework](docs/tools_framework.md)**: For details on the tool management system.
64
+ * **[Contributing Guidelines](CONTRIBUTING.md)**: For information on how to contribute to the project.
65
+
66
+ ## License
67
+
68
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,22 @@
1
+ # Universal MCP
2
+
3
+ Universal MCP acts as a middleware layer for your API applications, enabling seamless integration with various services through the Model Control Protocol (MCP). It simplifies credential management, authorization, dynamic app enablement, and provides a robust framework for building and managing AI-powered tools.
4
+
5
+ ## Documentation
6
+
7
+ The primary documentation for Universal MCP is available in `/docs` folder in this repository.
8
+
9
+ Please refer to the following for more detailed information:
10
+
11
+ * **[Main Documentation](docs/index.md)**: For an overview of the project, features, quick start, and installation.
12
+ * **[Playground Usage](docs/playground.md)**: For instructions on using the interactive playground.
13
+ * **[Applications Framework](docs/applications.md)**: For details on the applications module.
14
+ * **[Integrations & Authentication](docs/integrations.md)**: For information on integration types.
15
+ * **[Server Implementations](docs/servers.md)**: For details on server types.
16
+ * **[Credential Stores](docs/stores.md)**: For information on credential stores.
17
+ * **[Tools Framework](docs/tools_framework.md)**: For details on the tool management system.
18
+ * **[Contributing Guidelines](CONTRIBUTING.md)**: For information on how to contribute to the project.
19
+
20
+ ## License
21
+
22
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.23-rc2"
7
+ version = "0.1.24-rc3"
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 = [
@@ -19,20 +19,28 @@ dependencies = [
19
19
  "gql[all]>=3.5.2",
20
20
  "jsonref>=1.1.0",
21
21
  "keyring>=25.6.0",
22
- "langchain-mcp-adapters>=0.0.3",
23
- "litellm>=1.30.7",
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",
24
27
  "loguru>=0.7.3",
25
- "mcp>=1.9.3",
28
+ "mcp>=1.10.0",
29
+ "mkdocs>=1.6.1",
30
+ "mkdocs-material>=9.6.15",
26
31
  "posthog>=3.24.0",
27
32
  "pydantic>=2.11.1",
28
33
  "pydantic-settings>=2.8.1",
29
34
  "pyyaml>=6.0.2",
30
35
  "rich>=14.0.0",
36
+ "streamlit>=1.46.1",
37
+ "ty>=0.0.1a17",
31
38
  "typer>=0.15.2",
32
39
  ]
33
40
 
34
41
  [project.optional-dependencies]
35
42
  dev = [
43
+ "mypy>=1.16.0",
36
44
  "litellm>=1.30.7",
37
45
  "pre-commit>=4.2.0",
38
46
  "pyright>=1.1.398",
@@ -40,14 +48,11 @@ dev = [
40
48
  "pytest-asyncio>=0.26.0",
41
49
  "ruff>=0.11.4",
42
50
  ]
43
- playground = [
44
- "langchain-openai>=0.3.12",
45
- "langgraph>=0.3.24",
46
- "langgraph-checkpoint-sqlite>=2.0.6",
47
- "python-dotenv>=1.0.1",
48
- "streamlit>=1.44.1",
49
- "watchdog>=6.0.0",
50
- "litellm>=1.30.7",
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",
51
56
  ]
52
57
 
53
58
  [project.scripts]
@@ -71,34 +76,6 @@ packages = ["src/universal_mcp"]
71
76
  # Tool configurations
72
77
  # ------------------------------
73
78
  [tool.ruff]
74
- exclude = [
75
- ".bzr",
76
- ".direnv",
77
- ".eggs",
78
- ".git",
79
- ".git-rewrite",
80
- ".hg",
81
- ".ipynb_checkpoints",
82
- ".mypy_cache",
83
- ".nox",
84
- ".pants.d",
85
- ".pyenv",
86
- ".pytest_cache",
87
- ".pytype",
88
- ".ruff_cache",
89
- ".svn",
90
- ".tox",
91
- ".venv",
92
- ".vscode",
93
- "__pypackages__",
94
- "_build",
95
- "buck-out",
96
- "build",
97
- "dist",
98
- "node_modules",
99
- "site-packages",
100
- "venv",
101
- ]
102
79
  line-length = 120
103
80
  indent-width = 4
104
81
  target-version = "py312"
@@ -127,3 +104,8 @@ docstring-code-line-length = "dynamic"
127
104
  [tool.pytest.ini_options]
128
105
  asyncio_mode = "strict"
129
106
  asyncio_default_fixture_loop_scope = "function"
107
+
108
+ [tool.mypy]
109
+ exclude = [
110
+ '^src/universal_mcp/utils/openapi/\.*',
111
+ ]
@@ -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 app_from_slug
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
- # Create Perplexity app with the integration
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 app_from_slug
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
- app = app_from_slug(app_name)
73
- app_instance = app(integration=None)
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)
@@ -3,9 +3,9 @@ from typing import Annotated
3
3
 
4
4
  from pydantic import Field
5
5
 
6
+ from universal_mcp.tools.docstring_parser import parse_docstring # Assuming this is the updated one
6
7
  from universal_mcp.tools.func_metadata import FuncMetadata
7
8
  from universal_mcp.tools.tools import Tool
8
- from universal_mcp.utils.docstring_parser import parse_docstring # Assuming this is the updated one
9
9
 
10
10
 
11
11
  def test_func_metadata_annotated():
@@ -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(ToolError):
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,6 @@
1
+ from .agentr import Agentr
2
+ from .client import AgentrClient
3
+ from .integration import AgentrIntegration
4
+ from .registry import AgentrRegistry
5
+
6
+ __all__ = ["Agentr", "AgentrClient", "AgentrRegistry", "AgentrIntegration"]
@@ -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)
@@ -18,7 +18,8 @@ class AgentrClient:
18
18
  base_url (str, optional): Base URL for AgentR API. Defaults to https://api.agentr.dev
19
19
  """
20
20
 
21
- def __init__(self, api_key: str, base_url: str = "https://api.agentr.dev"):
21
+ def __init__(self, api_key: str | None = None, base_url: str | None = None):
22
+ base_url = base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
22
23
  self.base_url = base_url.rstrip("/")
23
24
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
24
25
  if not self.api_key:
@@ -62,9 +63,7 @@ class AgentrClient:
62
63
  Raises:
63
64
  HTTPError: If API request fails
64
65
  """
65
- response = self.client.get(
66
- f"/api/{integration_name}/authorize/",
67
- )
66
+ response = self.client.get(f"/api/{integration_name}/authorize/")
68
67
  response.raise_for_status()
69
68
  url = response.json()
70
69
  return f"Please ask the user to visit the following url to authorize the application: {url}. Render the url in proper markdown format with a clickable link."
@@ -83,6 +82,22 @@ class AgentrClient:
83
82
  data = response.json()
84
83
  return [AppConfig.model_validate(app) for app in data]
85
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
+
86
101
  def list_all_apps(self) -> list:
87
102
  """List all apps from AgentR API.
88
103
 
@@ -93,16 +108,16 @@ class AgentrClient:
93
108
  response.raise_for_status()
94
109
  return response.json()
95
110
 
96
- def list_actions(self, app_name: str):
111
+ def list_actions(self, app_id: str):
97
112
  """List actions for an app.
98
113
 
99
114
  Args:
100
- app_name (str): Name of the app to list actions for
115
+ app_id (str): ID of the app to list actions for
101
116
 
102
117
  Returns:
103
118
  List of action configurations
104
119
  """
105
120
 
106
- response = self.client.get(f"/apps/{app_name}/actions/")
121
+ response = self.client.get(f"/apps/{app_id}/actions/")
107
122
  response.raise_for_status()
108
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)