Add PR A CLI browser command parity with MCP (#4789)

This commit is contained in:
Marc Kelechava
2026-02-18 10:53:55 -08:00
committed by GitHub
parent 94fa05b689
commit 2c5d8022d7
9 changed files with 1119 additions and 5 deletions

View File

@@ -3,17 +3,26 @@ from __future__ import annotations
import os
from contextvars import ContextVar
import structlog
from skyvern.client import SkyvernEnvironment
from skyvern.config import settings
from skyvern.library.skyvern import Skyvern
_skyvern_instance: ContextVar[Skyvern | None] = ContextVar("skyvern_instance", default=None)
_global_skyvern_instance: Skyvern | None = None
LOG = structlog.get_logger(__name__)
def get_skyvern() -> Skyvern:
"""Get or create a Skyvern client instance."""
global _global_skyvern_instance
instance = _skyvern_instance.get()
if instance is None:
instance = _global_skyvern_instance
if instance is not None:
_skyvern_instance.set(instance)
return instance
api_key = settings.SKYVERN_API_KEY or os.environ.get("SKYVERN_API_KEY")
@@ -28,5 +37,28 @@ def get_skyvern() -> Skyvern:
else:
instance = Skyvern.local()
_global_skyvern_instance = instance
_skyvern_instance.set(instance)
return instance
async def close_skyvern() -> None:
"""Close active Skyvern client(s) and release Playwright resources."""
global _global_skyvern_instance
instances: list[Skyvern] = []
seen: set[int] = set()
for candidate in (_skyvern_instance.get(), _global_skyvern_instance):
if candidate is None or id(candidate) in seen:
continue
seen.add(id(candidate))
instances.append(candidate)
for instance in instances:
try:
await instance.aclose()
except Exception:
LOG.warning("Failed to close Skyvern client", exc_info=True)
_skyvern_instance.set(None)
_global_skyvern_instance = None

View File

@@ -5,9 +5,13 @@ from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, AsyncIterator
import structlog
from .client import get_skyvern
from .result import BrowserContext, ErrorCode, make_error
LOG = structlog.get_logger(__name__)
if TYPE_CHECKING:
from skyvern.library.skyvern_browser import SkyvernBrowser
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage
@@ -23,20 +27,46 @@ class SessionState:
_current_session: ContextVar[SessionState | None] = ContextVar("mcp_session", default=None)
_global_session: SessionState | None = None
def get_current_session() -> SessionState:
global _global_session
state = _current_session.get()
if state is None:
state = SessionState()
if _global_session is None:
_global_session = SessionState()
state = _global_session
_current_session.set(state)
return state
def set_current_session(state: SessionState) -> None:
global _global_session
_global_session = state
_current_session.set(state)
def _matches_current(
current: SessionState,
*,
session_id: str | None = None,
cdp_url: str | None = None,
local: bool = False,
) -> bool:
if current.browser is None or current.context is None:
return False
if session_id:
return current.context.mode == "cloud_session" and current.context.session_id == session_id
if cdp_url:
return current.context.mode == "cdp" and current.context.cdp_url == cdp_url
if local:
return current.context.mode == "local"
return False
async def resolve_browser(
session_id: str | None = None,
cdp_url: str | None = None,
@@ -54,6 +84,11 @@ async def resolve_browser(
skyvern = get_skyvern()
current = get_current_session()
if _matches_current(current, session_id=session_id, cdp_url=cdp_url, local=local):
# _matches_current() guarantees both are non-None
assert current.browser is not None and current.context is not None
return current.browser, current.context
browser: SkyvernBrowser | None = None
try:
if session_id:
@@ -94,6 +129,31 @@ async def resolve_browser(
raise BrowserNotAvailableError()
async def close_current_session() -> None:
"""Close the active browser session (if any) and clear local session state."""
from .session_ops import do_session_close
current = get_current_session()
try:
if current.context and current.context.mode == "cloud_session" and current.context.session_id:
try:
skyvern = get_skyvern()
await do_session_close(skyvern, current.context.session_id)
# Prevent SkyvernBrowser.close() from making a redundant API call
if current.browser is not None:
current.browser._browser_session_id = None
except Exception:
LOG.warning(
"Best-effort cloud session close failed",
session_id=current.context.session_id,
exc_info=True,
)
if current.browser is not None:
await current.browser.close()
finally:
set_current_session(SessionState())
async def get_page(
session_id: str | None = None,
cdp_url: str | None = None,