rmcp_openapi/tool/
metadata.rs

1use rmcp::model::{Tool, ToolAnnotations};
2use serde_json::Value;
3use std::sync::Arc;
4
5/// Internal metadata for tools generated from OpenAPI operations.
6///
7/// This struct contains all the information needed to execute HTTP requests
8/// and is used internally by the OpenAPI server. It includes fields that are
9/// not part of the MCP specification but are necessary for HTTP execution.
10///
11/// For MCP compliance, this struct is converted to `rmcp::model::Tool` using
12/// the `From` trait implementation, which only includes MCP-compliant fields.
13#[derive(Debug, Clone, serde::Serialize)]
14pub struct ToolMetadata {
15    /// Tool name - exposed to MCP clients
16    pub name: String,
17    /// Tool title - human-readable display name exposed to MCP clients
18    pub title: Option<String>,
19    /// Tool description - exposed to MCP clients  
20    pub description: String,
21    /// Input parameters schema - exposed to MCP clients as `inputSchema`
22    pub parameters: Value,
23    /// Output schema - exposed to MCP clients as `outputSchema`
24    pub output_schema: Option<Value>,
25    /// HTTP method (GET, POST, etc.) - internal only, not exposed to MCP
26    pub method: String,
27    /// URL path for the API endpoint - internal only, not exposed to MCP
28    pub path: String,
29}
30
31impl ToolMetadata {
32    /// Check if a parameter is required according to the tool metadata
33    pub fn is_parameter_required(&self, param_name: &str) -> bool {
34        self.parameters
35            .get("required")
36            .and_then(|r| r.as_array())
37            .map(|arr| arr.iter().any(|v| v.as_str() == Some(param_name)))
38            .unwrap_or(false)
39    }
40
41    /// Check if a parameter is an array type according to the tool metadata
42    pub fn is_parameter_array_type(&self, param_name: &str) -> bool {
43        self.parameters
44            .get("properties")
45            .and_then(|props| props.get(param_name))
46            .and_then(|param| param.get("type"))
47            .and_then(|type_val| type_val.as_str())
48            .map(|type_str| type_str == "array")
49            .unwrap_or(false)
50    }
51
52    /// Check if a parameter has a default value according to the tool metadata
53    pub fn has_parameter_default(&self, param_name: &str) -> bool {
54        self.parameters
55            .get("properties")
56            .and_then(|props| props.get(param_name))
57            .map(|param| param.get("default").is_some())
58            .unwrap_or(false)
59    }
60
61    /// Check if a JSON value is an empty array
62    pub fn is_empty_array(value: &Value) -> bool {
63        matches!(value, Value::Array(arr) if arr.is_empty())
64    }
65
66    /// Determine if an empty array parameter should be omitted from the HTTP request
67    pub fn should_omit_empty_array_parameter(&self, param_name: &str, value: &Value) -> bool {
68        // Only omit if:
69        // 1. Parameter is not required
70        // 2. Parameter is array type
71        // 3. Value is empty array
72        // 4. Parameter has no explicit default value
73        !self.is_parameter_required(param_name)
74            && self.is_parameter_array_type(param_name)
75            && Self::is_empty_array(value)
76            && !self.has_parameter_default(param_name)
77    }
78}
79
80/// Converts internal `ToolMetadata` to MCP-compliant `Tool`.
81///
82/// This implementation ensures that only MCP-compliant fields are exposed to clients.
83/// Internal fields like `method` and `path` are not included in the conversion.
84impl From<&ToolMetadata> for Tool {
85    fn from(metadata: &ToolMetadata) -> Self {
86        // Convert parameters to the expected Arc<Map> format
87        let input_schema = if let Value::Object(obj) = &metadata.parameters {
88            Arc::new(obj.clone())
89        } else {
90            Arc::new(serde_json::Map::new())
91        };
92
93        // Convert output_schema to the expected Arc<Map> format if present
94        let output_schema = metadata.output_schema.as_ref().and_then(|schema| {
95            if let Value::Object(obj) = schema {
96                Some(Arc::new(obj.clone()))
97            } else {
98                None
99            }
100        });
101
102        // Create annotations with title if present
103        let annotations = metadata.title.as_ref().map(|title| ToolAnnotations {
104            title: Some(title.clone()),
105            ..Default::default()
106        });
107
108        Tool {
109            name: metadata.name.clone().into(),
110            description: Some(metadata.description.clone().into()),
111            input_schema,
112            output_schema,
113            annotations,
114            // TODO: Consider migration to Tool.title when rmcp supports MCP 2025-06-18 (see issue #26)
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use serde_json::json;
123
124    fn create_test_metadata_with_params(parameters: Value) -> ToolMetadata {
125        ToolMetadata {
126            name: "test_tool".to_string(),
127            title: Some("Test Tool".to_string()),
128            description: "A test tool".to_string(),
129            parameters,
130            output_schema: None,
131            method: "GET".to_string(),
132            path: "/test".to_string(),
133        }
134    }
135
136    #[test]
137    fn test_is_parameter_required() {
138        let metadata = create_test_metadata_with_params(json!({
139            "type": "object",
140            "properties": {
141                "required_param": {"type": "string"},
142                "optional_param": {"type": "string"}
143            },
144            "required": ["required_param"]
145        }));
146
147        assert!(metadata.is_parameter_required("required_param"));
148        assert!(!metadata.is_parameter_required("optional_param"));
149        assert!(!metadata.is_parameter_required("nonexistent_param"));
150    }
151
152    #[test]
153    fn test_is_parameter_required_no_required_array() {
154        let metadata = create_test_metadata_with_params(json!({
155            "type": "object",
156            "properties": {
157                "param": {"type": "string"}
158            }
159        }));
160
161        assert!(!metadata.is_parameter_required("param"));
162    }
163
164    #[test]
165    fn test_is_parameter_array_type() {
166        let metadata = create_test_metadata_with_params(json!({
167            "type": "object",
168            "properties": {
169                "array_param": {"type": "array", "items": {"type": "string"}},
170                "string_param": {"type": "string"},
171                "number_param": {"type": "number"}
172            }
173        }));
174
175        assert!(metadata.is_parameter_array_type("array_param"));
176        assert!(!metadata.is_parameter_array_type("string_param"));
177        assert!(!metadata.is_parameter_array_type("number_param"));
178        assert!(!metadata.is_parameter_array_type("nonexistent_param"));
179    }
180
181    #[test]
182    fn test_has_parameter_default() {
183        let metadata = create_test_metadata_with_params(json!({
184            "type": "object",
185            "properties": {
186                "with_default": {"type": "string", "default": "test"},
187                "with_array_default": {"type": "array", "items": {"type": "string"}, "default": ["item1"]},
188                "without_default": {"type": "string"},
189                "with_null_default": {"type": "string", "default": null}
190            }
191        }));
192
193        assert!(metadata.has_parameter_default("with_default"));
194        assert!(metadata.has_parameter_default("with_array_default"));
195        assert!(!metadata.has_parameter_default("without_default"));
196        assert!(metadata.has_parameter_default("with_null_default")); // null is still a default
197        assert!(!metadata.has_parameter_default("nonexistent_param"));
198    }
199
200    #[test]
201    fn test_is_empty_array() {
202        assert!(ToolMetadata::is_empty_array(&json!([])));
203        assert!(!ToolMetadata::is_empty_array(&json!(["item"])));
204        assert!(!ToolMetadata::is_empty_array(&json!("string")));
205        assert!(!ToolMetadata::is_empty_array(&json!(42)));
206        assert!(!ToolMetadata::is_empty_array(&json!(null)));
207    }
208
209    #[test]
210    fn test_should_omit_empty_array_parameter() {
211        // Test case: optional array without default - should omit
212        let metadata1 = create_test_metadata_with_params(json!({
213            "type": "object",
214            "properties": {
215                "optional_array": {"type": "array", "items": {"type": "string"}}
216            },
217            "required": []
218        }));
219        assert!(metadata1.should_omit_empty_array_parameter("optional_array", &json!([])));
220        assert!(!metadata1.should_omit_empty_array_parameter("optional_array", &json!(["item"])));
221
222        // Test case: required array - should not omit
223        let metadata2 = create_test_metadata_with_params(json!({
224            "type": "object",
225            "properties": {
226                "required_array": {"type": "array", "items": {"type": "string"}}
227            },
228            "required": ["required_array"]
229        }));
230        assert!(!metadata2.should_omit_empty_array_parameter("required_array", &json!([])));
231
232        // Test case: optional array with default - should not omit
233        let metadata3 = create_test_metadata_with_params(json!({
234            "type": "object",
235            "properties": {
236                "optional_array_with_default": {
237                    "type": "array",
238                    "items": {"type": "string"},
239                    "default": ["default_item"]
240                }
241            },
242            "required": []
243        }));
244        assert!(
245            !metadata3.should_omit_empty_array_parameter("optional_array_with_default", &json!([]))
246        );
247
248        // Test case: optional non-array - should not omit
249        let metadata4 = create_test_metadata_with_params(json!({
250            "type": "object",
251            "properties": {
252                "optional_string": {"type": "string"}
253            },
254            "required": []
255        }));
256        assert!(!metadata4.should_omit_empty_array_parameter("optional_string", &json!([])));
257    }
258}