1use std::convert::Infallible;
103use std::future::Future;
104use std::pin::Pin;
105use std::sync::Arc;
106use std::task::{Context, Poll};
107
108use tower::{Layer, Service};
109
110use tower_mcp::protocol::{McpRequest, McpResponse};
111use tower_mcp::{RouterRequest, RouterResponse};
112use tower_mcp_types::JsonRpcError;
113
114use crate::config::BackendFilter;
115
116#[derive(Clone)]
131pub struct CapabilityFilterLayer {
132 filters: Vec<BackendFilter>,
133}
134
135impl CapabilityFilterLayer {
136 pub fn new(filters: Vec<BackendFilter>) -> Self {
138 Self { filters }
139 }
140}
141
142impl<S> Layer<S> for CapabilityFilterLayer {
143 type Service = CapabilityFilterService<S>;
144
145 fn layer(&self, inner: S) -> Self::Service {
146 CapabilityFilterService::new(inner, self.filters.clone())
147 }
148}
149
150#[derive(Clone)]
152pub struct CapabilityFilterService<S> {
153 inner: S,
154 filters: Arc<Vec<BackendFilter>>,
155}
156
157impl<S> CapabilityFilterService<S> {
158 pub fn new(inner: S, filters: Vec<BackendFilter>) -> Self {
160 Self {
161 inner,
162 filters: Arc::new(filters),
163 }
164 }
165}
166
167impl<S> Service<RouterRequest> for CapabilityFilterService<S>
168where
169 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
170 + Clone
171 + Send
172 + 'static,
173 S::Future: Send,
174{
175 type Response = RouterResponse;
176 type Error = Infallible;
177 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
178
179 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
180 self.inner.poll_ready(cx)
181 }
182
183 fn call(&mut self, req: RouterRequest) -> Self::Future {
184 let filters = Arc::clone(&self.filters);
185 let request_id = req.id.clone();
186
187 match &req.inner {
189 McpRequest::CallTool(params) => {
190 if let Some(reason) = check_tool_denied(&filters, ¶ms.name) {
191 return Box::pin(async move {
192 Ok(RouterResponse {
193 id: request_id,
194 inner: Err(JsonRpcError::invalid_params(reason)),
195 })
196 });
197 }
198 }
199 McpRequest::ReadResource(params) => {
200 if let Some(reason) = check_resource_denied(&filters, ¶ms.uri) {
201 return Box::pin(async move {
202 Ok(RouterResponse {
203 id: request_id,
204 inner: Err(JsonRpcError::invalid_params(reason)),
205 })
206 });
207 }
208 }
209 McpRequest::GetPrompt(params) => {
210 if let Some(reason) = check_prompt_denied(&filters, ¶ms.name) {
211 return Box::pin(async move {
212 Ok(RouterResponse {
213 id: request_id,
214 inner: Err(JsonRpcError::invalid_params(reason)),
215 })
216 });
217 }
218 }
219 _ => {}
220 }
221
222 let fut = self.inner.call(req);
223
224 Box::pin(async move {
225 let mut resp = fut.await?;
226
227 if let Ok(ref mut mcp_resp) = resp.inner {
229 match mcp_resp {
230 McpResponse::ListTools(result) => {
231 result.tools.retain(|tool| {
232 for f in filters.iter() {
233 if let Some(local_name) = tool.name.strip_prefix(&f.namespace) {
234 if !f.tool_filter.allows(local_name) {
235 return false;
236 }
237 if let Some(ref annotations) = tool.annotations {
239 if f.hide_destructive && annotations.destructive_hint {
240 return false;
241 }
242 if f.read_only_only && !annotations.read_only_hint {
243 return false;
244 }
245 } else if f.read_only_only {
246 return false;
248 }
249 return true;
250 }
251 }
252 true
253 });
254 }
255 McpResponse::ListResources(result) => {
256 result.resources.retain(|resource| {
257 for f in filters.iter() {
258 if let Some(local_uri) = resource.uri.strip_prefix(&f.namespace) {
259 return f.resource_filter.allows(local_uri);
260 }
261 }
262 true
263 });
264 }
265 McpResponse::ListResourceTemplates(result) => {
266 result.resource_templates.retain(|template| {
267 for f in filters.iter() {
268 if let Some(local_uri) =
269 template.uri_template.strip_prefix(&f.namespace)
270 {
271 return f.resource_filter.allows(local_uri);
272 }
273 }
274 true
275 });
276 }
277 McpResponse::ListPrompts(result) => {
278 result.prompts.retain(|prompt| {
279 for f in filters.iter() {
280 if let Some(local_name) = prompt.name.strip_prefix(&f.namespace) {
281 return f.prompt_filter.allows(local_name);
282 }
283 }
284 true
285 });
286 }
287 _ => {}
288 }
289 }
290
291 Ok(resp)
292 })
293 }
294}
295
296fn check_tool_denied(filters: &[BackendFilter], namespaced_name: &str) -> Option<String> {
299 for f in filters {
300 if let Some(local_name) = namespaced_name.strip_prefix(&f.namespace) {
301 if !f.tool_filter.allows(local_name) {
302 return Some(format!("Tool not available: {}", namespaced_name));
303 }
304 return None;
305 }
306 }
307 None
308}
309
310fn check_resource_denied(filters: &[BackendFilter], namespaced_uri: &str) -> Option<String> {
312 for f in filters {
313 if let Some(local_uri) = namespaced_uri.strip_prefix(&f.namespace) {
314 if !f.resource_filter.allows(local_uri) {
315 return Some(format!("Resource not available: {}", namespaced_uri));
316 }
317 return None;
318 }
319 }
320 None
321}
322
323fn check_prompt_denied(filters: &[BackendFilter], namespaced_name: &str) -> Option<String> {
325 for f in filters {
326 if let Some(local_name) = namespaced_name.strip_prefix(&f.namespace) {
327 if !f.prompt_filter.allows(local_name) {
328 return Some(format!("Prompt not available: {}", namespaced_name));
329 }
330 return None;
331 }
332 }
333 None
334}
335
336#[derive(Clone)]
343pub struct SearchModeFilterLayer {
344 prefix: String,
345}
346
347impl SearchModeFilterLayer {
348 pub fn new(prefix: impl Into<String>) -> Self {
350 Self {
351 prefix: prefix.into(),
352 }
353 }
354}
355
356impl<S> Layer<S> for SearchModeFilterLayer {
357 type Service = SearchModeFilterService<S>;
358
359 fn layer(&self, inner: S) -> Self::Service {
360 SearchModeFilterService {
361 inner,
362 prefix: self.prefix.clone(),
363 }
364 }
365}
366
367#[derive(Clone)]
373pub struct SearchModeFilterService<S> {
374 inner: S,
375 prefix: String,
376}
377
378impl<S> SearchModeFilterService<S> {
379 pub fn new(inner: S, prefix: impl Into<String>) -> Self {
381 Self {
382 inner,
383 prefix: prefix.into(),
384 }
385 }
386}
387
388impl<S> Service<RouterRequest> for SearchModeFilterService<S>
389where
390 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
391 + Clone
392 + Send
393 + 'static,
394 S::Future: Send,
395{
396 type Response = RouterResponse;
397 type Error = Infallible;
398 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
399
400 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
401 self.inner.poll_ready(cx)
402 }
403
404 fn call(&mut self, req: RouterRequest) -> Self::Future {
405 let prefix = self.prefix.clone();
406 let fut = self.inner.call(req);
407
408 Box::pin(async move {
409 let mut resp = fut.await?;
410
411 if let Ok(McpResponse::ListTools(ref mut result)) = resp.inner {
412 result.tools.retain(|tool| tool.name.starts_with(&prefix));
413 }
414
415 Ok(resp)
416 })
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use tower_mcp::protocol::{McpRequest, McpResponse};
423
424 use super::CapabilityFilterService;
425 use crate::config::{BackendFilter, NameFilter};
426 use crate::test_util::{MockService, call_service};
427
428 fn allow_filter(namespace: &str, tools: &[&str]) -> BackendFilter {
429 BackendFilter {
430 namespace: namespace.to_string(),
431 tool_filter: NameFilter::allow_list(tools.iter().map(|s| s.to_string())).unwrap(),
432 resource_filter: NameFilter::PassAll,
433 prompt_filter: NameFilter::PassAll,
434 hide_destructive: false,
435 read_only_only: false,
436 }
437 }
438
439 fn deny_filter(namespace: &str, tools: &[&str]) -> BackendFilter {
440 BackendFilter {
441 namespace: namespace.to_string(),
442 tool_filter: NameFilter::deny_list(tools.iter().map(|s| s.to_string())).unwrap(),
443 resource_filter: NameFilter::PassAll,
444 prompt_filter: NameFilter::PassAll,
445 hide_destructive: false,
446 read_only_only: false,
447 }
448 }
449
450 #[tokio::test]
451 async fn test_filter_allow_list_tools() {
452 let mock = MockService::with_tools(&["fs/read", "fs/write", "fs/delete"]);
453 let filters = vec![allow_filter("fs/", &["read", "write"])];
454 let mut svc = CapabilityFilterService::new(mock, filters);
455
456 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
457 match resp.inner.unwrap() {
458 McpResponse::ListTools(result) => {
459 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
460 assert!(names.contains(&"fs/read"));
461 assert!(names.contains(&"fs/write"));
462 assert!(!names.contains(&"fs/delete"), "delete should be filtered");
463 }
464 other => panic!("expected ListTools, got: {:?}", other),
465 }
466 }
467
468 #[tokio::test]
469 async fn test_filter_deny_list_tools() {
470 let mock = MockService::with_tools(&["fs/read", "fs/write", "fs/delete"]);
471 let filters = vec![deny_filter("fs/", &["delete"])];
472 let mut svc = CapabilityFilterService::new(mock, filters);
473
474 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
475 match resp.inner.unwrap() {
476 McpResponse::ListTools(result) => {
477 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
478 assert!(names.contains(&"fs/read"));
479 assert!(names.contains(&"fs/write"));
480 assert!(!names.contains(&"fs/delete"));
481 }
482 other => panic!("expected ListTools, got: {:?}", other),
483 }
484 }
485
486 #[tokio::test]
487 async fn test_filter_denies_call_to_hidden_tool() {
488 let mock = MockService::with_tools(&["fs/read", "fs/delete"]);
489 let filters = vec![allow_filter("fs/", &["read"])];
490 let mut svc = CapabilityFilterService::new(mock, filters);
491
492 let resp = call_service(
493 &mut svc,
494 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
495 name: "fs/delete".to_string(),
496 arguments: serde_json::json!({}),
497 input_responses: None,
498 request_state: None,
499 meta: None,
500 task: None,
501 }),
502 )
503 .await;
504
505 let err = resp.inner.unwrap_err();
506 assert!(
507 err.message.contains("not available"),
508 "should deny: {}",
509 err.message
510 );
511 }
512
513 #[tokio::test]
514 async fn test_filter_allows_call_to_permitted_tool() {
515 let mock = MockService::with_tools(&["fs/read"]);
516 let filters = vec![allow_filter("fs/", &["read"])];
517 let mut svc = CapabilityFilterService::new(mock, filters);
518
519 let resp = call_service(
520 &mut svc,
521 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
522 name: "fs/read".to_string(),
523 arguments: serde_json::json!({}),
524 input_responses: None,
525 request_state: None,
526 meta: None,
527 task: None,
528 }),
529 )
530 .await;
531
532 assert!(resp.inner.is_ok(), "allowed tool should succeed");
533 }
534
535 #[tokio::test]
536 async fn test_filter_pass_all_allows_everything() {
537 let mock = MockService::with_tools(&["fs/read", "fs/write", "fs/delete"]);
538 let filters = vec![BackendFilter {
539 namespace: "fs/".to_string(),
540 tool_filter: NameFilter::PassAll,
541 resource_filter: NameFilter::PassAll,
542 prompt_filter: NameFilter::PassAll,
543 hide_destructive: false,
544 read_only_only: false,
545 }];
546 let mut svc = CapabilityFilterService::new(mock, filters);
547
548 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
549 match resp.inner.unwrap() {
550 McpResponse::ListTools(result) => {
551 assert_eq!(result.tools.len(), 3);
552 }
553 other => panic!("expected ListTools, got: {:?}", other),
554 }
555 }
556
557 #[tokio::test]
558 async fn test_filter_unmatched_namespace_passes_through() {
559 let mock = MockService::with_tools(&["db/query"]);
560 let filters = vec![allow_filter("fs/", &["read"])];
561 let mut svc = CapabilityFilterService::new(mock, filters);
562
563 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
564 match resp.inner.unwrap() {
565 McpResponse::ListTools(result) => {
566 assert_eq!(result.tools.len(), 1, "unmatched namespace should pass");
567 assert_eq!(result.tools[0].name, "db/query");
568 }
569 other => panic!("expected ListTools, got: {:?}", other),
570 }
571 }
572
573 fn mock_with_annotated_tools() -> MockService {
577 use tower_mcp::protocol::ToolDefinition;
578 use tower_mcp_types::protocol::ToolAnnotations;
579
580 let tools = vec![
581 ToolDefinition {
582 name: "fs/read_file".to_string(),
583 title: None,
584 description: Some("Read a file".to_string()),
585 input_schema: serde_json::json!({"type": "object"}),
586 output_schema: None,
587 icons: None,
588 annotations: Some(ToolAnnotations {
589 title: None,
590 read_only_hint: true,
591 destructive_hint: false,
592 idempotent_hint: true,
593 open_world_hint: false,
594 }),
595 execution: None,
596 meta: None,
597 },
598 ToolDefinition {
599 name: "fs/delete_file".to_string(),
600 title: None,
601 description: Some("Delete a file".to_string()),
602 input_schema: serde_json::json!({"type": "object"}),
603 output_schema: None,
604 icons: None,
605 annotations: Some(ToolAnnotations {
606 title: None,
607 read_only_hint: false,
608 destructive_hint: true,
609 idempotent_hint: false,
610 open_world_hint: false,
611 }),
612 execution: None,
613 meta: None,
614 },
615 ToolDefinition {
616 name: "fs/write_file".to_string(),
617 title: None,
618 description: Some("Write a file".to_string()),
619 input_schema: serde_json::json!({"type": "object"}),
620 output_schema: None,
621 icons: None,
622 annotations: Some(ToolAnnotations {
623 title: None,
624 read_only_hint: false,
625 destructive_hint: false,
626 idempotent_hint: true,
627 open_world_hint: false,
628 }),
629 execution: None,
630 meta: None,
631 },
632 ];
633 MockService { tools }
634 }
635
636 #[tokio::test]
637 async fn test_filter_hide_destructive() {
638 let mock = mock_with_annotated_tools();
639 let filters = vec![BackendFilter {
640 namespace: "fs/".to_string(),
641 tool_filter: NameFilter::PassAll,
642 resource_filter: NameFilter::PassAll,
643 prompt_filter: NameFilter::PassAll,
644 hide_destructive: true,
645 read_only_only: false,
646 }];
647 let mut svc = CapabilityFilterService::new(mock, filters);
648
649 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
650 match resp.inner.unwrap() {
651 McpResponse::ListTools(result) => {
652 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
653 assert!(names.contains(&"fs/read_file"));
654 assert!(names.contains(&"fs/write_file"));
655 assert!(
656 !names.contains(&"fs/delete_file"),
657 "destructive tool should be hidden"
658 );
659 }
660 other => panic!("expected ListTools, got: {:?}", other),
661 }
662 }
663
664 #[tokio::test]
665 async fn test_filter_read_only_only() {
666 let mock = mock_with_annotated_tools();
667 let filters = vec![BackendFilter {
668 namespace: "fs/".to_string(),
669 tool_filter: NameFilter::PassAll,
670 resource_filter: NameFilter::PassAll,
671 prompt_filter: NameFilter::PassAll,
672 hide_destructive: false,
673 read_only_only: true,
674 }];
675 let mut svc = CapabilityFilterService::new(mock, filters);
676
677 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
678 match resp.inner.unwrap() {
679 McpResponse::ListTools(result) => {
680 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
681 assert!(names.contains(&"fs/read_file"), "read-only tool visible");
682 assert!(!names.contains(&"fs/delete_file"), "non-read-only hidden");
683 assert!(!names.contains(&"fs/write_file"), "non-read-only hidden");
684 }
685 other => panic!("expected ListTools, got: {:?}", other),
686 }
687 }
688
689 #[tokio::test]
692 async fn test_search_mode_only_shows_prefix_tools() {
693 let mock = MockService::with_tools(&[
694 "proxy/search_tools",
695 "proxy/call_tool",
696 "proxy/tool_categories",
697 "fs/read",
698 "fs/write",
699 "db/query",
700 ]);
701 let mut svc = super::SearchModeFilterService::new(mock, "proxy/");
702
703 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
704 match resp.inner.unwrap() {
705 McpResponse::ListTools(result) => {
706 let names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
707 assert_eq!(names.len(), 3, "only proxy/ tools should be listed");
708 assert!(names.contains(&"proxy/search_tools"));
709 assert!(names.contains(&"proxy/call_tool"));
710 assert!(names.contains(&"proxy/tool_categories"));
711 assert!(!names.contains(&"fs/read"));
712 assert!(!names.contains(&"db/query"));
713 }
714 other => panic!("expected ListTools, got: {:?}", other),
715 }
716 }
717
718 #[tokio::test]
719 async fn test_search_mode_allows_call_tool_for_backend() {
720 let mock = MockService::with_tools(&["proxy/call_tool", "fs/read"]);
721 let mut svc = super::SearchModeFilterService::new(mock, "proxy/");
722
723 let resp = call_service(
725 &mut svc,
726 McpRequest::CallTool(tower_mcp::protocol::CallToolParams {
727 name: "fs/read".to_string(),
728 arguments: serde_json::json!({}),
729 input_responses: None,
730 request_state: None,
731 meta: None,
732 task: None,
733 }),
734 )
735 .await;
736
737 assert!(
738 resp.inner.is_ok(),
739 "search mode should not block CallTool requests"
740 );
741 }
742
743 #[tokio::test]
744 async fn test_search_mode_no_proxy_tools_returns_empty() {
745 let mock = MockService::with_tools(&["fs/read", "db/query"]);
746 let mut svc = super::SearchModeFilterService::new(mock, "proxy/");
747
748 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
749 match resp.inner.unwrap() {
750 McpResponse::ListTools(result) => {
751 assert!(result.tools.is_empty(), "no proxy/ tools means empty list");
752 }
753 other => panic!("expected ListTools, got: {:?}", other),
754 }
755 }
756}