webpuppet_mcp/
tools.rs

1//! Tool definitions and registry for MCP server.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use serde::Deserialize;
7use serde_json::json;
8use tokio::sync::RwLock;
9
10use webpuppet::{
11    BrowserDetector, InterventionHandler, InterventionState, Operation, PermissionGuard,
12    PromptRequest, Provider, ScreeningConfig, WebPuppet,
13};
14
15use crate::error::{Error, Result};
16use crate::protocol::{ContentItem, ToolCallResult, ToolDefinition};
17
18/// Tool trait for implementing MCP tools.
19#[async_trait::async_trait]
20pub trait Tool: Send + Sync {
21    /// Get the tool definition.
22    fn definition(&self) -> ToolDefinition;
23
24    /// Execute the tool with the given arguments.
25    async fn execute(
26        &self,
27        arguments: serde_json::Value,
28        context: &ToolContext,
29    ) -> Result<ToolCallResult>;
30}
31
32/// Context passed to tools during execution.
33pub struct ToolContext {
34    /// WebPuppet instance (lazy-initialized).
35    pub puppet: Arc<RwLock<Option<WebPuppet>>>,
36    /// Permission guard.
37    pub permissions: Arc<PermissionGuard>,
38    /// Screening configuration.
39    pub screening_config: ScreeningConfig,
40    /// Intervention handler for human-in-the-loop.
41    pub intervention_handler: Arc<RwLock<InterventionHandler>>,
42    /// Whether to run browser in headless mode (default: true).
43    pub headless: bool,
44}
45
46impl ToolContext {
47    /// Create a new tool context.
48    pub fn new(permissions: PermissionGuard) -> Self {
49        Self {
50            puppet: Arc::new(RwLock::new(None)),
51            permissions: Arc::new(permissions),
52            screening_config: ScreeningConfig::default(),
53            intervention_handler: Arc::new(RwLock::new(InterventionHandler::new())),
54            headless: true,
55        }
56    }
57
58    /// Create a new tool context with visible browser (non-headless).
59    pub fn with_visible_browser(permissions: PermissionGuard) -> Self {
60        Self {
61            puppet: Arc::new(RwLock::new(None)),
62            permissions: Arc::new(permissions),
63            screening_config: ScreeningConfig::default(),
64            intervention_handler: Arc::new(RwLock::new(InterventionHandler::new())),
65            headless: false,
66        }
67    }
68
69    /// Get or create the WebPuppet instance.
70    pub async fn get_puppet(&self) -> Result<WebPuppet> {
71        let guard = self.puppet.read().await;
72        if let Some(ref _puppet) = *guard {
73            // Clone isn't implemented, so we'll need to recreate
74            drop(guard);
75        } else {
76            drop(guard);
77        }
78
79        // Create new puppet with headless setting
80        let puppet = WebPuppet::builder()
81            .with_all_providers()
82            .headless(self.headless)
83            .with_screening_config(self.screening_config.clone())
84            .build()
85            .await?;
86
87        Ok(puppet)
88    }
89}
90
91/// Registry of available tools.
92pub struct ToolRegistry {
93    tools: HashMap<String, Arc<dyn Tool>>,
94    context: Arc<ToolContext>,
95}
96
97impl ToolRegistry {
98    /// Create a new tool registry with default tools (headless browser).
99    pub fn new(permissions: PermissionGuard) -> Self {
100        Self::with_context(ToolContext::new(permissions))
101    }
102
103    /// Create a new tool registry with visible browser.
104    pub fn with_visible_browser(permissions: PermissionGuard) -> Self {
105        Self::with_context(ToolContext::with_visible_browser(permissions))
106    }
107
108    /// Create a new tool registry with custom context.
109    fn with_context(context: ToolContext) -> Self {
110        let context = Arc::new(context);
111        let mut tools: HashMap<String, Arc<dyn Tool>> = HashMap::new();
112
113        // Register built-in tools
114        let prompt_tool = Arc::new(PromptTool);
115        tools.insert(prompt_tool.definition().name.clone(), prompt_tool);
116
117        let list_providers_tool = Arc::new(ListProvidersTool);
118        tools.insert(
119            list_providers_tool.definition().name.clone(),
120            list_providers_tool,
121        );
122
123        let provider_caps_tool = Arc::new(ProviderCapabilitiesTool);
124        tools.insert(
125            provider_caps_tool.definition().name.clone(),
126            provider_caps_tool,
127        );
128
129        let detect_browsers_tool = Arc::new(DetectBrowsersTool);
130        tools.insert(
131            detect_browsers_tool.definition().name.clone(),
132            detect_browsers_tool,
133        );
134
135        let screenshot_tool = Arc::new(ScreenshotTool);
136        tools.insert(screenshot_tool.definition().name.clone(), screenshot_tool);
137
138        let check_permission_tool = Arc::new(CheckPermissionTool);
139        tools.insert(
140            check_permission_tool.definition().name.clone(),
141            check_permission_tool,
142        );
143
144        // Intervention tools
145        let intervention_status_tool = Arc::new(InterventionStatusTool);
146        tools.insert(
147            intervention_status_tool.definition().name.clone(),
148            intervention_status_tool,
149        );
150
151        let intervention_complete_tool = Arc::new(InterventionCompleteTool);
152        tools.insert(
153            intervention_complete_tool.definition().name.clone(),
154            intervention_complete_tool,
155        );
156
157        let intervention_pause_tool = Arc::new(InterventionPauseTool);
158        tools.insert(
159            intervention_pause_tool.definition().name.clone(),
160            intervention_pause_tool,
161        );
162
163        let intervention_resume_tool = Arc::new(InterventionResumeTool);
164        tools.insert(
165            intervention_resume_tool.definition().name.clone(),
166            intervention_resume_tool,
167        );
168
169        // Navigation and status tools
170        let navigate_tool = Arc::new(NavigateTool);
171        tools.insert(navigate_tool.definition().name.clone(), navigate_tool);
172
173        let browser_status_tool = Arc::new(BrowserStatusTool);
174        tools.insert(
175            browser_status_tool.definition().name.clone(),
176            browser_status_tool,
177        );
178
179        Self { tools, context }
180    }
181
182    /// Get tool definitions.
183    pub fn list_tools(&self) -> Vec<ToolDefinition> {
184        self.tools.values().map(|t| t.definition()).collect()
185    }
186
187    /// Execute a tool by name.
188    pub async fn execute(
189        &self,
190        name: &str,
191        arguments: serde_json::Value,
192    ) -> Result<ToolCallResult> {
193        let tool = self
194            .tools
195            .get(name)
196            .ok_or_else(|| Error::ToolNotFound(name.to_string()))?;
197
198        tool.execute(arguments, &self.context).await
199    }
200
201    /// Register a custom tool.
202    pub fn register(&mut self, tool: Arc<dyn Tool>) {
203        let name = tool.definition().name.clone();
204        self.tools.insert(name, tool);
205    }
206}
207
208// ============================================================================
209// Built-in Tools
210// ============================================================================
211
212/// Tool for sending prompts to AI providers.
213pub struct PromptTool;
214
215#[derive(Debug, Deserialize)]
216struct PromptArgs {
217    /// Provider to use (claude, grok, gemini).
218    provider: String,
219    /// Message to send.
220    message: String,
221    /// Optional context/system prompt.
222    context: Option<String>,
223}
224
225#[async_trait::async_trait]
226impl Tool for PromptTool {
227    fn definition(&self) -> ToolDefinition {
228        ToolDefinition {
229            name: "webpuppet_prompt".into(),
230            description: "Send a prompt through browser automation (AI providers + select web tools). Uses existing authenticated sessions.".into(),
231            input_schema: json!({
232                "type": "object",
233                "properties": {
234                    "provider": {
235                        "type": "string",
236                        "enum": ["claude", "grok", "gemini", "chatgpt", "perplexity", "notebooklm", "kaggle"],
237                        "description": "Provider/tool to use"
238                    },
239                    "message": {
240                        "type": "string",
241                        "description": "The prompt message to send"
242                    },
243                    "context": {
244                        "type": "string",
245                        "description": "Optional context or system instructions"
246                    }
247                },
248                "required": ["provider", "message"]
249            }),
250        }
251    }
252
253    async fn execute(
254        &self,
255        arguments: serde_json::Value,
256        context: &ToolContext,
257    ) -> Result<ToolCallResult> {
258        // Check permission
259        context
260            .permissions
261            .require(Operation::SendPrompt)
262            .map_err(|e| Error::PermissionDenied(e.to_string()))?;
263
264        // Parse arguments
265        let args: PromptArgs =
266            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
267
268        // Parse provider
269        let provider = match args.provider.to_lowercase().as_str() {
270            "claude" => Provider::Claude,
271            "grok" => Provider::Grok,
272            "gemini" => Provider::Gemini,
273            "chatgpt" | "openai" => Provider::ChatGpt,
274            "perplexity" => Provider::Perplexity,
275            "notebooklm" | "notebook" => Provider::NotebookLm,
276            "kaggle" => Provider::Kaggle,
277            _ => {
278                return Err(Error::InvalidParams(format!(
279                    "unknown provider: {}",
280                    args.provider
281                )))
282            }
283        };
284
285        // Build request
286        let mut request = PromptRequest::new(args.message);
287        if let Some(ctx) = args.context {
288            request = request.with_context(ctx);
289        }
290
291        // Get puppet and send prompt
292        let puppet = context.get_puppet().await?;
293
294        // Authenticate if needed
295        puppet.authenticate(provider).await?;
296
297        // Send with screening
298        let (response, screening) = puppet.prompt_screened(provider, request).await?;
299
300        // Close puppet
301        puppet.close().await.ok();
302
303        // Format result
304        let result_text = if screening.passed {
305            response.text
306        } else {
307            format!(
308                "[SECURITY WARNING: Response had risk score {:.2}]\n\n{}",
309                screening.risk_score, response.text
310            )
311        };
312
313        Ok(ToolCallResult {
314            content: vec![ContentItem::text(result_text)],
315            is_error: false,
316        })
317    }
318}
319
320/// Tool for listing available AI providers.
321pub struct ListProvidersTool;
322
323#[async_trait::async_trait]
324impl Tool for ListProvidersTool {
325    fn definition(&self) -> ToolDefinition {
326        ToolDefinition {
327            name: "webpuppet_list_providers".into(),
328            description: "List available AI providers and their status.".into(),
329            input_schema: json!({
330                "type": "object",
331                "properties": {},
332                "required": []
333            }),
334        }
335    }
336
337    async fn execute(
338        &self,
339        _arguments: serde_json::Value,
340        _context: &ToolContext,
341    ) -> Result<ToolCallResult> {
342        let providers = [
343            (
344                "claude",
345                "Claude (Anthropic)",
346                "https://claude.ai",
347                "Large context, artifacts, code",
348            ),
349            (
350                "grok",
351                "Grok (X/xAI)",
352                "https://x.com/i/grok",
353                "Real-time info, integrated with X",
354            ),
355            (
356                "gemini",
357                "Gemini (Google)",
358                "https://gemini.google.com",
359                "Google integration, large context",
360            ),
361            (
362                "chatgpt",
363                "ChatGPT (OpenAI)",
364                "https://chat.openai.com",
365                "GPT-4o, vision, code, web search",
366            ),
367            (
368                "perplexity",
369                "Perplexity AI",
370                "https://www.perplexity.ai",
371                "Search-focused, sources cited",
372            ),
373            (
374                "notebooklm",
375                "NotebookLM (Google)",
376                "https://notebooklm.google.com",
377                "Research assistant, 500k context",
378            ),
379            (
380                "kaggle",
381                "Kaggle (Datasets)",
382                "https://www.kaggle.com/datasets",
383                "Dataset search/catalog; returns dataset page links",
384            ),
385        ];
386
387        let text = providers
388            .iter()
389            .map(|(id, name, url, features)| {
390                format!(
391                    "- **{}** (`{}`): [{}]({})\n  _{}_",
392                    name, id, url, url, features
393                )
394            })
395            .collect::<Vec<_>>()
396            .join("\n");
397
398        Ok(ToolCallResult {
399            content: vec![ContentItem::text(format!(
400                "# Available Providers\n\n{}\n\n*Note: Uses browser sessions; some providers require login.*",
401                text
402            ))],
403            is_error: false,
404        })
405    }
406}
407
408/// Tool for retrieving declared provider capabilities.
409pub struct ProviderCapabilitiesTool;
410
411#[derive(Debug, Deserialize)]
412struct ProviderCapabilitiesArgs {
413    /// Provider/tool to inspect.
414    provider: String,
415}
416
417#[async_trait::async_trait]
418impl Tool for ProviderCapabilitiesTool {
419    fn definition(&self) -> ToolDefinition {
420        ToolDefinition {
421            name: "webpuppet_provider_capabilities".into(),
422            description: "Get declared capabilities for a provider/tool (conversation, vision, file upload, web search, etc).".into(),
423            input_schema: json!({
424                "type": "object",
425                "properties": {
426                    "provider": {
427                        "type": "string",
428                        "enum": ["claude", "grok", "gemini", "chatgpt", "perplexity", "notebooklm", "kaggle"],
429                        "description": "Provider/tool to inspect"
430                    }
431                },
432                "required": ["provider"]
433            }),
434        }
435    }
436
437    async fn execute(
438        &self,
439        arguments: serde_json::Value,
440        context: &ToolContext,
441    ) -> Result<ToolCallResult> {
442        context
443            .permissions
444            .require(Operation::ReadContent)
445            .map_err(|e| Error::PermissionDenied(e.to_string()))?;
446
447        let args: ProviderCapabilitiesArgs =
448            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
449
450        let provider = match args.provider.to_lowercase().as_str() {
451            "claude" => Provider::Claude,
452            "grok" => Provider::Grok,
453            "gemini" => Provider::Gemini,
454            "chatgpt" | "openai" => Provider::ChatGpt,
455            "perplexity" => Provider::Perplexity,
456            "notebooklm" | "notebook" => Provider::NotebookLm,
457            "kaggle" => Provider::Kaggle,
458            _ => {
459                return Err(Error::InvalidParams(format!(
460                    "unknown provider: {}",
461                    args.provider
462                )))
463            }
464        };
465
466        // Build a puppet (no auth needed just to query static capabilities).
467        let puppet = context.get_puppet().await?;
468
469        let caps = puppet
470            .provider_capabilities(provider)
471            .ok_or_else(|| Error::InvalidParams(format!("provider not available: {}", provider)))?;
472
473        puppet.close().await.ok();
474
475        Ok(ToolCallResult {
476            content: vec![ContentItem::text(
477                serde_json::to_string_pretty(&json!({
478                    "provider": provider.to_string(),
479                    "capabilities": {
480                        "conversation": caps.conversation,
481                        "vision": caps.vision,
482                        "file_upload": caps.file_upload,
483                        "code_execution": caps.code_execution,
484                        "web_search": caps.web_search,
485                        "max_context": caps.max_context,
486                        "models": caps.models,
487                        "note": "Declared capabilities (not runtime UI detection)."
488                    }
489                }))
490                .map_err(|e| Error::Internal(e.to_string()))?,
491            )],
492            is_error: false,
493        })
494    }
495}
496
497/// Tool for detecting installed browsers.
498pub struct DetectBrowsersTool;
499
500#[async_trait::async_trait]
501impl Tool for DetectBrowsersTool {
502    fn definition(&self) -> ToolDefinition {
503        ToolDefinition {
504            name: "webpuppet_detect_browsers".into(),
505            description: "Detect installed browsers that can be used for automation.".into(),
506            input_schema: json!({
507                "type": "object",
508                "properties": {},
509                "required": []
510            }),
511        }
512    }
513
514    async fn execute(
515        &self,
516        _arguments: serde_json::Value,
517        _context: &ToolContext,
518    ) -> Result<ToolCallResult> {
519        let browsers = BrowserDetector::detect_all();
520
521        if browsers.is_empty() {
522            return Ok(ToolCallResult {
523                content: vec![ContentItem::text(
524                    "No supported browsers detected. Please install Brave, Chrome, or Chromium.",
525                )],
526                is_error: true,
527            });
528        }
529
530        let text = browsers
531            .iter()
532            .map(|b| {
533                let version = b.version.as_deref().unwrap_or("unknown");
534                let profiles = b.list_profiles().unwrap_or_default();
535                format!(
536                    "- **{}** ({})\n  - Path: `{}`\n  - Data: `{}`\n  - Profiles: {}",
537                    b.browser_type,
538                    version,
539                    b.executable_path.display(),
540                    b.user_data_dir.display(),
541                    if profiles.is_empty() {
542                        "none".to_string()
543                    } else {
544                        profiles.join(", ")
545                    }
546                )
547            })
548            .collect::<Vec<_>>()
549            .join("\n\n");
550
551        Ok(ToolCallResult {
552            content: vec![ContentItem::text(format!(
553                "# Detected Browsers\n\n{}",
554                text
555            ))],
556            is_error: false,
557        })
558    }
559}
560
561/// Tool for taking screenshots.
562pub struct ScreenshotTool;
563
564#[derive(Debug, Deserialize)]
565struct ScreenshotArgs {
566    /// URL to screenshot.
567    url: String,
568}
569
570#[async_trait::async_trait]
571impl Tool for ScreenshotTool {
572    fn definition(&self) -> ToolDefinition {
573        ToolDefinition {
574            name: "webpuppet_screenshot".into(),
575            description: "Take a screenshot of a web page. Only allowed domains can be accessed."
576                .into(),
577            input_schema: json!({
578                "type": "object",
579                "properties": {
580                    "url": {
581                        "type": "string",
582                        "description": "URL to take a screenshot of"
583                    }
584                },
585                "required": ["url"]
586            }),
587        }
588    }
589
590    async fn execute(
591        &self,
592        arguments: serde_json::Value,
593        context: &ToolContext,
594    ) -> Result<ToolCallResult> {
595        let args: ScreenshotArgs =
596            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
597
598        // Check permissions for this URL
599        context
600            .permissions
601            .require_with_url(Operation::Navigate, &args.url)
602            .map_err(|e| Error::PermissionDenied(e.to_string()))?;
603
604        context
605            .permissions
606            .require(Operation::Screenshot)
607            .map_err(|e| Error::PermissionDenied(e.to_string()))?;
608
609        // For now, return a placeholder since actual screenshot requires full browser impl
610        Ok(ToolCallResult {
611            content: vec![ContentItem::text(format!(
612                "Screenshot of `{}` would be captured here.\n\n*Note: Full browser implementation required for actual screenshots.*",
613                args.url
614            ))],
615            is_error: false,
616        })
617    }
618}
619
620/// Tool for checking permissions.
621pub struct CheckPermissionTool;
622
623#[derive(Debug, Deserialize)]
624struct CheckPermissionArgs {
625    /// Operation to check.
626    operation: String,
627    /// Optional URL context.
628    url: Option<String>,
629}
630
631#[async_trait::async_trait]
632impl Tool for CheckPermissionTool {
633    fn definition(&self) -> ToolDefinition {
634        ToolDefinition {
635            name: "webpuppet_check_permission".into(),
636            description: "Check if an operation is allowed by the security policy.".into(),
637            input_schema: json!({
638                "type": "object",
639                "properties": {
640                    "operation": {
641                        "type": "string",
642                        "description": "Operation to check (e.g., Navigate, SendPrompt, DeleteAccount)"
643                    },
644                    "url": {
645                        "type": "string",
646                        "description": "Optional URL context for navigation checks"
647                    }
648                },
649                "required": ["operation"]
650            }),
651        }
652    }
653
654    async fn execute(
655        &self,
656        arguments: serde_json::Value,
657        context: &ToolContext,
658    ) -> Result<ToolCallResult> {
659        let args: CheckPermissionArgs =
660            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
661
662        // Map string to Operation
663        let operation = match args.operation.to_lowercase().as_str() {
664            "navigate" => Operation::Navigate,
665            "sendprompt" | "send_prompt" => Operation::SendPrompt,
666            "readresponse" | "read_response" => Operation::ReadResponse,
667            "screenshot" => Operation::Screenshot,
668            "click" => Operation::Click,
669            "typetext" | "type_text" => Operation::TypeText,
670            "deleteaccount" | "delete_account" => Operation::DeleteAccount,
671            "changepassword" | "change_password" => Operation::ChangePassword,
672            _ => {
673                return Ok(ToolCallResult {
674                    content: vec![ContentItem::text(format!(
675                        "Unknown operation: `{}`\n\nValid operations: Navigate, SendPrompt, ReadResponse, Screenshot, Click, TypeText, DeleteAccount, ChangePassword",
676                        args.operation
677                    ))],
678                    is_error: true,
679                });
680            }
681        };
682
683        let decision = if let Some(url) = args.url {
684            context.permissions.check_with_url(operation, &url)
685        } else {
686            context.permissions.check(operation)
687        };
688
689        let status = if decision.allowed {
690            "✅ ALLOWED"
691        } else {
692            "❌ DENIED"
693        };
694        let text = format!(
695            "# Permission Check\n\n**Operation**: `{}`\n**Status**: {}\n**Reason**: {}\n**Risk Level**: {}/10",
696            operation, status, decision.reason, decision.risk_level
697        );
698
699        Ok(ToolCallResult {
700            content: vec![ContentItem::text(text)],
701            is_error: false,
702        })
703    }
704}
705
706// ============================================================================
707// Intervention Tools
708// ============================================================================
709
710/// Tool for checking intervention status.
711pub struct InterventionStatusTool;
712
713#[async_trait::async_trait]
714impl Tool for InterventionStatusTool {
715    fn definition(&self) -> ToolDefinition {
716        ToolDefinition {
717            name: "webpuppet_intervention_status".into(),
718            description: "Check if human intervention is needed (captcha, 2FA, etc.). Returns current automation state and any pending intervention reason.".into(),
719            input_schema: json!({
720                "type": "object",
721                "properties": {},
722                "required": []
723            }),
724        }
725    }
726
727    async fn execute(
728        &self,
729        _arguments: serde_json::Value,
730        context: &ToolContext,
731    ) -> Result<ToolCallResult> {
732        let handler = context.intervention_handler.read().await;
733        let state = handler.state();
734        let reason = handler.current_reason();
735
736        let state_str = match state {
737            InterventionState::Running => "🟢 Running",
738            InterventionState::WaitingForHuman => "🟡 Waiting for human",
739            InterventionState::Resuming => "🔵 Resuming",
740            InterventionState::TimedOut => "🔴 Timed out",
741            InterventionState::Cancelled => "⚫ Cancelled",
742        };
743
744        let text = if let Some(reason) = reason {
745            format!(
746                "# Intervention Status\n\n**State**: {}\n**Reason**: {}\n\n⚠️ **Action Required**: Please complete the intervention in the browser, then call `webpuppet_intervention_complete` with success=true.",
747                state_str, reason
748            )
749        } else {
750            format!(
751                "# Intervention Status\n\n**State**: {}\n\nNo intervention currently required. Automation is running normally.",
752                state_str
753            )
754        };
755
756        Ok(ToolCallResult {
757            content: vec![ContentItem::text(text)],
758            is_error: false,
759        })
760    }
761}
762
763/// Tool for signaling intervention completion.
764pub struct InterventionCompleteTool;
765
766#[derive(Debug, Deserialize)]
767struct InterventionCompleteArgs {
768    /// Whether the intervention was successful.
769    success: bool,
770    /// Optional message about the intervention.
771    message: Option<String>,
772}
773
774#[async_trait::async_trait]
775impl Tool for InterventionCompleteTool {
776    fn definition(&self) -> ToolDefinition {
777        ToolDefinition {
778            name: "webpuppet_intervention_complete".into(),
779            description: "Signal that a human intervention (captcha, 2FA, etc.) has been completed. Call this after manually handling the intervention in the browser.".into(),
780            input_schema: json!({
781                "type": "object",
782                "properties": {
783                    "success": {
784                        "type": "boolean",
785                        "description": "Whether the intervention was completed successfully"
786                    },
787                    "message": {
788                        "type": "string",
789                        "description": "Optional message about what was done"
790                    }
791                },
792                "required": ["success"]
793            }),
794        }
795    }
796
797    async fn execute(
798        &self,
799        arguments: serde_json::Value,
800        context: &ToolContext,
801    ) -> Result<ToolCallResult> {
802        let args: InterventionCompleteArgs =
803            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
804
805        let handler = context.intervention_handler.read().await;
806        handler.complete(args.success, args.message.clone());
807
808        let status = if args.success {
809            "✅ SUCCESS"
810        } else {
811            "❌ FAILED"
812        };
813        let text = format!(
814            "# Intervention Complete\n\n**Status**: {}\n**Message**: {}\n\nAutomation will now resume.",
815            status,
816            args.message.unwrap_or_else(|| "None".into())
817        );
818
819        Ok(ToolCallResult {
820            content: vec![ContentItem::text(text)],
821            is_error: false,
822        })
823    }
824}
825
826/// Tool for pausing automation.
827pub struct InterventionPauseTool;
828
829#[async_trait::async_trait]
830impl Tool for InterventionPauseTool {
831    fn definition(&self) -> ToolDefinition {
832        ToolDefinition {
833            name: "webpuppet_pause".into(),
834            description: "Pause browser automation. Use this when you need to manually interact with the browser.".into(),
835            input_schema: json!({
836                "type": "object",
837                "properties": {},
838                "required": []
839            }),
840        }
841    }
842
843    async fn execute(
844        &self,
845        _arguments: serde_json::Value,
846        context: &ToolContext,
847    ) -> Result<ToolCallResult> {
848        let handler = context.intervention_handler.read().await;
849        handler.pause();
850
851        Ok(ToolCallResult {
852            content: vec![ContentItem::text(
853                "# Automation Paused\n\n⏸️ Automation is now paused. The browser is available for manual interaction.\n\nCall `webpuppet_resume` when ready to continue."
854            )],
855            is_error: false,
856        })
857    }
858}
859
860/// Tool for resuming automation.
861pub struct InterventionResumeTool;
862
863#[async_trait::async_trait]
864impl Tool for InterventionResumeTool {
865    fn definition(&self) -> ToolDefinition {
866        ToolDefinition {
867            name: "webpuppet_resume".into(),
868            description: "Resume browser automation after a pause or manual intervention.".into(),
869            input_schema: json!({
870                "type": "object",
871                "properties": {},
872                "required": []
873            }),
874        }
875    }
876
877    async fn execute(
878        &self,
879        _arguments: serde_json::Value,
880        context: &ToolContext,
881    ) -> Result<ToolCallResult> {
882        let handler = context.intervention_handler.read().await;
883        handler.resume();
884
885        Ok(ToolCallResult {
886            content: vec![ContentItem::text(
887                "# Automation Resumed\n\n▶️ Automation has been resumed. Browser operations will continue."
888            )],
889            is_error: false,
890        })
891    }
892}
893
894/// Tool for navigating to a URL (for testing).
895pub struct NavigateTool;
896
897#[derive(Debug, Deserialize)]
898struct NavigateArgs {
899    /// URL to navigate to.
900    url: String,
901}
902
903#[async_trait::async_trait]
904impl Tool for NavigateTool {
905    fn definition(&self) -> ToolDefinition {
906        ToolDefinition {
907            name: "webpuppet_navigate".into(),
908            description: "Navigate browser to a URL. Opens a browser window if not already open."
909                .into(),
910            input_schema: json!({
911                "type": "object",
912                "properties": {
913                    "url": {
914                        "type": "string",
915                        "description": "URL to navigate to"
916                    }
917                },
918                "required": ["url"]
919            }),
920        }
921    }
922
923    async fn execute(
924        &self,
925        arguments: serde_json::Value,
926        context: &ToolContext,
927    ) -> Result<ToolCallResult> {
928        // Check permission
929        context
930            .permissions
931            .require(Operation::Navigate)
932            .map_err(|e| Error::PermissionDenied(e.to_string()))?;
933
934        // Parse arguments
935        let args: NavigateArgs =
936            serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
937
938        // Get puppet and navigate
939        let puppet = context.get_puppet().await?;
940
941        // Get session (using Grok as default provider for navigation)
942        let session = puppet.get_session(Provider::Grok).await?;
943
944        // Navigate
945        session.navigate(&args.url).await?;
946
947        // Get current URL and title
948        let current_url = session
949            .current_url()
950            .await
951            .unwrap_or_else(|_| args.url.clone());
952        let title = session
953            .get_title()
954            .await
955            .unwrap_or_else(|_| "Unknown".into());
956
957        Ok(ToolCallResult {
958            content: vec![ContentItem::text(format!(
959                "# Browser Navigated\n\n✅ Successfully navigated to URL.\n\n- **URL**: {}\n- **Title**: {}",
960                current_url, title
961            ))],
962            is_error: false,
963        })
964    }
965}
966
967/// Tool for getting browser status.
968pub struct BrowserStatusTool;
969
970#[async_trait::async_trait]
971impl Tool for BrowserStatusTool {
972    fn definition(&self) -> ToolDefinition {
973        ToolDefinition {
974            name: "webpuppet_browser_status".into(),
975            description: "Get current browser status including URL, title, and visibility.".into(),
976            input_schema: json!({
977                "type": "object",
978                "properties": {},
979                "required": []
980            }),
981        }
982    }
983
984    async fn execute(
985        &self,
986        _arguments: serde_json::Value,
987        context: &ToolContext,
988    ) -> Result<ToolCallResult> {
989        let guard = context.puppet.read().await;
990
991        if guard.is_none() {
992            return Ok(ToolCallResult {
993                content: vec![ContentItem::text(
994                    "# Browser Status\n\n⚪ No browser session is currently active.\n\nA browser will be launched when you use `webpuppet_navigate` or `webpuppet_prompt`."
995                )],
996                is_error: false,
997            });
998        }
999
1000        // Return basic status
1001        let visibility = if context.headless {
1002            "Headless"
1003        } else {
1004            "Visible"
1005        };
1006
1007        Ok(ToolCallResult {
1008            content: vec![ContentItem::text(format!(
1009                "# Browser Status\n\n🟢 Browser session is active.\n\n- **Mode**: {}\n- **Providers**: Grok, Claude, Gemini",
1010                visibility
1011            ))],
1012            is_error: false,
1013        })
1014    }
1015}
1016
1017// We need async-trait
1018mod async_trait_impl {
1019    pub use async_trait::async_trait;
1020}
1021pub use async_trait_impl::async_trait;