rmcp_openapi/tool/
mod.rs

1pub mod metadata;
2pub mod tool_collection;
3
4pub use metadata::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 the tool has an output schema
89                let structured_content = if self.metadata.output_schema.is_some() {
90                    // Try to parse the response body as JSON
91                    match response.json() {
92                        Ok(json_value) => {
93                            // Wrap the response in our standard HTTP response structure
94                            Some(json!({
95                                "status": response.status_code,
96                                "body": json_value
97                            }))
98                        }
99                        Err(_) => None, // If parsing fails, fall back to text content
100                    }
101                } else {
102                    None
103                };
104
105                // For structured content, serialize to JSON for backwards compatibility
106                let content = if let Some(ref structured) = structured_content {
107                    // MCP Specification: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
108                    // "For backwards compatibility, a tool that returns structured content SHOULD also
109                    // return the serialized JSON in a TextContent block."
110                    match serde_json::to_string(structured) {
111                        Ok(json_string) => vec![Content::text(json_string)],
112                        Err(e) => {
113                            // Return error if we can't serialize the structured content
114                            let error = crate::error::ToolCallError::Execution(
115                                crate::error::ToolCallExecutionError::ResponseParsingError {
116                                    reason: format!("Failed to serialize structured content: {e}"),
117                                    raw_response: None,
118                                },
119                            );
120                            return Err(error);
121                        }
122                    }
123                } else {
124                    vec![Content::text(response.to_mcp_content())]
125                };
126
127                // Return successful response
128                Ok(CallToolResult {
129                    content,
130                    structured_content,
131                    is_error: Some(!response.is_success),
132                    meta: None,
133                })
134            }
135            Err(e) => {
136                // Return ToolCallError directly
137                Err(e)
138            }
139        }
140    }
141
142    /// Execute tool and return raw HTTP response
143    pub async fn execute(
144        &self,
145        arguments: &Value,
146        authorization: Authorization,
147    ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
148        // Extract authorization header if present
149        let auth_header: Option<&rmcp_actix_web::transport::AuthorizationHeader> =
150            match &authorization {
151                Authorization::None => None,
152                #[cfg(feature = "authorization-token-passthrough")]
153                Authorization::PassthroughWarn(header)
154                | Authorization::PassthroughSilent(header) => header.as_ref(),
155            };
156
157        // Create HTTP client with authorization if provided
158        let client = if let Some(auth) = auth_header {
159            self.http_client.with_authorization(&auth.0)
160        } else {
161            self.http_client.clone()
162        };
163
164        // Execute the HTTP request using the (potentially auth-enhanced) HTTP client
165        // Return the raw HttpResponse without MCP formatting
166        client.execute_tool_call(&self.metadata, arguments).await
167    }
168}
169
170/// MCP compliance - Convert Tool to rmcp::model::Tool
171impl From<&Tool> for McpTool {
172    fn from(tool: &Tool) -> Self {
173        (&tool.metadata).into()
174    }
175}