rmcp_openapi/tool/
openapi_tool.rs

1use super::ToolMetadata;
2use crate::error::OpenApiError;
3use crate::http_client::HttpClient;
4use reqwest::header::HeaderMap;
5use rmcp::model::{CallToolResult, Tool};
6use serde_json::Value;
7use url::Url;
8
9/// Self-contained OpenAPI tool with embedded HTTP client
10#[derive(Clone)]
11pub struct OpenApiTool {
12    pub metadata: ToolMetadata,
13    http_client: HttpClient,
14}
15
16impl OpenApiTool {
17    /// Create OpenAPI tool with HTTP configuration
18    pub fn new(
19        metadata: ToolMetadata,
20        base_url: Option<Url>,
21        default_headers: Option<HeaderMap>,
22    ) -> Result<Self, OpenApiError> {
23        let mut http_client = HttpClient::new();
24
25        if let Some(url) = base_url {
26            http_client = http_client.with_base_url(url)?;
27        }
28
29        if let Some(headers) = default_headers {
30            http_client = http_client.with_default_headers(headers);
31        }
32
33        Ok(Self {
34            metadata,
35            http_client,
36        })
37    }
38
39    /// Execute tool and return MCP-compliant result
40    pub async fn call(
41        &self,
42        arguments: &Value,
43    ) -> Result<CallToolResult, crate::error::ToolCallError> {
44        use rmcp::model::Content;
45        use serde_json::json;
46
47        // Execute the HTTP request using the embedded HTTP client
48        match self
49            .http_client
50            .execute_tool_call(&self.metadata, arguments)
51            .await
52        {
53            Ok(response) => {
54                // Check if the tool has an output schema
55                let structured_content = if self.metadata.output_schema.is_some() {
56                    // Try to parse the response body as JSON
57                    match response.json() {
58                        Ok(json_value) => {
59                            // Wrap the response in our standard HTTP response structure
60                            Some(json!({
61                                "status": response.status_code,
62                                "body": json_value
63                            }))
64                        }
65                        Err(_) => None, // If parsing fails, fall back to text content
66                    }
67                } else {
68                    None
69                };
70
71                // For structured content, serialize to JSON for backwards compatibility
72                let content = if let Some(ref structured) = structured_content {
73                    // MCP Specification: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
74                    // "For backwards compatibility, a tool that returns structured content SHOULD also
75                    // return the serialized JSON in a TextContent block."
76                    match serde_json::to_string(structured) {
77                        Ok(json_string) => Some(vec![Content::text(json_string)]),
78                        Err(e) => {
79                            // Return error if we can't serialize the structured content
80                            let error = crate::error::ToolCallError::Execution(
81                                crate::error::ToolCallExecutionError::ResponseParsingError {
82                                    reason: format!("Failed to serialize structured content: {e}"),
83                                    raw_response: None,
84                                },
85                            );
86                            return Err(error);
87                        }
88                    }
89                } else {
90                    Some(vec![Content::text(response.to_mcp_content())])
91                };
92
93                // Return successful response
94                Ok(CallToolResult {
95                    content,
96                    structured_content,
97                    is_error: Some(!response.is_success),
98                })
99            }
100            Err(e) => {
101                // Return ToolCallError directly
102                Err(e)
103            }
104        }
105    }
106
107    /// Execute tool and return raw HTTP response
108    pub async fn execute(
109        &self,
110        arguments: &Value,
111    ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
112        // Execute the HTTP request using the embedded HTTP client
113        // Return the raw HttpResponse without MCP formatting
114        self.http_client
115            .execute_tool_call(&self.metadata, arguments)
116            .await
117    }
118}
119
120/// MCP compliance - Convert OpenApiTool to rmcp::model::Tool
121impl From<&OpenApiTool> for Tool {
122    fn from(openapi_tool: &OpenApiTool) -> Self {
123        (&openapi_tool.metadata).into()
124    }
125}