align workflow CLI commands with MCP parity (#4792)

This commit is contained in:
Marc Kelechava
2026-02-18 11:34:12 -08:00
committed by GitHub
parent 2f6850ce20
commit 46a7ec1d26
12 changed files with 1609 additions and 151 deletions

View File

@@ -5,7 +5,7 @@ import logging
import os
import shutil
import subprocess
from typing import Any, List, Optional
from typing import Annotated, List, Literal, Optional
import psutil
import typer
@@ -13,17 +13,17 @@ import uvicorn
from dotenv import load_dotenv, set_key
from rich.panel import Panel
from rich.prompt import Confirm
from starlette.middleware import Middleware
from skyvern.cli.console import console
from skyvern.cli.core.client import close_skyvern
from skyvern.cli.core.session_manager import close_current_session
from skyvern.cli.core.mcp_http_auth import MCPAPIKeyMiddleware, close_auth_db
from skyvern.cli.core.session_manager import close_current_session, set_stateless_http_mode
from skyvern.cli.mcp_tools import mcp # Uses standalone fastmcp (v2.x)
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
@@ -36,7 +36,10 @@ async def _cleanup_mcp_resources() -> None:
try:
await close_current_session()
finally:
await close_skyvern()
try:
await close_skyvern()
finally:
await close_auth_db()
def _cleanup_mcp_resources_blocking() -> None:
@@ -67,39 +70,6 @@ def _cleanup_mcp_resources_sync() -> None:
logger.debug("Skipping MCP cleanup because event loop is still running")
@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)
output = res.model_dump()["output"]
if res.app_url:
task_url = res.app_url
else:
if res.run_id and res.run_id.startswith("wr_"):
task_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/runs/{res.run_id}/overview"
else:
task_url = f"{settings.SKYVERN_APP_URL.rstrip('/')}/tasks/{res.run_id}/actions"
return {"output": output, "task_url": task_url, "run_id": res.run_id}
def get_pids_on_port(port: int) -> List[int]:
"""Return a list of PIDs listening on the given port."""
pids = []
@@ -295,20 +265,61 @@ def run_dev() -> None:
@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"))
def run_mcp(
transport: Annotated[
Literal["stdio", "sse", "streamable-http"],
typer.Option(
"--transport",
help="MCP transport: stdio (default), sse, or streamable-http.",
),
] = "stdio",
host: Annotated[str, typer.Option("--host", help="Host for HTTP transports.")] = "0.0.0.0",
port: Annotated[int, typer.Option("--port", help="Port for HTTP transports.")] = 8000,
path: Annotated[str, typer.Option("--path", help="HTTP endpoint path for MCP transport.")] = "/mcp",
stateless_http: Annotated[
bool,
typer.Option(
"--stateless-http/--no-stateless-http",
help="Use stateless HTTP semantics for HTTP transports (ignored for stdio).",
),
] = True,
) -> None:
"""Run the MCP server with configurable transport for local or remote hosting."""
path = _normalize_mcp_path(path)
stateless_http_enabled = transport != "stdio" and stateless_http
# atexit covers signal-based exits (SIGTERM); finally covers normal
# mcp.run() completion or unhandled exceptions. Both are needed because
# atexit doesn't fire on normal return and finally doesn't fire on signals.
atexit.register(_cleanup_mcp_resources_sync)
set_stateless_http_mode(stateless_http_enabled)
try:
mcp.run(transport="stdio")
if transport == "stdio":
mcp.run(transport="stdio")
return
middleware = [Middleware(MCPAPIKeyMiddleware)]
mcp.run(
transport=transport,
host=host,
port=port,
path=path,
middleware=middleware,
stateless_http=stateless_http_enabled,
)
finally:
set_stateless_http_mode(False)
_cleanup_mcp_resources_blocking()
def _normalize_mcp_path(path: str) -> str:
path = path.strip()
if not path:
return "/mcp"
if not path.startswith("/"):
return f"/{path}"
return path
@run_app.command(
name="code",
context_settings={"allow_interspersed_args": False},