universal-mcp 0.1.13rc7__tar.gz → 0.1.14__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.13rc7 → universal_mcp-0.1.14}/PKG-INFO +2 -53
- universal_mcp-0.1.14/pyproject.toml +127 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_api_generator.py +7 -52
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_applications.py +4 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/applications/__init__.py +12 -2
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/cli.py +27 -11
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/api_generator.py +3 -93
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/docgen.py +2 -2
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/installation.py +8 -8
- universal_mcp-0.1.14/src/universal_mcp/utils/openapi.py +697 -0
- universal_mcp-0.1.14/src/universal_mcp/utils/readme.py +92 -0
- universal_mcp-0.1.13rc7/pyproject.toml +0 -201
- universal_mcp-0.1.13rc7/src/universal_mcp/utils/openapi.py +0 -471
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/.gitignore +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/__main__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/agents/react.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/client.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/schema.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/settings.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/streamlit.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/playground/utils.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_api_integration.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/integrations/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/integrations/integration.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/servers/server.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/templates/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/tools/adapters.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/tools/tools.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/agentr.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/dump_app_tools.py +0 -0
- {universal_mcp-0.1.13rc7 → universal_mcp-0.1.14}/src/universal_mcp/utils/singleton.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.14
|
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
|
@@ -18,60 +18,9 @@ Requires-Dist: pydantic>=2.11.1
|
|
18
18
|
Requires-Dist: pyyaml>=6.0.2
|
19
19
|
Requires-Dist: rich>=14.0.0
|
20
20
|
Requires-Dist: typer>=0.15.2
|
21
|
-
Provides-Extra: all
|
22
|
-
Requires-Dist: langchain-mcp-adapters>=0.0.3; extra == 'all'
|
23
|
-
Requires-Dist: langchain-openai>=0.3.12; extra == 'all'
|
24
|
-
Requires-Dist: langgraph-checkpoint-sqlite>=2.0.6; extra == 'all'
|
25
|
-
Requires-Dist: langgraph>=0.3.24; extra == 'all'
|
26
|
-
Requires-Dist: litellm>=1.30.7; extra == 'all'
|
27
|
-
Requires-Dist: pyright>=1.1.398; extra == 'all'
|
28
|
-
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'all'
|
29
|
-
Requires-Dist: pytest>=8.3.5; extra == 'all'
|
30
|
-
Requires-Dist: python-dotenv>=1.0.1; extra == 'all'
|
31
|
-
Requires-Dist: ruff>=0.11.4; extra == 'all'
|
32
|
-
Requires-Dist: streamlit>=1.44.1; extra == 'all'
|
33
|
-
Requires-Dist: watchdog>=6.0.0; extra == 'all'
|
34
|
-
Provides-Extra: applications
|
35
|
-
Requires-Dist: universal-mcp-ahrefs; extra == 'applications'
|
36
|
-
Requires-Dist: universal-mcp-cal-com-v2; extra == 'applications'
|
37
|
-
Requires-Dist: universal-mcp-calendly; extra == 'applications'
|
38
|
-
Requires-Dist: universal-mcp-clickup; extra == 'applications'
|
39
|
-
Requires-Dist: universal-mcp-coda; extra == 'applications'
|
40
|
-
Requires-Dist: universal-mcp-crustdata; extra == 'applications'
|
41
|
-
Requires-Dist: universal-mcp-e2b; extra == 'applications'
|
42
|
-
Requires-Dist: universal-mcp-elevenlabs; extra == 'applications'
|
43
|
-
Requires-Dist: universal-mcp-falai; extra == 'applications'
|
44
|
-
Requires-Dist: universal-mcp-figma; extra == 'applications'
|
45
|
-
Requires-Dist: universal-mcp-firecrawl; extra == 'applications'
|
46
|
-
Requires-Dist: universal-mcp-github; extra == 'applications'
|
47
|
-
Requires-Dist: universal-mcp-gong; extra == 'applications'
|
48
|
-
Requires-Dist: universal-mcp-google-calendar; extra == 'applications'
|
49
|
-
Requires-Dist: universal-mcp-google-docs; extra == 'applications'
|
50
|
-
Requires-Dist: universal-mcp-google-drive; extra == 'applications'
|
51
|
-
Requires-Dist: universal-mcp-google-mail; extra == 'applications'
|
52
|
-
Requires-Dist: universal-mcp-google-sheet; extra == 'applications'
|
53
|
-
Requires-Dist: universal-mcp-hashnode; extra == 'applications'
|
54
|
-
Requires-Dist: universal-mcp-heygen; extra == 'applications'
|
55
|
-
Requires-Dist: universal-mcp-mailchimp; extra == 'applications'
|
56
|
-
Requires-Dist: universal-mcp-markitdown; extra == 'applications'
|
57
|
-
Requires-Dist: universal-mcp-neon; extra == 'applications'
|
58
|
-
Requires-Dist: universal-mcp-notion; extra == 'applications'
|
59
|
-
Requires-Dist: universal-mcp-perplexity; extra == 'applications'
|
60
|
-
Requires-Dist: universal-mcp-reddit; extra == 'applications'
|
61
|
-
Requires-Dist: universal-mcp-replicate; extra == 'applications'
|
62
|
-
Requires-Dist: universal-mcp-resend; extra == 'applications'
|
63
|
-
Requires-Dist: universal-mcp-retell; extra == 'applications'
|
64
|
-
Requires-Dist: universal-mcp-rocketlane; extra == 'applications'
|
65
|
-
Requires-Dist: universal-mcp-serpapi; extra == 'applications'
|
66
|
-
Requires-Dist: universal-mcp-shortcut; extra == 'applications'
|
67
|
-
Requires-Dist: universal-mcp-spotify; extra == 'applications'
|
68
|
-
Requires-Dist: universal-mcp-supabase; extra == 'applications'
|
69
|
-
Requires-Dist: universal-mcp-tavily; extra == 'applications'
|
70
|
-
Requires-Dist: universal-mcp-wrike; extra == 'applications'
|
71
|
-
Requires-Dist: universal-mcp-youtube; extra == 'applications'
|
72
|
-
Requires-Dist: universal-mcp-zenquotes; extra == 'applications'
|
73
21
|
Provides-Extra: dev
|
74
22
|
Requires-Dist: litellm>=1.30.7; extra == 'dev'
|
23
|
+
Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
|
75
24
|
Requires-Dist: pyright>=1.1.398; extra == 'dev'
|
76
25
|
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
|
77
26
|
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
@@ -0,0 +1,127 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["hatchling"]
|
3
|
+
build-backend = "hatchling.build"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "universal-mcp"
|
7
|
+
version = "0.1.14"
|
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
|
+
readme = "README.md"
|
10
|
+
authors = [
|
11
|
+
{ name = "Manoj Bajaj", email = "manojbajaj95@gmail.com" }
|
12
|
+
]
|
13
|
+
requires-python = ">=3.11"
|
14
|
+
license = { text = "MIT" }
|
15
|
+
dependencies = [
|
16
|
+
"Jinja2>=3.1.3",
|
17
|
+
"cookiecutter>=2.6.0",
|
18
|
+
"gql[all]>=3.5.2",
|
19
|
+
"keyring>=25.6.0",
|
20
|
+
"litellm>=1.30.7",
|
21
|
+
"loguru>=0.7.3",
|
22
|
+
"mcp>=1.6.0",
|
23
|
+
"posthog>=3.24.0",
|
24
|
+
"pydantic>=2.11.1",
|
25
|
+
"pydantic-settings>=2.8.1",
|
26
|
+
"pyyaml>=6.0.2",
|
27
|
+
"rich>=14.0.0",
|
28
|
+
"typer>=0.15.2",
|
29
|
+
]
|
30
|
+
|
31
|
+
[project.optional-dependencies]
|
32
|
+
dev = [
|
33
|
+
"litellm>=1.30.7",
|
34
|
+
"pre-commit>=4.2.0",
|
35
|
+
"pyright>=1.1.398",
|
36
|
+
"pytest>=8.3.5",
|
37
|
+
"pytest-asyncio>=0.26.0",
|
38
|
+
"ruff>=0.11.4",
|
39
|
+
]
|
40
|
+
playground = [
|
41
|
+
"langchain-mcp-adapters>=0.0.3",
|
42
|
+
"langchain-openai>=0.3.12",
|
43
|
+
"langgraph>=0.3.24",
|
44
|
+
"langgraph-checkpoint-sqlite>=2.0.6",
|
45
|
+
"python-dotenv>=1.0.1",
|
46
|
+
"streamlit>=1.44.1",
|
47
|
+
"watchdog>=6.0.0",
|
48
|
+
]
|
49
|
+
|
50
|
+
[project.scripts]
|
51
|
+
universal_mcp = "universal_mcp.cli:app"
|
52
|
+
|
53
|
+
# ------------------------------
|
54
|
+
# Hatch build configuration
|
55
|
+
# ------------------------------
|
56
|
+
[tool.hatch.build.targets.sdist]
|
57
|
+
include = [
|
58
|
+
"/src",
|
59
|
+
"/templates", # Important: Include the templates directory!
|
60
|
+
]
|
61
|
+
|
62
|
+
[tool.hatch.build.targets.wheel]
|
63
|
+
packages = ["src/universal_mcp"]
|
64
|
+
|
65
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
66
|
+
"templates" = "universal_mcp/templates"
|
67
|
+
|
68
|
+
# ------------------------------
|
69
|
+
# Tool configurations
|
70
|
+
# ------------------------------
|
71
|
+
[tool.ruff]
|
72
|
+
exclude = [
|
73
|
+
".bzr",
|
74
|
+
".direnv",
|
75
|
+
".eggs",
|
76
|
+
".git",
|
77
|
+
".git-rewrite",
|
78
|
+
".hg",
|
79
|
+
".ipynb_checkpoints",
|
80
|
+
".mypy_cache",
|
81
|
+
".nox",
|
82
|
+
".pants.d",
|
83
|
+
".pyenv",
|
84
|
+
".pytest_cache",
|
85
|
+
".pytype",
|
86
|
+
".ruff_cache",
|
87
|
+
".svn",
|
88
|
+
".tox",
|
89
|
+
".venv",
|
90
|
+
".vscode",
|
91
|
+
"__pypackages__",
|
92
|
+
"_build",
|
93
|
+
"buck-out",
|
94
|
+
"build",
|
95
|
+
"dist",
|
96
|
+
"node_modules",
|
97
|
+
"site-packages",
|
98
|
+
"venv",
|
99
|
+
]
|
100
|
+
line-length = 88
|
101
|
+
indent-width = 4
|
102
|
+
target-version = "py312"
|
103
|
+
|
104
|
+
[tool.ruff.lint]
|
105
|
+
select = [
|
106
|
+
"E", # pycodestyle
|
107
|
+
"F", # Pyflakes
|
108
|
+
"UP", # pyupgrade
|
109
|
+
"B", # flake8-bugbear
|
110
|
+
"SIM", # flake8-simplify
|
111
|
+
"I", # isort
|
112
|
+
]
|
113
|
+
ignore = ['E501', 'B008']
|
114
|
+
fixable = ["ALL"]
|
115
|
+
unfixable = []
|
116
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
117
|
+
|
118
|
+
[tool.ruff.format]
|
119
|
+
quote-style = "double"
|
120
|
+
indent-style = "space"
|
121
|
+
skip-magic-trailing-comma = false
|
122
|
+
line-ending = "auto"
|
123
|
+
docstring-code-line-length = "dynamic"
|
124
|
+
|
125
|
+
[tool.pytest.ini_options]
|
126
|
+
asyncio_mode = "strict"
|
127
|
+
asyncio_default_fixture_loop_scope = "function"
|
@@ -53,9 +53,7 @@ def sample_schema(temp_dir):
|
|
53
53
|
@pytest.mark.asyncio
|
54
54
|
async def test_generate_api_without_output(sample_schema):
|
55
55
|
"""Test API generation without output file (return code only)."""
|
56
|
-
result = generate_api_from_schema(
|
57
|
-
schema_path=sample_schema, output_path=None, add_docstrings=False
|
58
|
-
)
|
56
|
+
result = generate_api_from_schema(schema_path=sample_schema, output_path=None)
|
59
57
|
|
60
58
|
assert "code" in result
|
61
59
|
assert isinstance(result["code"], str)
|
@@ -72,8 +70,8 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
|
|
72
70
|
"""Test API generation with output file."""
|
73
71
|
output_path = temp_dir / "test.py"
|
74
72
|
|
75
|
-
app_file
|
76
|
-
schema_path=sample_schema, output_path=output_path
|
73
|
+
app_file = generate_api_from_schema(
|
74
|
+
schema_path=sample_schema, output_path=output_path
|
77
75
|
)
|
78
76
|
|
79
77
|
assert "app_file" != None
|
@@ -87,14 +85,6 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
|
|
87
85
|
assert "def test_operation" in content
|
88
86
|
assert "def list_tools" in content
|
89
87
|
|
90
|
-
# Verify README exists and contains expected content
|
91
|
-
if readme_file:
|
92
|
-
assert readme_file.exists()
|
93
|
-
readme_content = readme_file.read_text()
|
94
|
-
assert "Test MCP Server" in readme_content
|
95
|
-
assert "Tool List" in readme_content
|
96
|
-
assert "test_operation" in readme_content
|
97
|
-
|
98
88
|
|
99
89
|
@pytest.mark.asyncio
|
100
90
|
async def test_generate_api_invalid_schema(temp_dir):
|
@@ -115,40 +105,16 @@ async def test_generate_api_nonexistent_schema():
|
|
115
105
|
)
|
116
106
|
|
117
107
|
|
118
|
-
@pytest.mark.asyncio
|
119
|
-
async def test_generate_api_with_docstrings(sample_schema, temp_dir):
|
120
|
-
"""Test API generation with docstring generation."""
|
121
|
-
output_path = temp_dir / "test_with_docs.py"
|
122
|
-
|
123
|
-
app_file, readme_file = generate_api_from_schema(
|
124
|
-
schema_path=sample_schema, output_path=output_path, add_docstrings=True
|
125
|
-
)
|
126
|
-
|
127
|
-
assert app_file is not None
|
128
|
-
assert readme_file is not None
|
129
|
-
assert app_file.exists()
|
130
|
-
|
131
|
-
# Check if docstrings were added
|
132
|
-
content = app_file.read_text()
|
133
|
-
# Check for required imports and class structure
|
134
|
-
assert "from universal_mcp.applications import APIApplication" in content
|
135
|
-
assert "from universal_mcp.integrations import Integration" in content
|
136
|
-
assert "def test_operation" in content
|
137
|
-
assert '"""' in content # Basic check for docstring presence
|
138
|
-
assert "Tags:" in content
|
139
|
-
|
140
|
-
|
141
108
|
@pytest.mark.asyncio
|
142
109
|
async def test_generate_api_without_docstrings(sample_schema, temp_dir):
|
143
110
|
"""Test API generation without docstring generation."""
|
144
111
|
output_path = temp_dir / "test_without_docs.py"
|
145
112
|
|
146
|
-
app_file
|
147
|
-
schema_path=sample_schema, output_path=output_path
|
113
|
+
app_file = generate_api_from_schema(
|
114
|
+
schema_path=sample_schema, output_path=output_path
|
148
115
|
)
|
149
116
|
|
150
117
|
assert app_file is not None
|
151
|
-
assert readme_file is not None
|
152
118
|
assert app_file.exists()
|
153
119
|
|
154
120
|
# Verify the app was generated
|
@@ -203,12 +169,11 @@ async def test_generate_api_with_complex_schema(temp_dir):
|
|
203
169
|
json.dump(schema, f)
|
204
170
|
|
205
171
|
output_path = temp_dir / "complex.py"
|
206
|
-
app_file
|
207
|
-
schema_path=schema_file, output_path=output_path
|
172
|
+
app_file = generate_api_from_schema(
|
173
|
+
schema_path=schema_file, output_path=output_path
|
208
174
|
)
|
209
175
|
|
210
176
|
assert app_file is not None
|
211
|
-
assert readme_file is not None
|
212
177
|
assert app_file.exists()
|
213
178
|
|
214
179
|
content = app_file.read_text()
|
@@ -231,13 +196,3 @@ async def test_generate_api_with_complex_schema(temp_dir):
|
|
231
196
|
|
232
197
|
# Check for proper typing imports
|
233
198
|
assert "from typing import" in content
|
234
|
-
|
235
|
-
# Verify README was generated
|
236
|
-
readme_content = readme_file.read_text()
|
237
|
-
|
238
|
-
# Check README content
|
239
|
-
assert "Complex MCP Server" in readme_content
|
240
|
-
assert "Tool List" in readme_content
|
241
|
-
assert "list_users" in readme_content
|
242
|
-
assert "create_user" in readme_content
|
243
|
-
assert "get_user" in readme_content
|
@@ -57,6 +57,10 @@ def test_application(app_name):
|
|
57
57
|
logger.info(f"Tools for {app_name}: {tools}")
|
58
58
|
assert len(tools) > 0
|
59
59
|
assert isinstance(tools[0], Callable)
|
60
|
+
important_tools = []
|
60
61
|
for tool in tools:
|
61
62
|
assert tool.__name__ is not None
|
62
63
|
assert tool.__doc__ is not None
|
64
|
+
if "important" in tool.__doc__:
|
65
|
+
important_tools.append(tool.__name__)
|
66
|
+
assert len(important_tools) > 0
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import importlib
|
2
|
+
import os
|
2
3
|
import subprocess
|
4
|
+
import sys
|
3
5
|
|
4
6
|
from loguru import logger
|
5
7
|
|
@@ -9,8 +11,16 @@ from universal_mcp.applications.application import (
|
|
9
11
|
GraphQLApplication,
|
10
12
|
)
|
11
13
|
|
14
|
+
UNIVERSAL_MCP_HOME = os.path.join(os.path.expanduser("~"), ".universal-mcp", "packages")
|
15
|
+
|
16
|
+
if not os.path.exists(UNIVERSAL_MCP_HOME):
|
17
|
+
os.makedirs(UNIVERSAL_MCP_HOME)
|
18
|
+
|
19
|
+
# set python path to include the universal-mcp home directory
|
20
|
+
sys.path.append(UNIVERSAL_MCP_HOME)
|
21
|
+
|
22
|
+
|
12
23
|
# Name are in the format of "app-name", eg, google-calendar
|
13
|
-
# Folder name is "app_name", eg, google_calendar
|
14
24
|
# Class name is NameApp, eg, GoogleCalendarApp
|
15
25
|
|
16
26
|
|
@@ -38,7 +48,7 @@ def _install_package(slug_clean: str):
|
|
38
48
|
Helper to install a package via pip from the universal-mcp GitHub repository.
|
39
49
|
"""
|
40
50
|
repo_url = f"git+https://github.com/universal-mcp/{slug_clean}"
|
41
|
-
cmd = ["uv", "pip", "install", repo_url]
|
51
|
+
cmd = ["uv", "pip", "install", repo_url, "--target", UNIVERSAL_MCP_HOME]
|
42
52
|
logger.info(f"Installing package '{slug_clean}' with command: {' '.join(cmd)}")
|
43
53
|
try:
|
44
54
|
subprocess.check_call(cmd)
|
@@ -23,6 +23,12 @@ def generate(
|
|
23
23
|
"-o",
|
24
24
|
help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
|
25
25
|
),
|
26
|
+
class_name: str = typer.Option(
|
27
|
+
None,
|
28
|
+
"--class-name",
|
29
|
+
"-c",
|
30
|
+
help="Class name to use for the API client",
|
31
|
+
),
|
26
32
|
):
|
27
33
|
"""Generate API client from OpenAPI schema with optional docstring generation.
|
28
34
|
|
@@ -38,25 +44,35 @@ def generate(
|
|
38
44
|
|
39
45
|
try:
|
40
46
|
# Run the async function in the event loop
|
41
|
-
|
47
|
+
app_file = generate_api_from_schema(
|
42
48
|
schema_path=schema_path,
|
43
49
|
output_path=output_path,
|
50
|
+
class_name=class_name,
|
44
51
|
)
|
45
|
-
|
46
|
-
|
47
|
-
# Print to stdout if no output path
|
48
|
-
print(result["code"])
|
49
|
-
else:
|
50
|
-
typer.echo("API client successfully generated and installed.")
|
51
|
-
if "app_file" in result:
|
52
|
-
typer.echo(f"Application file: {result['app_file']}")
|
53
|
-
if "readme_file" in result and result["readme_file"]:
|
54
|
-
typer.echo(f"Documentation: {result['readme_file']}")
|
52
|
+
typer.echo("API client successfully generated and installed.")
|
53
|
+
typer.echo(f"Application file: {app_file}")
|
55
54
|
except Exception as e:
|
56
55
|
typer.echo(f"Error generating API client: {e}", err=True)
|
57
56
|
raise typer.Exit(1) from e
|
58
57
|
|
59
58
|
|
59
|
+
@app.command()
|
60
|
+
def readme(
|
61
|
+
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
62
|
+
class_name: str = typer.Option(
|
63
|
+
None,
|
64
|
+
"--class-name",
|
65
|
+
"-c",
|
66
|
+
help="Class name to use for the API client",
|
67
|
+
),
|
68
|
+
):
|
69
|
+
"""Generate a README.md file for the API client."""
|
70
|
+
from universal_mcp.utils.readme import generate_readme
|
71
|
+
|
72
|
+
readme_file = generate_readme(file_path, class_name)
|
73
|
+
typer.echo(f"README.md file generated at: {readme_file}")
|
74
|
+
|
75
|
+
|
60
76
|
@app.command()
|
61
77
|
def docgen(
|
62
78
|
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
@@ -4,7 +4,6 @@ import os
|
|
4
4
|
import shutil
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
|
-
from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
|
8
7
|
from loguru import logger
|
9
8
|
|
10
9
|
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
@@ -36,67 +35,6 @@ def get_class_info(module: any) -> tuple[str | None, any]:
|
|
36
35
|
return None, None
|
37
36
|
|
38
37
|
|
39
|
-
def generate_readme(app_dir: Path, folder_name: str, tools: list) -> Path:
|
40
|
-
"""Generate README.md with API documentation.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
app_dir: Directory where the README will be generated
|
44
|
-
folder_name: Name of the application folder
|
45
|
-
tools: List of Function objects from the OpenAPI schema
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
Path to the generated README file
|
49
|
-
|
50
|
-
Raises:
|
51
|
-
FileNotFoundError: If the template directory doesn't exist
|
52
|
-
TemplateError: If there's an error rendering the template
|
53
|
-
IOError: If there's an error writing the README file
|
54
|
-
"""
|
55
|
-
app = folder_name.replace("_", " ").title()
|
56
|
-
logger.info(f"Generating README for {app} in {app_dir}")
|
57
|
-
|
58
|
-
# Format tools into (name, description) tuples
|
59
|
-
formatted_tools = []
|
60
|
-
for tool in tools:
|
61
|
-
name = tool.__name__
|
62
|
-
description = tool.__doc__.strip().split("\n")[0]
|
63
|
-
formatted_tools.append((name, description))
|
64
|
-
|
65
|
-
# Set up Jinja2 environment
|
66
|
-
template_dir = Path(__file__).parent.parent / "templates"
|
67
|
-
if not template_dir.exists():
|
68
|
-
logger.error(f"Template directory not found: {template_dir}")
|
69
|
-
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
70
|
-
|
71
|
-
try:
|
72
|
-
env = Environment(
|
73
|
-
loader=FileSystemLoader(template_dir), autoescape=select_autoescape()
|
74
|
-
)
|
75
|
-
template = env.get_template("README.md.j2")
|
76
|
-
except Exception as e:
|
77
|
-
logger.error(f"Error loading template: {e}")
|
78
|
-
raise TemplateError(f"Error loading template: {e}") from e
|
79
|
-
|
80
|
-
# Render the template
|
81
|
-
try:
|
82
|
-
readme_content = template.render(name=app, tools=formatted_tools)
|
83
|
-
except Exception as e:
|
84
|
-
logger.error(f"Error rendering template: {e}")
|
85
|
-
raise TemplateError(f"Error rendering template: {e}") from e
|
86
|
-
|
87
|
-
# Write the README file
|
88
|
-
readme_file = app_dir / "README.md"
|
89
|
-
try:
|
90
|
-
with open(readme_file, "w") as f:
|
91
|
-
f.write(readme_content)
|
92
|
-
logger.info(f"Documentation generated at: {readme_file}")
|
93
|
-
except Exception as e:
|
94
|
-
logger.error(f"Error writing README file: {e}")
|
95
|
-
raise OSError(f"Error writing README file: {e}") from e
|
96
|
-
|
97
|
-
return readme_file
|
98
|
-
|
99
|
-
|
100
38
|
def test_correct_output(gen_file: Path):
|
101
39
|
# Check file is non-empty
|
102
40
|
if gen_file.stat().st_size == 0:
|
@@ -120,7 +58,7 @@ def test_correct_output(gen_file: Path):
|
|
120
58
|
def generate_api_from_schema(
|
121
59
|
schema_path: Path,
|
122
60
|
output_path: Path | None = None,
|
123
|
-
|
61
|
+
class_name: str | None = None,
|
124
62
|
) -> tuple[Path, Path]:
|
125
63
|
"""
|
126
64
|
Generate API client from OpenAPI schema and write to app.py with a README.
|
@@ -147,7 +85,7 @@ def generate_api_from_schema(
|
|
147
85
|
|
148
86
|
# 2. Generate client code
|
149
87
|
try:
|
150
|
-
code = generate_api_client(schema)
|
88
|
+
code = generate_api_client(schema, class_name)
|
151
89
|
logger.info("API client code generated.")
|
152
90
|
except Exception as e:
|
153
91
|
logger.error("Code generation failed: %s", e)
|
@@ -192,34 +130,6 @@ def generate_api_from_schema(
|
|
192
130
|
shutil.copy(gen_file, app_file)
|
193
131
|
logger.info("App file written to: %s", app_file)
|
194
132
|
|
195
|
-
# 6. Collect tools and generate README
|
196
|
-
import importlib.util
|
197
|
-
import sys
|
198
|
-
|
199
|
-
# Load the generated module as "temp_module"
|
200
|
-
spec = importlib.util.spec_from_file_location("temp_module", str(app_file))
|
201
|
-
module = importlib.util.module_from_spec(spec)
|
202
|
-
sys.modules["temp_module"] = module
|
203
|
-
spec.loader.exec_module(module)
|
204
|
-
|
205
|
-
# Retrieve the generated API class
|
206
|
-
class_name, cls = get_class_info(module)
|
207
|
-
|
208
|
-
# Instantiate client and collect its tools
|
209
|
-
tools = []
|
210
|
-
if cls:
|
211
|
-
try:
|
212
|
-
client = cls()
|
213
|
-
tools = client.list_tools()
|
214
|
-
except Exception as e:
|
215
|
-
logger.warning(
|
216
|
-
"Failed to instantiate '%s' or list tools: %s", class_name, e
|
217
|
-
)
|
218
|
-
else:
|
219
|
-
logger.warning("No generated class found in module 'temp_module'")
|
220
|
-
readme_file = generate_readme(target_dir, output_path.stem, tools)
|
221
|
-
logger.info("README generated at: %s", readme_file)
|
222
|
-
|
223
133
|
# Cleanup intermediate file
|
224
134
|
try:
|
225
135
|
os.remove(gen_file)
|
@@ -227,4 +137,4 @@ def generate_api_from_schema(
|
|
227
137
|
except Exception as e:
|
228
138
|
logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
|
229
139
|
|
230
|
-
return app_file
|
140
|
+
return app_file
|
@@ -176,7 +176,7 @@ def extract_json_from_text(text):
|
|
176
176
|
|
177
177
|
|
178
178
|
def generate_docstring(
|
179
|
-
function_code: str, model: str = "perplexity/sonar
|
179
|
+
function_code: str, model: str = "perplexity/sonar"
|
180
180
|
) -> DocstringOutput:
|
181
181
|
"""
|
182
182
|
Generate a docstring for a Python function using litellm with structured output.
|
@@ -509,7 +509,7 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
|
|
509
509
|
return function_code
|
510
510
|
|
511
511
|
|
512
|
-
def process_file(file_path: str, model: str = "perplexity/sonar
|
512
|
+
def process_file(file_path: str, model: str = "perplexity/sonar") -> int:
|
513
513
|
"""
|
514
514
|
Process a Python file and add docstrings to all functions in it.
|
515
515
|
|
@@ -61,7 +61,7 @@ def install_claude(api_key: str) -> None:
|
|
61
61
|
config["mcpServers"] = {}
|
62
62
|
config["mcpServers"]["universal_mcp"] = {
|
63
63
|
"command": get_uvx_path(),
|
64
|
-
"args": ["universal_mcp
|
64
|
+
"args": ["universal_mcp@latest", "run"],
|
65
65
|
"env": {"AGENTR_API_KEY": api_key},
|
66
66
|
}
|
67
67
|
with open(config_path, "w") as f:
|
@@ -90,7 +90,7 @@ def install_cursor(api_key: str) -> None:
|
|
90
90
|
config["mcpServers"] = {}
|
91
91
|
config["mcpServers"]["universal_mcp"] = {
|
92
92
|
"command": get_uvx_path(),
|
93
|
-
"args": ["universal_mcp
|
93
|
+
"args": ["universal_mcp@latest", "run"],
|
94
94
|
"env": {"AGENTR_API_KEY": api_key},
|
95
95
|
}
|
96
96
|
|
@@ -120,7 +120,7 @@ def install_cline(api_key: str) -> None:
|
|
120
120
|
config["mcpServers"] = {}
|
121
121
|
config["mcpServers"]["universal_mcp"] = {
|
122
122
|
"command": get_uvx_path(),
|
123
|
-
"args": ["universal_mcp
|
123
|
+
"args": ["universal_mcp@latest", "run"],
|
124
124
|
"env": {"AGENTR_API_KEY": api_key},
|
125
125
|
}
|
126
126
|
|
@@ -156,7 +156,7 @@ def install_continue(api_key: str) -> None:
|
|
156
156
|
config["mcpServers"] = {}
|
157
157
|
config["mcpServers"]["universal_mcp"] = {
|
158
158
|
"command": get_uvx_path(),
|
159
|
-
"args": ["universal_mcp
|
159
|
+
"args": ["universal_mcp@latest", "run"],
|
160
160
|
"env": {"AGENTR_API_KEY": api_key},
|
161
161
|
}
|
162
162
|
|
@@ -192,7 +192,7 @@ def install_goose(api_key: str) -> None:
|
|
192
192
|
config["mcpServers"] = {}
|
193
193
|
config["mcpServers"]["universal_mcp"] = {
|
194
194
|
"command": get_uvx_path(),
|
195
|
-
"args": ["universal_mcp
|
195
|
+
"args": ["universal_mcp@latest", "run"],
|
196
196
|
"env": {"AGENTR_API_KEY": api_key},
|
197
197
|
}
|
198
198
|
|
@@ -228,7 +228,7 @@ def install_windsurf(api_key: str) -> None:
|
|
228
228
|
config["mcpServers"] = {}
|
229
229
|
config["mcpServers"]["universal_mcp"] = {
|
230
230
|
"command": get_uvx_path(),
|
231
|
-
"args": ["universal_mcp
|
231
|
+
"args": ["universal_mcp@latest", "run"],
|
232
232
|
"env": {"AGENTR_API_KEY": api_key},
|
233
233
|
}
|
234
234
|
|
@@ -267,7 +267,7 @@ def install_zed(api_key: str) -> None:
|
|
267
267
|
server.update(
|
268
268
|
{
|
269
269
|
"command": get_uvx_path(),
|
270
|
-
"args": ["universal_mcp
|
270
|
+
"args": ["universal_mcp@latest", "run"],
|
271
271
|
"env": {"AGENTR_API_KEY": api_key},
|
272
272
|
}
|
273
273
|
)
|
@@ -278,7 +278,7 @@ def install_zed(api_key: str) -> None:
|
|
278
278
|
{
|
279
279
|
"name": "universal_mcp",
|
280
280
|
"command": get_uvx_path(),
|
281
|
-
"args": ["universal_mcp
|
281
|
+
"args": ["universal_mcp@latest", "run"],
|
282
282
|
"env": {"AGENTR_API_KEY": api_key},
|
283
283
|
}
|
284
284
|
)
|