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