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#[derive(Clone)]
18pub struct Tool {
19 pub metadata: ToolMetadata,
20 http_client: HttpClient,
21 pub(crate) response_transformer: Option<Arc<dyn ResponseTransformer>>,
23}
24
25impl Tool {
26 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 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 let observer = SecurityObserver::new(&authorization);
53
54 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 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 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 let transformer = self
83 .response_transformer
84 .as_ref()
85 .map(|t| t.as_ref() as &dyn ResponseTransformer)
86 .or(server_transformer);
87
88 match client.execute_tool_call(&self.metadata, arguments).await {
90 Ok(response) => {
91 if response.is_image()
93 && let Some(bytes) = &response.body_bytes
94 {
95 use base64::{Engine as _, engine::general_purpose::STANDARD};
97 let base64_data = STANDARD.encode(bytes);
98
99 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 Ok(if response.is_success {
111 CallToolResult::success(vec![Content::image(base64_data, mime_type)])
112 } else {
113 CallToolResult::error(vec![Content::image(base64_data, mime_type)])
114 });
115 }
116
117 let structured_content = if self.metadata.output_schema.is_some() {
119 match response.json() {
121 Ok(json_value) => {
122 let transformed_body = if let Some(t) = transformer {
124 t.transform_response(json_value)
125 } else {
126 json_value
127 };
128
129 Some(json!({
131 "status": response.status_code,
132 "body": transformed_body
133 }))
134 }
135 Err(_) => None, }
137 } else {
138 None
139 };
140
141 let content = if let Some(ref structured) = structured_content {
143 match serde_json::to_string(structured) {
147 Ok(json_string) => vec![Content::text(json_string)],
148 Err(e) => {
149 let error = crate::error::ToolCallError::Execution(
151 crate::error::ToolCallExecutionError::ResponseParsingError {
152 reason: format!("Failed to serialize structured content: {e}"),
153 raw_response: None,
154 },
155 );
156 return Err(error);
157 }
158 }
159 } else {
160 vec![Content::text(response.to_mcp_content())]
161 };
162
163 let mut result = if response.is_success {
165 CallToolResult::success(content)
166 } else {
167 CallToolResult::error(content)
168 };
169 result.structured_content = structured_content;
170 Ok(result)
171 }
172 Err(e) => {
173 Err(e)
175 }
176 }
177 }
178
179 pub async fn execute(
181 &self,
182 arguments: &Value,
183 authorization: Authorization,
184 ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
185 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 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 client.execute_tool_call(&self.metadata, arguments).await
204 }
205}
206
207impl From<&Tool> for McpTool {
209 fn from(tool: &Tool) -> Self {
210 (&tool.metadata).into()
211 }
212}