rmcp_openapi/tool/
mod.rs

1pub mod metadata;
2pub mod tool_collection;
3
4pub use metadata::{ParameterMapping, ToolMetadata};
5pub use tool_collection::ToolCollection;
6
7use crate::config::Authorization;
8use crate::error::Error;
9use crate::http_client::HttpClient;
10use crate::security::SecurityObserver;
11use reqwest::header::HeaderMap;
12use rmcp::model::{CallToolResult, Tool as McpTool};
13use serde_json::Value;
14use url::Url;
15
16/// Self-contained tool with embedded HTTP client
17#[derive(Clone)]
18pub struct Tool {
19    pub metadata: ToolMetadata,
20    http_client: HttpClient,
21}
22
23impl Tool {
24    /// Create tool with HTTP configuration
25    pub fn new(
26        metadata: ToolMetadata,
27        base_url: Option<Url>,
28        default_headers: Option<HeaderMap>,
29    ) -> Result<Self, Error> {
30        let mut http_client = HttpClient::new();
31
32        if let Some(url) = base_url {
33            http_client = http_client.with_base_url(url)?;
34        }
35
36        if let Some(headers) = default_headers {
37            http_client = http_client.with_default_headers(headers);
38        }
39
40        Ok(Self {
41            metadata,
42            http_client,
43        })
44    }
45
46    /// Execute tool and return MCP-compliant result
47    pub async fn call(
48        &self,
49        arguments: &Value,
50        authorization: Authorization,
51    ) -> Result<CallToolResult, crate::error::ToolCallError> {
52        use rmcp::model::Content;
53        use serde_json::json;
54
55        // Create security observer for logging
56        let observer = SecurityObserver::new(&authorization);
57
58        // Log the authorization decision
59        let has_auth = match &authorization {
60            Authorization::None => false,
61            #[cfg(feature = "authorization-token-passthrough")]
62            Authorization::PassthroughWarn(header) | Authorization::PassthroughSilent(header) => {
63                header.is_some()
64            }
65        };
66
67        observer.observe_request(&self.metadata.name, has_auth, self.metadata.requires_auth());
68
69        // Extract authorization header if present
70        let auth_header: Option<&rmcp_actix_web::transport::AuthorizationHeader> =
71            match &authorization {
72                Authorization::None => None,
73                #[cfg(feature = "authorization-token-passthrough")]
74                Authorization::PassthroughWarn(header)
75                | Authorization::PassthroughSilent(header) => header.as_ref(),
76            };
77
78        // Create HTTP client with authorization if provided
79        let client = if let Some(auth) = auth_header {
80            self.http_client.with_authorization(&auth.0)
81        } else {
82            self.http_client.clone()
83        };
84
85        // Execute the HTTP request using the (potentially auth-enhanced) HTTP client
86        match client.execute_tool_call(&self.metadata, arguments).await {
87            Ok(response) => {
88                // Check if response is an image and return image content
89                if response.is_image()
90                    && let Some(bytes) = &response.body_bytes
91                {
92                    // Base64 encode the image data
93                    use base64::{Engine as _, engine::general_purpose::STANDARD};
94                    let base64_data = STANDARD.encode(bytes);
95
96                    // Get the MIME type - it must be present for image responses
97                    let mime_type = response.content_type.as_deref().ok_or_else(|| {
98                        crate::error::ToolCallError::Execution(
99                            crate::error::ToolCallExecutionError::ResponseParsingError {
100                                reason: "Image response missing Content-Type header".to_string(),
101                                raw_response: None,
102                            },
103                        )
104                    })?;
105
106                    // Return image content
107                    return Ok(CallToolResult {
108                        content: vec![Content::image(base64_data, mime_type)],
109                        structured_content: None,
110                        is_error: Some(!response.is_success),
111                        meta: None,
112                    });
113                }
114
115                // Check if the tool has an output schema
116                let structured_content = if self.metadata.output_schema.is_some() {
117                    // Try to parse the response body as JSON
118                    match response.json() {
119                        Ok(json_value) => {
120                            // Wrap the response in our standard HTTP response structure
121                            Some(json!({
122                                "status": response.status_code,
123                                "body": json_value
124                            }))
125                        }
126                        Err(_) => None, // If parsing fails, fall back to text content
127                    }
128                } else {
129                    None
130                };
131
132                // For structured content, serialize to JSON for backwards compatibility
133                let content = if let Some(ref structured) = structured_content {
134                    // MCP Specification: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
135                    // "For backwards compatibility, a tool that returns structured content SHOULD also
136                    // return the serialized JSON in a TextContent block."
137                    match serde_json::to_string(structured) {
138                        Ok(json_string) => vec![Content::text(json_string)],
139                        Err(e) => {
140                            // Return error if we can't serialize the structured content
141                            let error = crate::error::ToolCallError::Execution(
142                                crate::error::ToolCallExecutionError::ResponseParsingError {
143                                    reason: format!("Failed to serialize structured content: {e}"),
144                                    raw_response: None,
145                                },
146                            );
147                            return Err(error);
148                        }
149                    }
150                } else {
151                    vec![Content::text(response.to_mcp_content())]
152                };
153
154                // Return successful response
155                Ok(CallToolResult {
156                    content,
157                    structured_content,
158                    is_error: Some(!response.is_success),
159                    meta: None,
160                })
161            }
162            Err(e) => {
163                // Return ToolCallError directly
164                Err(e)
165            }
166        }
167    }
168
169    /// Execute tool and return raw HTTP response
170    pub async fn execute(
171        &self,
172        arguments: &Value,
173        authorization: Authorization,
174    ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
175        // Extract authorization header if present
176        let auth_header: Option<&rmcp_actix_web::transport::AuthorizationHeader> =
177            match &authorization {
178                Authorization::None => None,
179                #[cfg(feature = "authorization-token-passthrough")]
180                Authorization::PassthroughWarn(header)
181                | Authorization::PassthroughSilent(header) => header.as_ref(),
182            };
183
184        // Create HTTP client with authorization if provided
185        let client = if let Some(auth) = auth_header {
186            self.http_client.with_authorization(&auth.0)
187        } else {
188            self.http_client.clone()
189        };
190
191        // Execute the HTTP request using the (potentially auth-enhanced) HTTP client
192        // Return the raw HttpResponse without MCP formatting
193        client.execute_tool_call(&self.metadata, arguments).await
194    }
195}
196
197/// MCP compliance - Convert Tool to rmcp::model::Tool
198impl From<&Tool> for McpTool {
199    fn from(tool: &Tool) -> Self {
200        (&tool.metadata).into()
201    }
202}