universal-mcp 0.1.13rc3__py3-none-any.whl → 0.1.13rc14__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 +32 -8
- universal_mcp/cli.py +41 -27
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/servers/server.py +0 -2
- universal_mcp/stores/store.py +2 -0
- universal_mcp/tools/tools.py +1 -3
- universal_mcp/utils/agentr.py +8 -3
- universal_mcp/utils/api_generator.py +14 -101
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/openapi.py +527 -285
- universal_mcp/utils/readme.py +92 -0
- {universal_mcp-0.1.13rc3.dist-info → universal_mcp-0.1.13rc14.dist-info}/METADATA +2 -53
- {universal_mcp-0.1.13rc3.dist-info → universal_mcp-0.1.13rc14.dist-info}/RECORD +15 -14
- {universal_mcp-0.1.13rc3.dist-info → universal_mcp-0.1.13rc14.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.13rc3.dist-info → universal_mcp-0.1.13rc14.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,7 @@
|
|
1
1
|
import importlib
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
import sys
|
2
5
|
|
3
6
|
from loguru import logger
|
4
7
|
|
@@ -7,12 +10,20 @@ from universal_mcp.applications.application import (
|
|
7
10
|
BaseApplication,
|
8
11
|
GraphQLApplication,
|
9
12
|
)
|
10
|
-
|
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
|
+
|
11
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
|
|
26
|
+
|
16
27
|
def _import_class(module_path: str, class_name: str):
|
17
28
|
"""
|
18
29
|
Helper to import a class by name from a module.
|
@@ -27,23 +38,29 @@ def _import_class(module_path: str, class_name: str):
|
|
27
38
|
return getattr(module, class_name)
|
28
39
|
except AttributeError as e:
|
29
40
|
logger.error(f"Class '{class_name}' not found in module '{module_path}'")
|
30
|
-
raise ModuleNotFoundError(
|
41
|
+
raise ModuleNotFoundError(
|
42
|
+
f"Class '{class_name}' not found in module '{module_path}'"
|
43
|
+
) from e
|
44
|
+
|
31
45
|
|
32
46
|
def _install_package(slug_clean: str):
|
33
47
|
"""
|
34
48
|
Helper to install a package via pip from the universal-mcp GitHub repository.
|
35
49
|
"""
|
36
50
|
repo_url = f"git+https://github.com/universal-mcp/{slug_clean}"
|
37
|
-
cmd = ["uv", "pip", "install", repo_url]
|
51
|
+
cmd = ["uv", "pip", "install", repo_url, "--target", UNIVERSAL_MCP_HOME]
|
38
52
|
logger.info(f"Installing package '{slug_clean}' with command: {' '.join(cmd)}")
|
39
53
|
try:
|
40
54
|
subprocess.check_call(cmd)
|
41
55
|
except subprocess.CalledProcessError as e:
|
42
56
|
logger.error(f"Installation failed for '{slug_clean}': {e}")
|
43
|
-
raise ModuleNotFoundError(
|
57
|
+
raise ModuleNotFoundError(
|
58
|
+
f"Installation failed for package '{slug_clean}'"
|
59
|
+
) from e
|
44
60
|
else:
|
45
61
|
logger.info(f"Package '{slug_clean}' installed successfully")
|
46
62
|
|
63
|
+
|
47
64
|
def app_from_slug(slug: str):
|
48
65
|
"""
|
49
66
|
Dynamically resolve and return the application class for the given slug.
|
@@ -54,19 +71,26 @@ def app_from_slug(slug: str):
|
|
54
71
|
package_prefix = f"universal_mcp_{slug_clean.replace('-', '_')}"
|
55
72
|
module_path = f"{package_prefix}.app"
|
56
73
|
|
57
|
-
logger.info(
|
74
|
+
logger.info(
|
75
|
+
f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
|
76
|
+
)
|
58
77
|
try:
|
59
78
|
return _import_class(module_path, class_name)
|
60
79
|
except ModuleNotFoundError as orig_err:
|
61
|
-
logger.warning(
|
80
|
+
logger.warning(
|
81
|
+
f"Module '{module_path}' not found locally: {orig_err}. Installing..."
|
82
|
+
)
|
62
83
|
_install_package(slug_clean)
|
63
84
|
# Retry import after installation
|
64
85
|
try:
|
65
86
|
return _import_class(module_path, class_name)
|
66
87
|
except ModuleNotFoundError as retry_err:
|
67
|
-
logger.error(
|
88
|
+
logger.error(
|
89
|
+
f"Still cannot import '{module_path}' after installation: {retry_err}"
|
90
|
+
)
|
68
91
|
raise
|
69
92
|
|
93
|
+
|
70
94
|
__all__ = [
|
71
95
|
"app_from_slug",
|
72
96
|
"BaseApplication",
|
universal_mcp/cli.py
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
import
|
2
|
-
import os
|
1
|
+
import re
|
3
2
|
from pathlib import Path
|
4
3
|
|
5
4
|
import typer
|
6
5
|
from rich import print as rprint
|
7
6
|
from rich.panel import Panel
|
8
|
-
import re
|
9
7
|
|
10
8
|
from universal_mcp.utils.installation import (
|
11
9
|
get_supported_apps,
|
@@ -25,6 +23,12 @@ def generate(
|
|
25
23
|
"-o",
|
26
24
|
help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
|
27
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
|
+
),
|
28
32
|
):
|
29
33
|
"""Generate API client from OpenAPI schema with optional docstring generation.
|
30
34
|
|
@@ -40,25 +44,35 @@ def generate(
|
|
40
44
|
|
41
45
|
try:
|
42
46
|
# Run the async function in the event loop
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
print(result["code"])
|
51
|
-
else:
|
52
|
-
typer.echo("API client successfully generated and installed.")
|
53
|
-
if "app_file" in result:
|
54
|
-
typer.echo(f"Application file: {result['app_file']}")
|
55
|
-
if "readme_file" in result and result["readme_file"]:
|
56
|
-
typer.echo(f"Documentation: {result['readme_file']}")
|
47
|
+
app_file = generate_api_from_schema(
|
48
|
+
schema_path=schema_path,
|
49
|
+
output_path=output_path,
|
50
|
+
class_name=class_name,
|
51
|
+
)
|
52
|
+
typer.echo("API client successfully generated and installed.")
|
53
|
+
typer.echo(f"Application file: {app_file}")
|
57
54
|
except Exception as e:
|
58
55
|
typer.echo(f"Error generating API client: {e}", err=True)
|
59
56
|
raise typer.Exit(1) from e
|
60
57
|
|
61
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
|
+
|
62
76
|
@app.command()
|
63
77
|
def docgen(
|
64
78
|
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
@@ -68,7 +82,6 @@ def docgen(
|
|
68
82
|
"-m",
|
69
83
|
help="Model to use for generating docstrings",
|
70
84
|
),
|
71
|
-
|
72
85
|
):
|
73
86
|
"""Generate docstrings for Python files using LLMs.
|
74
87
|
|
@@ -154,6 +167,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
154
167
|
typer.echo(f"Error installing app: {e}", err=True)
|
155
168
|
raise typer.Exit(1) from e
|
156
169
|
|
170
|
+
|
157
171
|
@app.command()
|
158
172
|
def init(
|
159
173
|
output_dir: Path | None = typer.Option(
|
@@ -162,13 +176,13 @@ def init(
|
|
162
176
|
"-o",
|
163
177
|
help="Output directory for the project (must exist)",
|
164
178
|
),
|
165
|
-
app_name: str|None = typer.Option(
|
179
|
+
app_name: str | None = typer.Option(
|
166
180
|
None,
|
167
181
|
"--app-name",
|
168
182
|
"-a",
|
169
183
|
help="App name (letters, numbers, hyphens, underscores only)",
|
170
184
|
),
|
171
|
-
integration_type: str|None = typer.Option(
|
185
|
+
integration_type: str | None = typer.Option(
|
172
186
|
None,
|
173
187
|
"--integration-type",
|
174
188
|
"-i",
|
@@ -195,7 +209,7 @@ def init(
|
|
195
209
|
app_name = typer.prompt(
|
196
210
|
"Enter the app name",
|
197
211
|
default="app_name",
|
198
|
-
prompt_suffix=" (e.g., reddit, youtube): "
|
212
|
+
prompt_suffix=" (e.g., reddit, youtube): ",
|
199
213
|
).strip()
|
200
214
|
validate_pattern(app_name, "app name")
|
201
215
|
|
@@ -203,10 +217,10 @@ def init(
|
|
203
217
|
path_str = typer.prompt(
|
204
218
|
"Enter the output directory for the project",
|
205
219
|
default=str(Path.cwd()),
|
206
|
-
prompt_suffix=": "
|
220
|
+
prompt_suffix=": ",
|
207
221
|
).strip()
|
208
222
|
output_dir = Path(path_str)
|
209
|
-
|
223
|
+
|
210
224
|
if not output_dir.exists():
|
211
225
|
try:
|
212
226
|
output_dir.mkdir(parents=True, exist_ok=True)
|
@@ -219,7 +233,7 @@ def init(
|
|
219
233
|
f"❌ Failed to create output directory '{output_dir}': {e}",
|
220
234
|
fg=typer.colors.RED,
|
221
235
|
)
|
222
|
-
raise typer.Exit(code=1)
|
236
|
+
raise typer.Exit(code=1) from e
|
223
237
|
elif not output_dir.is_dir():
|
224
238
|
typer.secho(
|
225
239
|
f"❌ Output path '{output_dir}' exists but is not a directory.",
|
@@ -232,7 +246,7 @@ def init(
|
|
232
246
|
integration_type = typer.prompt(
|
233
247
|
"Choose the integration type",
|
234
248
|
default="agentr",
|
235
|
-
prompt_suffix=" (api_key, oauth, agentr, none): "
|
249
|
+
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
236
250
|
).lower()
|
237
251
|
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
238
252
|
typer.secho(
|
@@ -240,7 +254,6 @@ def init(
|
|
240
254
|
fg=typer.colors.RED,
|
241
255
|
)
|
242
256
|
raise typer.Exit(code=1)
|
243
|
-
|
244
257
|
|
245
258
|
typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
|
246
259
|
try:
|
@@ -255,10 +268,11 @@ def init(
|
|
255
268
|
)
|
256
269
|
except Exception as exc:
|
257
270
|
typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
|
258
|
-
raise typer.Exit(code=1)
|
271
|
+
raise typer.Exit(code=1) from exc
|
259
272
|
|
260
273
|
project_dir = output_dir / f"universal-mcp-{app_name}"
|
261
274
|
typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
|
262
275
|
|
276
|
+
|
263
277
|
if __name__ == "__main__":
|
264
278
|
app()
|
universal_mcp/servers/server.py
CHANGED
universal_mcp/stores/store.py
CHANGED
@@ -11,11 +11,13 @@ class StoreError(Exception):
|
|
11
11
|
|
12
12
|
pass
|
13
13
|
|
14
|
+
|
14
15
|
class KeyNotFoundError(StoreError):
|
15
16
|
"""Exception raised when a key is not found in the store."""
|
16
17
|
|
17
18
|
pass
|
18
19
|
|
20
|
+
|
19
21
|
class BaseStore(ABC):
|
20
22
|
"""
|
21
23
|
Abstract base class defining the interface for credential stores.
|
universal_mcp/tools/tools.py
CHANGED
@@ -260,9 +260,7 @@ class ToolManager:
|
|
260
260
|
try:
|
261
261
|
available_tool_functions = app.list_tools()
|
262
262
|
except TypeError as e:
|
263
|
-
logger.error(
|
264
|
-
f"Error calling list_tools for app '{app.name}'. Error: {e}"
|
265
|
-
)
|
263
|
+
logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
|
266
264
|
return
|
267
265
|
except Exception as e:
|
268
266
|
logger.error(f"Failed to get tool list from app '{app.name}': {e}")
|
universal_mcp/utils/agentr.py
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
from loguru import logger
|
2
1
|
import os
|
2
|
+
|
3
3
|
import httpx
|
4
|
+
from loguru import logger
|
5
|
+
|
4
6
|
from universal_mcp.config import AppConfig
|
7
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
5
8
|
from universal_mcp.utils.singleton import Singleton
|
6
9
|
|
10
|
+
|
7
11
|
class AgentrClient(metaclass=Singleton):
|
8
12
|
"""Helper class for AgentR API operations.
|
9
13
|
|
@@ -22,7 +26,9 @@ class AgentrClient(metaclass=Singleton):
|
|
22
26
|
"API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
|
23
27
|
)
|
24
28
|
raise ValueError("AgentR API key required - get one at https://agentr.dev")
|
25
|
-
self.base_url = (
|
29
|
+
self.base_url = (
|
30
|
+
base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
31
|
+
).rstrip("/")
|
26
32
|
|
27
33
|
def get_credentials(self, integration_name: str) -> dict:
|
28
34
|
"""Get credentials for an integration from the AgentR API.
|
@@ -87,4 +93,3 @@ class AgentrClient(metaclass=Singleton):
|
|
87
93
|
response.raise_for_status()
|
88
94
|
data = response.json()
|
89
95
|
return [AppConfig.model_validate(app) for app in data]
|
90
|
-
|
@@ -1,10 +1,10 @@
|
|
1
|
+
import importlib.util
|
1
2
|
import inspect
|
2
3
|
import os
|
4
|
+
import shutil
|
3
5
|
from pathlib import Path
|
6
|
+
|
4
7
|
from loguru import logger
|
5
|
-
import shutil
|
6
|
-
import importlib.util
|
7
|
-
from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
|
8
8
|
|
9
9
|
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
10
10
|
|
@@ -26,6 +26,7 @@ def validate_and_load_schema(schema_path: Path) -> dict:
|
|
26
26
|
echo(f"Error loading schema: {e}", err=True)
|
27
27
|
raise
|
28
28
|
|
29
|
+
|
29
30
|
def get_class_info(module: any) -> tuple[str | None, any]:
|
30
31
|
"""Find the main class in the generated module."""
|
31
32
|
for name, obj in inspect.getmembers(module):
|
@@ -33,71 +34,6 @@ def get_class_info(module: any) -> tuple[str | None, any]:
|
|
33
34
|
return name, obj
|
34
35
|
return None, None
|
35
36
|
|
36
|
-
def generate_readme(
|
37
|
-
app_dir: Path, folder_name: str, tools: list
|
38
|
-
) -> Path:
|
39
|
-
"""Generate README.md with API documentation.
|
40
|
-
|
41
|
-
Args:
|
42
|
-
app_dir: Directory where the README will be generated
|
43
|
-
folder_name: Name of the application folder
|
44
|
-
tools: List of Function objects from the OpenAPI schema
|
45
|
-
|
46
|
-
Returns:
|
47
|
-
Path to the generated README file
|
48
|
-
|
49
|
-
Raises:
|
50
|
-
FileNotFoundError: If the template directory doesn't exist
|
51
|
-
TemplateError: If there's an error rendering the template
|
52
|
-
IOError: If there's an error writing the README file
|
53
|
-
"""
|
54
|
-
app = folder_name.replace("_", " ").title()
|
55
|
-
logger.info(f"Generating README for {app} in {app_dir}")
|
56
|
-
|
57
|
-
# Format tools into (name, description) tuples
|
58
|
-
formatted_tools = []
|
59
|
-
for tool in tools:
|
60
|
-
name = tool.__name__
|
61
|
-
description = tool.__doc__.strip().split("\n")[0]
|
62
|
-
formatted_tools.append((name, description))
|
63
|
-
|
64
|
-
# Set up Jinja2 environment
|
65
|
-
template_dir = Path(__file__).parent.parent / "templates"
|
66
|
-
if not template_dir.exists():
|
67
|
-
logger.error(f"Template directory not found: {template_dir}")
|
68
|
-
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
69
|
-
|
70
|
-
try:
|
71
|
-
env = Environment(
|
72
|
-
loader=FileSystemLoader(template_dir),
|
73
|
-
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}")
|
79
|
-
|
80
|
-
# Render the template
|
81
|
-
try:
|
82
|
-
readme_content = template.render(
|
83
|
-
name=app,
|
84
|
-
tools=formatted_tools
|
85
|
-
)
|
86
|
-
except Exception as e:
|
87
|
-
logger.error(f"Error rendering template: {e}")
|
88
|
-
raise TemplateError(f"Error rendering template: {e}")
|
89
|
-
|
90
|
-
# Write the README file
|
91
|
-
readme_file = app_dir / "README.md"
|
92
|
-
try:
|
93
|
-
with open(readme_file, "w") as f:
|
94
|
-
f.write(readme_content)
|
95
|
-
logger.info(f"Documentation generated at: {readme_file}")
|
96
|
-
except Exception as e:
|
97
|
-
logger.error(f"Error writing README file: {e}")
|
98
|
-
raise IOError(f"Error writing README file: {e}")
|
99
|
-
|
100
|
-
return readme_file
|
101
37
|
|
102
38
|
def test_correct_output(gen_file: Path):
|
103
39
|
# Check file is non-empty
|
@@ -122,7 +58,7 @@ def test_correct_output(gen_file: Path):
|
|
122
58
|
def generate_api_from_schema(
|
123
59
|
schema_path: Path,
|
124
60
|
output_path: Path | None = None,
|
125
|
-
|
61
|
+
class_name: str | None = None,
|
126
62
|
) -> tuple[Path, Path]:
|
127
63
|
"""
|
128
64
|
Generate API client from OpenAPI schema and write to app.py with a README.
|
@@ -137,7 +73,6 @@ def generate_api_from_schema(
|
|
137
73
|
"""
|
138
74
|
# Local imports for logging and file operations
|
139
75
|
|
140
|
-
|
141
76
|
logger.info("Starting API generation for schema: %s", schema_path)
|
142
77
|
|
143
78
|
# 1. Parse and validate schema
|
@@ -150,7 +85,7 @@ def generate_api_from_schema(
|
|
150
85
|
|
151
86
|
# 2. Generate client code
|
152
87
|
try:
|
153
|
-
code = generate_api_client(schema)
|
88
|
+
code = generate_api_client(schema, class_name)
|
154
89
|
logger.info("API client code generated.")
|
155
90
|
except Exception as e:
|
156
91
|
logger.error("Code generation failed: %s", e)
|
@@ -174,10 +109,15 @@ def generate_api_from_schema(
|
|
174
109
|
f.write(code)
|
175
110
|
|
176
111
|
if not test_correct_output(gen_file):
|
177
|
-
logger.error(
|
112
|
+
logger.error(
|
113
|
+
"Generated code validation failed for '%s'. Aborting generation.", gen_file
|
114
|
+
)
|
178
115
|
logger.info("Next steps:")
|
179
116
|
logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
|
180
|
-
logger.info(
|
117
|
+
logger.info(
|
118
|
+
" 2) Inspect '%s' for syntax or logic errors in the generated code.",
|
119
|
+
gen_file,
|
120
|
+
)
|
181
121
|
logger.info(" 3) Correct the issues and re-run the command.")
|
182
122
|
return {"error": "Validation failed. See logs above for detailed instructions."}
|
183
123
|
|
@@ -190,33 +130,6 @@ def generate_api_from_schema(
|
|
190
130
|
shutil.copy(gen_file, app_file)
|
191
131
|
logger.info("App file written to: %s", app_file)
|
192
132
|
|
193
|
-
# 6. Collect tools and generate README
|
194
|
-
import importlib.util
|
195
|
-
import sys
|
196
|
-
|
197
|
-
# Load the generated module as "temp_module"
|
198
|
-
spec = importlib.util.spec_from_file_location("temp_module", str(app_file))
|
199
|
-
module = importlib.util.module_from_spec(spec)
|
200
|
-
sys.modules["temp_module"] = module
|
201
|
-
spec.loader.exec_module(module)
|
202
|
-
|
203
|
-
# Retrieve the generated API class
|
204
|
-
class_name, cls = get_class_info(module)
|
205
|
-
|
206
|
-
# Instantiate client and collect its tools
|
207
|
-
tools = []
|
208
|
-
if cls:
|
209
|
-
try:
|
210
|
-
client = cls()
|
211
|
-
tools = client.list_tools()
|
212
|
-
except Exception as e:
|
213
|
-
logger.warning("Failed to instantiate '%s' or list tools: %s", class_name, e)
|
214
|
-
else:
|
215
|
-
logger.warning("No generated class found in module 'temp_module'")
|
216
|
-
readme_file = generate_readme(target_dir, output_path.stem, tools)
|
217
|
-
logger.info("README generated at: %s", readme_file)
|
218
|
-
|
219
|
-
|
220
133
|
# Cleanup intermediate file
|
221
134
|
try:
|
222
135
|
os.remove(gen_file)
|
@@ -224,4 +137,4 @@ def generate_api_from_schema(
|
|
224
137
|
except Exception as e:
|
225
138
|
logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
|
226
139
|
|
227
|
-
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
|
|