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.
- universal_mcp/applications/__init__.py +76 -8
- universal_mcp/cli.py +136 -30
- 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 -31
- universal_mcp/stores/README.md +74 -0
- 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 -3
- universal_mcp/utils/agentr.py +95 -0
- universal_mcp/utils/api_generator.py +90 -219
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi.py +353 -211
- universal_mcp/utils/readme.py +92 -0
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
- universal_mcp-0.1.13.dist-info/RECORD +39 -0
- universal_mcp/applications/ahrefs/README.md +0 -76
- universal_mcp/applications/ahrefs/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +0 -2291
- universal_mcp/applications/cal_com_v2/README.md +0 -175
- universal_mcp/applications/cal_com_v2/__init__.py +0 -0
- universal_mcp/applications/cal_com_v2/app.py +0 -5390
- universal_mcp/applications/calendly/README.md +0 -78
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +0 -1195
- universal_mcp/applications/clickup/README.md +0 -160
- universal_mcp/applications/clickup/__init__.py +0 -0
- universal_mcp/applications/clickup/app.py +0 -5009
- universal_mcp/applications/coda/README.md +0 -133
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +0 -3671
- universal_mcp/applications/e2b/README.md +0 -37
- universal_mcp/applications/e2b/app.py +0 -65
- universal_mcp/applications/elevenlabs/README.md +0 -84
- universal_mcp/applications/elevenlabs/__init__.py +0 -0
- universal_mcp/applications/elevenlabs/app.py +0 -1402
- universal_mcp/applications/falai/README.md +0 -42
- universal_mcp/applications/falai/__init__.py +0 -0
- universal_mcp/applications/falai/app.py +0 -332
- universal_mcp/applications/figma/README.md +0 -74
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +0 -1261
- universal_mcp/applications/firecrawl/README.md +0 -45
- universal_mcp/applications/firecrawl/app.py +0 -268
- universal_mcp/applications/github/README.md +0 -47
- universal_mcp/applications/github/app.py +0 -429
- universal_mcp/applications/gong/README.md +0 -88
- universal_mcp/applications/gong/__init__.py +0 -0
- universal_mcp/applications/gong/app.py +0 -2297
- universal_mcp/applications/google_calendar/app.py +0 -442
- universal_mcp/applications/google_docs/README.md +0 -40
- universal_mcp/applications/google_docs/app.py +0 -88
- universal_mcp/applications/google_drive/README.md +0 -44
- universal_mcp/applications/google_drive/app.py +0 -286
- universal_mcp/applications/google_mail/README.md +0 -47
- universal_mcp/applications/google_mail/app.py +0 -664
- universal_mcp/applications/google_sheet/README.md +0 -42
- universal_mcp/applications/google_sheet/app.py +0 -150
- universal_mcp/applications/hashnode/app.py +0 -81
- universal_mcp/applications/hashnode/prompt.md +0 -23
- universal_mcp/applications/heygen/README.md +0 -69
- universal_mcp/applications/heygen/__init__.py +0 -0
- universal_mcp/applications/heygen/app.py +0 -956
- universal_mcp/applications/mailchimp/README.md +0 -306
- universal_mcp/applications/mailchimp/__init__.py +0 -0
- universal_mcp/applications/mailchimp/app.py +0 -10937
- universal_mcp/applications/markitdown/app.py +0 -44
- universal_mcp/applications/notion/README.md +0 -55
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +0 -527
- universal_mcp/applications/perplexity/README.md +0 -37
- universal_mcp/applications/perplexity/app.py +0 -65
- universal_mcp/applications/reddit/README.md +0 -45
- universal_mcp/applications/reddit/app.py +0 -379
- universal_mcp/applications/replicate/README.md +0 -65
- universal_mcp/applications/replicate/__init__.py +0 -0
- universal_mcp/applications/replicate/app.py +0 -980
- universal_mcp/applications/resend/README.md +0 -38
- universal_mcp/applications/resend/app.py +0 -37
- universal_mcp/applications/retell_ai/README.md +0 -46
- universal_mcp/applications/retell_ai/__init__.py +0 -0
- universal_mcp/applications/retell_ai/app.py +0 -333
- universal_mcp/applications/rocketlane/README.md +0 -42
- universal_mcp/applications/rocketlane/__init__.py +0 -0
- universal_mcp/applications/rocketlane/app.py +0 -194
- universal_mcp/applications/serpapi/README.md +0 -37
- universal_mcp/applications/serpapi/app.py +0 -73
- universal_mcp/applications/spotify/README.md +0 -116
- universal_mcp/applications/spotify/__init__.py +0 -0
- universal_mcp/applications/spotify/app.py +0 -2526
- universal_mcp/applications/supabase/README.md +0 -112
- universal_mcp/applications/supabase/__init__.py +0 -0
- universal_mcp/applications/supabase/app.py +0 -2970
- universal_mcp/applications/tavily/README.md +0 -38
- universal_mcp/applications/tavily/app.py +0 -51
- universal_mcp/applications/wrike/README.md +0 -71
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +0 -1372
- universal_mcp/applications/youtube/README.md +0 -82
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +0 -1428
- universal_mcp/applications/zenquotes/README.md +0 -37
- universal_mcp/applications/zenquotes/app.py +0 -31
- universal_mcp/integrations/agentr.py +0 -112
- universal_mcp-0.1.12.dist-info/RECORD +0 -119
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {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
|
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}'. 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
|
4
|
+
import shutil
|
6
5
|
from pathlib import Path
|
7
6
|
|
8
|
-
from
|
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
|
-
|
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
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
58
|
+
def generate_api_from_schema(
|
199
59
|
schema_path: Path,
|
200
60
|
output_path: Path | None = None,
|
201
|
-
|
202
|
-
) ->
|
61
|
+
class_name: str | None = None,
|
62
|
+
) -> tuple[Path, Path]:
|
203
63
|
"""
|
204
|
-
Generate API client from OpenAPI schema
|
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
|
-
|
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
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
249
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
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
|
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
|
|