4848 is_namespaced_name ,
4949)
5050from fast_agent .mcp .mcp_aggregator import MCPAggregator , NamespacedTool , ServerStatus
51- from fast_agent .skills .registry import format_skills_for_prompt
51+ from fast_agent .skills import SkillManifest
52+ from fast_agent .skills .registry import SkillRegistry , format_skills_for_prompt
5253from fast_agent .tools .elicitation import (
5354 get_elicitation_tool ,
5455 run_elicitation_form ,
5556 set_elicitation_input_callback ,
5657)
5758from fast_agent .tools .shell_runtime import ShellRuntime
59+ from fast_agent .tools .skill_reader import SkillReader
5860from fast_agent .types import PromptMessageExtended , RequestParams
5961from fast_agent .ui import console
6062
6971
7072 from fast_agent .context import Context
7173 from fast_agent .llm .usage_tracking import UsageAccumulator
72- from fast_agent .skills import SkillManifest
7374
7475
7576class McpAgent (ABC , ToolAgent ):
@@ -109,17 +110,23 @@ def __init__(
109110 self .executor = context .executor if context else None
110111 self .logger = get_logger (f"{ __name__ } .{ self ._name } " )
111112 manifests : list [SkillManifest ] = list (getattr (self .config , "skill_manifests" , []) or [])
112- if not manifests and context and getattr ( context , " skill_registry" , None ) :
113+ if not manifests and context and context . skill_registry :
113114 try :
114115 manifests = list (context .skill_registry .load_manifests ()) # type: ignore[assignment]
115116 except Exception :
116117 manifests = []
117118
118- self ._skill_manifests = list (manifests )
119- self ._skill_map : dict [str , SkillManifest ] = {
120- manifest .name : manifest for manifest in manifests
121- }
122- self ._agent_skills_warning_shown = False
119+ self ._skill_manifests : list [SkillManifest ] = []
120+ self ._skill_map : dict [str , SkillManifest ] = {}
121+ self ._skill_reader : SkillReader | None = None
122+ self .set_skill_manifests (manifests )
123+ self .skill_registry : SkillRegistry | None = None
124+ if isinstance (self .config .skills , SkillRegistry ):
125+ self .skill_registry = self .config .skills
126+ elif self .config .skills is None and context and context .skill_registry :
127+ self .skill_registry = context .skill_registry
128+ self ._warnings : list [str ] = []
129+ self ._warning_messages_seen : set [str ] = set ()
123130 shell_flag_requested = bool (context and getattr (context , "shell_runtime" , False ))
124131 skills_configured = bool (self ._skill_manifests )
125132 self ._shell_runtime_activation_reason : str | None = None
@@ -285,21 +292,12 @@ async def _apply_instruction_templates(self) -> None:
285292 self .instruction = await self ._instruction_builder .build ()
286293
287294 # Warn if skills configured but placeholder missing
288- if (
289- self ._skill_manifests
290- and not self ._agent_skills_warning_shown
291- and "{{agentSkills}}" not in self ._instruction_builder .template
292- ):
295+ if self ._skill_manifests and "{{agentSkills}}" not in self ._instruction_builder .template :
293296 warning_message = (
294297 "Agent skills are configured but the system prompt does not include {{agentSkills}}. "
295298 "Skill descriptions will not be added to the system prompt."
296299 )
297- self .logger .warning (warning_message )
298- try :
299- console .console .print (f"[yellow]{ warning_message } [/yellow]" )
300- except Exception : # pragma: no cover - console fallback
301- pass
302- self ._agent_skills_warning_shown = True
300+ self ._record_warning (warning_message )
303301
304302 # Update default request params to match
305303 if self ._default_request_params :
@@ -322,8 +320,36 @@ async def _resolve_server_instructions(self) -> str:
322320
323321 async def _resolve_agent_skills (self ) -> str :
324322 """Resolver for {{agentSkills}} placeholder."""
325- self ._agent_skills_warning_shown = True
326- return format_skills_for_prompt (self ._skill_manifests )
323+ # Determine which tool to reference in the preamble
324+ # ACP context provides read_text_file; otherwise use read_skill
325+ if self ._filesystem_runtime and hasattr (self ._filesystem_runtime , "tools" ):
326+ read_tool_name = "read_text_file"
327+ else :
328+ read_tool_name = "read_skill"
329+ return format_skills_for_prompt (self ._skill_manifests , read_tool_name = read_tool_name )
330+
331+ def set_skill_manifests (self , manifests : Sequence [SkillManifest ]) -> None :
332+ self ._skill_manifests = list (manifests )
333+ self ._skill_map = {manifest .name : manifest for manifest in self ._skill_manifests }
334+ if self ._skill_manifests :
335+ self ._skill_reader = SkillReader (self ._skill_manifests , self .logger )
336+ else :
337+ self ._skill_reader = None
338+
339+ def _record_warning (self , message : str ) -> None :
340+ if message in self ._warning_messages_seen :
341+ return
342+ self ._warning_messages_seen .add (message )
343+ self ._warnings .append (message )
344+ self .logger .warning (message )
345+ try :
346+ console .console .print (f"[yellow]{ message } [/yellow]" )
347+ except Exception : # pragma: no cover - console fallback
348+ pass
349+
350+ @property
351+ def warnings (self ) -> list [str ]:
352+ return list (self ._warnings )
327353
328354 def set_instruction_context (self , context : dict [str , str ]) -> None :
329355 """
@@ -585,6 +611,10 @@ async def call_tool(
585611 arguments , tool_use_id
586612 )
587613
614+ # Check skill reader (non-ACP context with skills)
615+ if self ._skill_reader and name == "read_skill" :
616+ return await self ._skill_reader .execute (arguments )
617+
588618 # Fall back to shell runtime
589619 if self ._shell_runtime .tool and name == self ._shell_runtime .tool .name :
590620 return await self ._shell_runtime .execute (arguments )
@@ -909,12 +939,16 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
909939 and hasattr (self ._filesystem_runtime , "tools" )
910940 and any (tool .name == tool_name for tool in self ._filesystem_runtime .tools )
911941 )
942+ is_skill_reader_tool = (
943+ self ._skill_reader and self ._skill_reader .enabled and tool_name == "read_skill"
944+ )
912945
913946 tool_available = (
914947 tool_name == HUMAN_INPUT_TOOL_NAME
915948 or (self ._shell_runtime .tool and tool_name == self ._shell_runtime .tool .name )
916949 or is_external_runtime_tool
917950 or is_filesystem_runtime_tool
951+ or is_skill_reader_tool
918952 or namespaced_tool is not None
919953 or local_tool is not None
920954 or candidate_namespaced_tool is not None
@@ -1208,6 +1242,12 @@ async def list_tools(self) -> ListToolsResult:
12081242 if fs_tool and fs_tool .name not in existing_names :
12091243 merged_tools .append (fs_tool )
12101244 existing_names .add (fs_tool .name )
1245+ elif self ._skill_reader and self ._skill_reader .enabled :
1246+ # Non-ACP context with skills: provide read_skill tool
1247+ skill_tool = self ._skill_reader .tool
1248+ if skill_tool .name not in existing_names :
1249+ merged_tools .append (skill_tool )
1250+ existing_names .add (skill_tool .name )
12111251
12121252 if self .config .human_input :
12131253 human_tool = getattr (self , "_human_input_tool" , None )
0 commit comments