@@ -162,6 +162,11 @@ def get_entity_filter_type(entity: Any) -> Optional[str]:
162162 "edit_chat_photo" : 50 * 1024 * 1024 ,
163163}
164164ROOTS_UNSUPPORTED_ERROR_CODES = {- 32601 }
165+ ROOTS_STATUS_READY = "ready"
166+ ROOTS_STATUS_NOT_CONFIGURED = "not_configured"
167+ ROOTS_STATUS_UNSUPPORTED_FALLBACK = "unsupported_fallback"
168+ ROOTS_STATUS_CLIENT_DENY_ALL = "client_deny_all"
169+ ROOTS_STATUS_ERROR = "error"
165170
166171
167172# Error code prefix mapping for better error tracing
@@ -463,29 +468,47 @@ def _ensure_size_within_limit(tool_name: str, candidate: Path) -> Optional[str]:
463468
464469
465470async def _get_effective_allowed_roots (ctx : Optional [Context ]) -> List [Path ]:
466- fallback_roots = list (SERVER_ALLOWED_ROOTS )
467- if ctx is None :
468- return fallback_roots
471+ roots , _status = await _get_effective_allowed_roots_with_status (ctx )
472+ return roots
469473
470- try :
471- list_roots_result = await ctx . session . list_roots ()
472- except McpError as error :
474+
475+ def _is_roots_unsupported_error ( error : Exception ) -> bool :
476+ if isinstance ( error , McpError ) :
473477 error_code = getattr (getattr (error , "error" , None ), "code" , None )
474478 error_message = (
475479 getattr (getattr (error , "error" , None ), "message" , None ) or str (error )
476480 ).lower ()
477- if error_code in ROOTS_UNSUPPORTED_ERROR_CODES or "method not found" in error_message :
478- # Fallback is allowed only when roots are unsupported in this MCP client session.
479- return fallback_roots
481+ if error_code in ROOTS_UNSUPPORTED_ERROR_CODES :
482+ return True
483+ return "method not found" in error_message or "not implemented" in error_message
484+
485+ if isinstance (error , NotImplementedError ):
486+ return True
487+ if isinstance (error , AttributeError ):
488+ return "list_roots" in str (error )
489+ return False
490+
491+
492+ async def _get_effective_allowed_roots_with_status (
493+ ctx : Optional [Context ],
494+ ) -> tuple [List [Path ], str ]:
495+ fallback_roots = list (SERVER_ALLOWED_ROOTS )
496+ if ctx is None :
497+ if fallback_roots :
498+ return fallback_roots , ROOTS_STATUS_READY
499+ return [], ROOTS_STATUS_NOT_CONFIGURED
500+
501+ try :
502+ list_roots_result = await ctx .session .list_roots ()
503+ except Exception as error :
504+ if _is_roots_unsupported_error (error ):
505+ if fallback_roots :
506+ return fallback_roots , ROOTS_STATUS_UNSUPPORTED_FALLBACK
507+ return [], ROOTS_STATUS_NOT_CONFIGURED
480508 logger .error (
481509 "MCP roots request failed; disabling file-path tools for safety." , exc_info = True
482510 )
483- return []
484- except Exception :
485- logger .error (
486- "Unexpected MCP roots failure; disabling file-path tools for safety." , exc_info = True
487- )
488- return []
511+ return [], ROOTS_STATUS_ERROR
489512
490513 client_roots : List [Path ] = []
491514 for root in list_roots_result .roots :
@@ -496,17 +519,33 @@ async def _get_effective_allowed_roots(ctx: Optional[Context]) -> List[Path]:
496519 continue
497520
498521 if client_roots :
499- return _dedupe_paths (client_roots )
522+ return _dedupe_paths (client_roots ), ROOTS_STATUS_READY
500523
501524 # Roots API succeeded; an empty roots list is treated as explicit deny-all.
502- return []
525+ return [], ROOTS_STATUS_CLIENT_DENY_ALL
503526
504527
505528async def _ensure_allowed_roots (
506529 ctx : Optional [Context ], tool_name : str
507530) -> tuple [List [Path ], Optional [str ]]:
508- roots = await _get_effective_allowed_roots (ctx )
531+ roots , status = await _get_effective_allowed_roots_with_status (ctx )
509532 if not roots :
533+ if status == ROOTS_STATUS_CLIENT_DENY_ALL :
534+ return (
535+ [],
536+ (
537+ f"{ tool_name } is disabled because the client provided an empty "
538+ "MCP Roots list (deny-all)."
539+ ),
540+ )
541+ if status == ROOTS_STATUS_ERROR :
542+ return (
543+ [],
544+ (
545+ f"{ tool_name } is disabled because MCP Roots could not be verified safely. "
546+ "Check MCP client/server logs."
547+ ),
548+ )
510549 return (
511550 [],
512551 (
0 commit comments