universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc3__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/agentr/__init__.py +6 -0
- universal_mcp/agentr/agentr.py +30 -0
- universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
- universal_mcp/agentr/integration.py +104 -0
- universal_mcp/agentr/registry.py +91 -0
- universal_mcp/agentr/server.py +51 -0
- universal_mcp/agents/__init__.py +6 -0
- universal_mcp/agents/auto.py +576 -0
- universal_mcp/agents/base.py +88 -0
- universal_mcp/agents/cli.py +27 -0
- universal_mcp/agents/codeact/__init__.py +243 -0
- universal_mcp/agents/codeact/sandbox.py +27 -0
- universal_mcp/agents/codeact/test.py +15 -0
- universal_mcp/agents/codeact/utils.py +61 -0
- universal_mcp/agents/hil.py +104 -0
- universal_mcp/agents/llm.py +10 -0
- universal_mcp/agents/react.py +58 -0
- universal_mcp/agents/simple.py +40 -0
- universal_mcp/agents/utils.py +111 -0
- universal_mcp/analytics.py +44 -14
- universal_mcp/applications/__init__.py +42 -75
- universal_mcp/applications/application.py +187 -133
- universal_mcp/applications/sample/app.py +245 -0
- universal_mcp/cli.py +14 -231
- universal_mcp/client/oauth.py +122 -18
- universal_mcp/client/token_store.py +62 -3
- universal_mcp/client/{client.py → transport.py} +127 -48
- universal_mcp/config.py +189 -49
- universal_mcp/exceptions.py +54 -6
- universal_mcp/integrations/__init__.py +0 -18
- universal_mcp/integrations/integration.py +185 -168
- universal_mcp/servers/__init__.py +2 -14
- universal_mcp/servers/server.py +84 -258
- universal_mcp/stores/store.py +126 -93
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +20 -11
- universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp/tools/manager.py +38 -53
- universal_mcp/tools/registry.py +41 -0
- universal_mcp/tools/tools.py +24 -3
- universal_mcp/types.py +10 -0
- universal_mcp/utils/common.py +245 -0
- universal_mcp/utils/installation.py +3 -4
- universal_mcp/utils/openapi/api_generator.py +71 -17
- universal_mcp/utils/openapi/api_splitter.py +0 -1
- universal_mcp/utils/openapi/cli.py +669 -0
- universal_mcp/utils/openapi/filters.py +114 -0
- universal_mcp/utils/openapi/openapi.py +315 -23
- universal_mcp/utils/openapi/postprocessor.py +275 -0
- universal_mcp/utils/openapi/preprocessor.py +63 -8
- universal_mcp/utils/openapi/test_generator.py +287 -0
- universal_mcp/utils/prompts.py +634 -0
- universal_mcp/utils/singleton.py +4 -1
- universal_mcp/utils/testing.py +196 -8
- universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
- universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
- universal_mcp/applications/README.md +0 -122
- universal_mcp/client/__main__.py +0 -30
- universal_mcp/client/agent.py +0 -96
- universal_mcp/integrations/README.md +0 -25
- universal_mcp/servers/README.md +0 -79
- universal_mcp/stores/README.md +0 -74
- universal_mcp/tools/README.md +0 -86
- universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
- universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
- /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,245 @@
|
|
1
|
+
import datetime
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
|
5
|
+
from universal_mcp.applications.application import BaseApplication
|
6
|
+
|
7
|
+
|
8
|
+
class SampleToolApp(BaseApplication):
|
9
|
+
"""A sample application providing basic utility tools."""
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
"""Initializes the SampleToolApp with the name 'sample_tool_app'."""
|
13
|
+
super().__init__(name="sample_tool_app")
|
14
|
+
|
15
|
+
def get_current_time(self):
|
16
|
+
"""Get the current system time as a formatted string.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
str: The current time in the format 'YYYY-MM-DD HH:MM:SS'.
|
20
|
+
"""
|
21
|
+
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
22
|
+
|
23
|
+
def get_current_date(self):
|
24
|
+
"""Get the current system date as a formatted string.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
str: The current date in the format 'YYYY-MM-DD'.
|
28
|
+
"""
|
29
|
+
return datetime.datetime.now().strftime("%Y-%m-%d")
|
30
|
+
|
31
|
+
def calculate(self, expression: str):
|
32
|
+
"""Safely evaluate a mathematical expression.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
expression (str): The mathematical expression to evaluate.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
str: The result of the calculation, or an error message if evaluation fails.
|
39
|
+
"""
|
40
|
+
try:
|
41
|
+
# Safe evaluation of mathematical expressions
|
42
|
+
result = eval(expression, {"__builtins__": {}}, {}) # noqa: S102
|
43
|
+
return f"Result: {result}"
|
44
|
+
except Exception as e:
|
45
|
+
return f"Error in calculation: {str(e)}"
|
46
|
+
|
47
|
+
def file_operations(self, operation: str, filename: str, content: str = ""):
|
48
|
+
"""Perform file read or write operations.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
operation (str): The operation to perform, either 'read' or 'write'.
|
52
|
+
filename (str): The name of the file to operate on.
|
53
|
+
content (str, optional): The content to write to the file (used only for 'write'). Defaults to "".
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
str: The result of the file operation, or an error message if the operation fails.
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
if operation == "read":
|
60
|
+
with open(filename) as f:
|
61
|
+
return f"File content:\n{f.read()}"
|
62
|
+
elif operation == "write":
|
63
|
+
with open(filename, "w") as f:
|
64
|
+
f.write(content)
|
65
|
+
return f"Successfully wrote to {filename}"
|
66
|
+
else:
|
67
|
+
return "Invalid operation. Use 'read' or 'write'"
|
68
|
+
except Exception as e:
|
69
|
+
return f"File operation error: {str(e)}"
|
70
|
+
|
71
|
+
def get_weather(
|
72
|
+
self,
|
73
|
+
latitude: float,
|
74
|
+
longitude: float,
|
75
|
+
current: list[str] | None = None,
|
76
|
+
hourly: list[str] | None = None,
|
77
|
+
daily: list[str] | None = None,
|
78
|
+
timezone: str = "auto",
|
79
|
+
temperature_unit: str = "celsius",
|
80
|
+
wind_speed_unit: str = "kmh",
|
81
|
+
precipitation_unit: str = "mm",
|
82
|
+
) -> dict:
|
83
|
+
"""
|
84
|
+
Get weather data from Open-Meteo API.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
latitude (float): Latitude coordinate
|
88
|
+
longitude (float): Longitude coordinate
|
89
|
+
current (List[str], optional): Current weather parameters to fetch
|
90
|
+
hourly (List[str], optional): Hourly weather parameters to fetch
|
91
|
+
daily (List[str], optional): Daily weather parameters to fetch
|
92
|
+
timezone (str): Timezone (default: "auto")
|
93
|
+
temperature_unit (str): Temperature unit - "celsius" or "fahrenheit"
|
94
|
+
wind_speed_unit (str): Wind speed unit - "kmh", "ms", "mph", "kn"
|
95
|
+
precipitation_unit (str): Precipitation unit - "mm" or "inch"
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Dict: Weather data from the API
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
httpx.RequestError: If API request fails
|
102
|
+
ValueError: If coordinates are invalid
|
103
|
+
"""
|
104
|
+
# Validate coordinates
|
105
|
+
if not (-90 <= latitude <= 90):
|
106
|
+
raise ValueError("Latitude must be between -90 and 90")
|
107
|
+
if not (-180 <= longitude <= 180):
|
108
|
+
raise ValueError("Longitude must be between -180 and 180")
|
109
|
+
|
110
|
+
# Base URL
|
111
|
+
base_url = "https://api.open-meteo.com/v1/forecast"
|
112
|
+
|
113
|
+
# Default parameters if none provided
|
114
|
+
if current is None:
|
115
|
+
current = ["temperature_2m", "relative_humidity_2m", "weather_code", "wind_speed_10m", "wind_direction_10m"]
|
116
|
+
|
117
|
+
if daily is None:
|
118
|
+
daily = ["temperature_2m_max", "temperature_2m_min", "weather_code", "precipitation_sum"]
|
119
|
+
|
120
|
+
# Build parameters
|
121
|
+
params = {
|
122
|
+
"latitude": latitude,
|
123
|
+
"longitude": longitude,
|
124
|
+
"timezone": timezone,
|
125
|
+
"temperature_unit": temperature_unit,
|
126
|
+
"wind_speed_unit": wind_speed_unit,
|
127
|
+
"precipitation_unit": precipitation_unit,
|
128
|
+
}
|
129
|
+
|
130
|
+
# Add weather parameters
|
131
|
+
if current:
|
132
|
+
params["current"] = ",".join(current)
|
133
|
+
if hourly:
|
134
|
+
params["hourly"] = ",".join(hourly)
|
135
|
+
if daily:
|
136
|
+
params["daily"] = ",".join(daily)
|
137
|
+
|
138
|
+
try:
|
139
|
+
# Make API request
|
140
|
+
with httpx.Client(timeout=10) as client:
|
141
|
+
response = client.get(base_url, params=params)
|
142
|
+
response.raise_for_status()
|
143
|
+
return response.json()
|
144
|
+
|
145
|
+
except httpx.TimeoutException as e:
|
146
|
+
raise httpx.RequestError("Request timed out") from e
|
147
|
+
except httpx.ConnectError as e:
|
148
|
+
raise httpx.RequestError("Connection error") from e
|
149
|
+
except httpx.HTTPStatusError as e:
|
150
|
+
raise httpx.RequestError(f"HTTP error: {e}") from e
|
151
|
+
except httpx.RequestError as e:
|
152
|
+
raise httpx.RequestError(f"Request failed: {e}") from e
|
153
|
+
|
154
|
+
def get_simple_weather(self, latitude: float, longitude: float) -> dict:
|
155
|
+
"""
|
156
|
+
Get simplified current weather data.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
latitude (float): Latitude coordinate
|
160
|
+
longitude (float): Longitude coordinate
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
Dict: Simplified weather data with current conditions
|
164
|
+
"""
|
165
|
+
|
166
|
+
try:
|
167
|
+
weather_data = self.get_weather(
|
168
|
+
latitude=latitude,
|
169
|
+
longitude=longitude,
|
170
|
+
current=[
|
171
|
+
"temperature_2m",
|
172
|
+
"relative_humidity_2m",
|
173
|
+
"weather_code",
|
174
|
+
"wind_speed_10m",
|
175
|
+
"wind_direction_10m",
|
176
|
+
"precipitation",
|
177
|
+
],
|
178
|
+
)
|
179
|
+
|
180
|
+
# Weather code descriptions (WMO Weather interpretation codes)
|
181
|
+
weather_codes = {
|
182
|
+
0: "Clear sky",
|
183
|
+
1: "Mainly clear",
|
184
|
+
2: "Partly cloudy",
|
185
|
+
3: "Overcast",
|
186
|
+
45: "Fog",
|
187
|
+
48: "Depositing rime fog",
|
188
|
+
51: "Light drizzle",
|
189
|
+
53: "Moderate drizzle",
|
190
|
+
55: "Dense drizzle",
|
191
|
+
61: "Slight rain",
|
192
|
+
63: "Moderate rain",
|
193
|
+
65: "Heavy rain",
|
194
|
+
71: "Slight snow fall",
|
195
|
+
73: "Moderate snow fall",
|
196
|
+
75: "Heavy snow fall",
|
197
|
+
80: "Slight rain showers",
|
198
|
+
81: "Moderate rain showers",
|
199
|
+
82: "Violent rain showers",
|
200
|
+
95: "Thunderstorm",
|
201
|
+
96: "Thunderstorm with slight hail",
|
202
|
+
99: "Thunderstorm with heavy hail",
|
203
|
+
}
|
204
|
+
|
205
|
+
current = weather_data.get("current", {})
|
206
|
+
weather_code = current.get("weather_code", 0)
|
207
|
+
|
208
|
+
simplified = {
|
209
|
+
"location": {
|
210
|
+
"latitude": weather_data.get("latitude"),
|
211
|
+
"longitude": weather_data.get("longitude"),
|
212
|
+
"timezone": weather_data.get("timezone"),
|
213
|
+
},
|
214
|
+
"current": {
|
215
|
+
"time": current.get("time"),
|
216
|
+
"temperature": current.get("temperature_2m"),
|
217
|
+
"temperature_unit": weather_data.get("current_units", {}).get("temperature_2m", "°C"),
|
218
|
+
"humidity": current.get("relative_humidity_2m"),
|
219
|
+
"weather_description": weather_codes.get(weather_code, "Unknown"),
|
220
|
+
"weather_code": weather_code,
|
221
|
+
"wind_speed": current.get("wind_speed_10m"),
|
222
|
+
"wind_speed_unit": weather_data.get("current_units", {}).get("wind_speed_10m", "km/h"),
|
223
|
+
"wind_direction": current.get("wind_direction_10m"),
|
224
|
+
"precipitation": current.get("precipitation", 0),
|
225
|
+
},
|
226
|
+
}
|
227
|
+
|
228
|
+
return simplified
|
229
|
+
|
230
|
+
except Exception as e:
|
231
|
+
return {"error": str(e)}
|
232
|
+
|
233
|
+
def list_tools(self):
|
234
|
+
"""List all available tool methods in this application.
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
list: A list of callable tool methods.
|
238
|
+
"""
|
239
|
+
return [
|
240
|
+
self.get_current_time,
|
241
|
+
self.get_current_date,
|
242
|
+
self.calculate,
|
243
|
+
self.file_operations,
|
244
|
+
self.get_simple_weather,
|
245
|
+
]
|
universal_mcp/cli.py
CHANGED
@@ -1,113 +1,22 @@
|
|
1
|
-
import re
|
2
1
|
from pathlib import Path
|
3
2
|
|
4
3
|
import typer
|
5
4
|
from rich.console import Console
|
6
5
|
from rich.panel import Panel
|
7
6
|
|
7
|
+
from universal_mcp.agents.cli import app as client_app
|
8
8
|
from universal_mcp.utils.installation import (
|
9
9
|
get_supported_apps,
|
10
10
|
install_app,
|
11
11
|
)
|
12
|
+
from universal_mcp.utils.openapi.cli import app as codegen_app
|
12
13
|
|
13
14
|
# Setup rich console and logging
|
14
15
|
console = Console()
|
15
16
|
|
16
|
-
app = typer.Typer()
|
17
|
-
|
18
|
-
|
19
|
-
@app.command()
|
20
|
-
def generate(
|
21
|
-
schema_path: Path = typer.Option(..., "--schema", "-s"),
|
22
|
-
output_path: Path = typer.Option(
|
23
|
-
None,
|
24
|
-
"--output",
|
25
|
-
"-o",
|
26
|
-
help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
|
27
|
-
),
|
28
|
-
class_name: str = typer.Option(
|
29
|
-
None,
|
30
|
-
"--class-name",
|
31
|
-
"-c",
|
32
|
-
help="Class name to use for the API client",
|
33
|
-
),
|
34
|
-
):
|
35
|
-
"""Generate API client from OpenAPI schema with optional docstring generation.
|
36
|
-
|
37
|
-
The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
|
38
|
-
This name will be used for the folder in applications/.
|
39
|
-
"""
|
40
|
-
# Import here to avoid circular imports
|
41
|
-
from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
|
42
|
-
|
43
|
-
if not schema_path.exists():
|
44
|
-
console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
|
45
|
-
raise typer.Exit(1)
|
46
|
-
|
47
|
-
try:
|
48
|
-
app_file_data = generate_api_from_schema(
|
49
|
-
schema_path=schema_path,
|
50
|
-
output_path=output_path,
|
51
|
-
class_name=class_name,
|
52
|
-
)
|
53
|
-
if isinstance(app_file_data, dict) and "code" in app_file_data:
|
54
|
-
console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
|
55
|
-
console.print(app_file_data["code"])
|
56
|
-
elif isinstance(app_file_data, Path):
|
57
|
-
console.print("[green]API client successfully generated and installed.[/green]")
|
58
|
-
console.print(f"[blue]Application file: {app_file_data}[/blue]")
|
59
|
-
else:
|
60
|
-
# Handle the error case from api_generator if validation fails
|
61
|
-
if isinstance(app_file_data, dict) and "error" in app_file_data:
|
62
|
-
console.print(f"[red]{app_file_data['error']}[/red]")
|
63
|
-
raise typer.Exit(1)
|
64
|
-
else:
|
65
|
-
console.print("[red]Unexpected return value from API generator.[/red]")
|
66
|
-
raise typer.Exit(1)
|
67
|
-
|
68
|
-
except Exception as e:
|
69
|
-
console.print(f"[red]Error generating API client: {e}[/red]")
|
70
|
-
raise typer.Exit(1) from e
|
71
|
-
|
72
|
-
|
73
|
-
@app.command()
|
74
|
-
def readme(
|
75
|
-
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
76
|
-
):
|
77
|
-
"""Generate a README.md file for the API client."""
|
78
|
-
from universal_mcp.utils.openapi.readme import generate_readme
|
79
|
-
|
80
|
-
readme_file = generate_readme(file_path)
|
81
|
-
console.print(f"[green]README.md file generated at: {readme_file}[/green]")
|
82
|
-
|
83
|
-
|
84
|
-
@app.command()
|
85
|
-
def docgen(
|
86
|
-
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
87
|
-
model: str = typer.Option(
|
88
|
-
"perplexity/sonar",
|
89
|
-
"--model",
|
90
|
-
"-m",
|
91
|
-
help="Model to use for generating docstrings",
|
92
|
-
),
|
93
|
-
):
|
94
|
-
"""Generate docstrings for Python files using LLMs.
|
95
|
-
|
96
|
-
This command uses litellm with structured output to generate high-quality
|
97
|
-
Google-style docstrings for all functions in the specified Python file.
|
98
|
-
"""
|
99
|
-
from universal_mcp.utils.openapi.docgen import process_file
|
100
|
-
|
101
|
-
if not file_path.exists():
|
102
|
-
console.print(f"[red]Error: File not found: {file_path}[/red]")
|
103
|
-
raise typer.Exit(1)
|
104
|
-
|
105
|
-
try:
|
106
|
-
processed = process_file(str(file_path), model)
|
107
|
-
console.print(f"[green]Successfully processed {processed} functions[/green]")
|
108
|
-
except Exception as e:
|
109
|
-
console.print(f"[red]Error: {e}[/red]")
|
110
|
-
raise typer.Exit(1) from e
|
17
|
+
app = typer.Typer(name="mcp")
|
18
|
+
app.add_typer(codegen_app, name="codegen", help="Code generation and manipulation commands")
|
19
|
+
app.add_typer(client_app, name="client", help="Client commands")
|
111
20
|
|
112
21
|
|
113
22
|
@app.command()
|
@@ -115,13 +24,20 @@ def run(
|
|
115
24
|
config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to the config file"),
|
116
25
|
):
|
117
26
|
"""Run the MCP server"""
|
27
|
+
from universal_mcp.agentr.server import AgentrServer
|
118
28
|
from universal_mcp.config import ServerConfig
|
119
29
|
from universal_mcp.logger import setup_logger
|
120
|
-
from universal_mcp.servers import
|
30
|
+
from universal_mcp.servers import LocalServer
|
121
31
|
|
122
32
|
config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
|
123
33
|
setup_logger(level=config.log_level)
|
124
|
-
|
34
|
+
|
35
|
+
if config.type == "agentr":
|
36
|
+
server = AgentrServer(config=config, api_key=config.api_key)
|
37
|
+
elif config.type == "local":
|
38
|
+
server = LocalServer(config=config)
|
39
|
+
else:
|
40
|
+
raise ValueError(f"Unsupported server type: {config.type}")
|
125
41
|
server.run(transport=config.transport)
|
126
42
|
|
127
43
|
|
@@ -162,138 +78,5 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
162
78
|
raise typer.Exit(1) from e
|
163
79
|
|
164
80
|
|
165
|
-
@app.command()
|
166
|
-
def init(
|
167
|
-
output_dir: Path | None = typer.Option(
|
168
|
-
None,
|
169
|
-
"--output-dir",
|
170
|
-
"-o",
|
171
|
-
help="Output directory for the project (must exist)",
|
172
|
-
),
|
173
|
-
app_name: str | None = typer.Option(
|
174
|
-
None,
|
175
|
-
"--app-name",
|
176
|
-
"-a",
|
177
|
-
help="App name (letters, numbers, hyphens, underscores only)",
|
178
|
-
),
|
179
|
-
integration_type: str | None = typer.Option(
|
180
|
-
None,
|
181
|
-
"--integration-type",
|
182
|
-
"-i",
|
183
|
-
help="Integration type (api_key, oauth, agentr, none)",
|
184
|
-
case_sensitive=False,
|
185
|
-
show_choices=True,
|
186
|
-
),
|
187
|
-
):
|
188
|
-
"""Initialize a new MCP project using the cookiecutter template."""
|
189
|
-
from cookiecutter.main import cookiecutter
|
190
|
-
|
191
|
-
NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
|
192
|
-
|
193
|
-
def validate_pattern(value: str, field_name: str) -> None:
|
194
|
-
if not re.match(NAME_PATTERN, value):
|
195
|
-
console.print(
|
196
|
-
f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
|
197
|
-
)
|
198
|
-
raise typer.Exit(code=1)
|
199
|
-
|
200
|
-
# App name
|
201
|
-
if not app_name:
|
202
|
-
app_name = typer.prompt(
|
203
|
-
"Enter the app name",
|
204
|
-
default="app_name",
|
205
|
-
prompt_suffix=" (e.g., reddit, youtube): ",
|
206
|
-
).strip()
|
207
|
-
validate_pattern(app_name, "app name")
|
208
|
-
app_name = app_name.lower()
|
209
|
-
if not output_dir:
|
210
|
-
path_str = typer.prompt(
|
211
|
-
"Enter the output directory for the project",
|
212
|
-
default=str(Path.cwd()),
|
213
|
-
prompt_suffix=": ",
|
214
|
-
).strip()
|
215
|
-
output_dir = Path(path_str)
|
216
|
-
|
217
|
-
if not output_dir.exists():
|
218
|
-
try:
|
219
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
220
|
-
console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
|
221
|
-
except Exception as e:
|
222
|
-
console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
|
223
|
-
raise typer.Exit(code=1) from e
|
224
|
-
elif not output_dir.is_dir():
|
225
|
-
console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
|
226
|
-
raise typer.Exit(code=1)
|
227
|
-
|
228
|
-
# Integration type
|
229
|
-
if not integration_type:
|
230
|
-
integration_type = typer.prompt(
|
231
|
-
"Choose the integration type",
|
232
|
-
default="agentr",
|
233
|
-
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
234
|
-
).lower()
|
235
|
-
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
236
|
-
console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
|
237
|
-
raise typer.Exit(code=1)
|
238
|
-
|
239
|
-
console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
|
240
|
-
try:
|
241
|
-
cookiecutter(
|
242
|
-
"https://github.com/AgentrDev/universal-mcp-app-template.git",
|
243
|
-
output_dir=str(output_dir),
|
244
|
-
no_input=True,
|
245
|
-
extra_context={
|
246
|
-
"app_name": app_name,
|
247
|
-
"integration_type": integration_type,
|
248
|
-
},
|
249
|
-
)
|
250
|
-
except Exception as exc:
|
251
|
-
console.print(f"❌ Project generation failed: {exc}")
|
252
|
-
raise typer.Exit(code=1) from exc
|
253
|
-
|
254
|
-
project_dir = output_dir / f"{app_name}"
|
255
|
-
console.print(f"✅ Project created at {project_dir}")
|
256
|
-
|
257
|
-
|
258
|
-
@app.command()
|
259
|
-
def preprocess(
|
260
|
-
schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
|
261
|
-
output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
|
262
|
-
):
|
263
|
-
from universal_mcp.utils.openapi.preprocessor import run_preprocessing
|
264
|
-
|
265
|
-
"""Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
|
266
|
-
run_preprocessing(schema_path, output_path)
|
267
|
-
|
268
|
-
|
269
|
-
@app.command()
|
270
|
-
def split_api(
|
271
|
-
input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
|
272
|
-
output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
|
273
|
-
package_name: str = typer.Option(None, "--package-name", "-p", help="Package name for absolute imports (e.g., 'hubspot')"),
|
274
|
-
):
|
275
|
-
"""Splits a single generated API client file into multiple files based on path groups."""
|
276
|
-
from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
|
277
|
-
|
278
|
-
if not input_app_file.exists() or not input_app_file.is_file():
|
279
|
-
console.print(f"[red]Error: Input file {input_app_file} does not exist or is not a file.[/red]")
|
280
|
-
raise typer.Exit(1)
|
281
|
-
|
282
|
-
if not output_dir.exists():
|
283
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
284
|
-
console.print(f"[green]Created output directory: {output_dir}[/green]")
|
285
|
-
elif not output_dir.is_dir():
|
286
|
-
console.print(f"[red]Error: Output path {output_dir} is not a directory.[/red]")
|
287
|
-
raise typer.Exit(1)
|
288
|
-
|
289
|
-
try:
|
290
|
-
split_generated_app_file(input_app_file, output_dir, package_name)
|
291
|
-
console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
|
292
|
-
except Exception as e:
|
293
|
-
console.print(f"[red]Error splitting API client: {e}[/red]")
|
294
|
-
|
295
|
-
raise typer.Exit(1) from e
|
296
|
-
|
297
|
-
|
298
81
|
if __name__ == "__main__":
|
299
82
|
app()
|