Skip to main content

rmcp_openapi/
transformer.rs

1//! Response transformers for modifying tool call responses.
2//!
3//! This module provides the [`ResponseTransformer`] trait that allows users to modify/filter
4//! tool call responses before they are returned to the MCP client (LLM). Common use cases include:
5//!
6//! - Removing irrelevant fields to reduce token count
7//! - Removing null value fields
8//! - Making responses more suitable for LLM consumption
9//!
10//! # Example
11//!
12//! ```rust
13//! use rmcp_openapi::ResponseTransformer;
14//! use serde_json::Value;
15//!
16//! /// Transformer that removes null fields from responses
17//! struct RemoveNulls;
18//!
19//! impl ResponseTransformer for RemoveNulls {
20//!     fn transform_response(&self, response: Value) -> Value {
21//!         remove_nulls(response)
22//!     }
23//!
24//!     fn transform_schema(&self, schema: Value) -> Value {
25//!         // Mark all fields as optional since they may be removed
26//!         mark_fields_optional(schema)
27//!     }
28//! }
29//!
30//! fn remove_nulls(value: Value) -> Value {
31//!     match value {
32//!         Value::Object(map) => {
33//!             let filtered: serde_json::Map<String, Value> = map
34//!                 .into_iter()
35//!                 .filter(|(_, v)| !v.is_null())
36//!                 .map(|(k, v)| (k, remove_nulls(v)))
37//!                 .collect();
38//!             Value::Object(filtered)
39//!         }
40//!         Value::Array(arr) => {
41//!             Value::Array(arr.into_iter().map(remove_nulls).collect())
42//!         }
43//!         other => other,
44//!     }
45//! }
46//!
47//! fn mark_fields_optional(schema: Value) -> Value {
48//!     // Implementation would remove required fields from schema
49//!     schema
50//! }
51//! ```
52
53use serde_json::Value;
54
55/// Transforms tool responses and their corresponding schemas.
56///
57/// Implementors must ensure [`transform_response`](Self::transform_response) and
58/// [`transform_schema`](Self::transform_schema) are consistent - if a field is removed
59/// from responses, it should also be removed from the schema.
60///
61/// # Usage
62///
63/// Response transformers can be applied globally to all tools or per-tool:
64///
65/// ```rust,ignore
66/// use std::sync::Arc;
67/// use rmcp_openapi::{Server, ResponseTransformer};
68///
69/// // Global transformer
70/// let mut server = Server::builder()
71///     .openapi_spec(spec)
72///     .base_url(url)
73///     .response_transformer(Arc::new(RemoveNulls))
74///     .build();
75///
76/// server.load_openapi_spec()?;
77///
78/// // Per-tool override
79/// server.set_tool_transformer("verbose-endpoint", Arc::new(AggressiveFilter))?;
80/// ```
81///
82/// # Resolution Order
83///
84/// 1. Per-tool transformer (if set) takes precedence
85/// 2. Else global server transformer
86/// 3. Else no transformation
87pub trait ResponseTransformer: Send + Sync {
88    /// Transform the response body before returning to MCP client.
89    ///
90    /// This method is called after each successful tool call to transform
91    /// the JSON response body. The transformation should be consistent with
92    /// [`transform_schema`](Self::transform_schema) - any fields removed here
93    /// should also be removed from the schema.
94    ///
95    /// # Arguments
96    ///
97    /// * `response` - The JSON response body from the HTTP call
98    ///
99    /// # Returns
100    ///
101    /// The transformed response body
102    fn transform_response(&self, response: Value) -> Value;
103
104    /// Transform the output schema to match response transformations.
105    ///
106    /// This method is called when:
107    /// - Loading the OpenAPI spec (for global transformers)
108    /// - Setting a per-tool transformer
109    ///
110    /// The schema transformation should reflect what [`transform_response`](Self::transform_response)
111    /// will do to the actual responses.
112    ///
113    /// # Arguments
114    ///
115    /// * `schema` - The JSON Schema for the tool's output
116    ///
117    /// # Returns
118    ///
119    /// The transformed schema
120    fn transform_schema(&self, schema: Value) -> Value;
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use serde_json::json;
127
128    /// Simple transformer that removes null fields
129    struct RemoveNulls;
130
131    impl ResponseTransformer for RemoveNulls {
132        fn transform_response(&self, response: Value) -> Value {
133            remove_nulls(response)
134        }
135
136        fn transform_schema(&self, schema: Value) -> Value {
137            // For simplicity, just return the schema as-is in tests
138            schema
139        }
140    }
141
142    fn remove_nulls(value: Value) -> Value {
143        match value {
144            Value::Object(map) => {
145                let filtered: serde_json::Map<String, Value> = map
146                    .into_iter()
147                    .filter(|(_, v)| !v.is_null())
148                    .map(|(k, v)| (k, remove_nulls(v)))
149                    .collect();
150                Value::Object(filtered)
151            }
152            Value::Array(arr) => Value::Array(arr.into_iter().map(remove_nulls).collect()),
153            other => other,
154        }
155    }
156
157    #[test]
158    fn test_remove_nulls_transformer() {
159        let transformer = RemoveNulls;
160
161        let response = json!({
162            "id": 1,
163            "name": "Test",
164            "description": null,
165            "nested": {
166                "value": 42,
167                "optional": null
168            }
169        });
170
171        let transformed = transformer.transform_response(response);
172
173        assert_eq!(
174            transformed,
175            json!({
176                "id": 1,
177                "name": "Test",
178                "nested": {
179                    "value": 42
180                }
181            })
182        );
183    }
184
185    #[test]
186    fn test_transformer_with_arrays() {
187        let transformer = RemoveNulls;
188
189        let response = json!({
190            "items": [
191                {"id": 1, "value": null},
192                {"id": 2, "value": "test"}
193            ]
194        });
195
196        let transformed = transformer.transform_response(response);
197
198        assert_eq!(
199            transformed,
200            json!({
201                "items": [
202                    {"id": 1},
203                    {"id": 2, "value": "test"}
204                ]
205            })
206        );
207    }
208
209    #[test]
210    fn test_transformer_preserves_non_null_values() {
211        let transformer = RemoveNulls;
212
213        let response = json!({
214            "string": "hello",
215            "number": 42,
216            "boolean": true,
217            "array": [1, 2, 3],
218            "object": {"key": "value"}
219        });
220
221        let transformed = transformer.transform_response(response.clone());
222
223        assert_eq!(transformed, response);
224    }
225}