import asyncio import os import shutil import subprocess from pathlib import Path from typing import Any, List 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.utils import start_services from skyvern.config import settings from skyvern.library.skyvern import Skyvern from skyvern.utils import detect_os from .console import console 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( 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() load_dotenv(".env") 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:app", host="0.0.0.0", port=port, log_level="info", ) @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]") # Try multiple methods to find the frontend directory frontend_dir = None # Method 1: Relative to current working directory cwd_frontend = Path.cwd() / "skyvern-frontend" if cwd_frontend.exists(): frontend_dir = cwd_frontend # Method 2: Relative to the module file (original method) if frontend_dir is None: module_based_frontend = Path(__file__).parent.parent.parent / "skyvern-frontend" if module_based_frontend.exists(): frontend_dir = module_based_frontend # Method 3: Search up the directory tree from current working directory if frontend_dir is None: current = Path.cwd() while current != current.parent: # Stop at filesystem root candidate = current / "skyvern-frontend" if candidate.exists() and candidate.is_dir(): frontend_dir = candidate break current = current.parent if frontend_dir is None: console.print( f"[bold red]ERROR: Skyvern Frontend directory not found. Searched in:[/bold red]\n" f" • {cwd_frontend}\n" f" • {Path(__file__).parent.parent.parent / 'skyvern-frontend'}\n" f" • Parent directories of {Path.cwd()}\n" f"[bold red]Are you in the right repo?[/bold red]" ) return frontend_env_path = frontend_dir / ".env" 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]") # Look for .env file in multiple locations main_env_path = None env_search_paths = [ Path.cwd() / ".env", frontend_dir.parent / ".env", Path(__file__).parent.parent.parent / ".env", ] for env_path in env_search_paths: if env_path.exists(): main_env_path = env_path break if main_env_path and main_env_path.exists(): load_dotenv(main_env_path) skyvern_api_key = os.getenv("SKYVERN_API_KEY") if skyvern_api_key: set_key(str(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("[red]ERROR: .env file not found[/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")