import asyncio import json import logging import os import shutil import subprocess from typing import Any, List, Optional import psutil import typer import uvicorn from dotenv import load_dotenv, set_key from mcp.server.fastmcp import FastMCP from rich.panel import Panel from rich.prompt import Confirm from skyvern.cli.console import console from skyvern.cli.utils import start_services from skyvern.client import SkyvernEnvironment from skyvern.config import settings from skyvern.forge.sdk.core import skyvern_context from skyvern.forge.sdk.forge_log import setup_logger from skyvern.library.skyvern import Skyvern from skyvern.services.script_service import run_script from skyvern.utils import detect_os from skyvern.utils.env_paths import resolve_backend_env_path, resolve_frontend_env_path run_app = typer.Typer(help="Commands to run Skyvern services such as the API server or UI.") mcp = FastMCP("Skyvern") @mcp.tool() async def skyvern_run_task(prompt: str, url: str) -> dict[str, Any]: """Use Skyvern to execute anything in the browser. Useful for accomplishing tasks that require browser automation. This tool uses Skyvern's browser automation to navigate websites and perform actions to achieve the user's intended outcome. It can handle tasks like form filling, clicking buttons, data extraction, and multi-step workflows. It can even help you find updated data on the internet if your model information is outdated. Args: prompt: A natural language description of what needs to be accomplished (e.g. "Book a flight from NYC to LA", "Sign up for the newsletter", "Find the price of item X", "Apply to a job") url: The starting URL of the website where the task should be performed """ skyvern_agent = Skyvern( environment=SkyvernEnvironment.CLOUD, base_url=settings.SKYVERN_BASE_URL, api_key=settings.SKYVERN_API_KEY, ) res = await skyvern_agent.run_task(prompt=prompt, url=url, user_agent="skyvern-mcp", wait_for_completion=True) # TODO: It would be nice if we could return the task URL here output = res.model_dump()["output"] base_url = settings.SKYVERN_BASE_URL run_history_url = ( "https://app.skyvern.com/history" if "skyvern.com" in base_url else "http://localhost:8080/history" ) return {"output": output, "run_history_url": run_history_url} def get_pids_on_port(port: int) -> List[int]: """Return a list of PIDs listening on the given port.""" pids = [] try: for conn in psutil.net_connections(kind="inet"): if conn.laddr and conn.laddr.port == port and conn.pid: pids.append(conn.pid) except Exception: pass return list(set(pids)) def kill_pids(pids: List[int]) -> None: """Kill the given list of PIDs in a cross-platform way.""" host_system = detect_os() for pid in pids: try: if host_system in {"windows", "wsl"}: subprocess.run(f"taskkill /PID {pid} /F", shell=True, check=False) else: os.kill(pid, 9) except Exception: console.print(f"[red]Failed to kill process {pid}[/red]") @run_app.command(name="server") def run_server() -> None: """Run the Skyvern API server.""" load_dotenv(resolve_backend_env_path()) from skyvern.config import settings # noqa: PLC0415 port = settings.PORT console.print(Panel(f"[bold green]Starting Skyvern API Server on port {port}...", border_style="green")) uvicorn.run( "skyvern.forge.api_app:create_api_app", host="0.0.0.0", port=port, log_level="info", factory=True, ) @run_app.command(name="ui") def run_ui() -> None: """Run the Skyvern UI server.""" console.print(Panel("[bold blue]Starting Skyvern UI Server...[/bold blue]", border_style="blue")) try: with console.status("[bold green]Checking for existing process on port 8080...") as status: pids = get_pids_on_port(8080) if pids: status.stop() response = Confirm.ask("Process already running on port 8080. [yellow]Kill it?[/yellow]") if response: kill_pids(pids) console.print("✅ [green]Process killed.[/green]") else: console.print("[yellow]UI server not started. Process already running on port 8080.[/yellow]") return status.stop() except Exception as e: # pragma: no cover - CLI safeguards console.print(f"[red]Error checking for process: {e}[/red]") frontend_env_path = resolve_frontend_env_path() if frontend_env_path is None: console.print("[bold red]ERROR: Skyvern Frontend directory not found.[/bold red]") return frontend_dir = frontend_env_path.parent if not frontend_env_path.exists(): console.print("[bold blue]Setting up frontend .env file...[/bold blue]") shutil.copy(frontend_dir / ".env.example", frontend_env_path) console.print("✅ [green]Successfully set up frontend .env file[/green]") backend_env_path = resolve_backend_env_path() if backend_env_path.exists(): load_dotenv(backend_env_path) skyvern_api_key = os.getenv("SKYVERN_API_KEY") if skyvern_api_key: set_key(frontend_env_path, "VITE_SKYVERN_API_KEY", skyvern_api_key) else: console.print("[red]ERROR: SKYVERN_API_KEY not found in .env file[/red]") else: console.print(f"[red]ERROR: Backend .env file not found at {backend_env_path}[/red]") os.chdir(frontend_dir) try: console.print("đŸ“Ļ [bold blue]Running npm install...[/bold blue]") subprocess.run("npm install --silent", shell=True, check=True) console.print("✅ [green]npm install complete.[/green]") console.print("🚀 [bold blue]Starting npm UI server...[/bold blue]") subprocess.run("npm run start", shell=True, check=True) except subprocess.CalledProcessError as e: console.print(f"[bold red]Error running UI server: {e}[/bold red]") return @run_app.command(name="all") def run_all() -> None: """Run the Skyvern API server and UI server in parallel.""" asyncio.run(start_services()) @run_app.command(name="mcp") def run_mcp() -> None: """Run the MCP server.""" # This breaks the MCP processing because it expects json output only # console.print(Panel("[bold green]Starting MCP Server...[/bold green]", border_style="green")) mcp.run(transport="stdio") @run_app.command( name="code", context_settings={"allow_interspersed_args": False}, ) def run_code( script_path: str = typer.Argument(..., help="Path to the Python script to run"), params: List[str] = typer.Option([], "-p", help="Parameters in format param=value (without leading dash)"), params_json: str = typer.Option(None, "--params", help="JSON string of parameters"), params_file: str = typer.Option(None, "--params-file", help="Path to JSON file with parameters"), ai: Optional[str] = typer.Option( "fallback", "--ai", help="AI mode to use for the script. Options: fallback, proactive or None" ), ) -> None: """Run a Python script with parameters. Supports three ways to pass parameters (in order of priority): 1. JSON file (highest priority): skyvern run code main.py --params-file params.json 2. JSON string: skyvern run code main.py --params '{"param1": "val1", "param2": "val2"}' 3. Individual flags (lowest priority): skyvern run code main.py -p param1=val1 -p param2=val2 Note: For backward compatibility, leading dashes in -p values are automatically stripped. """ # Disable LiteLLM loggers os.environ["LITELLM_LOG"] = "CRITICAL" import litellm # noqa: PLC0415 litellm.suppress_debug_info = True litellm.set_verbose = False logging.getLogger("LiteLLM").setLevel(logging.CRITICAL) logging.getLogger("LiteLLM Router").setLevel(logging.CRITICAL) logging.getLogger("LiteLLM Proxy").setLevel(logging.CRITICAL) settings.LOG_LEVEL = "CRITICAL" setup_logger() # Validate script path if not script_path: console.print("[red]❌ Error: No script path provided[/red]") console.print("[yellow]→ Action: Provide a path to your Python script[/yellow]") console.print("[blue]Example: skyvern run code main.py -p param1=value1[/blue]") raise typer.Exit(code=1) if not os.path.exists(script_path): console.print("[red]❌ Error: Cannot find script file[/red]") console.print(f"[yellow]→ Looked for: {script_path}[/yellow]") console.print("[yellow]→ Action: Check that the file exists and the path is correct[/yellow]") # Show current directory to help user understand relative paths console.print(f"[blue]Current directory: {os.getcwd()}[/blue]") raise typer.Exit(code=1) if not script_path.endswith(".py"): console.print("[red]❌ Error: Invalid file type[/red]") console.print(f"[yellow]→ Provided: {script_path}[/yellow]") console.print("[yellow]→ Action: Please provide a Python script file ending with .py[/yellow]") console.print("[blue]Example: skyvern run code my_script.py[/blue]") raise typer.Exit(code=1) parameters = {} # Priority: params_file > params_json > individual -p flags if params_file: try: with open(params_file) as f: parameters = json.load(f) console.print(f"[blue]✓ Loaded parameters from file: {params_file}[/blue]") except FileNotFoundError: console.print("[red]❌ Error: Cannot find parameters file[/red]") console.print(f"[yellow]→ Looked for: {params_file}[/yellow]") console.print("[yellow]→ Action: Check that the file exists and the path is correct[/yellow]") console.print(f"[blue]Current directory: {os.getcwd()}[/blue]") raise typer.Exit(code=1) except json.JSONDecodeError as e: console.print("[red]❌ Error: Invalid JSON format in parameters file[/red]") console.print(f"[yellow]→ File: {params_file}[/yellow]") console.print(f"[yellow]→ Details: {e}[/yellow]") console.print("[yellow]→ Action: Fix the JSON syntax in your parameters file[/yellow]") console.print('[blue]Expected format: {{"param1": "value1", "param2": "value2"}}[/blue]') raise typer.Exit(code=1) elif params_json: try: parameters = json.loads(params_json) console.print("[blue]✓ Loaded parameters from JSON string[/blue]") except json.JSONDecodeError as e: console.print("[red]❌ Error: Invalid JSON format in --params string[/red]") console.print(f"[yellow]→ Details: {e}[/yellow]") console.print("[yellow]→ Action: Check your JSON syntax (quotes, brackets, commas)[/yellow]") console.print('[blue]Example: --params \'{{"param1": "value1", "param2": "value2"}}\'[/blue]') raise typer.Exit(code=1) elif params: for param in params: # Remove leading dash if present (for backward compatibility) if param.startswith("-"): param = param[1:] if "=" in param: key, value = param.split("=", 1) parameters[key] = value else: console.print("[yellow]âš ī¸ Warning: Skipping invalid parameter format[/yellow]") console.print(f"[yellow]→ Invalid: {param}[/yellow]") console.print("[yellow]→ Expected format: -p param=value[/yellow]") console.print("[blue]Example: -p download_start_date=31/07/2025[/blue]") console.print("[blue]✓ Loaded parameters from command-line flags[/blue]") console.print(Panel(f"[bold green]Running script: {script_path}[/bold green]", border_style="green")) if parameters: console.print("[blue]📋 Parameters:[/blue]") console.print(f"[blue]{json.dumps(parameters, indent=2)}[/blue]") else: console.print("[blue]â„šī¸ Running script without parameters[/blue]") console.print("[dim]Tip: Add parameters with -p, --params, or --params-file[/dim]") # set up skyvern context skyvern_context.set(skyvern_context.SkyvernContext(script_mode=True, ai_mode_override=ai)) try: asyncio.run(run_script(path=script_path, parameters=parameters)) console.print("✅ [green]Script execution completed successfully![/green]") except Exception as e: console.print("[red]❌ Error: Script execution failed[/red]") console.print(f"[yellow]→ Script: {script_path}[/yellow]") console.print(f"[yellow]→ Details: {e}[/yellow]") console.print("[yellow]→ Action: Check the error message above and fix any issues in your script[/yellow]") raise typer.Exit(code=1)