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