snm_brightdata_client/
tool.rs

1// src/tool.rs
2
3use async_trait::async_trait;
4use crate::error::BrightDataError;
5use serde_json::Value;
6use serde::{Deserialize, Serialize};
7
8// MCP-compatible content structure
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct McpContent {
11    #[serde(rename = "type")]
12    pub content_type: String,
13    pub text: String,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub data: Option<String>, // For base64 encoded data like images
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub media_type: Option<String>, // MIME type for binary content
18}
19
20impl McpContent {
21    pub fn text(text: String) -> Self {
22        Self {
23            content_type: "text".to_string(),
24            text,
25            data: None,
26            media_type: None,
27        }
28    }
29
30    pub fn image(data: String, media_type: String) -> Self {
31        Self {
32            content_type: "image".to_string(),
33            text: "Screenshot captured".to_string(),
34            data: Some(data),
35            media_type: Some(media_type),
36        }
37    }
38}
39
40// Enhanced tool result for MCP compatibility
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ToolResult {
43    pub content: Vec<McpContent>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub is_error: Option<bool>,
46    // Preserve backward compatibility - raw value for existing integrations
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub raw_value: Option<Value>,
49}
50
51impl ToolResult {
52    pub fn success(content: Vec<McpContent>) -> Self {
53        Self {
54            content,
55            is_error: Some(false),
56            raw_value: None,
57        }
58    }
59
60    pub fn success_with_text(text: String) -> Self {
61        Self {
62            content: vec![McpContent::text(text)],
63            is_error: Some(false),
64            raw_value: None,
65        }
66    }
67
68    pub fn success_with_raw(content: Vec<McpContent>, raw: Value) -> Self {
69        Self {
70            content,
71            is_error: Some(false),
72            raw_value: Some(raw),
73        }
74    }
75
76    pub fn error(message: String) -> Self {
77        Self {
78            content: vec![McpContent::text(format!("Error: {}", message))],
79            is_error: Some(true),
80            raw_value: None,
81        }
82    }
83
84    // Backward compatibility method
85    pub fn from_legacy_value(value: Value) -> Self {
86        let text = if let Some(raw_text) = value.get("raw").and_then(|v| v.as_str()) {
87            raw_text.to_string()
88        } else {
89            serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
90        };
91
92        Self {
93            content: vec![McpContent::text(text)],
94            is_error: Some(false),
95            raw_value: Some(value),
96        }
97    }
98}
99
100#[async_trait]
101pub trait Tool {
102    fn name(&self) -> &str;
103    fn description(&self) -> &str;
104    fn input_schema(&self) -> Value; // New method for tool schema
105    
106    // Enhanced execute method returning MCP-compatible result
107    async fn execute(&self, parameters: Value) -> Result<ToolResult, BrightDataError>;
108    
109    // Legacy method for backward compatibility
110    async fn execute_legacy(&self, parameters: Value) -> Result<Value, BrightDataError> {
111        let result = self.execute(parameters).await?;
112        if let Some(raw) = result.raw_value {
113            Ok(raw)
114        } else if !result.content.is_empty() {
115            Ok(serde_json::json!({
116                "content": result.content[0].text
117            }))
118        } else {
119            Ok(serde_json::json!({}))
120        }
121    }
122}
123
124// Enhanced tool resolver with schema support
125pub struct ToolResolver;
126
127impl Default for ToolResolver {
128    fn default() -> Self {
129        Self
130    }
131}
132
133impl ToolResolver {
134    pub fn resolve(&self, name: &str) -> Option<Box<dyn Tool + Send + Sync>> {
135        match name {
136            // Existing tools
137            "scrape_website" => Some(Box::new(crate::tools::scrape::ScrapeMarkdown)),
138            "search_web" => Some(Box::new(crate::tools::search::SearchEngine)),
139            "extract_data" => Some(Box::new(crate::tools::extract::Extractor)),
140            "get_crypto_data" => Some(Box::new(crate::tools::search::SearchEngine)),
141            "take_screenshot" => Some(Box::new(crate::tools::screenshot::ScreenshotTool)),
142            "multi_zone_search" => Some(Box::new(crate::tools::multi_zone_search::MultiZoneSearch)),
143            // Add these missing tools:
144            "get_stock_data" => Some(Box::new(crate::tools::stock::StockDataTool)),
145            "get_crypto_data" => Some(Box::new(crate::tools::crypto::CryptoDataTool)),
146            "get_etf_data" => Some(Box::new(crate::tools::etf::ETFDataTool)),
147            "get_bond_data" => Some(Box::new(crate::tools::bond::BondDataTool)),
148            "get_mutual_fund_data" => Some(Box::new(crate::tools::mutual_fund::MutualFundDataTool)),
149            "get_commodity_data" => Some(Box::new(crate::tools::commodity::CommodityDataTool)),
150            "get_market_overview" => Some(Box::new(crate::tools::market::MarketOverviewTool)),
151            _ => None,
152        }
153    }
154
155    pub fn list_tools(&self) -> Vec<Value> {
156        vec![
157            serde_json::json!({
158                "name": "scrape_website",
159                "description": "Scrape a webpage into markdown",
160                "inputSchema": {
161                    "type": "object",
162                    "required": ["url"],
163                    "properties": {
164                        "url": { "type": "string" },
165                        "format": { "type": "string", "enum": ["markdown", "raw"] }
166                    }
167                }
168            }),
169            serde_json::json!({
170                "name": "search_web",
171                "description": "Search the web using BrightData",
172                "inputSchema": {
173                    "type": "object",
174                    "required": ["query"],
175                    "properties": {
176                        "query": { "type": "string" },
177                        "engine": {
178                            "type": "string",
179                            "enum": ["google", "bing", "yandex", "duckduckgo"]
180                        },
181                        "cursor": { "type": "string" }
182                    }
183                }
184            }),
185            serde_json::json!({
186                "name": "extract_data",
187                "description": "Extract structured data from a webpage",
188                "inputSchema": {
189                    "type": "object",
190                    "required": ["url"],
191                    "properties": {
192                        "url": { "type": "string" },
193                        "schema": { "type": "object" }
194                    }
195                }
196            }),
197            serde_json::json!({
198                "name": "take_screenshot",
199                "description": "Take a screenshot of a webpage",
200                "inputSchema": {
201                    "type": "object",
202                    "required": ["url"],
203                    "properties": {
204                        "url": { "type": "string" },
205                        "width": { "type": "integer" },
206                        "height": { "type": "integer" },
207                        "full_page": { "type": "boolean" }
208                    }
209                }
210            }),
211            serde_json::json!({
212                "name": "get_crypto_data",
213                "description": "Crypto search using BrightData",
214                "inputSchema": {
215                    "type": "object",
216                    "required": ["query"],
217                    "properties": {
218                        "query": { "type": "string" },
219                        "engine": {
220                            "type": "string",
221                            "enum": ["google", "bing", "yandex", "duckduckgo"]
222                        },
223                        "cursor": { "type": "string" }
224                    }
225                }
226            }),
227            serde_json::json!({
228                "name": "multi_zone_search",
229                "description": "Multi-region search across zones",
230                "inputSchema": {
231                    "type": "object",
232                    "required": ["query", "zones"],
233                    "properties": {
234                        "query": { "type": "string" },
235                        "engine": {
236                            "type": "string",
237                            "enum": ["google", "bing", "yandex", "duckduckgo"]
238                        },
239                        "zones": {
240                            "type": "array",
241                            "items": { "type": "string" }
242                        }
243                    }
244                }
245            })
246        ]
247    }
248
249
250    // Helper method to get all available tool names
251    pub fn get_available_tool_names(&self) -> Vec<&'static str> {
252        vec![
253            "scrape_website",
254            "search_web", 
255            "extract_data",
256            "take_screenshot",
257            "get_crypto_data",
258            "multi_zone_search"
259        ]
260    }
261
262    // Helper method to check if a tool exists
263    pub fn tool_exists(&self, name: &str) -> bool {
264        self.get_available_tool_names().contains(&name)
265    }
266
267    // Helper method to get tool count
268    pub fn tool_count(&self) -> usize {
269        self.get_available_tool_names().len()
270    }
271}
272