1use std::collections::HashMap;
33use std::convert::Infallible;
34use std::future::Future;
35use std::pin::Pin;
36use std::sync::Arc;
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::task::{Context, Poll};
39
40use tower::{Layer, Service};
41use tower_mcp::router::{Extensions, RouterRequest, RouterResponse};
42use tower_mcp_types::protocol::{CallToolParams, GetPromptParams, McpRequest, ReadResourceParams};
43
44#[derive(Clone)]
46pub struct MirrorLayer {
47 mirrors: HashMap<String, (String, u32)>,
48 separator: String,
49}
50
51impl MirrorLayer {
52 pub fn new(mirrors: HashMap<String, (String, u32)>, separator: impl Into<String>) -> Self {
56 Self {
57 mirrors,
58 separator: separator.into(),
59 }
60 }
61}
62
63impl<S> Layer<S> for MirrorLayer {
64 type Service = MirrorService<S>;
65
66 fn layer(&self, inner: S) -> Self::Service {
67 MirrorService::new(inner, self.mirrors.clone(), &self.separator)
68 }
69}
70
71#[derive(Debug, Clone)]
73struct MirrorMapping {
74 source_prefix: String,
76 mirror_prefix: String,
78 percent: u32,
80 counter: Arc<AtomicU64>,
82}
83
84#[derive(Clone)]
90pub struct MirrorService<S> {
91 inner: S,
92 mappings: Arc<Vec<MirrorMapping>>,
93}
94
95impl<S> MirrorService<S> {
96 pub fn new(inner: S, mirrors: HashMap<String, (String, u32)>, separator: &str) -> Self {
101 let mappings = mirrors
102 .into_iter()
103 .map(|(source, (mirror, percent))| MirrorMapping {
104 source_prefix: format!("{source}{separator}"),
105 mirror_prefix: format!("{mirror}{separator}"),
106 percent: percent.clamp(1, 100),
107 counter: Arc::new(AtomicU64::new(0)),
108 })
109 .collect();
110
111 Self {
112 inner,
113 mappings: Arc::new(mappings),
114 }
115 }
116}
117
118fn find_mirror<'a>(name: &str, mappings: &'a [MirrorMapping]) -> Option<&'a MirrorMapping> {
121 mappings.iter().find(|m| name.starts_with(&m.source_prefix))
122}
123
124fn rewrite_name(name: &str, source_prefix: &str, mirror_prefix: &str) -> String {
126 let suffix = &name[source_prefix.len()..];
127 format!("{mirror_prefix}{suffix}")
128}
129
130fn clone_for_mirror(
132 req: &RouterRequest,
133 source_prefix: &str,
134 mirror_prefix: &str,
135) -> Option<RouterRequest> {
136 let new_inner = match &req.inner {
137 McpRequest::CallTool(params) if params.name.starts_with(source_prefix) => {
138 McpRequest::CallTool(CallToolParams {
139 name: rewrite_name(¶ms.name, source_prefix, mirror_prefix),
140 arguments: params.arguments.clone(),
141 input_responses: params.input_responses.clone(),
142 request_state: params.request_state.clone(),
143 meta: params.meta.clone(),
144 task: params.task.clone(),
145 })
146 }
147 McpRequest::ReadResource(params) if params.uri.starts_with(source_prefix) => {
148 McpRequest::ReadResource(ReadResourceParams {
149 uri: rewrite_name(¶ms.uri, source_prefix, mirror_prefix),
150 input_responses: params.input_responses.clone(),
151 request_state: params.request_state.clone(),
152 meta: params.meta.clone(),
153 })
154 }
155 McpRequest::GetPrompt(params) if params.name.starts_with(source_prefix) => {
156 McpRequest::GetPrompt(GetPromptParams {
157 name: rewrite_name(¶ms.name, source_prefix, mirror_prefix),
158 arguments: params.arguments.clone(),
159 input_responses: params.input_responses.clone(),
160 request_state: params.request_state.clone(),
161 meta: params.meta.clone(),
162 })
163 }
164 _ => return None,
166 };
167
168 Some(RouterRequest {
169 id: req.id.clone(),
170 inner: new_inner,
171 extensions: Extensions::new(),
172 })
173}
174
175fn should_mirror(mapping: &MirrorMapping) -> bool {
177 if mapping.percent >= 100 {
178 return true;
179 }
180 let count = mapping.counter.fetch_add(1, Ordering::Relaxed);
181 (count % 100) < mapping.percent as u64
182}
183
184fn request_name(req: &McpRequest) -> Option<&str> {
186 match req {
187 McpRequest::CallTool(params) => Some(¶ms.name),
188 McpRequest::ReadResource(params) => Some(¶ms.uri),
189 McpRequest::GetPrompt(params) => Some(¶ms.name),
190 _ => None,
191 }
192}
193
194impl<S> Service<RouterRequest> for MirrorService<S>
195where
196 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
197 + Clone
198 + Send
199 + 'static,
200 S::Future: Send,
201{
202 type Response = RouterResponse;
203 type Error = Infallible;
204 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
205
206 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
207 self.inner.poll_ready(cx)
208 }
209
210 fn call(&mut self, req: RouterRequest) -> Self::Future {
211 let mirror_req = request_name(&req.inner)
213 .and_then(|name| find_mirror(name, &self.mappings))
214 .filter(|mapping| should_mirror(mapping))
215 .and_then(|mapping| {
216 clone_for_mirror(&req, &mapping.source_prefix, &mapping.mirror_prefix)
217 });
218
219 let primary_fut = self.inner.call(req);
221
222 let mut mirror_svc = if mirror_req.is_some() {
224 Some(self.inner.clone())
225 } else {
226 None
227 };
228
229 Box::pin(async move {
230 if let Some(mirror) = mirror_req
232 && let Some(ref mut svc) = mirror_svc
233 {
234 let mut svc = svc.clone();
235 tokio::spawn(async move {
236 match svc.call(mirror).await {
237 Ok(resp) => {
238 if resp.inner.is_err() {
239 tracing::debug!("Mirror request returned error (discarded)");
240 }
241 }
242 Err(e) => match e {},
243 }
244 });
245 }
246
247 primary_fut.await
248 })
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::test_util::{MockService, call_service};
256 use tower_mcp::protocol::RequestId;
257 use tower_mcp::router::Extensions;
258 use tower_mcp_types::protocol::McpRequest;
259
260 fn make_mirrors(source: &str, mirror: &str, percent: u32) -> HashMap<String, (String, u32)> {
261 let mut m = HashMap::new();
262 m.insert(source.to_string(), (mirror.to_string(), percent));
263 m
264 }
265
266 #[test]
267 fn test_rewrite_name() {
268 assert_eq!(
269 rewrite_name("api/search", "api/", "api-v2/"),
270 "api-v2/search"
271 );
272 assert_eq!(
273 rewrite_name("api/nested/tool", "api/", "mirror/"),
274 "mirror/nested/tool"
275 );
276 }
277
278 #[test]
279 fn test_find_mirror_match() {
280 let mappings = vec![MirrorMapping {
281 source_prefix: "api/".to_string(),
282 mirror_prefix: "api-v2/".to_string(),
283 percent: 100,
284 counter: Arc::new(AtomicU64::new(0)),
285 }];
286 assert!(find_mirror("api/search", &mappings).is_some());
287 assert!(find_mirror("other/search", &mappings).is_none());
288 }
289
290 #[test]
291 fn test_should_mirror_100_percent() {
292 let mapping = MirrorMapping {
293 source_prefix: "api/".to_string(),
294 mirror_prefix: "api-v2/".to_string(),
295 percent: 100,
296 counter: Arc::new(AtomicU64::new(0)),
297 };
298 for _ in 0..10 {
300 assert!(should_mirror(&mapping));
301 }
302 }
303
304 #[test]
305 fn test_should_mirror_percentage() {
306 let mapping = MirrorMapping {
307 source_prefix: "api/".to_string(),
308 mirror_prefix: "api-v2/".to_string(),
309 percent: 10,
310 counter: Arc::new(AtomicU64::new(0)),
311 };
312 let mirrored: u32 = (0..100).filter(|_| should_mirror(&mapping)).count() as u32;
314 assert_eq!(mirrored, 10);
315 }
316
317 #[test]
318 fn test_clone_for_mirror_call_tool() {
319 let req = RouterRequest {
320 id: RequestId::Number(1),
321 inner: McpRequest::CallTool(CallToolParams {
322 name: "api/search".to_string(),
323 arguments: serde_json::json!({"q": "test"}),
324 input_responses: Some(Default::default()),
325 request_state: Some("continuation-1".to_string()),
326 meta: None,
327 task: None,
328 }),
329 extensions: Extensions::new(),
330 };
331
332 let mirrored = clone_for_mirror(&req, "api/", "api-v2/").unwrap();
333 match &mirrored.inner {
334 McpRequest::CallTool(params) => {
335 assert_eq!(params.name, "api-v2/search");
336 assert_eq!(params.arguments, serde_json::json!({"q": "test"}));
337 assert!(params.input_responses.is_some());
338 assert_eq!(params.request_state.as_deref(), Some("continuation-1"));
339 }
340 _ => panic!("expected CallTool"),
341 }
342 }
343
344 #[test]
345 fn test_clone_for_mirror_read_resource() {
346 let req = RouterRequest {
347 id: RequestId::Number(1),
348 inner: McpRequest::ReadResource(ReadResourceParams {
349 uri: "api/docs/readme".to_string(),
350 input_responses: None,
351 request_state: None,
352 meta: None,
353 }),
354 extensions: Extensions::new(),
355 };
356
357 let mirrored = clone_for_mirror(&req, "api/", "mirror/").unwrap();
358 match &mirrored.inner {
359 McpRequest::ReadResource(params) => {
360 assert_eq!(params.uri, "mirror/docs/readme");
361 }
362 _ => panic!("expected ReadResource"),
363 }
364 }
365
366 #[test]
367 fn test_clone_for_mirror_list_tools_returns_none() {
368 let req = RouterRequest {
369 id: RequestId::Number(1),
370 inner: McpRequest::ListTools(Default::default()),
371 extensions: Extensions::new(),
372 };
373 assert!(clone_for_mirror(&req, "api/", "mirror/").is_none());
374 }
375
376 #[tokio::test]
377 async fn test_mirror_service_passes_through() {
378 let mock = MockService::with_tools(&["api/search", "api-v2/search"]);
379 let mirrors = make_mirrors("api", "api-v2", 100);
380 let mut svc = MirrorService::new(mock, mirrors, "/");
381
382 let resp = call_service(
383 &mut svc,
384 McpRequest::CallTool(CallToolParams {
385 name: "api/search".to_string(),
386 arguments: serde_json::json!({}),
387 input_responses: None,
388 request_state: None,
389 meta: None,
390 task: None,
391 }),
392 )
393 .await;
394
395 assert!(resp.inner.is_ok());
397 }
398
399 #[tokio::test]
400 async fn test_mirror_service_non_mirrored_passes_through() {
401 let mock = MockService::with_tools(&["other/tool"]);
402 let mirrors = make_mirrors("api", "api-v2", 100);
403 let mut svc = MirrorService::new(mock, mirrors, "/");
404
405 let resp = call_service(
406 &mut svc,
407 McpRequest::CallTool(CallToolParams {
408 name: "other/tool".to_string(),
409 arguments: serde_json::json!({}),
410 input_responses: None,
411 request_state: None,
412 meta: None,
413 task: None,
414 }),
415 )
416 .await;
417
418 assert!(resp.inner.is_ok());
419 }
420
421 #[tokio::test]
422 async fn test_mirror_service_list_tools_not_mirrored() {
423 let mock = MockService::with_tools(&["api/search"]);
424 let mirrors = make_mirrors("api", "api-v2", 100);
425 let mut svc = MirrorService::new(mock, mirrors, "/");
426
427 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
428 assert!(resp.inner.is_ok());
429 }
430}