universal-mcp 0.1.12__py3-none-any.whl → 0.1.13__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 (111) hide show
  1. universal_mcp/applications/__init__.py +76 -8
  2. universal_mcp/cli.py +136 -30
  3. universal_mcp/integrations/__init__.py +1 -1
  4. universal_mcp/integrations/integration.py +79 -0
  5. universal_mcp/servers/README.md +79 -0
  6. universal_mcp/servers/server.py +17 -31
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/templates/README.md.j2 +93 -0
  9. universal_mcp/templates/api_client.py.j2 +27 -0
  10. universal_mcp/tools/README.md +86 -0
  11. universal_mcp/tools/tools.py +1 -3
  12. universal_mcp/utils/agentr.py +95 -0
  13. universal_mcp/utils/api_generator.py +90 -219
  14. universal_mcp/utils/docgen.py +2 -2
  15. universal_mcp/utils/installation.py +8 -8
  16. universal_mcp/utils/openapi.py +353 -211
  17. universal_mcp/utils/readme.py +92 -0
  18. universal_mcp/utils/singleton.py +23 -0
  19. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
  20. universal_mcp-0.1.13.dist-info/RECORD +39 -0
  21. universal_mcp/applications/ahrefs/README.md +0 -76
  22. universal_mcp/applications/ahrefs/__init__.py +0 -0
  23. universal_mcp/applications/ahrefs/app.py +0 -2291
  24. universal_mcp/applications/cal_com_v2/README.md +0 -175
  25. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  26. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  27. universal_mcp/applications/calendly/README.md +0 -78
  28. universal_mcp/applications/calendly/__init__.py +0 -0
  29. universal_mcp/applications/calendly/app.py +0 -1195
  30. universal_mcp/applications/clickup/README.md +0 -160
  31. universal_mcp/applications/clickup/__init__.py +0 -0
  32. universal_mcp/applications/clickup/app.py +0 -5009
  33. universal_mcp/applications/coda/README.md +0 -133
  34. universal_mcp/applications/coda/__init__.py +0 -0
  35. universal_mcp/applications/coda/app.py +0 -3671
  36. universal_mcp/applications/e2b/README.md +0 -37
  37. universal_mcp/applications/e2b/app.py +0 -65
  38. universal_mcp/applications/elevenlabs/README.md +0 -84
  39. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  40. universal_mcp/applications/elevenlabs/app.py +0 -1402
  41. universal_mcp/applications/falai/README.md +0 -42
  42. universal_mcp/applications/falai/__init__.py +0 -0
  43. universal_mcp/applications/falai/app.py +0 -332
  44. universal_mcp/applications/figma/README.md +0 -74
  45. universal_mcp/applications/figma/__init__.py +0 -0
  46. universal_mcp/applications/figma/app.py +0 -1261
  47. universal_mcp/applications/firecrawl/README.md +0 -45
  48. universal_mcp/applications/firecrawl/app.py +0 -268
  49. universal_mcp/applications/github/README.md +0 -47
  50. universal_mcp/applications/github/app.py +0 -429
  51. universal_mcp/applications/gong/README.md +0 -88
  52. universal_mcp/applications/gong/__init__.py +0 -0
  53. universal_mcp/applications/gong/app.py +0 -2297
  54. universal_mcp/applications/google_calendar/app.py +0 -442
  55. universal_mcp/applications/google_docs/README.md +0 -40
  56. universal_mcp/applications/google_docs/app.py +0 -88
  57. universal_mcp/applications/google_drive/README.md +0 -44
  58. universal_mcp/applications/google_drive/app.py +0 -286
  59. universal_mcp/applications/google_mail/README.md +0 -47
  60. universal_mcp/applications/google_mail/app.py +0 -664
  61. universal_mcp/applications/google_sheet/README.md +0 -42
  62. universal_mcp/applications/google_sheet/app.py +0 -150
  63. universal_mcp/applications/hashnode/app.py +0 -81
  64. universal_mcp/applications/hashnode/prompt.md +0 -23
  65. universal_mcp/applications/heygen/README.md +0 -69
  66. universal_mcp/applications/heygen/__init__.py +0 -0
  67. universal_mcp/applications/heygen/app.py +0 -956
  68. universal_mcp/applications/mailchimp/README.md +0 -306
  69. universal_mcp/applications/mailchimp/__init__.py +0 -0
  70. universal_mcp/applications/mailchimp/app.py +0 -10937
  71. universal_mcp/applications/markitdown/app.py +0 -44
  72. universal_mcp/applications/notion/README.md +0 -55
  73. universal_mcp/applications/notion/__init__.py +0 -0
  74. universal_mcp/applications/notion/app.py +0 -527
  75. universal_mcp/applications/perplexity/README.md +0 -37
  76. universal_mcp/applications/perplexity/app.py +0 -65
  77. universal_mcp/applications/reddit/README.md +0 -45
  78. universal_mcp/applications/reddit/app.py +0 -379
  79. universal_mcp/applications/replicate/README.md +0 -65
  80. universal_mcp/applications/replicate/__init__.py +0 -0
  81. universal_mcp/applications/replicate/app.py +0 -980
  82. universal_mcp/applications/resend/README.md +0 -38
  83. universal_mcp/applications/resend/app.py +0 -37
  84. universal_mcp/applications/retell_ai/README.md +0 -46
  85. universal_mcp/applications/retell_ai/__init__.py +0 -0
  86. universal_mcp/applications/retell_ai/app.py +0 -333
  87. universal_mcp/applications/rocketlane/README.md +0 -42
  88. universal_mcp/applications/rocketlane/__init__.py +0 -0
  89. universal_mcp/applications/rocketlane/app.py +0 -194
  90. universal_mcp/applications/serpapi/README.md +0 -37
  91. universal_mcp/applications/serpapi/app.py +0 -73
  92. universal_mcp/applications/spotify/README.md +0 -116
  93. universal_mcp/applications/spotify/__init__.py +0 -0
  94. universal_mcp/applications/spotify/app.py +0 -2526
  95. universal_mcp/applications/supabase/README.md +0 -112
  96. universal_mcp/applications/supabase/__init__.py +0 -0
  97. universal_mcp/applications/supabase/app.py +0 -2970
  98. universal_mcp/applications/tavily/README.md +0 -38
  99. universal_mcp/applications/tavily/app.py +0 -51
  100. universal_mcp/applications/wrike/README.md +0 -71
  101. universal_mcp/applications/wrike/__init__.py +0 -0
  102. universal_mcp/applications/wrike/app.py +0 -1372
  103. universal_mcp/applications/youtube/README.md +0 -82
  104. universal_mcp/applications/youtube/__init__.py +0 -0
  105. universal_mcp/applications/youtube/app.py +0 -1428
  106. universal_mcp/applications/zenquotes/README.md +0 -37
  107. universal_mcp/applications/zenquotes/app.py +0 -31
  108. universal_mcp/integrations/agentr.py +0 -112
  109. universal_mcp-0.1.12.dist-info/RECORD +0 -119
  110. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
  111. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,93 @@
1
+ # {{ name }} MCP Server
2
+
3
+ An MCP Server for the {{ name }} API.
4
+
5
+ ## 📋 Prerequisites
6
+
7
+ Before you begin, ensure you have met the following requirements:
8
+ * Python 3.11+ (Recommended)
9
+ * [uv](https://github.com/astral-sh/uv) installed globally (`pip install uv`)
10
+
11
+ ## 🛠️ Setup Instructions
12
+
13
+ Follow these steps to get the development environment up and running:
14
+
15
+ ### 1. Sync Project Dependencies
16
+ Navigate to the project root directory (where `pyproject.toml` is located).
17
+ ```bash
18
+ uv sync
19
+ ```
20
+ This command uses `uv` to install all dependencies listed in `pyproject.toml` into a virtual environment (`.venv`) located in the project root.
21
+
22
+ ### 2. Activate the Virtual Environment
23
+ Activating the virtual environment ensures that you are using the project's specific dependencies and Python interpreter.
24
+ - On **Linux/macOS**:
25
+ ```bash
26
+ source .venv/bin/activate
27
+ ```
28
+ - On **Windows**:
29
+ ```bash
30
+ .venv\\Scripts\\activate
31
+ ```
32
+
33
+ ### 3. Start the MCP Inspector
34
+ Use the MCP CLI to start the application in development mode.
35
+ ```bash
36
+ mcp dev src/{{ name.lower() }}/mcp.py
37
+ ```
38
+ The MCP inspector should now be running. Check the console output for the exact address and port.
39
+
40
+ ## 🔌 Supported Integrations
41
+
42
+ - AgentR
43
+ - API Key (Coming Soon)
44
+ - OAuth (Coming Soon)
45
+
46
+ ## 🛠️ Tool List
47
+
48
+ This is automatically generated from OpenAPI schema for the {{ name }} API.
49
+
50
+ {% if tools %}
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ {%- for tool_name, tool_desc in tools %}
54
+ | `{{ tool_name }}` | {{ tool_desc }} |
55
+ {%- endfor %}
56
+ {% else %}
57
+ No tools with documentation were found in this API client.
58
+ {% endif %}
59
+
60
+ ## 📁 Project Structure
61
+
62
+ The generated project has a standard layout:
63
+ ```
64
+ .
65
+ ├── src/ # Source code directory
66
+ │ └── {{ name.lower() }}/
67
+ │ ├── __init__.py
68
+ │ └── mcp.py # Server is launched here
69
+ │ └── app.py # Application tools are defined here
70
+ ├── tests/ # Directory for project tests
71
+ ├── .env # Environment variables (for local development)
72
+ ├── pyproject.toml # Project dependencies managed by uv
73
+ ├── README.md # This file
74
+ ```
75
+
76
+ ## 📝 License
77
+
78
+ This project is licensed under the MIT License.
79
+
80
+ ---
81
+
82
+ _This project was generated using **MCP CLI** — Happy coding! 🚀_
83
+
84
+ ## Usage
85
+
86
+ - Login to AgentR
87
+ - Follow the quickstart guide to setup MCP Server for your client
88
+ - Visit Apps Store and enable the {{ name }} app
89
+ - Restart the MCP Server
90
+
91
+ ### Local Development
92
+
93
+ - Follow the README to test with the local MCP Server
@@ -0,0 +1,27 @@
1
+ from typing import Any, Annotated
2
+ from universal_mcp.applications import APIApplication
3
+ from universal_mcp.integrations import Integration
4
+
5
+ class {{ class_name }}(APIApplication):
6
+ def __init__(self, integration: Integration = None, **kwargs) -> None:
7
+ super().__init__(name='{{ class_name.lower() }}', integration=integration, **kwargs)
8
+ self.base_url = "{{ base_url }}"
9
+
10
+ {% for method in methods %}
11
+ def {{ method.name }}(self, {{ method.args_str }}) -> {{ method.return_type }}:
12
+ """
13
+ {{ method.description }}
14
+ {% if method.tags %}
15
+ Tags: {{ method.tags|join(', ') }}
16
+ {% endif %}
17
+ """
18
+
19
+ {{ method.implementation|indent(8) }}
20
+ {% endfor %}
21
+
22
+ def list_tools(self):
23
+ return [
24
+ {% for method in methods %}
25
+ self.{{ method.name }}{% if not loop.last %},{% endif %}
26
+ {%- endfor %}
27
+ ]
@@ -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
@@ -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}'. Does its list_tools method accept arguments? It shouldn't. 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}")
@@ -0,0 +1,95 @@
1
+ import os
2
+
3
+ import httpx
4
+ from loguru import logger
5
+
6
+ from universal_mcp.config import AppConfig
7
+ from universal_mcp.exceptions import NotAuthorizedError
8
+ from universal_mcp.utils.singleton import Singleton
9
+
10
+
11
+ class AgentrClient(metaclass=Singleton):
12
+ """Helper class for AgentR API operations.
13
+
14
+ This class provides utility methods for interacting with the AgentR API,
15
+ including authentication, authorization, and credential management.
16
+
17
+ Args:
18
+ api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
19
+ base_url (str, optional): Base URL for AgentR API. Defaults to https://api.agentr.dev
20
+ """
21
+
22
+ def __init__(self, api_key: str = None, base_url: str = None):
23
+ self.api_key = api_key or os.getenv("AGENTR_API_KEY")
24
+ if not self.api_key:
25
+ logger.error(
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."
27
+ )
28
+ raise ValueError("AgentR API key required - get one at https://agentr.dev")
29
+ self.base_url = (
30
+ base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
31
+ ).rstrip("/")
32
+
33
+ def get_credentials(self, integration_name: str) -> dict:
34
+ """Get credentials for an integration from the AgentR API.
35
+
36
+ Args:
37
+ integration_name (str): Name of the integration to get credentials for
38
+
39
+ Returns:
40
+ dict: Credentials data from API response
41
+
42
+ Raises:
43
+ NotAuthorizedError: If credentials are not found (404 response)
44
+ HTTPError: For other API errors
45
+ """
46
+ response = httpx.get(
47
+ f"{self.base_url}/api/{integration_name}/credentials/",
48
+ headers={"accept": "application/json", "X-API-KEY": self.api_key},
49
+ )
50
+ if response.status_code == 404:
51
+ logger.warning(
52
+ f"No credentials found for {integration_name}. Requesting authorization..."
53
+ )
54
+ action = self.get_authorization_url(integration_name)
55
+ raise NotAuthorizedError(action)
56
+ response.raise_for_status()
57
+ return response.json()
58
+
59
+ def get_authorization_url(self, integration_name: str) -> str:
60
+ """Get authorization URL for an integration.
61
+
62
+ Args:
63
+ integration_name (str): Name of the integration to get authorization URL for
64
+
65
+ Returns:
66
+ str: Message containing authorization URL
67
+
68
+ Raises:
69
+ HTTPError: If API request fails
70
+ """
71
+ response = httpx.get(
72
+ f"{self.base_url}/api/{integration_name}/authorize/",
73
+ headers={"X-API-KEY": self.api_key},
74
+ )
75
+ response.raise_for_status()
76
+ url = response.json()
77
+ 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."
78
+
79
+ def fetch_apps(self) -> list[dict]:
80
+ """Fetch available apps from AgentR API.
81
+
82
+ Returns:
83
+ List of application configurations
84
+
85
+ Raises:
86
+ httpx.HTTPError: If API request fails
87
+ """
88
+ response = httpx.get(
89
+ f"{self.base_url}/api/apps/",
90
+ headers={"X-API-KEY": self.api_key},
91
+ timeout=10,
92
+ )
93
+ response.raise_for_status()
94
+ data = response.json()
95
+ return [AppConfig.model_validate(app) for app in data]
@@ -1,39 +1,12 @@
1
- import ast
2
1
  import importlib.util
3
2
  import inspect
4
3
  import os
5
- import traceback
4
+ import shutil
6
5
  from pathlib import Path
7
6
 
8
- from universal_mcp.utils.docgen import process_file
9
- from universal_mcp.utils.openapi import generate_api_client, load_schema
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
7
+ from loguru import logger
27
8
 
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
- """
9
+ from universal_mcp.utils.openapi import generate_api_client, load_schema
37
10
 
38
11
 
39
12
  def echo(message: str, err: bool = False) -> None:
@@ -54,69 +27,6 @@ def validate_and_load_schema(schema_path: Path) -> dict:
54
27
  raise
55
28
 
56
29
 
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
30
  def get_class_info(module: any) -> tuple[str | None, any]:
121
31
  """Find the main class in the generated module."""
122
32
  for name, obj in inspect.getmembers(module):
@@ -125,145 +35,106 @@ def get_class_info(module: any) -> tuple[str | None, any]:
125
35
  return None, None
126
36
 
127
37
 
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()
38
+ def test_correct_output(gen_file: Path):
39
+ # Check file is non-empty
40
+ if gen_file.stat().st_size == 0:
41
+ msg = f"Generated file {gen_file} is empty."
42
+ logger.error(msg)
43
+ return False
137
44
 
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
- def generate_readme(
163
- app_dir: Path, folder_name: str, tools: list[tuple[str, str]]
164
- ) -> Path:
165
- """Generate README.md with API documentation."""
166
- app = folder_name.replace("_", " ").title()
167
-
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"
183
- )
184
-
185
- readme_content = README_TEMPLATE.format(
186
- name=app,
187
- tools=tools_content,
188
- usage="",
189
- )
190
- readme_file = app_dir / "README.md"
191
- with open(readme_file, "w") as f:
192
- f.write(readme_content)
193
-
194
- echo(f"Documentation generated at: {readme_file}")
195
- return readme_file
45
+ # Basic import test on generated code
46
+ try:
47
+ spec = importlib.util.spec_from_file_location("temp_module", gen_file)
48
+ module = importlib.util.module_from_spec(spec)
49
+ spec.loader.exec_module(module) # type: ignore
50
+ logger.info("Intermediate code import test passed.")
51
+ return True
52
+ except Exception as e:
53
+ logger.error(f"Import test failed for generated code: {e}")
54
+ return False
55
+ return True
196
56
 
197
57
 
198
- async def generate_api_from_schema(
58
+ def generate_api_from_schema(
199
59
  schema_path: Path,
200
60
  output_path: Path | None = None,
201
- add_docstrings: bool = True,
202
- ) -> dict[str, str | None]:
61
+ class_name: str | None = None,
62
+ ) -> tuple[Path, Path]:
203
63
  """
204
- Generate API client from OpenAPI schema with optional docstring generation.
64
+ Generate API client from OpenAPI schema and write to app.py with a README.
65
+
66
+ Steps:
67
+ 1. Parse and validate the OpenAPI schema.
68
+ 2. Generate client code.
69
+ 3. Ensure output directory exists.
70
+ 4. Write code to an intermediate app_generated.py and perform basic import checks.
71
+ 5. Copy/overwrite intermediate file to app.py.
72
+ 6. Collect tools and generate README.md.
73
+ """
74
+ # Local imports for logging and file operations
205
75
 
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
76
+ logger.info("Starting API generation for schema: %s", schema_path)
210
77
 
211
- Returns:
212
- dict: A dictionary with information about the generated files
213
- """
78
+ # 1. Parse and validate schema
214
79
  try:
215
80
  schema = validate_and_load_schema(schema_path)
216
- code = generate_api_client(schema)
217
-
218
- if not output_path:
219
- return {"code": code}
220
-
221
- folder_name = output_path.stem
222
- temp_output_path = output_path
223
-
224
- write_and_verify_code(temp_output_path, code)
225
-
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")
235
-
236
- app_dir, app_file = setup_app_directory(folder_name, temp_output_path)
81
+ logger.info("Schema loaded and validated successfully.")
82
+ except Exception as e:
83
+ logger.error("Failed to load or validate schema: %s", e)
84
+ raise
237
85
 
238
- 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)
86
+ # 2. Generate client code
87
+ try:
88
+ code = generate_api_client(schema, class_name)
89
+ logger.info("API client code generated.")
90
+ except Exception as e:
91
+ logger.error("Code generation failed: %s", e)
92
+ raise
243
93
 
244
- class_name, class_obj = get_class_info(module)
245
- if not class_name:
246
- class_name = folder_name.capitalize() + "App"
94
+ # If no output_path provided, return raw code
95
+ if not output_path:
96
+ logger.debug("No output_path provided, returning code as string.")
97
+ return {"code": code}
98
+
99
+ # 3. Ensure output directory exists
100
+ target_dir = output_path
101
+ if not target_dir.exists():
102
+ logger.info("Creating output directory: %s", target_dir)
103
+ target_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ # 4. Write to intermediate file and perform basic checks
106
+ gen_file = target_dir / "app_generated.py"
107
+ logger.info("Writing generated code to intermediate file: %s", gen_file)
108
+ with open(gen_file, "w") as f:
109
+ f.write(code)
247
110
 
248
- tools = collect_tools(class_obj, folder_name)
249
- readme_file = generate_readme(app_dir, folder_name, tools)
111
+ if not test_correct_output(gen_file):
112
+ logger.error(
113
+ "Generated code validation failed for '%s'. Aborting generation.", gen_file
114
+ )
115
+ logger.info("Next steps:")
116
+ logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
117
+ logger.info(
118
+ " 2) Inspect '%s' for syntax or logic errors in the generated code.",
119
+ gen_file,
120
+ )
121
+ logger.info(" 3) Correct the issues and re-run the command.")
122
+ return {"error": "Validation failed. See logs above for detailed instructions."}
250
123
 
251
- except Exception as e:
252
- echo(f"Error generating documentation: {e}", err=True)
253
- readme_file = None
124
+ # 5. Copy to final app.py (overwrite if exists)
125
+ app_file = target_dir / "app.py"
126
+ if app_file.exists():
127
+ logger.warning("Overwriting existing file: %s", app_file)
128
+ else:
129
+ logger.info("Creating new file: %s", app_file)
130
+ shutil.copy(gen_file, app_file)
131
+ logger.info("App file written to: %s", app_file)
254
132
 
255
- return {
256
- "app_file": str(app_file),
257
- "readme_file": str(readme_file) if readme_file else None,
258
- }
133
+ # Cleanup intermediate file
134
+ try:
135
+ os.remove(gen_file)
136
+ logger.debug("Cleaned up intermediate file: %s", gen_file)
137
+ except Exception as e:
138
+ logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
259
139
 
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
- )
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