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#[derive(Clone)]
18pub struct Tool {
19    pub metadata: ToolMetadata,
20    http_client: HttpClient,
21}
22
23impl Tool {
24    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    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        let observer = SecurityObserver::new(&authorization);
57
58        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        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        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        match client.execute_tool_call(&self.metadata, arguments).await {
87            Ok(response) => {
88                if response.is_image()
90                    && let Some(bytes) = &response.body_bytes
91                {
92                    use base64::{Engine as _, engine::general_purpose::STANDARD};
94                    let base64_data = STANDARD.encode(bytes);
95
96                    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 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                let structured_content = if self.metadata.output_schema.is_some() {
117                    match response.json() {
119                        Ok(json_value) => {
120                            Some(json!({
122                                "status": response.status_code,
123                                "body": json_value
124                            }))
125                        }
126                        Err(_) => None, }
128                } else {
129                    None
130                };
131
132                let content = if let Some(ref structured) = structured_content {
134                    match serde_json::to_string(structured) {
138                        Ok(json_string) => vec![Content::text(json_string)],
139                        Err(e) => {
140                            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                Ok(CallToolResult {
156                    content,
157                    structured_content,
158                    is_error: Some(!response.is_success),
159                    meta: None,
160                })
161            }
162            Err(e) => {
163                Err(e)
165            }
166        }
167    }
168
169    pub async fn execute(
171        &self,
172        arguments: &Value,
173        authorization: Authorization,
174    ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
175        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        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        client.execute_tool_call(&self.metadata, arguments).await
194    }
195}
196
197impl From<&Tool> for McpTool {
199    fn from(tool: &Tool) -> Self {
200        (&tool.metadata).into()
201    }
202}