"""Browser tools — session lifecycle and capabilities.""" from __future__ import annotations import asyncio from typing import Literal import structlog from marketing_intelligence.browser.pool import _pool from marketing_intelligence.core.config import settings from marketing_intelligence.evasion.profiles import list_profiles from marketing_intelligence.evasion.proxy import ProxyManager from marketing_intelligence.evasion.session import ( create_session, delete_session, get_session, update_session, ) from marketing_intelligence.mcp.app import mcp from marketing_intelligence.mcp.tools.guard import tool_guard from marketing_intelligence.mcp.tools.responses import ( BrowserCapability, BrowsersResponse, ProfilesResponse, SessionBehaviorResponse, SessionClosedResponse, SessionResponse, StealthOption, ) logger = structlog.get_logger("mcp.tools") BrowserName = Literal["playwright", "nodriver", "drissionpage"] StealthEngine = Literal["camoufox", "chromium", "webkit"] ProfileName = Literal["desktop", "mobile_ios", "mobile_android"] ScrollSpeed = Literal["slow", "normal", "fast"] _BROWSERS = [ BrowserCapability( name="playwright", stealths=[ StealthOption( name="camoufox", description="Firefox + fingerprint spoofing.", use_when="default", ), StealthOption( name="chromium", description="Chrome fingerprint.", use_when="camoufox blocked", ), StealthOption( name="webkit", description="Safari fingerprint.", use_when="both blocked", ), ], supports=["scrape_tag_page", "scrape_sound_page", "scrape_video"], ), BrowserCapability( name="nodriver", stealths=[ StealthOption( name="chrome", description="Chrome without WebDriver detection." ) ], supports=["scrape_tag_page", "scrape_sound_page"], note="scrape_video NOT supported", ), BrowserCapability( name="drissionpage", stealths=[StealthOption(name="chrome", description="Chrome via CDP.")], supports=["scrape_tag_page", "scrape_sound_page"], note="scrape_video NOT supported", ), ] class SessionManager: """Manages browser session lifecycle.""" def create( self, browser: str = "playwright", stealth: str = "camoufox", profile: str = "desktop", human_behavior: bool = True, delay_factor: float = 1.0, scroll_speed: str = "normal", proxy_country: str | None = None, ) -> SessionResponse: """Acquire a proxy (if configured) and register a new browser session.""" engine = stealth if browser == "playwright" else browser proxy = None if settings.proxy_list or settings.proxy_lambda_name: try: mgr = ProxyManager() ps = mgr.acquire(country=proxy_country) proxy = ps.to_playwright_dict() logger.info( "session.proxy_acquired", server=proxy.get("server"), country=proxy_country or ps.country or "any", ) except (OSError, RuntimeError, ValueError) as e: logger.warning("session.proxy_acquire_failed", error=str(e)[:200]) else: logger.info("session.proxy_none", reason="no PROXY_LIST configured") session = create_session( engine=engine, profile_name=profile, human_behavior=human_behavior, delay_factor=delay_factor, scroll_speed=scroll_speed, proxy=proxy, ) return SessionResponse( session_id=session.session_id, browser=browser, stealth=stealth if browser == "playwright" else None, engine=session.engine, profile=session.profile_name, human_behavior=session.human_behavior, delay_factor=session.delay_factor, scroll_speed=session.scroll_speed, ) def close(self, session_id: str) -> SessionClosedResponse: """Delete the session record and schedule the browser page for closure.""" if not delete_session(session_id): return SessionClosedResponse( success=False, session_id=session_id, error=f"Session '{session_id}' not found", ) task = asyncio.ensure_future(_pool.close(session_id)) _background_tasks.add(task) task.add_done_callback(_background_tasks.discard) return SessionClosedResponse(session_id=session_id) def update_tactics( self, session_id: str, delay_factor: float | None = None, scroll_speed: str | None = None, human_behavior: bool | None = None, ) -> SessionBehaviorResponse: """Update one or more behavior settings on an active session.""" if not get_session(session_id): return SessionBehaviorResponse( success=False, session_id=session_id, error=f"Session '{session_id}' not found", ) updates = { k: v for k, v in { "delay_factor": delay_factor, "scroll_speed": scroll_speed, "human_behavior": human_behavior, }.items() if v is not None } updated = update_session(session_id, **updates) if not updated: return SessionBehaviorResponse( success=False, session_id=session_id, error=f"Session '{session_id}' not found", ) return SessionBehaviorResponse( session_id=session_id, delay_factor=updated.delay_factor, scroll_speed=updated.scroll_speed, human_behavior=updated.human_behavior, ) @staticmethod def browsers() -> BrowsersResponse: """Return available browsers and their stealth engine options.""" return BrowsersResponse( browsers=_BROWSERS, tip="On block: close session → create with next engine." ) @staticmethod def profiles() -> ProfilesResponse: """Return available fingerprint profiles.""" return ProfilesResponse(profiles=list_profiles()) _mgr = SessionManager() _background_tasks: set[asyncio.Future[None]] = set() # ── MCP tool wrappers ──────────────────────────────────────────────────────── @mcp.tool() @tool_guard(SessionResponse) async def create_browser_session( browser: BrowserName = "playwright", stealth: StealthEngine = "camoufox", profile: ProfileName = "desktop", human_behavior: bool = True, delay_factor: float = 1.0, scroll_speed: ScrollSpeed = "normal", proxy_country: str | None = None, ) -> SessionResponse: """Create a browser session. Returns session_id to pass to scrape tools. proxy_country: 2-letter ISO code (e.g. 'gb', 'us', 'de') to pin the proxy to a country. Omit to pick randomly from all available proxies. """ return _mgr.create( browser, stealth, profile, human_behavior, delay_factor, scroll_speed, proxy_country, ) @mcp.tool() @tool_guard(SessionClosedResponse, echo=("session_id",)) async def close_browser_session(session_id: str) -> SessionClosedResponse: """Close a browser session and release it from the pool.""" return _mgr.close(session_id) @mcp.tool() @tool_guard(SessionBehaviorResponse, echo=("session_id",)) async def apply_behavior_tactics( session_id: str, delay_factor: float | None = None, scroll_speed: ScrollSpeed | None = None, human_behavior: bool | None = None, ) -> SessionBehaviorResponse: """Adjust behavior tactics on an existing session.""" return _mgr.update_tactics(session_id, delay_factor, scroll_speed, human_behavior) @mcp.tool() @tool_guard(BrowsersResponse) async def list_available_browsers() -> BrowsersResponse: """List available browsers and their stealth engines.""" return _mgr.browsers() @mcp.tool() @tool_guard(ProfilesResponse) async def list_available_profiles() -> ProfilesResponse: """List available device/browser fingerprint profiles.""" return _mgr.profiles()