1use std::sync::Arc;
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use synaptic_core::{SynapticError, Tool};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BrowserConfig {
17 #[serde(default = "default_debug_url")]
19 pub debug_url: String,
20}
21
22fn default_debug_url() -> String {
23 "http://localhost:9222".to_string()
24}
25
26impl Default for BrowserConfig {
27 fn default() -> Self {
28 Self {
29 debug_url: default_debug_url(),
30 }
31 }
32}
33
34pub fn browser_tools(config: &BrowserConfig) -> Vec<Arc<dyn Tool>> {
36 vec![
37 Arc::new(NavigateTool::new(config.clone())),
38 Arc::new(ScreenshotTool::new(config.clone())),
39 Arc::new(EvalJsTool::new(config.clone())),
40 ]
41}
42
43pub struct NavigateTool {
48 #[allow(dead_code)]
49 config: BrowserConfig,
50}
51
52impl NavigateTool {
53 pub fn new(config: BrowserConfig) -> Self {
54 Self { config }
55 }
56}
57
58#[async_trait]
59impl Tool for NavigateTool {
60 fn name(&self) -> &'static str {
61 "browser_navigate"
62 }
63
64 fn description(&self) -> &'static str {
65 "Navigate the browser to a URL"
66 }
67
68 fn parameters(&self) -> Option<Value> {
69 Some(json!({
70 "type": "object",
71 "properties": {
72 "url": {
73 "type": "string",
74 "description": "The URL to navigate to"
75 }
76 },
77 "required": ["url"]
78 }))
79 }
80
81 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
82 let url = args["url"]
83 .as_str()
84 .ok_or_else(|| SynapticError::Tool("missing 'url' parameter".to_string()))?;
85
86 #[cfg(feature = "cdp")]
87 {
88 let client = reqwest::Client::new();
89 let resp = client
90 .get(format!("{}/json/new?{}", self.config.debug_url, url))
91 .send()
92 .await
93 .map_err(|e| SynapticError::Tool(format!("CDP navigate failed: {}", e)))?;
94
95 if resp.status().is_success() {
96 Ok(json!(format!("Navigated to {}", url)))
97 } else {
98 Err(SynapticError::Tool(format!(
99 "CDP navigate error: {}",
100 resp.status()
101 )))
102 }
103 }
104
105 #[cfg(not(feature = "cdp"))]
106 {
107 let _ = url;
108 Err(SynapticError::Tool(
109 "CDP feature not enabled. Enable 'cdp' feature or use MCP browser integration."
110 .to_string(),
111 ))
112 }
113 }
114}
115
116pub struct ScreenshotTool {
121 #[allow(dead_code)]
122 config: BrowserConfig,
123}
124
125impl ScreenshotTool {
126 pub fn new(config: BrowserConfig) -> Self {
127 Self { config }
128 }
129}
130
131#[async_trait]
132impl Tool for ScreenshotTool {
133 fn name(&self) -> &'static str {
134 "browser_screenshot"
135 }
136
137 fn description(&self) -> &'static str {
138 "Take a screenshot of the current browser page"
139 }
140
141 fn parameters(&self) -> Option<Value> {
142 Some(json!({
143 "type": "object",
144 "properties": {}
145 }))
146 }
147
148 async fn call(&self, _args: Value) -> Result<Value, SynapticError> {
149 #[cfg(feature = "cdp")]
150 {
151 Ok(json!("Screenshot capture requires full CDP WebSocket connection. Use MCP browser integration for full support."))
153 }
154
155 #[cfg(not(feature = "cdp"))]
156 {
157 Err(SynapticError::Tool(
158 "CDP feature not enabled. Enable 'cdp' feature or use MCP browser integration."
159 .to_string(),
160 ))
161 }
162 }
163}
164
165pub struct EvalJsTool {
170 #[allow(dead_code)]
171 config: BrowserConfig,
172}
173
174impl EvalJsTool {
175 pub fn new(config: BrowserConfig) -> Self {
176 Self { config }
177 }
178}
179
180#[async_trait]
181impl Tool for EvalJsTool {
182 fn name(&self) -> &'static str {
183 "browser_eval_js"
184 }
185
186 fn description(&self) -> &'static str {
187 "Evaluate JavaScript in the browser page"
188 }
189
190 fn parameters(&self) -> Option<Value> {
191 Some(json!({
192 "type": "object",
193 "properties": {
194 "expression": {
195 "type": "string",
196 "description": "JavaScript expression to evaluate"
197 }
198 },
199 "required": ["expression"]
200 }))
201 }
202
203 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
204 let _expression = args["expression"]
205 .as_str()
206 .ok_or_else(|| SynapticError::Tool("missing 'expression' parameter".to_string()))?;
207
208 #[cfg(feature = "cdp")]
209 {
210 Ok(json!("JavaScript evaluation requires full CDP WebSocket connection. Use MCP browser integration for full support."))
212 }
213
214 #[cfg(not(feature = "cdp"))]
215 {
216 Err(SynapticError::Tool(
217 "CDP feature not enabled. Enable 'cdp' feature or use MCP browser integration."
218 .to_string(),
219 ))
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_browser_config_default() {
230 let config = BrowserConfig::default();
231 assert_eq!(config.debug_url, "http://localhost:9222");
232 }
233
234 #[test]
235 fn test_browser_tools_count() {
236 let config = BrowserConfig::default();
237 let tools = browser_tools(&config);
238 assert_eq!(tools.len(), 3);
239 }
240
241 #[test]
242 fn test_tool_names() {
243 let config = BrowserConfig::default();
244 let tools = browser_tools(&config);
245 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
246 assert!(names.contains(&"browser_navigate"));
247 assert!(names.contains(&"browser_screenshot"));
248 assert!(names.contains(&"browser_eval_js"));
249 }
250
251 #[tokio::test]
252 async fn test_navigate_without_cdp() {
253 let tool = NavigateTool::new(BrowserConfig::default());
254 let result = tool.call(json!({"url": "https://example.com"})).await;
255 #[cfg(not(feature = "cdp"))]
257 assert!(result.is_err());
258 #[cfg(feature = "cdp")]
259 let _ = result; }
261}