1use reqwest::{Client, Method, RequestBuilder};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::time::Duration;
5
6use crate::error::OpenApiError;
7use crate::server::ToolMetadata;
8use crate::tool_generator::{ExtractedParameters, ToolGenerator};
9
10pub struct HttpClient {
12 client: Client,
13 base_url: Option<String>,
14}
15
16impl HttpClient {
17 pub fn new() -> Self {
19 let client = Client::builder()
20 .timeout(Duration::from_secs(30))
21 .build()
22 .expect("Failed to create HTTP client");
23
24 Self {
25 client,
26 base_url: None,
27 }
28 }
29
30 pub fn with_timeout(timeout_seconds: u64) -> Self {
32 let client = Client::builder()
33 .timeout(Duration::from_secs(timeout_seconds))
34 .build()
35 .expect("Failed to create HTTP client");
36
37 Self {
38 client,
39 base_url: None,
40 }
41 }
42
43 pub fn with_base_url(mut self, base_url: String) -> Self {
45 self.base_url = Some(base_url);
46 self
47 }
48
49 pub async fn execute_tool_call(
51 &self,
52 tool_metadata: &ToolMetadata,
53 arguments: &Value,
54 ) -> Result<HttpResponse, OpenApiError> {
55 let extracted_params = ToolGenerator::extract_parameters(tool_metadata, arguments)?;
57
58 let url = self.build_url(tool_metadata, &extracted_params)?;
60
61 let mut request = self.create_request(&tool_metadata.method, &url)?;
63
64 if !extracted_params.query.is_empty() {
66 request = self.add_query_parameters(request, &extracted_params.query)?;
67 }
68
69 if !extracted_params.headers.is_empty() {
71 request = self.add_headers(request, &extracted_params.headers)?;
72 }
73
74 if !extracted_params.cookies.is_empty() {
76 request = self.add_cookies(request, &extracted_params.cookies)?;
77 }
78
79 if !extracted_params.body.is_empty() {
81 request =
82 self.add_request_body(request, &extracted_params.body, &extracted_params.config)?;
83 }
84
85 if extracted_params.config.timeout_seconds != 30 {
87 request = request.timeout(Duration::from_secs(
88 extracted_params.config.timeout_seconds as u64,
89 ));
90 }
91
92 let request_body_string = if !extracted_params.body.is_empty() {
94 if extracted_params.body.len() == 1
95 && extracted_params.body.contains_key("request_body")
96 {
97 serde_json::to_string(&extracted_params.body["request_body"]).unwrap_or_default()
98 } else {
99 let body_object = Value::Object(
100 extracted_params
101 .body
102 .iter()
103 .map(|(k, v)| (k.clone(), v.clone()))
104 .collect(),
105 );
106 serde_json::to_string(&body_object).unwrap_or_default()
107 }
108 } else {
109 String::new()
110 };
111
112 let response = request.send().await.map_err(|e| {
114 if e.is_timeout() {
116 OpenApiError::Http(format!(
117 "Request timeout after {} seconds while calling {} {}",
118 extracted_params.config.timeout_seconds,
119 tool_metadata.method.to_uppercase(),
120 url
121 ))
122 } else if e.is_connect() {
123 OpenApiError::Http(format!(
124 "Connection failed to {url} - check if the server is running and the URL is correct"
125 ))
126 } else if e.is_request() {
127 OpenApiError::Http(format!(
128 "Request error: {} (URL: {}, Method: {})",
129 e,
130 url,
131 tool_metadata.method.to_uppercase()
132 ))
133 } else {
134 OpenApiError::Http(format!(
135 "HTTP request failed: {} (URL: {}, Method: {})",
136 e,
137 url,
138 tool_metadata.method.to_uppercase()
139 ))
140 }
141 })?;
142
143 self.process_response_with_request(
145 response,
146 &tool_metadata.method,
147 &url,
148 &request_body_string,
149 )
150 .await
151 }
152
153 fn build_url(
155 &self,
156 tool_metadata: &ToolMetadata,
157 extracted_params: &ExtractedParameters,
158 ) -> Result<String, OpenApiError> {
159 let mut path = tool_metadata.path.clone();
160
161 for (param_name, param_value) in &extracted_params.path {
163 let placeholder = format!("{{{param_name}}}");
164 let value_str = match param_value {
165 Value::String(s) => s.clone(),
166 Value::Number(n) => n.to_string(),
167 Value::Bool(b) => b.to_string(),
168 _ => param_value.to_string(),
169 };
170 path = path.replace(&placeholder, &value_str);
171 }
172
173 if let Some(base_url) = &self.base_url {
175 let base = base_url.trim_end_matches('/');
176 let path = path.trim_start_matches('/');
177 Ok(format!("{base}/{path}"))
178 } else {
179 if path.starts_with("http") {
181 Ok(path)
182 } else {
183 Err(OpenApiError::Http(
184 "No base URL configured and path is not a complete URL".to_string(),
185 ))
186 }
187 }
188 }
189
190 fn create_request(&self, method: &str, url: &str) -> Result<RequestBuilder, OpenApiError> {
192 let http_method = method.to_uppercase();
193 let method = match http_method.as_str() {
194 "GET" => Method::GET,
195 "POST" => Method::POST,
196 "PUT" => Method::PUT,
197 "DELETE" => Method::DELETE,
198 "PATCH" => Method::PATCH,
199 "HEAD" => Method::HEAD,
200 "OPTIONS" => Method::OPTIONS,
201 _ => {
202 return Err(OpenApiError::Http(format!(
203 "Unsupported HTTP method: {method}"
204 )));
205 }
206 };
207
208 Ok(self.client.request(method, url))
209 }
210
211 fn add_query_parameters(
213 &self,
214 mut request: RequestBuilder,
215 query_params: &HashMap<String, Value>,
216 ) -> Result<RequestBuilder, OpenApiError> {
217 for (key, value) in query_params {
218 let value_str = match value {
219 Value::String(s) => s.clone(),
220 Value::Number(n) => n.to_string(),
221 Value::Bool(b) => b.to_string(),
222 Value::Array(arr) => {
223 arr.iter()
225 .map(|v| match v {
226 Value::String(s) => s.clone(),
227 Value::Number(n) => n.to_string(),
228 Value::Bool(b) => b.to_string(),
229 _ => v.to_string(),
230 })
231 .collect::<Vec<_>>()
232 .join(",")
233 }
234 _ => value.to_string(),
235 };
236 request = request.query(&[(key, value_str)]);
237 }
238 Ok(request)
239 }
240
241 fn add_headers(
243 &self,
244 mut request: RequestBuilder,
245 headers: &HashMap<String, Value>,
246 ) -> Result<RequestBuilder, OpenApiError> {
247 for (key, value) in headers {
248 let value_str = match value {
249 Value::String(s) => s.clone(),
250 Value::Number(n) => n.to_string(),
251 Value::Bool(b) => b.to_string(),
252 _ => value.to_string(),
253 };
254 request = request.header(key, value_str);
255 }
256 Ok(request)
257 }
258
259 fn add_cookies(
261 &self,
262 mut request: RequestBuilder,
263 cookies: &HashMap<String, Value>,
264 ) -> Result<RequestBuilder, OpenApiError> {
265 if !cookies.is_empty() {
266 let cookie_header = cookies
267 .iter()
268 .map(|(key, value)| {
269 let value_str = match value {
270 Value::String(s) => s.clone(),
271 Value::Number(n) => n.to_string(),
272 Value::Bool(b) => b.to_string(),
273 _ => value.to_string(),
274 };
275 format!("{key}={value_str}")
276 })
277 .collect::<Vec<_>>()
278 .join("; ");
279
280 request = request.header("Cookie", cookie_header);
281 }
282 Ok(request)
283 }
284
285 fn add_request_body(
287 &self,
288 mut request: RequestBuilder,
289 body: &HashMap<String, Value>,
290 config: &crate::tool_generator::RequestConfig,
291 ) -> Result<RequestBuilder, OpenApiError> {
292 if body.is_empty() {
293 return Ok(request);
294 }
295
296 request = request.header("Content-Type", &config.content_type);
298
299 match config.content_type.as_str() {
301 "application/json" => {
302 if body.len() == 1 && body.contains_key("request_body") {
304 let body_value = &body["request_body"];
306 let json_string = serde_json::to_string(body_value).map_err(|e| {
307 OpenApiError::Http(format!("Failed to serialize request body: {e}"))
308 })?;
309 request = request.body(json_string);
310 } else {
311 let body_object =
313 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
314 let json_string = serde_json::to_string(&body_object).map_err(|e| {
315 OpenApiError::Http(format!("Failed to serialize request body: {e}"))
316 })?;
317 request = request.body(json_string);
318 }
319 }
320 "application/x-www-form-urlencoded" => {
321 let form_data: Vec<(String, String)> = body
323 .iter()
324 .map(|(key, value)| {
325 let value_str = match value {
326 Value::String(s) => s.clone(),
327 Value::Number(n) => n.to_string(),
328 Value::Bool(b) => b.to_string(),
329 _ => value.to_string(),
330 };
331 (key.clone(), value_str)
332 })
333 .collect();
334 request = request.form(&form_data);
335 }
336 _ => {
337 let body_object =
339 Value::Object(body.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
340 let json_string = serde_json::to_string(&body_object).map_err(|e| {
341 OpenApiError::Http(format!("Failed to serialize request body: {e}"))
342 })?;
343 request = request.body(json_string);
344 }
345 }
346
347 Ok(request)
348 }
349
350 #[allow(dead_code)]
352 async fn process_response(
353 &self,
354 response: reqwest::Response,
355 ) -> Result<HttpResponse, OpenApiError> {
356 self.process_response_with_request(response, "", "", "")
357 .await
358 }
359
360 async fn process_response_with_request(
362 &self,
363 response: reqwest::Response,
364 method: &str,
365 url: &str,
366 request_body: &str,
367 ) -> Result<HttpResponse, OpenApiError> {
368 let status = response.status();
369 let headers = response
370 .headers()
371 .iter()
372 .map(|(name, value)| {
373 (
374 name.to_string(),
375 value.to_str().unwrap_or("<invalid>").to_string(),
376 )
377 })
378 .collect();
379
380 let body = response
381 .text()
382 .await
383 .map_err(|e| OpenApiError::Http(format!("Failed to read response body: {e}")))?;
384
385 let is_success = status.is_success();
386 let status_code = status.as_u16();
387 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
388
389 let enhanced_status_text = match status_code {
391 400 => format!("{status_text} - Bad Request: Check request parameters"),
392 401 => format!("{status_text} - Unauthorized: Authentication required"),
393 403 => format!("{status_text} - Forbidden: Access denied"),
394 404 => format!("{status_text} - Not Found: Endpoint or resource does not exist"),
395 405 => format!(
396 "{} - Method Not Allowed: {} method not supported",
397 status_text,
398 method.to_uppercase()
399 ),
400 422 => format!("{status_text} - Unprocessable Entity: Request validation failed"),
401 429 => format!("{status_text} - Too Many Requests: Rate limit exceeded"),
402 500 => format!("{status_text} - Internal Server Error: Server encountered an error"),
403 502 => format!("{status_text} - Bad Gateway: Upstream server error"),
404 503 => format!("{status_text} - Service Unavailable: Server temporarily unavailable"),
405 504 => format!("{status_text} - Gateway Timeout: Upstream server timeout"),
406 _ => status_text,
407 };
408
409 Ok(HttpResponse {
410 status_code,
411 status_text: enhanced_status_text,
412 headers,
413 body,
414 is_success,
415 request_method: method.to_string(),
416 request_url: url.to_string(),
417 request_body: request_body.to_string(),
418 })
419 }
420}
421
422impl Default for HttpClient {
423 fn default() -> Self {
424 Self::new()
425 }
426}
427
428#[derive(Debug, Clone)]
430pub struct HttpResponse {
431 pub status_code: u16,
432 pub status_text: String,
433 pub headers: HashMap<String, String>,
434 pub body: String,
435 pub is_success: bool,
436 pub request_method: String,
437 pub request_url: String,
438 pub request_body: String,
439}
440
441impl HttpResponse {
442 pub fn json(&self) -> Result<Value, OpenApiError> {
444 serde_json::from_str(&self.body)
445 .map_err(|e| OpenApiError::Http(format!("Failed to parse response as JSON: {e}")))
446 }
447
448 pub fn to_mcp_content(&self) -> String {
450 let method = if self.request_method.is_empty() {
451 None
452 } else {
453 Some(self.request_method.as_str())
454 };
455 let url = if self.request_url.is_empty() {
456 None
457 } else {
458 Some(self.request_url.as_str())
459 };
460 let body = if self.request_body.is_empty() {
461 None
462 } else {
463 Some(self.request_body.as_str())
464 };
465 self.to_mcp_content_with_request(method, url, body)
466 }
467
468 pub fn to_mcp_content_with_request(
470 &self,
471 method: Option<&str>,
472 url: Option<&str>,
473 request_body: Option<&str>,
474 ) -> String {
475 let mut result = format!(
476 "HTTP {} {}\n\nStatus: {} {}\n",
477 if self.is_success { "✅" } else { "❌" },
478 if self.is_success { "Success" } else { "Error" },
479 self.status_code,
480 self.status_text
481 );
482
483 if let (Some(method), Some(url)) = (method, url) {
485 result.push_str(&format!("\nRequest: {} {}\n", method.to_uppercase(), url));
486
487 if let Some(body) = request_body {
488 if !body.is_empty() && body != "{}" {
489 result.push_str("\nRequest Body:\n");
490 if let Ok(parsed) = serde_json::from_str::<Value>(body) {
491 if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
492 result.push_str(&pretty);
493 } else {
494 result.push_str(body);
495 }
496 } else {
497 result.push_str(body);
498 }
499 result.push('\n');
500 }
501 }
502 }
503
504 if !self.headers.is_empty() {
506 result.push_str("\nHeaders:\n");
507 for (key, value) in &self.headers {
508 if ["content-type", "content-length", "location", "set-cookie"]
510 .iter()
511 .any(|&h| key.to_lowercase().contains(h))
512 {
513 result.push_str(&format!(" {key}: {value}\n"));
514 }
515 }
516 }
517
518 result.push_str("\nResponse Body:\n");
520 if self.body.is_empty() {
521 result.push_str("(empty)");
522 } else if let Ok(json_value) = self.json() {
523 match serde_json::to_string_pretty(&json_value) {
525 Ok(pretty) => result.push_str(&pretty),
526 Err(_) => result.push_str(&self.body),
527 }
528 } else {
529 if self.body.len() > 2000 {
531 result.push_str(&self.body[..2000]);
532 result.push_str(&format!(
533 "\n... ({} more characters)",
534 self.body.len() - 2000
535 ));
536 } else {
537 result.push_str(&self.body);
538 }
539 }
540
541 result
542 }
543}