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}