snm_brightdata_client/tools/
etf.rs

1// src/tools/etf.rs
2use crate::tool::{Tool, ToolResult, McpContent};
3use crate::error::BrightDataError;
4use async_trait::async_trait;
5use reqwest::Client;
6use serde_json::{json, Value};
7use std::env;
8use std::time::Duration;
9
10pub struct ETFDataTool;
11
12#[async_trait]
13impl Tool for ETFDataTool {
14    fn name(&self) -> &str {
15        "get_etf_data"
16    }
17
18    fn description(&self) -> &str {
19        "Get ETF and index fund data including NAV, holdings, performance, expense ratios"
20    }
21
22    fn input_schema(&self) -> Value {
23        json!({
24            "type": "object",
25            "properties": {
26                "query": {
27                    "type": "string",
28                    "description": "ETF symbol (SPY, NIFTYBEES), ETF name, or ETF market analysis query"
29                },
30                "market": {
31                    "type": "string",
32                    "enum": ["indian", "us", "global"],
33                    "default": "indian",
34                    "description": "Market region"
35                }
36            },
37            "required": ["query"]
38        })
39    }
40
41    async fn execute(&self, parameters: Value) -> Result<ToolResult, BrightDataError> {
42        let query = parameters
43            .get("query")
44            .and_then(|v| v.as_str())
45            .ok_or_else(|| BrightDataError::ToolError("Missing 'query' parameter".into()))?;
46        
47        let market = parameters
48            .get("market")
49            .and_then(|v| v.as_str())
50            .unwrap_or("indian");
51
52        let result = self.fetch_etf_data(query, market).await?;
53
54        let content_text = result.get("content").and_then(|c| c.as_str()).unwrap_or("No ETF data found");
55        let mcp_content = vec![McpContent::text(format!(
56            "📊 **ETF Data for {}**\n\nMarket: {}\n\n{}",
57            query,
58            market.to_uppercase(),
59            content_text
60        ))];
61
62        Ok(ToolResult::success_with_raw(mcp_content, result))
63    }
64}
65
66impl ETFDataTool {
67    async fn fetch_etf_data(&self, query: &str, market: &str) -> Result<Value, BrightDataError> {
68        let api_token = env::var("BRIGHTDATA_API_TOKEN")
69            .or_else(|_| env::var("API_TOKEN"))
70            .map_err(|_| BrightDataError::ToolError("Missing BRIGHTDATA_API_TOKEN".into()))?;
71
72        let base_url = env::var("BRIGHTDATA_BASE_URL")
73            .unwrap_or_else(|_| "https://api.brightdata.com".to_string());
74
75        let zone = env::var("WEB_UNLOCKER_ZONE")
76            .unwrap_or_else(|_| "default".to_string());
77
78        // Build ETF-specific search URL
79        let search_url = match market {
80            "indian" => format!("https://www.google.com/search?q={} ETF NAV performance india NSE BSE", urlencoding::encode(query)),
81            "us" => format!("https://www.google.com/search?q={} ETF price performance expense ratio holdings", urlencoding::encode(query)),
82            "global" => format!("https://www.google.com/search?q={} ETF global performance holdings", urlencoding::encode(query)),
83            _ => format!("https://www.google.com/search?q={} ETF performance NAV", urlencoding::encode(query))
84        };
85
86        let payload = json!({
87            "url": search_url,
88            "zone": zone,
89            "format": "raw",
90            "data_format": "markdown"
91        });
92
93        let client = Client::builder()
94            .timeout(Duration::from_secs(120))
95            .build()
96            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
97
98        let response = client
99            .post(&format!("{}/request", base_url))
100            .header("Authorization", format!("Bearer {}", api_token))
101            .header("Content-Type", "application/json")
102            .json(&payload)
103            .send()
104            .await
105            .map_err(|e| BrightDataError::ToolError(format!("ETF data request failed: {}", e)))?;
106
107        let status = response.status();
108        if !status.is_success() {
109            let error_text = response.text().await.unwrap_or_default();
110            return Err(BrightDataError::ToolError(format!(
111                "BrightData ETF data error {}: {}",
112                status, error_text
113            )));
114        }
115
116        let content = response.text().await
117            .map_err(|e| BrightDataError::ToolError(e.to_string()))?;
118
119        Ok(json!({
120            "content": content,
121            "query": query,
122            "market": market,
123            "success": true
124        }))
125    }
126}