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#[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 let structured_content = if self.metadata.output_schema.is_some() {
90 match response.json() {
92 Ok(json_value) => {
93 Some(json!({
95 "status": response.status_code,
96 "body": json_value
97 }))
98 }
99 Err(_) => None, }
101 } else {
102 None
103 };
104
105 let content = if let Some(ref structured) = structured_content {
107 match serde_json::to_string(structured) {
111 Ok(json_string) => vec![Content::text(json_string)],
112 Err(e) => {
113 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 Ok(CallToolResult {
129 content,
130 structured_content,
131 is_error: Some(!response.is_success),
132 meta: None,
133 })
134 }
135 Err(e) => {
136 Err(e)
138 }
139 }
140 }
141
142 pub async fn execute(
144 &self,
145 arguments: &Value,
146 authorization: Authorization,
147 ) -> Result<crate::http_client::HttpResponse, crate::error::ToolCallError> {
148 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 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 client.execute_tool_call(&self.metadata, arguments).await
167 }
168}
169
170impl From<&Tool> for McpTool {
172 fn from(tool: &Tool) -> Self {
173 (&tool.metadata).into()
174 }
175}