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