turbomcp_client/plugins/
macros.rs

1//! Plugin execution macros for reducing boilerplate
2//!
3//! These macros provide ergonomic ways to execute plugin middleware chains
4//! without the verbose manual implementation required in each client method.
5
6/// Execute a protocol call with full plugin middleware support
7///
8/// This macro handles the complete plugin execution pipeline:
9/// 1. Creates RequestContext
10/// 2. Executes before_request plugin chain
11/// 3. Executes the provided protocol call
12/// 4. Creates ResponseContext  
13/// 5. Executes after_response plugin chain
14/// 6. Returns the final result
15///
16/// # Usage
17///
18/// ```rust,ignore
19/// use turbomcp_client::with_plugins;
20/// use std::collections::HashMap;
21///
22/// impl Client {
23///     pub async fn call_tool(&mut self, name: &str, args: Option<HashMap<String, serde_json::Value>>) -> turbomcp_core::Result<serde_json::Value> {
24///         let request_data = turbomcp_protocol::types::CallToolRequest {
25///             name: name.to_string(),
26///             arguments: Some(args.unwrap_or_default()),
27///         };
28///
29///         with_plugins!(self, "tools/call", request_data, {
30///             // Your protocol call here - plugins execute automatically
31///             let result: turbomcp_protocol::types::CallToolResult = self.protocol
32///                 .request("tools/call", Some(serde_json::to_value(&request_data)?))
33///                 .await?;
34///             
35///             Ok(self.extract_tool_content(&result))
36///         })
37///     }
38/// }
39/// ```
40///
41/// The macro automatically:
42/// - ✅ Creates proper RequestContext with JSON-RPC structure
43/// - ✅ Executes all registered plugins before the request
44/// - ✅ Times the operation for metrics
45/// - ✅ Creates ResponseContext with results and timing
46/// - ✅ Executes all registered plugins after the response
47/// - ✅ Handles errors gracefully with proper context
48/// - ✅ Returns the final processed result
49#[macro_export]
50macro_rules! with_plugins {
51    ($client:expr, $method:expr, $request_data:expr, $protocol_call:block) => {{
52        // Create JSON-RPC request for plugin context with unique ID
53        let request_id = turbomcp_core::MessageId::Number(
54            std::time::SystemTime::now()
55                .duration_since(std::time::UNIX_EPOCH)
56                .unwrap_or_default()
57                .as_nanos() as i64
58        );
59
60        let json_rpc_request = turbomcp_protocol::jsonrpc::JsonRpcRequest {
61            jsonrpc: turbomcp_protocol::jsonrpc::JsonRpcVersion,
62            id: request_id,
63            method: $method.to_string(),
64            params: Some(serde_json::to_value(&$request_data)
65                .map_err(|e| turbomcp_core::Error::bad_request(
66                    format!("Failed to serialize request data: {}", e)
67                ))?),
68        };
69
70        // 1. Create request context for plugins
71        let mut req_ctx = $crate::plugins::RequestContext::new(
72            json_rpc_request,
73            std::collections::HashMap::new()
74        );
75
76        // 2. Execute before_request plugin middleware
77        $client.plugin_registry.execute_before_request(&mut req_ctx).await
78            .map_err(|e| turbomcp_core::Error::bad_request(
79                format!("Plugin before_request failed: {}", e)
80            ))?;
81
82        // 3. Execute the actual protocol call with timing
83        let start_time = std::time::Instant::now();
84        let protocol_result: turbomcp_core::Result<_> = async $protocol_call.await;
85        let duration = start_time.elapsed();
86
87        // 4. Create response context based on result
88        let mut resp_ctx = match protocol_result {
89            Ok(ref response_value) => {
90                let serialized_response = serde_json::to_value(response_value)
91                    .map_err(|e| turbomcp_core::Error::bad_request(
92                        format!("Failed to serialize response: {}", e)
93                    ))?;
94                $crate::plugins::ResponseContext::new(
95                    req_ctx,
96                    Some(serialized_response),
97                    None,
98                    duration
99                )
100            },
101            Err(ref e) => {
102                $crate::plugins::ResponseContext::new(
103                    req_ctx,
104                    None,
105                    Some(*e.clone()),
106                    duration
107                )
108            }
109        };
110
111        // 5. Execute after_response plugin middleware
112        $client.plugin_registry.execute_after_response(&mut resp_ctx).await
113            .map_err(|e| turbomcp_core::Error::bad_request(
114                format!("Plugin after_response failed: {}", e)
115            ))?;
116
117        // 6. Return the final result, respecting plugin modifications
118        match protocol_result {
119            Ok(original_response) => {
120                // Check if plugins modified the response
121                if let Some(modified_response) = resp_ctx.response {
122                    // Try to deserialize back to original type if plugins modified it
123                    match serde_json::from_value(modified_response.clone()) {
124                        Ok(plugin_modified_result) => Ok(plugin_modified_result),
125                        Err(_) => {
126                            // Plugin returned a different format, return the original
127                            // This maintains type safety while allowing plugin flexibility
128                            Ok(original_response)
129                        }
130                    }
131                } else {
132                    // No plugin modifications, return original
133                    Ok(original_response)
134                }
135            },
136            Err(original_error) => {
137                // Check if plugins provided error recovery
138                if let Some(recovery_response) = resp_ctx.response {
139                    match serde_json::from_value(recovery_response) {
140                        Ok(recovered_result) => Ok(recovered_result),
141                        Err(_) => Err(original_error), // Recovery failed, return original error
142                    }
143                } else {
144                    Err(original_error)
145                }
146            }
147        }
148    }};
149}
150
151/// Execute a simple protocol call with plugin middleware for methods without complex request data
152///
153/// This is a lighter version for methods that don't need complex request context.
154///
155/// # Usage
156///
157/// ```rust,ignore
158/// use turbomcp_client::with_simple_plugins;
159///
160/// impl Client {
161///     pub async fn ping(&mut self) -> turbomcp_core::Result<()> {
162///         with_simple_plugins!(self, "ping", {
163///             self.protocol.request("ping", None).await
164///         })
165///     }
166/// }
167/// ```
168#[macro_export]
169macro_rules! with_simple_plugins {
170    ($client:expr, $method:expr, $protocol_call:block) => {{
171        // Use the full macro with empty request data
172        let empty_request = serde_json::Value::Null;
173        $crate::with_plugins!($client, $method, empty_request, $protocol_call)
174    }};
175}
176
177/// Execute plugin middleware for methods that return lists (common pattern)
178///
179/// Many MCP methods return lists and have similar patterns. This macro
180/// provides a specialized version for list-returning methods.
181#[macro_export]
182macro_rules! with_plugins_list {
183    ($client:expr, $method:expr, $request_data:expr, $protocol_call:block) => {{ $crate::with_plugins!($client, $method, $request_data, $protocol_call) }};
184}
185
186#[cfg(test)]
187mod tests {
188
189    #[tokio::test]
190    async fn test_macro_generates_unique_request_ids() {
191        // Test that multiple macro invocations generate different IDs
192        // This tests our fix for the hardcoded ID issue
193        let request_data = serde_json::json!({"test": "data"});
194
195        let id1 = {
196            let json_rpc_request = turbomcp_protocol::jsonrpc::JsonRpcRequest {
197                jsonrpc: turbomcp_protocol::jsonrpc::JsonRpcVersion,
198                id: turbomcp_core::MessageId::Number(
199                    std::time::SystemTime::now()
200                        .duration_since(std::time::UNIX_EPOCH)
201                        .unwrap_or_default()
202                        .as_nanos() as i64,
203                ),
204                method: "test".to_string(),
205                params: Some(request_data.clone()),
206            };
207            json_rpc_request.id
208        };
209
210        // Small delay to ensure different timestamps
211        tokio::time::sleep(tokio::time::Duration::from_nanos(1)).await;
212
213        let id2 = {
214            let json_rpc_request = turbomcp_protocol::jsonrpc::JsonRpcRequest {
215                jsonrpc: turbomcp_protocol::jsonrpc::JsonRpcVersion,
216                id: turbomcp_core::MessageId::Number(
217                    std::time::SystemTime::now()
218                        .duration_since(std::time::UNIX_EPOCH)
219                        .unwrap_or_default()
220                        .as_nanos() as i64,
221                ),
222                method: "test".to_string(),
223                params: Some(request_data),
224            };
225            json_rpc_request.id
226        };
227
228        // IDs should be different
229        assert_ne!(id1, id2, "Request IDs should be unique");
230    }
231
232    #[test]
233    fn test_error_propagation() {
234        // Test that our error wrapping works correctly
235        let error_message = "Failed to serialize request data: test error";
236        let wrapped_error = turbomcp_core::Error::bad_request(error_message);
237
238        assert!(
239            wrapped_error
240                .to_string()
241                .contains("Failed to serialize request data")
242        );
243        assert!(wrapped_error.to_string().contains("test error"));
244
245        // Test response serialization error wrapping as well
246        let response_error_message = "Failed to serialize response: test response error";
247        let wrapped_response_error = turbomcp_core::Error::bad_request(response_error_message);
248
249        assert!(
250            wrapped_response_error
251                .to_string()
252                .contains("Failed to serialize response")
253        );
254        assert!(
255            wrapped_response_error
256                .to_string()
257                .contains("test response error")
258        );
259    }
260
261    #[test]
262    fn test_macro_compilation() {
263        // These tests ensure the macros compile correctly
264        // Real integration testing happens in the client tests
265    }
266}