4141from fast_agent .core .exceptions import PromptExitError
4242from fast_agent .core .logging .logger import get_logger
4343from fast_agent .interfaces import FastAgentLLMProtocol
44+ from fast_agent .mcp .common import SEP
4445from fast_agent .mcp .mcp_aggregator import MCPAggregator , ServerStatus
4546from fast_agent .skills .registry import format_skills_for_prompt
4647from fast_agent .tools .elicitation import (
@@ -397,40 +398,51 @@ async def list_tools(self) -> ListToolsResult:
397398 Returns:
398399 ListToolsResult with available tools
399400 """
400- # Get all tools from the aggregator
401- result = await self ._aggregator .list_tools ()
402- aggregator_tools = list (result .tools )
401+ aggregator_result = await self ._aggregator .list_tools ()
402+ aggregator_tools = list (aggregator_result .tools or [])
403403
404404 # Apply filtering if tools are specified in config
405405 if self .config .tools is not None :
406- filtered_tools = []
406+ filtered_tools : list [ Tool ] = []
407407 for tool in aggregator_tools :
408408 # Extract server name from tool name, handling server names with hyphens
409409 server_name = None
410410 for configured_server in self .config .tools .keys ():
411- if tool .name .startswith (f"{ configured_server } - " ):
411+ if tool .name .startswith (f"{ configured_server } { SEP } " ):
412412 server_name = configured_server
413413 break
414414
415- # Check if this server has tool filters
416- if server_name and server_name in self .config .tools :
417- # Check if tool matches any pattern for this server
418- for pattern in self .config .tools [server_name ]:
419- if self ._matches_pattern (tool .name , pattern , server_name ):
420- filtered_tools .append (tool )
421- break
415+ if not server_name :
416+ continue
417+
418+ # Check if tool matches any pattern for this server
419+ for pattern in self .config .tools [server_name ]:
420+ if self ._matches_pattern (tool .name , pattern , server_name ):
421+ filtered_tools .append (tool )
422+ break
422423 aggregator_tools = filtered_tools
423424
424- result .tools = aggregator_tools
425+ # Start with filtered aggregator tools and merge in subclass/local tools
426+ merged_tools : list [Tool ] = list (aggregator_tools )
427+ existing_names = {tool .name for tool in merged_tools }
425428
426- if self ._bash_tool and all (tool .name != self ._bash_tool .name for tool in result .tools ):
427- result .tools .append (self ._bash_tool )
429+ local_tools = (await ToolAgent .list_tools (self )).tools
430+ for tool in local_tools :
431+ if tool .name not in existing_names :
432+ merged_tools .append (tool )
433+ existing_names .add (tool .name )
428434
429- # Append human input tool if enabled and available
430- if self . config . human_input and getattr (self , "_human_input_tool" , None ):
431- result . tools . append (self ._human_input_tool )
435+ if self . _bash_tool and self . _bash_tool . name not in existing_names :
436+ merged_tools . append (self . _bash_tool )
437+ existing_names . add (self ._bash_tool . name )
432438
433- return result
439+ if self .config .human_input :
440+ human_tool = getattr (self , "_human_input_tool" , None )
441+ if human_tool and human_tool .name not in existing_names :
442+ merged_tools .append (human_tool )
443+ existing_names .add (human_tool .name )
444+
445+ return ListToolsResult (tools = merged_tools )
434446
435447 async def call_tool (self , name : str , arguments : Dict [str , Any ] | None = None ) -> CallToolResult :
436448 """
@@ -449,6 +461,9 @@ async def call_tool(self, name: str, arguments: Dict[str, Any] | None = None) ->
449461 if name == HUMAN_INPUT_TOOL_NAME :
450462 # Call the elicitation-backed human input tool
451463 return await self ._call_human_input_tool (arguments )
464+
465+ if name in self ._execution_tools :
466+ return await super ().call_tool (name , arguments )
452467 else :
453468 return await self ._aggregator .call_tool (name , arguments )
454469
@@ -703,38 +718,61 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
703718 tool_results : dict [str , CallToolResult ] = {}
704719 tool_loop_error : str | None = None
705720
706- # Cache available tool names (original, not namespaced) for display
707- available_tools = [
708- namespaced_tool .tool .name
709- for namespaced_tool in self ._aggregator ._namespaced_tool_map .values ()
710- ]
711- if self ._shell_runtime .tool :
712- available_tools .append (self ._shell_runtime .tool .name )
721+ # Cache available tool names exactly as advertised to the LLM for display/highlighting
722+ try :
723+ listed_tools = await self .list_tools ()
724+ except Exception as exc : # pragma: no cover - defensive guard, should not happen
725+ self .logger .warning (f"Failed to list tools before execution: { exc } " )
726+ listed_tools = ListToolsResult (tools = [])
727+
728+ available_tools : list [str ] = []
729+ seen_tool_names : set [str ] = set ()
730+ for tool_schema in listed_tools .tools :
731+ if tool_schema .name in seen_tool_names :
732+ continue
733+ available_tools .append (tool_schema .name )
734+ seen_tool_names .add (tool_schema .name )
713735
714- available_tools = list (dict .fromkeys (available_tools ))
736+ # Cache namespaced tools for routing/metadata
737+ namespaced_tools = self ._aggregator ._namespaced_tool_map
715738
716739 # Process each tool call using our aggregator
717740 for correlation_id , tool_request in request .tool_calls .items ():
718741 tool_name = tool_request .params .name
719742 tool_args = tool_request .params .arguments or {}
720743
721- # Get the original tool name for display (not namespaced)
722- namespaced_tool = self ._aggregator ._namespaced_tool_map .get (tool_name )
723- display_tool_name = namespaced_tool .tool .name if namespaced_tool else tool_name
724-
725- tool_available = False
726- if tool_name == HUMAN_INPUT_TOOL_NAME :
727- tool_available = True
728- elif self ._bash_tool and tool_name == self ._bash_tool .name :
729- tool_available = True
730- elif namespaced_tool :
731- tool_available = True
732- else :
733- tool_available = any (
734- candidate .tool .name == tool_name
735- for candidate in self ._aggregator ._namespaced_tool_map .values ()
744+ # Determine which tool we are calling (namespaced MCP, local, etc.)
745+ namespaced_tool = namespaced_tools .get (tool_name )
746+ local_tool = self ._execution_tools .get (tool_name )
747+ candidate_namespaced_tool = None
748+ if namespaced_tool is None and local_tool is None :
749+ candidate_namespaced_tool = next (
750+ (
751+ candidate
752+ for candidate in namespaced_tools .values ()
753+ if candidate .tool .name == tool_name
754+ ),
755+ None ,
736756 )
737757
758+ # Select display/highlight names
759+ display_tool_name = tool_name
760+ highlight_name = tool_name
761+ if namespaced_tool is not None :
762+ display_tool_name = namespaced_tool .namespaced_tool_name
763+ highlight_name = namespaced_tool .namespaced_tool_name
764+ elif candidate_namespaced_tool is not None :
765+ display_tool_name = candidate_namespaced_tool .namespaced_tool_name
766+ highlight_name = candidate_namespaced_tool .namespaced_tool_name
767+
768+ tool_available = (
769+ tool_name == HUMAN_INPUT_TOOL_NAME
770+ or (self ._shell_runtime .tool and tool_name == self ._shell_runtime .tool .name )
771+ or namespaced_tool is not None
772+ or local_tool is not None
773+ or candidate_namespaced_tool is not None
774+ )
775+
738776 if not tool_available :
739777 error_message = f"Tool '{ display_tool_name } ' is not available"
740778 self .logger .error (error_message )
@@ -748,7 +786,7 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
748786 # Find the index of the current tool in available_tools for highlighting
749787 highlight_index = None
750788 try :
751- highlight_index = available_tools .index (display_tool_name )
789+ highlight_index = available_tools .index (highlight_name )
752790 except ValueError :
753791 # Tool not found in list, no highlighting
754792 pass
@@ -757,7 +795,7 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
757795 if (
758796 self ._shell_runtime_enabled
759797 and self ._shell_runtime .tool
760- and display_tool_name == self ._shell_runtime .tool .name
798+ and tool_name == self ._shell_runtime .tool .name
761799 ):
762800 metadata = self ._shell_runtime .metadata (tool_args .get ("command" ))
763801
@@ -778,9 +816,10 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
778816
779817 # Show tool result (like ToolAgent does)
780818 skybridge_config = None
781- if namespaced_tool :
819+ skybridge_tool = namespaced_tool or candidate_namespaced_tool
820+ if skybridge_tool :
782821 skybridge_config = await self ._aggregator .get_skybridge_config (
783- namespaced_tool .server_name
822+ skybridge_tool .server_name
784823 )
785824
786825 if not getattr (result , "_suppress_display" , False ):
0 commit comments