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}