"""Typed response envelopes for MCP tools. Every tool returns a ToolResult subclass. FastMCP serializes the model and exposes its JSON schema to the agent, so field names here are part of the tool contract the LLM sees. """ from __future__ import annotations from pydantic import BaseModel from marketing_intelligence.core.models import VideoData, VideoRef from marketing_intelligence.evasion.detection import PageSignals class ToolResult(BaseModel): """Base envelope for all MCP tool responses.""" success: bool = True error: str | None = None class PageToolResult(ToolResult): """Result of a tool that loaded a TikTok page — carries page-health signals.""" signals: PageSignals | None = None class TagPageResponse(PageToolResult): """Result of scraping a TikTok hashtag page.""" tag: str tag_url: str | None = None video_count_text: str | None = None videos: list[VideoRef] = [] class SoundPageResponse(PageToolResult): """Result of scraping a TikTok sound page.""" sound_id: str sound_page_url: str | None = None title: str | None = None video_count_text: str | None = None video_urls: list[str] = [] class VideoScrapeResponse(VideoData, PageToolResult): """All VideoData fields plus the tool envelope; url is the only required field.""" comment_texts: list[str] = [] class UrlScrapeResponse(PageToolResult): """Result of scraping an arbitrary TikTok URL.""" url: str content: str = "" class PersistResponse(ToolResult): """Result of a checkpoint persist operation.""" mission: str = "unspecified" count: int = 0 class StoreResponse(ToolResult): """Result of a storage write operation.""" mission: str = "unspecified" count: int = 0 class SessionResponse(ToolResult): """Result of creating a browser session.""" session_id: str | None = None browser: str | None = None stealth: str | None = None # playwright only engine: str | None = None profile: str | None = None human_behavior: bool = True delay_factor: float = 1.0 scroll_speed: str = "normal" class SessionClosedResponse(ToolResult): """Result of closing a browser session.""" session_id: str class SessionBehaviorResponse(ToolResult): """Result of updating session behavior tactics.""" session_id: str delay_factor: float | None = None scroll_speed: str | None = None human_behavior: bool | None = None class StealthOption(BaseModel): """A stealth engine option available for a browser.""" name: str description: str use_when: str | None = None class BrowserCapability(BaseModel): """A browser and its available stealth engine options.""" name: str stealths: list[StealthOption] = [] supports: list[str] = [] note: str | None = None class BrowsersResponse(ToolResult): """Result of listing available browsers and their stealth engines.""" browsers: list[BrowserCapability] = [] tip: str | None = None class ProfilesResponse(ToolResult): """Result of listing available fingerprint profiles.""" profiles: list[str] = [] class RunRecordResponse(ToolResult): """Result of registering a discovery run.""" run_id: str | None = None class SoundUrlResponse(ToolResult): """Result of building a TikTok sound page URL.""" sound_id: str url: str | None = None class ArtistProfileResponse(PageToolResult): """Result of scraping a TikTok artist profile page.""" handle: str nickname: str | None = None followers: int | None = None following: int | None = None likes: int | None = None video_count: int | None = None class FindSoundIdResponse(ToolResult): """Result of searching for the original sound_id for a track.""" artist_handle: str track_tag: str sound_id: str | None = None class ScrapeTagCommentsResponse(ToolResult): """Result of scrape_tag_comments_stream — all videos for one tag processed in one call.""" tag: str videos_processed: int = 0 errors: int = 0 video_ids: list[str] = [] class BatchVideoResult(BaseModel): """Single-video result within a batch scrape.""" url: str success: bool = True video_id: str | None = None views: int | None = None likes: int | None = None comments: int | None = None shares: int | None = None favorites: int | None = None sound_id: str | None = None error: str | None = None class BatchScrapeResponse(ToolResult): """Result of a batch video scrape operation.""" results: list[BatchVideoResult] = [] count: int = 0 failed_count: int = 0 class VideoForSentiment(BaseModel): """Video record with comments, used as sentiment analysis input.""" video_id: str | None = None url: str = "" track_name: str | None = None caption: str | None = None hashtags: list[str] = [] sound_id: str | None = None is_original_sound: bool = False location: str | None = None video_description: str | None = None comment_count: int = 0 comment_texts: list[str] = [] class AllVideoCommentsResponse(ToolResult): """Result of fetching videos with comments from storage.""" videos: list[VideoForSentiment] = [] total_videos: int = 0 class VideoSentimentSummary(BaseModel): """Sentiment analysis summary for a single video.""" video_id: str | None = None url: str = "" track_name: str | None = None comment_count: int = 0 sentiment: str | None = None confidence: float | None = None summary: str | None = None key_themes: list[str] = [] video_description: str | None = None class AllVideoSentimentsResponse(ToolResult): """Result of fetching video sentiment summaries from storage.""" videos: list[VideoSentimentSummary] = [] total_videos: int = 0