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.
Files changed (32) hide show
  1. universal_mcp/applications/__init__.py +51 -7
  2. universal_mcp/applications/curstdata/README.md +50 -0
  3. universal_mcp/applications/curstdata/__init__.py +0 -0
  4. universal_mcp/applications/curstdata/app.py +551 -0
  5. universal_mcp/applications/neon/README.md +99 -0
  6. universal_mcp/applications/neon/__init__.py +0 -0
  7. universal_mcp/applications/neon/app.py +1924 -0
  8. universal_mcp/applications/shortcut/README.md +153 -0
  9. universal_mcp/applications/shortcut/__init__.py +0 -0
  10. universal_mcp/applications/shortcut/app.py +3880 -0
  11. universal_mcp/cli.py +109 -17
  12. universal_mcp/integrations/__init__.py +1 -1
  13. universal_mcp/integrations/integration.py +79 -0
  14. universal_mcp/servers/README.md +79 -0
  15. universal_mcp/servers/server.py +17 -29
  16. universal_mcp/stores/README.md +74 -0
  17. universal_mcp/stores/store.py +0 -2
  18. universal_mcp/templates/README.md.j2 +93 -0
  19. universal_mcp/templates/api_client.py.j2 +27 -0
  20. universal_mcp/tools/README.md +86 -0
  21. universal_mcp/tools/tools.py +1 -1
  22. universal_mcp/utils/agentr.py +90 -0
  23. universal_mcp/utils/api_generator.py +166 -208
  24. universal_mcp/utils/openapi.py +221 -321
  25. universal_mcp/utils/singleton.py +23 -0
  26. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/METADATA +16 -41
  27. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/RECORD +29 -16
  28. universal_mcp/applications/hashnode/app.py +0 -81
  29. universal_mcp/applications/hashnode/prompt.md +0 -23
  30. universal_mcp/integrations/agentr.py +0 -112
  31. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc1.dist-info}/WHEEL +0 -0
  32. {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
@@ -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}'. Does its list_tools method accept arguments? It shouldn't. Error: {e}"
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[tuple[str, str]]
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
- tools_content = f"This is automatically generated from OpenAPI schema for the {folder_name.replace('_', ' ').title()} API.\n\n"
169
- tools_content += "## Supported Integrations\n\n"
170
- tools_content += (
171
- "This tool can be integrated with any service that supports HTTP requests.\n\n"
172
- )
173
- tools_content += "## Tool List\n\n"
174
-
175
- if tools:
176
- tools_content += "| Tool | Description |\n|------|-------------|\n"
177
- for tool_name, tool_desc in tools:
178
- tools_content += f"| {tool_name} | {tool_desc} |\n"
179
- tools_content += "\n"
180
- else:
181
- tools_content += (
182
- "No tools with documentation were found in this API client.\n\n"
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
- readme_content = README_TEMPLATE.format(
186
- name=app,
187
- tools=tools_content,
188
- usage="",
189
- )
90
+ # Write the README file
190
91
  readme_file = app_dir / "README.md"
191
- with open(readme_file, "w") as f:
192
- f.write(readme_content)
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
- async def generate_api_from_schema(
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
- ) -> dict[str, str | None]:
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
- Generate API client from OpenAPI schema with optional docstring generation.
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
- Returns:
212
- dict: A dictionary with information about the generated files
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
- if not output_path:
219
- return {"code": code}
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
- folder_name = output_path.stem
222
- temp_output_path = output_path
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
- write_and_verify_code(temp_output_path, code)
193
+ # 6. Collect tools and generate README
194
+ import importlib.util
195
+ import sys
225
196
 
226
- if add_docstrings:
227
- result = await generate_docstrings(str(temp_output_path))
228
- if result:
229
- if "functions_processed" in result:
230
- echo(f"Processed {result['functions_processed']} functions")
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
- app_dir, app_file = setup_app_directory(folder_name, temp_output_path)
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
- echo("Generating README.md from function information...")
240
- spec = importlib.util.spec_from_file_location("temp_module", app_file)
241
- module = importlib.util.module_from_spec(spec)
242
- spec.loader.exec_module(module)
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
- tools = collect_tools(class_obj, folder_name)
249
- readme_file = generate_readme(app_dir, folder_name, tools)
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
- except Exception as e:
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