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.
@@ -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
- result = generate_api_from_schema(
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
- if not output_path:
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
- add_docstrings: bool = True,
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, readme_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-pro"
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-pro") -> int:
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
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[applications]@latest", "run"],
281
+ "args": ["universal_mcp@latest", "run"],
282
282
  "env": {"AGENTR_API_KEY": api_key},
283
283
  }
284
284
  )