universal-mcp 0.1.24rc2__py3-none-any.whl → 0.1.24rc4__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/README.md +201 -0
- universal_mcp/agentr/__init__.py +6 -0
- universal_mcp/agentr/agentr.py +30 -0
- universal_mcp/{utils/agentr.py → agentr/client.py} +19 -3
- 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 +5 -7
- universal_mcp/applications/__init__.py +42 -75
- universal_mcp/applications/application.py +1 -1
- universal_mcp/applications/sample/app.py +245 -0
- universal_mcp/cli.py +10 -3
- universal_mcp/config.py +33 -7
- universal_mcp/exceptions.py +4 -0
- universal_mcp/integrations/__init__.py +0 -15
- universal_mcp/integrations/integration.py +9 -91
- universal_mcp/servers/__init__.py +2 -14
- universal_mcp/servers/server.py +10 -51
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +20 -11
- universal_mcp/tools/manager.py +29 -56
- universal_mcp/tools/registry.py +41 -0
- universal_mcp/tools/tools.py +22 -1
- universal_mcp/types.py +10 -0
- universal_mcp/utils/common.py +245 -0
- universal_mcp/utils/openapi/api_generator.py +46 -18
- universal_mcp/utils/openapi/cli.py +445 -19
- universal_mcp/utils/openapi/openapi.py +284 -21
- universal_mcp/utils/openapi/postprocessor.py +275 -0
- universal_mcp/utils/openapi/preprocessor.py +1 -1
- universal_mcp/utils/openapi/test_generator.py +287 -0
- universal_mcp/utils/prompts.py +188 -341
- universal_mcp/utils/testing.py +190 -2
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/METADATA +17 -3
- universal_mcp-0.1.24rc4.dist-info/RECORD +71 -0
- universal_mcp/applications/sample_tool_app.py +0 -80
- universal_mcp/client/agents/__init__.py +0 -4
- universal_mcp/client/agents/base.py +0 -38
- universal_mcp/client/agents/llm.py +0 -115
- universal_mcp/client/agents/react.py +0 -67
- universal_mcp/client/cli.py +0 -181
- universal_mcp-0.1.24rc2.dist-info/RECORD +0 -53
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,81 @@
|
|
1
|
+
import os
|
1
2
|
import re
|
2
3
|
from pathlib import Path
|
3
4
|
|
5
|
+
import litellm
|
4
6
|
import typer
|
5
7
|
from rich.console import Console
|
8
|
+
from rich.panel import Panel
|
9
|
+
from rich.prompt import Confirm, Prompt
|
10
|
+
from rich.status import Status
|
11
|
+
from rich.syntax import Syntax
|
6
12
|
|
7
|
-
# Setup rich console and logging
|
8
13
|
console = Console()
|
9
|
-
|
10
14
|
app = typer.Typer(name="codegen")
|
11
15
|
|
12
16
|
|
17
|
+
def _validate_filter_config(filter_config: Path | None) -> None:
|
18
|
+
"""
|
19
|
+
Validate filter configuration file if provided.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
filter_config: Path to filter config file or None
|
23
|
+
|
24
|
+
Raises:
|
25
|
+
typer.Exit: If validation fails
|
26
|
+
"""
|
27
|
+
if filter_config is None:
|
28
|
+
return
|
29
|
+
|
30
|
+
# Handle edge case of empty string or invalid path
|
31
|
+
if str(filter_config).strip() == "":
|
32
|
+
console.print("[red]Error: Filter configuration path cannot be empty[/red]")
|
33
|
+
raise typer.Exit(1)
|
34
|
+
|
35
|
+
if not filter_config.exists():
|
36
|
+
console.print(f"[red]Error: Filter configuration file '{filter_config}' does not exist[/red]")
|
37
|
+
raise typer.Exit(1)
|
38
|
+
|
39
|
+
if not filter_config.is_file():
|
40
|
+
console.print(f"[red]Error: Filter configuration path '{filter_config}' is not a file[/red]")
|
41
|
+
raise typer.Exit(1)
|
42
|
+
|
43
|
+
|
44
|
+
def _display_selective_mode_info(filter_config: Path | None, mode_name: str) -> None:
|
45
|
+
"""Display selective processing mode information if filter config is provided."""
|
46
|
+
if filter_config:
|
47
|
+
console.print(f"[bold cyan]Selective {mode_name} Mode Enabled[/bold cyan]")
|
48
|
+
console.print(f"[cyan]Filter configuration: {filter_config}[/cyan]")
|
49
|
+
console.print()
|
50
|
+
|
51
|
+
|
52
|
+
def _model_callback(model: str) -> str:
|
53
|
+
"""
|
54
|
+
Validates the model and checks if the required API key is set.
|
55
|
+
This callback is now silent on success.
|
56
|
+
"""
|
57
|
+
if model is not None:
|
58
|
+
api_key_env_var = None
|
59
|
+
if "claude" in model:
|
60
|
+
api_key_env_var = "ANTHROPIC_API_KEY"
|
61
|
+
elif "gpt" in model:
|
62
|
+
api_key_env_var = "OPENAI_API_KEY"
|
63
|
+
elif "gemini" in model:
|
64
|
+
api_key_env_var = "GEMINI_API_KEY"
|
65
|
+
elif "perplexity" in model:
|
66
|
+
api_key_env_var = "PERPLEXITYAI_API_KEY"
|
67
|
+
|
68
|
+
if api_key_env_var and not os.getenv(api_key_env_var):
|
69
|
+
error_message = (
|
70
|
+
f"Environment variable '{api_key_env_var}' is not set. Please set it to use the '{model}' model."
|
71
|
+
)
|
72
|
+
raise typer.BadParameter(error_message)
|
73
|
+
elif not api_key_env_var:
|
74
|
+
pass
|
75
|
+
|
76
|
+
return model
|
77
|
+
|
78
|
+
|
13
79
|
@app.command()
|
14
80
|
def generate(
|
15
81
|
schema_path: Path = typer.Option(..., "--schema", "-s"),
|
@@ -25,11 +91,32 @@ def generate(
|
|
25
91
|
"-c",
|
26
92
|
help="Class name to use for the API client",
|
27
93
|
),
|
94
|
+
filter_config: Path = typer.Option(
|
95
|
+
None,
|
96
|
+
"--filter-config",
|
97
|
+
"-f",
|
98
|
+
help="Path to JSON filter configuration file for selective API client generation.",
|
99
|
+
),
|
28
100
|
):
|
29
|
-
"""
|
30
|
-
|
31
|
-
|
32
|
-
|
101
|
+
"""Generates Python client code from an OpenAPI (Swagger) schema.
|
102
|
+
|
103
|
+
This command automates the creation of an API client class, including
|
104
|
+
methods for each API operation defined in the schema. The generated
|
105
|
+
code is designed to integrate with the Universal MCP application framework.
|
106
|
+
|
107
|
+
It's recommended that the output filename (if specified via -o) matches
|
108
|
+
the API's service name (e.g., 'twitter.py' for a Twitter API client)
|
109
|
+
as this convention is used for organizing applications within U-MCP.
|
110
|
+
If no output path is provided, the generated code will be printed to the console.
|
111
|
+
|
112
|
+
Selective Generation:
|
113
|
+
Use --filter-config to specify which API operations to generate methods for.
|
114
|
+
The JSON configuration format is:
|
115
|
+
{
|
116
|
+
"/users/{user-id}/profile": "get",
|
117
|
+
"/users/{user-id}/settings": "all",
|
118
|
+
"/orders": ["get", "post"]
|
119
|
+
}
|
33
120
|
"""
|
34
121
|
# Import here to avoid circular imports
|
35
122
|
from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
|
@@ -38,16 +125,30 @@ def generate(
|
|
38
125
|
console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
|
39
126
|
raise typer.Exit(1)
|
40
127
|
|
128
|
+
# Validate filter config and display info
|
129
|
+
_validate_filter_config(filter_config)
|
130
|
+
_display_selective_mode_info(filter_config, "API Client Generation")
|
131
|
+
|
41
132
|
try:
|
42
133
|
app_file_data = generate_api_from_schema(
|
43
134
|
schema_path=schema_path,
|
44
135
|
output_path=output_path,
|
45
136
|
class_name=class_name,
|
137
|
+
filter_config_path=str(filter_config) if filter_config else None,
|
46
138
|
)
|
47
139
|
if isinstance(app_file_data, dict) and "code" in app_file_data:
|
48
140
|
console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
|
49
141
|
console.print(app_file_data["code"])
|
142
|
+
if "schemas_code" in app_file_data:
|
143
|
+
console.print("[yellow]Generated schemas code:[/yellow]")
|
144
|
+
console.print(app_file_data["schemas_code"])
|
145
|
+
elif isinstance(app_file_data, tuple) and len(app_file_data) == 2:
|
146
|
+
app_file, schemas_file = app_file_data
|
147
|
+
console.print("[green]API client successfully generated and installed.[/green]")
|
148
|
+
console.print(f"[blue]Application file: {app_file}[/blue]")
|
149
|
+
console.print(f"[blue]Schemas file: {schemas_file}[/blue]")
|
50
150
|
elif isinstance(app_file_data, Path):
|
151
|
+
# Legacy support for single path return
|
51
152
|
console.print("[green]API client successfully generated and installed.[/green]")
|
52
153
|
console.print(f"[blue]Application file: {app_file_data}[/blue]")
|
53
154
|
else:
|
@@ -104,6 +205,194 @@ def docgen(
|
|
104
205
|
raise typer.Exit(1) from e
|
105
206
|
|
106
207
|
|
208
|
+
@app.command()
|
209
|
+
def generate_from_llm(
|
210
|
+
output_dir: Path = typer.Option(
|
211
|
+
None,
|
212
|
+
"--output-dir",
|
213
|
+
"-o",
|
214
|
+
help="Directory to save 'app.py'. If omitted, you will be prompted.",
|
215
|
+
resolve_path=True,
|
216
|
+
),
|
217
|
+
model: str = typer.Option(
|
218
|
+
None,
|
219
|
+
"--model",
|
220
|
+
"-m",
|
221
|
+
help="The LLM model to use for generation. If omitted, you will be prompted.",
|
222
|
+
callback=_model_callback,
|
223
|
+
),
|
224
|
+
):
|
225
|
+
"""
|
226
|
+
Generates an 'app.py' file from a natural language prompt using an LLM.
|
227
|
+
"""
|
228
|
+
from universal_mcp.utils.prompts import APP_GENERATOR_SYSTEM_PROMPT
|
229
|
+
|
230
|
+
console.print(
|
231
|
+
Panel(
|
232
|
+
"🚀 [bold]Welcome to the Universal App Generator[/bold] 🚀",
|
233
|
+
title="[bold blue]MCP[/bold blue]",
|
234
|
+
border_style="blue",
|
235
|
+
expand=False,
|
236
|
+
)
|
237
|
+
)
|
238
|
+
|
239
|
+
if output_dir is None:
|
240
|
+
dir_str = Prompt.ask("[cyan]Enter the directory to save 'app.py'[/cyan]", default=str(Path.cwd()))
|
241
|
+
output_dir = Path(dir_str).resolve()
|
242
|
+
|
243
|
+
if model is None:
|
244
|
+
model_str = Prompt.ask("[cyan]Enter the LLM model to use[/cyan]", default="perplexity/sonar")
|
245
|
+
model = _model_callback(model_str)
|
246
|
+
|
247
|
+
prompt_guidance = (
|
248
|
+
"You can provide the application description (the prompt) in two ways:\n\n"
|
249
|
+
"1. [bold green]From a Text File (Recommended)[/bold green]\n"
|
250
|
+
" - Ideal for longer, detailed, or multi-line prompts.\n"
|
251
|
+
" - Allows you to easily copy, paste, and edit your prompt in a text editor.\n\n"
|
252
|
+
"2. [bold yellow]Directly in the Terminal[/bold yellow]\n"
|
253
|
+
" - Suitable for short, simple, single-line test prompts.\n"
|
254
|
+
" - Not recommended for complex applications as multi-line input is difficult."
|
255
|
+
)
|
256
|
+
console.print(
|
257
|
+
Panel(prompt_guidance, title="[bold]How to Provide Your Prompt[/bold]", border_style="blue", padding=(1, 2))
|
258
|
+
)
|
259
|
+
|
260
|
+
use_file = Confirm.ask("\n[bold]Would you like to provide the prompt from a text file?[/bold]", default=True)
|
261
|
+
|
262
|
+
prompt = ""
|
263
|
+
if use_file:
|
264
|
+
prompt_file = Prompt.ask("[cyan]Please enter the path to your prompt .txt file[/cyan]")
|
265
|
+
try:
|
266
|
+
prompt_path = Path(prompt_file).resolve()
|
267
|
+
if not prompt_path.exists() or not prompt_path.is_file():
|
268
|
+
console.print(f"[bold red]❌ File not found at: {prompt_path}[/bold red]")
|
269
|
+
raise typer.Exit(code=1) from None
|
270
|
+
prompt = prompt_path.read_text(encoding="utf-8")
|
271
|
+
except Exception as e:
|
272
|
+
console.print(f"[bold red]❌ Failed to read prompt file: {e}[/bold red]")
|
273
|
+
raise typer.Exit(code=1) from e
|
274
|
+
else:
|
275
|
+
prompt = Prompt.ask("[cyan]Please describe the application you want to build[/cyan]")
|
276
|
+
if not prompt.strip():
|
277
|
+
console.print("[bold red]❌ Prompt cannot be empty. Aborting.[/bold red]")
|
278
|
+
raise typer.Exit(code=1)
|
279
|
+
|
280
|
+
PROMPT_DISPLAY_LIMIT = 400
|
281
|
+
prompt_for_display = prompt
|
282
|
+
|
283
|
+
if len(prompt) > PROMPT_DISPLAY_LIMIT:
|
284
|
+
total_lines = len(prompt.splitlines())
|
285
|
+
total_chars = len(prompt)
|
286
|
+
|
287
|
+
prompt_for_display = (
|
288
|
+
f"{prompt[:PROMPT_DISPLAY_LIMIT]}...\n\n"
|
289
|
+
f"[italic grey50](Prompt truncated for display. "
|
290
|
+
f"Full prompt is {total_lines} lines, {total_chars} characters)"
|
291
|
+
f"[/italic]"
|
292
|
+
)
|
293
|
+
|
294
|
+
config_summary = (
|
295
|
+
f"[bold blue]📝 Using Prompt:[/bold blue]\n[grey70]{prompt_for_display}[/grey70]\n\n"
|
296
|
+
f"[bold blue]🤖 Using Model:[/bold blue] [cyan]{model}[/cyan]\n"
|
297
|
+
f"[bold blue]💾 Output Directory:[/bold blue] [cyan]{output_dir}[/cyan]"
|
298
|
+
)
|
299
|
+
console.print(
|
300
|
+
Panel(config_summary, title="[bold green]Configuration[/bold green]", border_style="green", padding=(1, 2))
|
301
|
+
)
|
302
|
+
|
303
|
+
try:
|
304
|
+
if not output_dir.exists():
|
305
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
306
|
+
except Exception as e:
|
307
|
+
console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
|
308
|
+
raise typer.Exit(code=1) from e
|
309
|
+
|
310
|
+
output_file_path = output_dir / "app.py"
|
311
|
+
console.print(f"\n[green]Will save generated file to:[/green] [cyan]{output_file_path}[/cyan]\n")
|
312
|
+
|
313
|
+
messages = [
|
314
|
+
{"role": "system", "content": APP_GENERATOR_SYSTEM_PROMPT},
|
315
|
+
{"role": "user", "content": prompt},
|
316
|
+
]
|
317
|
+
|
318
|
+
response = None
|
319
|
+
with Status("[bold green]Generating app code... (this may take a moment)[/bold green]", console=console) as status:
|
320
|
+
try:
|
321
|
+
response = litellm.completion(
|
322
|
+
model=model,
|
323
|
+
messages=messages,
|
324
|
+
temperature=0.1,
|
325
|
+
timeout=120,
|
326
|
+
)
|
327
|
+
except Exception as e:
|
328
|
+
status.update("[bold red]❌ An error occurred during LLM API call.[/bold red]")
|
329
|
+
console.print(Panel(f"Error: {e}", title="[bold red]API Error[/bold red]", border_style="red"))
|
330
|
+
raise typer.Exit(code=1) from e
|
331
|
+
|
332
|
+
if not response or not response.choices:
|
333
|
+
console.print("[bold red]❌ Failed to get a valid response from the LLM.[/bold red]")
|
334
|
+
raise typer.Exit(code=1)
|
335
|
+
|
336
|
+
generated_content = response.choices[0].message.content
|
337
|
+
code_match = re.search(r"```python\n(.*?)\n```", generated_content, re.DOTALL)
|
338
|
+
if code_match:
|
339
|
+
final_code = code_match.group(1).strip()
|
340
|
+
else:
|
341
|
+
console.print(
|
342
|
+
"[yellow]Warning: LLM response did not contain a markdown code block. Using the raw response.[/yellow]"
|
343
|
+
)
|
344
|
+
final_code = generated_content.strip()
|
345
|
+
|
346
|
+
if not final_code:
|
347
|
+
console.print("[bold red]❌ The LLM returned an empty code block. Aborting.[/bold red]")
|
348
|
+
raise typer.Exit(code=1)
|
349
|
+
|
350
|
+
CODE_DISPLAY_LINE_LIMIT = 200
|
351
|
+
code_for_display = final_code
|
352
|
+
is_truncated = False
|
353
|
+
|
354
|
+
code_lines = final_code.splitlines()
|
355
|
+
num_lines = len(code_lines)
|
356
|
+
|
357
|
+
if num_lines > CODE_DISPLAY_LINE_LIMIT:
|
358
|
+
is_truncated = True
|
359
|
+
code_for_display = "\n".join(code_lines[:CODE_DISPLAY_LINE_LIMIT])
|
360
|
+
|
361
|
+
console.print(
|
362
|
+
Panel(
|
363
|
+
Syntax(code_for_display, "python", theme="monokai", line_numbers=True),
|
364
|
+
title="[bold magenta]Generated Code Preview: app.py[/bold magenta]",
|
365
|
+
border_style="magenta",
|
366
|
+
subtitle=f"Total lines: {num_lines}",
|
367
|
+
)
|
368
|
+
)
|
369
|
+
|
370
|
+
if is_truncated:
|
371
|
+
console.print(
|
372
|
+
f"[italic yellow]... Output truncated. Showing the first {CODE_DISPLAY_LINE_LIMIT} of {num_lines} lines. "
|
373
|
+
f"The full code has been saved to the {output_file_path}.[/italic yellow]\n"
|
374
|
+
)
|
375
|
+
|
376
|
+
try:
|
377
|
+
output_file_path.write_text(final_code, encoding="utf-8")
|
378
|
+
console.print(
|
379
|
+
Panel(
|
380
|
+
f"✅ [bold green]Success![/bold green]\nApplication code saved to [cyan]{output_file_path}[/cyan]",
|
381
|
+
title="[bold green]Complete[/bold green]",
|
382
|
+
border_style="green",
|
383
|
+
)
|
384
|
+
)
|
385
|
+
except Exception as e:
|
386
|
+
console.print(
|
387
|
+
Panel(
|
388
|
+
f"Failed to write the generated code to file: {e}",
|
389
|
+
title="[bold red]File Error[/bold red]",
|
390
|
+
border_style="red",
|
391
|
+
)
|
392
|
+
)
|
393
|
+
raise typer.Exit(code=1) from e
|
394
|
+
|
395
|
+
|
107
396
|
@app.command()
|
108
397
|
def init(
|
109
398
|
output_dir: Path | None = typer.Option(
|
@@ -116,7 +405,7 @@ def init(
|
|
116
405
|
None,
|
117
406
|
"--app-name",
|
118
407
|
"-a",
|
119
|
-
help="App name (letters, numbers
|
408
|
+
help="App name (letters, numbers and hyphens only , underscores not allowed)",
|
120
409
|
),
|
121
410
|
integration_type: str | None = typer.Option(
|
122
411
|
None,
|
@@ -129,24 +418,26 @@ def init(
|
|
129
418
|
):
|
130
419
|
"""Initialize a new MCP project using the cookiecutter template."""
|
131
420
|
from cookiecutter.main import cookiecutter
|
421
|
+
from typer import confirm, prompt
|
132
422
|
|
133
|
-
|
423
|
+
from .cli import generate, generate_from_llm
|
134
424
|
|
135
|
-
|
425
|
+
NAME_PATTERN = r"^[a-zA-Z0-9-]+$"
|
426
|
+
|
427
|
+
def validate_app_name(value: str, field_name: str) -> None:
|
136
428
|
if not re.match(NAME_PATTERN, value):
|
137
|
-
console.print(
|
138
|
-
f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
|
139
|
-
)
|
429
|
+
console.print(f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens allowed[/red]")
|
140
430
|
raise typer.Exit(code=1)
|
141
431
|
|
142
|
-
# App name
|
143
432
|
if not app_name:
|
144
433
|
app_name = typer.prompt(
|
145
434
|
"Enter the app name",
|
146
435
|
default="app_name",
|
147
436
|
prompt_suffix=" (e.g., reddit, youtube): ",
|
148
437
|
).strip()
|
149
|
-
|
438
|
+
|
439
|
+
validate_app_name(app_name, "app name")
|
440
|
+
|
150
441
|
app_name = app_name.lower()
|
151
442
|
if not output_dir:
|
152
443
|
path_str = typer.prompt(
|
@@ -167,7 +458,6 @@ def init(
|
|
167
458
|
console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
|
168
459
|
raise typer.Exit(code=1)
|
169
460
|
|
170
|
-
# Integration type
|
171
461
|
if not integration_type:
|
172
462
|
integration_type = typer.prompt(
|
173
463
|
"Choose the integration type",
|
@@ -196,16 +486,106 @@ def init(
|
|
196
486
|
project_dir = output_dir / f"{app_name}"
|
197
487
|
console.print(f"✅ Project created at {project_dir}")
|
198
488
|
|
489
|
+
generate_client = confirm("Do you want to also generate api_client ?")
|
490
|
+
|
491
|
+
if not generate_client:
|
492
|
+
console.print("[yellow]Skipping API client generation.[/yellow]")
|
493
|
+
return
|
494
|
+
|
495
|
+
has_openapi_spec = confirm("Do you have openapi spec for the application just created ?")
|
496
|
+
|
497
|
+
app_dir = app_name.lower().replace("-", "_")
|
498
|
+
target_app_dir = project_dir / "src" / f"universal_mcp_{app_dir}"
|
499
|
+
|
500
|
+
try:
|
501
|
+
target_app_dir.mkdir(parents=True, exist_ok=True)
|
502
|
+
except Exception as e:
|
503
|
+
console.print(f"[red]❌ Failed to create target app directory '{target_app_dir}': {e}[/red]")
|
504
|
+
raise typer.Exit(code=1) from e
|
505
|
+
|
506
|
+
if has_openapi_spec:
|
507
|
+
schema_path_str = prompt("Enter the path to the OpenAPI schema file (JSON or YAML)")
|
508
|
+
schema_path = Path(schema_path_str)
|
509
|
+
|
510
|
+
if not schema_path.exists():
|
511
|
+
console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
|
512
|
+
raise typer.Exit(1)
|
513
|
+
|
514
|
+
class_name = "".join([part.title() for part in app_name.split("-")]) + "App"
|
515
|
+
|
516
|
+
try:
|
517
|
+
console.print("\n[bold blue]Calling 'codegen generate' with provided schema...[/bold blue]")
|
518
|
+
generate(schema_path=schema_path, output_path=target_app_dir, class_name=class_name)
|
519
|
+
|
520
|
+
except typer.Exit as e:
|
521
|
+
console.print(f"[red]API client generation from schema failed. Exit code: {e.exit_code}[/red]")
|
522
|
+
raise
|
523
|
+
except Exception as e:
|
524
|
+
console.print(f"[red]An unexpected error occurred during API client generation from schema: {e}[/red]")
|
525
|
+
raise typer.Exit(code=1) from e
|
526
|
+
|
527
|
+
else:
|
528
|
+
try:
|
529
|
+
llm_model = prompt("Enter the LLM model to use", default="perplexity/sonar")
|
530
|
+
try:
|
531
|
+
_model_callback(llm_model)
|
532
|
+
except typer.BadParameter as e:
|
533
|
+
console.print(f"[red]Validation Error: {e}[/red]")
|
534
|
+
raise typer.Exit(code=1) from e
|
535
|
+
|
536
|
+
llm_prompt_text = prompt("Describe the application and its tools (natural language)")
|
537
|
+
|
538
|
+
console.print("\n[bold blue]Calling 'codegen generate-from-llm'...[/bold blue]")
|
539
|
+
generate_from_llm(output_dir=target_app_dir, model=llm_model, prompt=llm_prompt_text)
|
540
|
+
|
541
|
+
except typer.Exit as e:
|
542
|
+
console.print(f"[red]API client generation from LLM failed. Exit code: {e.exit_code}[/red]")
|
543
|
+
raise
|
544
|
+
except Exception as e:
|
545
|
+
console.print(f"[red]An unexpected error occurred during API client generation from LLM: {e}[/red]")
|
546
|
+
raise typer.Exit(code=1) from e
|
547
|
+
|
199
548
|
|
200
549
|
@app.command()
|
201
550
|
def preprocess(
|
202
|
-
schema_path: Path = typer.Option(
|
203
|
-
|
551
|
+
schema_path: Path = typer.Option(
|
552
|
+
None, "--schema", "-s", help="Path to the input OpenAPI schema file (JSON or YAML)."
|
553
|
+
),
|
554
|
+
output_path: Path = typer.Option(
|
555
|
+
None, "--output", "-o", help="Path to save the processed (enhanced) OpenAPI schema file."
|
556
|
+
),
|
557
|
+
filter_config: Path = typer.Option(
|
558
|
+
None, "--filter-config", "-f", help="Path to JSON filter configuration file for selective processing."
|
559
|
+
),
|
204
560
|
):
|
561
|
+
"""Enhances an OpenAPI schema's descriptions using an LLM.
|
562
|
+
|
563
|
+
This command takes an existing OpenAPI schema and uses a Large Language
|
564
|
+
Model (LLM) to automatically generate or improve the descriptions for
|
565
|
+
API paths, operations, parameters, and schemas. This is particularly
|
566
|
+
helpful for schemas that are auto-generated or lack comprehensive
|
567
|
+
human-written documentation, making the schema more understandable and
|
568
|
+
usable for client generation or manual review.
|
569
|
+
|
570
|
+
Use --filter-config to process only specific paths and methods defined
|
571
|
+
in a JSON configuration file. Format:
|
572
|
+
{
|
573
|
+
"/users/{user-id}/profile": "get",
|
574
|
+
"/users/{user-id}/settings": "all",
|
575
|
+
"/orders/{order-id}": ["get", "put", "delete"]
|
576
|
+
}
|
577
|
+
"""
|
205
578
|
from universal_mcp.utils.openapi.preprocessor import run_preprocessing
|
206
579
|
|
207
|
-
|
208
|
-
|
580
|
+
# Validate filter config and display info
|
581
|
+
_validate_filter_config(filter_config)
|
582
|
+
_display_selective_mode_info(filter_config, "Processing")
|
583
|
+
|
584
|
+
run_preprocessing(
|
585
|
+
schema_path=schema_path,
|
586
|
+
output_path=output_path,
|
587
|
+
filter_config_path=str(filter_config) if filter_config else None,
|
588
|
+
)
|
209
589
|
|
210
590
|
|
211
591
|
@app.command()
|
@@ -239,5 +619,51 @@ def split_api(
|
|
239
619
|
raise typer.Exit(1) from e
|
240
620
|
|
241
621
|
|
622
|
+
@app.command()
|
623
|
+
def generate_tests(
|
624
|
+
app_name: str = typer.Argument(..., help="Name of the app (e.g., 'outlook')"),
|
625
|
+
class_name: str = typer.Argument(..., help="Name of the app class (e.g., 'OutlookApp')"),
|
626
|
+
output_dir: str = typer.Option("tests", "--output", "-o", help="Output directory for the test file"),
|
627
|
+
):
|
628
|
+
"""Generate automated test cases for an app"""
|
629
|
+
from universal_mcp.utils.openapi.test_generator import generate_test_cases
|
630
|
+
|
631
|
+
console.print(f"[blue]Generating test cases for {app_name} ({class_name})...[/blue]")
|
632
|
+
|
633
|
+
try:
|
634
|
+
response = generate_test_cases(app_name, class_name, output_dir)
|
635
|
+
console.print(f"[green]✅ Successfully generated {len(response.test_cases)} test cases![/green]")
|
636
|
+
except ImportError as e:
|
637
|
+
console.print(f"[red]Import Error: {e}[/red]")
|
638
|
+
console.print(f"[yellow]Make sure the module 'universal_mcp_{app_name}' is installed and available.[/yellow]")
|
639
|
+
raise typer.Exit(1) from e
|
640
|
+
except AttributeError as e:
|
641
|
+
console.print(f"[red]Attribute Error: {e}[/red]")
|
642
|
+
console.print(f"[yellow]Make sure the class '{class_name}' exists in 'universal_mcp_{app_name}.app'.[/yellow]")
|
643
|
+
raise typer.Exit(1) from e
|
644
|
+
except Exception as e:
|
645
|
+
console.print(f"[red]Error generating test cases: {e}[/red]")
|
646
|
+
raise typer.Exit(1) from e
|
647
|
+
|
648
|
+
|
649
|
+
@app.command()
|
650
|
+
def postprocess(
|
651
|
+
input_file: Path = typer.Argument(..., help="Path to the input API client Python file"),
|
652
|
+
output_file: Path = typer.Argument(..., help="Path to save the postprocessed API client Python file"),
|
653
|
+
):
|
654
|
+
"""Postprocess API client: add hint tags to docstrings based on HTTP method."""
|
655
|
+
from universal_mcp.utils.openapi.postprocessor import add_hint_tags_to_docstrings
|
656
|
+
|
657
|
+
if not input_file.exists() or not input_file.is_file():
|
658
|
+
console.print(f"[red]Error: Input file {input_file} does not exist or is not a file.[/red]")
|
659
|
+
raise typer.Exit(1)
|
660
|
+
try:
|
661
|
+
add_hint_tags_to_docstrings(str(input_file), str(output_file))
|
662
|
+
console.print(f"[green]Successfully postprocessed {input_file} and saved to {output_file}[/green]")
|
663
|
+
except Exception as e:
|
664
|
+
console.print(f"[red]Error during postprocessing: {e}[/red]")
|
665
|
+
raise typer.Exit(1) from e
|
666
|
+
|
667
|
+
|
242
668
|
if __name__ == "__main__":
|
243
669
|
app()
|