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}