universal-mcp 0.1.13rc7__py3-none-any.whl → 0.1.14__py3-none-any.whl
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/applications/__init__.py +12 -2
- universal_mcp/cli.py +27 -11
- universal_mcp/utils/api_generator.py +3 -93
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi.py +523 -297
- universal_mcp/utils/readme.py +92 -0
- {universal_mcp-0.1.13rc7.dist-info → universal_mcp-0.1.14.dist-info}/METADATA +2 -53
- {universal_mcp-0.1.13rc7.dist-info → universal_mcp-0.1.14.dist-info}/RECORD +11 -10
- {universal_mcp-0.1.13rc7.dist-info → universal_mcp-0.1.14.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.13rc7.dist-info → universal_mcp-0.1.14.dist-info}/entry_points.txt +0 -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)
|
universal_mcp/cli.py
CHANGED
@@ -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
|
universal_mcp/utils/docgen.py
CHANGED
@@ -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
|
)
|