Skip to main content

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