snm_brightdata_client/tools/
etf.rs1use 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 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}