rmcp_openapi/
http_client.rs

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
10/// HTTP client for executing OpenAPI requests
11pub struct HttpClient {
12    client: Client,
13    base_url: Option<String>,
14}
15
16impl HttpClient {
17    /// Create a new HTTP client
18    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    /// Create a new HTTP client with custom timeout
31    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    /// Set the base URL for all requests
44    pub fn with_base_url(mut self, base_url: String) -> Self {
45        self.base_url = Some(base_url);
46        self
47    }
48
49    /// Execute an OpenAPI tool call
50    pub async fn execute_tool_call(
51        &self,
52        tool_metadata: &ToolMetadata,
53        arguments: &Value,
54    ) -> Result<HttpResponse, OpenApiError> {
55        // Extract parameters from arguments
56        let extracted_params = ToolGenerator::extract_parameters(tool_metadata, arguments)?;
57
58        // Build the URL with path parameters
59        let url = self.build_url(tool_metadata, &extracted_params)?;
60
61        // Create the HTTP request
62        let mut request = self.create_request(&tool_metadata.method, &url)?;
63
64        // Add query parameters
65        if !extracted_params.query.is_empty() {
66            request = self.add_query_parameters(request, &extracted_params.query)?;
67        }
68
69        // Add headers
70        if !extracted_params.headers.is_empty() {
71            request = self.add_headers(request, &extracted_params.headers)?;
72        }
73
74        // Add cookies
75        if !extracted_params.cookies.is_empty() {
76            request = self.add_cookies(request, &extracted_params.cookies)?;
77        }
78
79        // Add request body if present
80        if !extracted_params.body.is_empty() {
81            request =
82                self.add_request_body(request, &extracted_params.body, &extracted_params.config)?;
83        }
84
85        // Apply custom timeout if specified
86        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        // Capture request details for response formatting
93        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        // Execute the request
113        let response = request.send().await.map_err(|e| {
114            // Provide more specific error information
115            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        // Convert response to our format with request details
144        self.process_response_with_request(
145            response,
146            &tool_metadata.method,
147            &url,
148            &request_body_string,
149        )
150        .await
151    }
152
153    /// Build the complete URL with path parameters substituted
154    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        // Substitute path parameters
162        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        // Combine with base URL if available
174        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            // Assume the path is already a complete URL
180            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    /// Create a new HTTP request with the specified method and URL
191    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    /// Add query parameters to the request
212    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                    // Handle array parameters (comma-separated for now)
224                    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    /// Add headers to the request
242    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    /// Add cookies to the request
260    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    /// Add request body to the request
286    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        // Set content type header
297        request = request.header("Content-Type", &config.content_type);
298
299        // Handle different content types
300        match config.content_type.as_str() {
301            "application/json" => {
302                // For JSON content type, serialize the body
303                if body.len() == 1 && body.contains_key("request_body") {
304                    // Use the request_body directly if it's the only parameter
305                    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                    // Create JSON object from all body parameters
312                    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                // Handle form data
322                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                // For other content types, try to serialize as JSON
338                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    /// Process the HTTP response into our response format
351    #[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    /// Process the HTTP response with request details for better formatting
361    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        // Add additional context for common error status codes
390        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/// HTTP response from an API call
429#[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    /// Try to parse the response body as JSON
443    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    /// Get a formatted response summary for MCP
449    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    /// Get a formatted response summary for MCP with request details
469    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        // Add request details if provided
484        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        // Add important headers
505        if !self.headers.is_empty() {
506            result.push_str("\nHeaders:\n");
507            for (key, value) in &self.headers {
508                // Only show commonly useful headers
509                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        // Add body content
519        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            // Pretty print JSON if possible
524            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            // Truncate very long responses
530            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}