universal-mcp 0.1.12__py3-none-any.whl → 0.1.13rc1__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 +51 -7
- universal_mcp/applications/curstdata/README.md +50 -0
- universal_mcp/applications/curstdata/__init__.py +0 -0
- universal_mcp/applications/curstdata/app.py +551 -0
- universal_mcp/applications/neon/README.md +99 -0
- universal_mcp/applications/neon/__init__.py +0 -0
- universal_mcp/applications/neon/app.py +1924 -0
- universal_mcp/applications/shortcut/README.md +153 -0
- universal_mcp/applications/shortcut/__init__.py +0 -0
- universal_mcp/applications/shortcut/app.py +3880 -0
- universal_mcp/cli.py +109 -17
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/integrations/integration.py +79 -0
- universal_mcp/servers/README.md +79 -0
- universal_mcp/servers/server.py +17 -29
- universal_mcp/stores/README.md +74 -0
- universal_mcp/stores/store.py +0 -2
- universal_mcp/templates/README.md.j2 +93 -0
- universal_mcp/templates/api_client.py.j2 +27 -0
- universal_mcp/tools/README.md +86 -0
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +90 -0
- universal_mcp/utils/api_generator.py +166 -208
- universal_mcp/utils/openapi.py +221 -321
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/METADATA +16 -41
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/RECORD +29 -16
- universal_mcp/applications/hashnode/app.py +0 -81
- universal_mcp/applications/hashnode/prompt.md +0 -23
- universal_mcp/integrations/agentr.py +0 -112
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
# Universal MCP Tools
|
2
|
+
|
3
|
+
This directory contains the core tooling infrastructure for Universal MCP, providing a flexible and extensible framework for defining, managing, and converting tools across different formats.
|
4
|
+
|
5
|
+
## Components
|
6
|
+
|
7
|
+
### `tools.py`
|
8
|
+
The main module containing the core tool management functionality:
|
9
|
+
|
10
|
+
- `Tool` class: Represents a tool with metadata, validation, and execution capabilities
|
11
|
+
- `ToolManager` class: Manages tool registration, lookup, and execution
|
12
|
+
- Conversion utilities for different tool formats (OpenAI, LangChain, MCP)
|
13
|
+
|
14
|
+
### `adapters.py`
|
15
|
+
Contains adapters for converting tools between different formats:
|
16
|
+
- `convert_tool_to_mcp_tool`: Converts a tool to MCP format
|
17
|
+
- `convert_tool_to_langchain_tool`: Converts a tool to LangChain format
|
18
|
+
|
19
|
+
### `func_metadata.py`
|
20
|
+
Provides function metadata and argument validation:
|
21
|
+
- `FuncMetadata` class: Handles function signature analysis and argument validation
|
22
|
+
- `ArgModelBase` class: Base model for function arguments
|
23
|
+
- Utilities for parsing and validating function signatures
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Creating a Tool
|
28
|
+
|
29
|
+
```python
|
30
|
+
from universal_mcp.tools import Tool
|
31
|
+
|
32
|
+
def my_tool(param1: str, param2: int) -> str:
|
33
|
+
"""A simple tool that does something.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
param1: Description of param1
|
37
|
+
param2: Description of param2
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Description of return value
|
41
|
+
"""
|
42
|
+
return f"Result: {param1} {param2}"
|
43
|
+
|
44
|
+
tool = Tool.from_function(my_tool)
|
45
|
+
```
|
46
|
+
|
47
|
+
### Managing Tools
|
48
|
+
|
49
|
+
```python
|
50
|
+
from universal_mcp.tools import ToolManager
|
51
|
+
|
52
|
+
manager = ToolManager()
|
53
|
+
manager.add_tool(my_tool)
|
54
|
+
|
55
|
+
# Get a tool by name
|
56
|
+
tool = manager.get_tool("my_tool")
|
57
|
+
|
58
|
+
# List all tools in a specific format
|
59
|
+
tools = manager.list_tools(format="openai") # or "langchain" or "mcp"
|
60
|
+
```
|
61
|
+
|
62
|
+
### Converting Tools
|
63
|
+
|
64
|
+
```python
|
65
|
+
from universal_mcp.tools import convert_tool_to_langchain_tool
|
66
|
+
|
67
|
+
langchain_tool = convert_tool_to_langchain_tool(tool)
|
68
|
+
```
|
69
|
+
|
70
|
+
## Features
|
71
|
+
|
72
|
+
- Automatic docstring parsing for tool metadata
|
73
|
+
- Type validation using Pydantic
|
74
|
+
- Support for both sync and async tools
|
75
|
+
- JSON schema generation for tool parameters
|
76
|
+
- Error handling and analytics tracking
|
77
|
+
- Tag-based tool organization
|
78
|
+
- Multiple format support (OpenAI, LangChain, MCP)
|
79
|
+
|
80
|
+
## Best Practices
|
81
|
+
|
82
|
+
1. Always provide clear docstrings for your tools
|
83
|
+
2. Use type hints for better validation
|
84
|
+
3. Handle errors appropriately in your tool implementations
|
85
|
+
4. Use tags to organize related tools
|
86
|
+
5. Consider async implementations for I/O-bound operations
|
universal_mcp/tools/tools.py
CHANGED
@@ -261,7 +261,7 @@ class ToolManager:
|
|
261
261
|
available_tool_functions = app.list_tools()
|
262
262
|
except TypeError as e:
|
263
263
|
logger.error(
|
264
|
-
f"Error calling list_tools for app '{app.name}'.
|
264
|
+
f"Error calling list_tools for app '{app.name}'. Error: {e}"
|
265
265
|
)
|
266
266
|
return
|
267
267
|
except Exception as e:
|
@@ -0,0 +1,90 @@
|
|
1
|
+
from loguru import logger
|
2
|
+
import os
|
3
|
+
import httpx
|
4
|
+
from universal_mcp.config import AppConfig
|
5
|
+
from universal_mcp.utils.singleton import Singleton
|
6
|
+
|
7
|
+
class AgentrClient(metaclass=Singleton):
|
8
|
+
"""Helper class for AgentR API operations.
|
9
|
+
|
10
|
+
This class provides utility methods for interacting with the AgentR API,
|
11
|
+
including authentication, authorization, and credential management.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
|
15
|
+
base_url (str, optional): Base URL for AgentR API. Defaults to https://api.agentr.dev
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, api_key: str = None, base_url: str = None):
|
19
|
+
self.api_key = api_key or os.getenv("AGENTR_API_KEY")
|
20
|
+
if not self.api_key:
|
21
|
+
logger.error(
|
22
|
+
"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
|
+
)
|
24
|
+
raise ValueError("AgentR API key required - get one at https://agentr.dev")
|
25
|
+
self.base_url = (base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")).rstrip("/")
|
26
|
+
|
27
|
+
def get_credentials(self, integration_name: str) -> dict:
|
28
|
+
"""Get credentials for an integration from the AgentR API.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
integration_name (str): Name of the integration to get credentials for
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
dict: Credentials data from API response
|
35
|
+
|
36
|
+
Raises:
|
37
|
+
NotAuthorizedError: If credentials are not found (404 response)
|
38
|
+
HTTPError: For other API errors
|
39
|
+
"""
|
40
|
+
response = httpx.get(
|
41
|
+
f"{self.base_url}/api/{integration_name}/credentials/",
|
42
|
+
headers={"accept": "application/json", "X-API-KEY": self.api_key},
|
43
|
+
)
|
44
|
+
if response.status_code == 404:
|
45
|
+
logger.warning(
|
46
|
+
f"No credentials found for {integration_name}. Requesting authorization..."
|
47
|
+
)
|
48
|
+
action = self.get_authorization_url(integration_name)
|
49
|
+
raise NotAuthorizedError(action)
|
50
|
+
response.raise_for_status()
|
51
|
+
return response.json()
|
52
|
+
|
53
|
+
def get_authorization_url(self, integration_name: str) -> str:
|
54
|
+
"""Get authorization URL for an integration.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
integration_name (str): Name of the integration to get authorization URL for
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
str: Message containing authorization URL
|
61
|
+
|
62
|
+
Raises:
|
63
|
+
HTTPError: If API request fails
|
64
|
+
"""
|
65
|
+
response = httpx.get(
|
66
|
+
f"{self.base_url}/api/{integration_name}/authorize/",
|
67
|
+
headers={"X-API-KEY": self.api_key},
|
68
|
+
)
|
69
|
+
response.raise_for_status()
|
70
|
+
url = response.json()
|
71
|
+
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."
|
72
|
+
|
73
|
+
def fetch_apps(self) -> list[dict]:
|
74
|
+
"""Fetch available apps from AgentR API.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
List of application configurations
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
httpx.HTTPError: If API request fails
|
81
|
+
"""
|
82
|
+
response = httpx.get(
|
83
|
+
f"{self.base_url}/api/apps/",
|
84
|
+
headers={"X-API-KEY": self.api_key},
|
85
|
+
timeout=10,
|
86
|
+
)
|
87
|
+
response.raise_for_status()
|
88
|
+
data = response.json()
|
89
|
+
return [AppConfig.model_validate(app) for app in data]
|
90
|
+
|
@@ -1,40 +1,13 @@
|
|
1
|
-
import ast
|
2
|
-
import importlib.util
|
3
1
|
import inspect
|
4
2
|
import os
|
5
|
-
import traceback
|
6
3
|
from pathlib import Path
|
4
|
+
from loguru import logger
|
5
|
+
import shutil
|
6
|
+
import importlib.util
|
7
|
+
from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
|
7
8
|
|
8
|
-
from universal_mcp.utils.docgen import process_file
|
9
9
|
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
10
10
|
|
11
|
-
README_TEMPLATE = """
|
12
|
-
# {name} MCP Server
|
13
|
-
|
14
|
-
An MCP Server for the {name} API.
|
15
|
-
|
16
|
-
## Supported Integrations
|
17
|
-
|
18
|
-
- AgentR
|
19
|
-
- API Key (Coming Soon)
|
20
|
-
- OAuth (Coming Soon)
|
21
|
-
|
22
|
-
## Tools
|
23
|
-
|
24
|
-
{tools}
|
25
|
-
|
26
|
-
## Usage
|
27
|
-
|
28
|
-
- Login to AgentR
|
29
|
-
- Follow the quickstart guide to setup MCP Server for your client
|
30
|
-
- Visit Apps Store and enable the {name} app
|
31
|
-
- Restart the MCP Server
|
32
|
-
|
33
|
-
### Local Development
|
34
|
-
|
35
|
-
- Follow the README to test with the local MCP Server
|
36
|
-
"""
|
37
|
-
|
38
11
|
|
39
12
|
def echo(message: str, err: bool = False) -> None:
|
40
13
|
"""Echo a message to the console, with optional error flag."""
|
@@ -53,70 +26,6 @@ def validate_and_load_schema(schema_path: Path) -> dict:
|
|
53
26
|
echo(f"Error loading schema: {e}", err=True)
|
54
27
|
raise
|
55
28
|
|
56
|
-
|
57
|
-
def write_and_verify_code(output_path: Path, code: str) -> None:
|
58
|
-
"""Write generated code to file and verify its contents."""
|
59
|
-
with open(output_path, "w") as f:
|
60
|
-
f.write(code)
|
61
|
-
echo(f"Generated API client at: {output_path}")
|
62
|
-
|
63
|
-
try:
|
64
|
-
with open(output_path) as f:
|
65
|
-
file_content = f.read()
|
66
|
-
echo(f"Successfully wrote {len(file_content)} bytes to {output_path}")
|
67
|
-
ast.parse(file_content)
|
68
|
-
echo("Python syntax check passed")
|
69
|
-
except SyntaxError as e:
|
70
|
-
echo(f"Warning: Generated file has syntax error: {e}", err=True)
|
71
|
-
except Exception as e:
|
72
|
-
echo(f"Error verifying output file: {e}", err=True)
|
73
|
-
|
74
|
-
|
75
|
-
async def generate_docstrings(script_path: str) -> dict[str, int]:
|
76
|
-
"""Generate docstrings for the given script file."""
|
77
|
-
echo(f"Adding docstrings to {script_path}...")
|
78
|
-
|
79
|
-
if not os.path.exists(script_path):
|
80
|
-
echo(f"Warning: File {script_path} does not exist", err=True)
|
81
|
-
return {"functions_processed": 0}
|
82
|
-
|
83
|
-
try:
|
84
|
-
with open(script_path) as f:
|
85
|
-
content = f.read()
|
86
|
-
echo(f"Successfully read {len(content)} bytes from {script_path}")
|
87
|
-
except Exception as e:
|
88
|
-
echo(f"Error reading file for docstring generation: {e}", err=True)
|
89
|
-
return {"functions_processed": 0}
|
90
|
-
|
91
|
-
try:
|
92
|
-
processed = process_file(script_path)
|
93
|
-
return {"functions_processed": processed}
|
94
|
-
except Exception as e:
|
95
|
-
echo(f"Error running docstring generation: {e}", err=True)
|
96
|
-
traceback.print_exc()
|
97
|
-
return {"functions_processed": 0}
|
98
|
-
|
99
|
-
|
100
|
-
def setup_app_directory(folder_name: str, source_file: Path) -> tuple[Path, Path]:
|
101
|
-
"""Set up application directory structure and copy generated code."""
|
102
|
-
applications_dir = Path(__file__).parent.parent / "applications"
|
103
|
-
app_dir = applications_dir / folder_name
|
104
|
-
app_dir.mkdir(exist_ok=True)
|
105
|
-
|
106
|
-
init_file = app_dir / "__init__.py"
|
107
|
-
if not init_file.exists():
|
108
|
-
with open(init_file, "w") as f:
|
109
|
-
f.write("")
|
110
|
-
|
111
|
-
app_file = app_dir / "app.py"
|
112
|
-
with open(source_file) as src, open(app_file, "w") as dest:
|
113
|
-
app_content = src.read()
|
114
|
-
dest.write(app_content)
|
115
|
-
|
116
|
-
echo(f"API client installed at: {app_file}")
|
117
|
-
return app_dir, app_file
|
118
|
-
|
119
|
-
|
120
29
|
def get_class_info(module: any) -> tuple[str | None, any]:
|
121
30
|
"""Find the main class in the generated module."""
|
122
31
|
for name, obj in inspect.getmembers(module):
|
@@ -124,146 +33,195 @@ def get_class_info(module: any) -> tuple[str | None, any]:
|
|
124
33
|
return name, obj
|
125
34
|
return None, None
|
126
35
|
|
127
|
-
|
128
|
-
def collect_tools(class_obj: any, folder_name: str) -> list[tuple[str, str]]:
|
129
|
-
"""Collect tool information from the class."""
|
130
|
-
tools = []
|
131
|
-
|
132
|
-
# Try to get tools from list_tools method
|
133
|
-
if class_obj and hasattr(class_obj, "list_tools"):
|
134
|
-
try:
|
135
|
-
instance = class_obj()
|
136
|
-
tool_list = instance.list_tools()
|
137
|
-
|
138
|
-
for tool in tool_list:
|
139
|
-
func_name = tool.__name__
|
140
|
-
if func_name.startswith("_") or func_name in ("__init__", "list_tools"):
|
141
|
-
continue
|
142
|
-
|
143
|
-
doc = tool.__doc__ or f"Function for {func_name.replace('_', ' ')}"
|
144
|
-
summary = doc.split("\n\n")[0].strip()
|
145
|
-
tools.append((func_name, summary))
|
146
|
-
except Exception as e:
|
147
|
-
echo(f"Note: Couldn't instantiate class to get tool list: {e}")
|
148
|
-
|
149
|
-
# Fall back to inspecting class methods directly
|
150
|
-
if not tools and class_obj:
|
151
|
-
for name, method in inspect.getmembers(class_obj, inspect.isfunction):
|
152
|
-
if name.startswith("_") or name in ("__init__", "list_tools"):
|
153
|
-
continue
|
154
|
-
|
155
|
-
doc = method.__doc__ or f"Function for {name.replace('_', ' ')}"
|
156
|
-
summary = doc.split("\n\n")[0].strip()
|
157
|
-
tools.append((name, summary))
|
158
|
-
|
159
|
-
return tools
|
160
|
-
|
161
|
-
|
162
36
|
def generate_readme(
|
163
|
-
app_dir: Path, folder_name: str, tools: list
|
37
|
+
app_dir: Path, folder_name: str, tools: list
|
164
38
|
) -> Path:
|
165
|
-
"""Generate README.md with API documentation.
|
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
|
+
"""
|
166
54
|
app = folder_name.replace("_", " ").title()
|
55
|
+
logger.info(f"Generating README for {app} in {app_dir}")
|
167
56
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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()
|
183
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}")
|
184
89
|
|
185
|
-
|
186
|
-
name=app,
|
187
|
-
tools=tools_content,
|
188
|
-
usage="",
|
189
|
-
)
|
90
|
+
# Write the README file
|
190
91
|
readme_file = app_dir / "README.md"
|
191
|
-
|
192
|
-
|
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}")
|
193
99
|
|
194
|
-
echo(f"Documentation generated at: {readme_file}")
|
195
100
|
return readme_file
|
196
101
|
|
102
|
+
def test_correct_output(gen_file: Path):
|
103
|
+
# Check file is non-empty
|
104
|
+
if gen_file.stat().st_size == 0:
|
105
|
+
msg = f"Generated file {gen_file} is empty."
|
106
|
+
logger.error(msg)
|
107
|
+
return False
|
108
|
+
|
109
|
+
# Basic import test on generated code
|
110
|
+
try:
|
111
|
+
spec = importlib.util.spec_from_file_location("temp_module", gen_file)
|
112
|
+
module = importlib.util.module_from_spec(spec)
|
113
|
+
spec.loader.exec_module(module) # type: ignore
|
114
|
+
logger.info("Intermediate code import test passed.")
|
115
|
+
return True
|
116
|
+
except Exception as e:
|
117
|
+
logger.error(f"Import test failed for generated code: {e}")
|
118
|
+
return False
|
119
|
+
return True
|
197
120
|
|
198
|
-
|
121
|
+
|
122
|
+
def generate_api_from_schema(
|
199
123
|
schema_path: Path,
|
200
124
|
output_path: Path | None = None,
|
201
125
|
add_docstrings: bool = True,
|
202
|
-
) ->
|
126
|
+
) -> tuple[Path, Path]:
|
127
|
+
"""
|
128
|
+
Generate API client from OpenAPI schema and write to app.py with a README.
|
129
|
+
|
130
|
+
Steps:
|
131
|
+
1. Parse and validate the OpenAPI schema.
|
132
|
+
2. Generate client code.
|
133
|
+
3. Ensure output directory exists.
|
134
|
+
4. Write code to an intermediate app_generated.py and perform basic import checks.
|
135
|
+
5. Copy/overwrite intermediate file to app.py.
|
136
|
+
6. Collect tools and generate README.md.
|
203
137
|
"""
|
204
|
-
|
138
|
+
# Local imports for logging and file operations
|
205
139
|
|
206
|
-
Args:
|
207
|
-
schema_path: Path to the OpenAPI schema file
|
208
|
-
output_path: Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)
|
209
|
-
add_docstrings: Whether to add docstrings to the generated code
|
210
140
|
|
211
|
-
|
212
|
-
|
213
|
-
|
141
|
+
logger.info("Starting API generation for schema: %s", schema_path)
|
142
|
+
|
143
|
+
# 1. Parse and validate schema
|
214
144
|
try:
|
215
145
|
schema = validate_and_load_schema(schema_path)
|
146
|
+
logger.info("Schema loaded and validated successfully.")
|
147
|
+
except Exception as e:
|
148
|
+
logger.error("Failed to load or validate schema: %s", e)
|
149
|
+
raise
|
150
|
+
|
151
|
+
# 2. Generate client code
|
152
|
+
try:
|
216
153
|
code = generate_api_client(schema)
|
154
|
+
logger.info("API client code generated.")
|
155
|
+
except Exception as e:
|
156
|
+
logger.error("Code generation failed: %s", e)
|
157
|
+
raise
|
217
158
|
|
218
|
-
|
219
|
-
|
159
|
+
# If no output_path provided, return raw code
|
160
|
+
if not output_path:
|
161
|
+
logger.debug("No output_path provided, returning code as string.")
|
162
|
+
return {"code": code}
|
163
|
+
|
164
|
+
# 3. Ensure output directory exists
|
165
|
+
target_dir = output_path
|
166
|
+
if not target_dir.exists():
|
167
|
+
logger.info("Creating output directory: %s", target_dir)
|
168
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
169
|
+
|
170
|
+
# 4. Write to intermediate file and perform basic checks
|
171
|
+
gen_file = target_dir / "app_generated.py"
|
172
|
+
logger.info("Writing generated code to intermediate file: %s", gen_file)
|
173
|
+
with open(gen_file, "w") as f:
|
174
|
+
f.write(code)
|
220
175
|
|
221
|
-
|
222
|
-
|
176
|
+
if not test_correct_output(gen_file):
|
177
|
+
logger.error("Generated code validation failed for '%s'. Aborting generation.", gen_file)
|
178
|
+
logger.info("Next steps:")
|
179
|
+
logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
|
180
|
+
logger.info(" 2) Inspect '%s' for syntax or logic errors in the generated code.", gen_file)
|
181
|
+
logger.info(" 3) Correct the issues and re-run the command.")
|
182
|
+
return {"error": "Validation failed. See logs above for detailed instructions."}
|
183
|
+
|
184
|
+
# 5. Copy to final app.py (overwrite if exists)
|
185
|
+
app_file = target_dir / "app.py"
|
186
|
+
if app_file.exists():
|
187
|
+
logger.warning("Overwriting existing file: %s", app_file)
|
188
|
+
else:
|
189
|
+
logger.info("Creating new file: %s", app_file)
|
190
|
+
shutil.copy(gen_file, app_file)
|
191
|
+
logger.info("App file written to: %s", app_file)
|
223
192
|
|
224
|
-
|
193
|
+
# 6. Collect tools and generate README
|
194
|
+
import importlib.util
|
195
|
+
import sys
|
225
196
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
else:
|
232
|
-
echo("Docstring generation failed", err=True)
|
233
|
-
else:
|
234
|
-
echo("Skipping docstring generation as requested")
|
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)
|
235
202
|
|
236
|
-
|
203
|
+
# Retrieve the generated API class
|
204
|
+
class_name, cls = get_class_info(module)
|
237
205
|
|
206
|
+
# Instantiate client and collect its tools
|
207
|
+
tools = []
|
208
|
+
if cls:
|
238
209
|
try:
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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)
|
243
218
|
|
244
|
-
class_name, class_obj = get_class_info(module)
|
245
|
-
if not class_name:
|
246
|
-
class_name = folder_name.capitalize() + "App"
|
247
219
|
|
248
|
-
|
249
|
-
|
220
|
+
# Cleanup intermediate file
|
221
|
+
try:
|
222
|
+
os.remove(gen_file)
|
223
|
+
logger.debug("Cleaned up intermediate file: %s", gen_file)
|
224
|
+
except Exception as e:
|
225
|
+
logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
|
250
226
|
|
251
|
-
|
252
|
-
echo(f"Error generating documentation: {e}", err=True)
|
253
|
-
readme_file = None
|
254
|
-
|
255
|
-
return {
|
256
|
-
"app_file": str(app_file),
|
257
|
-
"readme_file": str(readme_file) if readme_file else None,
|
258
|
-
}
|
259
|
-
|
260
|
-
finally:
|
261
|
-
if output_path and output_path.exists():
|
262
|
-
try:
|
263
|
-
output_path.unlink()
|
264
|
-
echo(f"Cleaned up temporary file: {output_path}")
|
265
|
-
except Exception as e:
|
266
|
-
echo(
|
267
|
-
f"Warning: Could not remove temporary file {output_path}: {e}",
|
268
|
-
err=True,
|
269
|
-
)
|
227
|
+
return app_file, readme_file
|