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