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