"""Directory scanning provider for discovering multiple skills.""" from __future__ import annotations from collections.abc import Sequence from pathlib import Path from typing import Literal from fastmcp.resources.base import Resource from fastmcp.resources.template import ResourceTemplate from fastmcp.server.providers.aggregate import AggregateProvider from fastmcp.server.providers.skills.skill_provider import SkillProvider from fastmcp.utilities.logging import get_logger from fastmcp.utilities.versions import VersionSpec logger = get_logger(__name__) class SkillsDirectoryProvider(AggregateProvider): """Provider that scans directories and creates a SkillProvider per skill folder. This extends AggregateProvider to combine multiple SkillProviders into one. Each subdirectory containing a main file (default: SKILL.md) becomes a skill. Can scan multiple root directories - if a skill name appears in multiple roots, the first one found wins. Args: roots: Root directory(ies) containing skill folders. Can be a single path or a sequence of paths. reload: If True, re-discover skills on each request. Defaults to False. main_file_name: Name of the main skill file. Defaults to "SKILL.md". supporting_files: How supporting files are exposed in child SkillProviders: - "template": Accessed via ResourceTemplate, hidden from list_resources(). - "resources": Each file exposed as individual Resource in list_resources(). Example: ```python from pathlib import Path from fastmcp import FastMCP from fastmcp.server.providers.skills import SkillsDirectoryProvider mcp = FastMCP("Skills") # Single directory mcp.add_provider(SkillsDirectoryProvider( roots=Path.home() / ".claude" / "skills", reload=True, # Re-scan on each request )) # Multiple directories mcp.add_provider(SkillsDirectoryProvider( roots=[Path("/etc/skills"), Path.home() / ".local" / "skills"], )) ``` """ def __init__( self, roots: str | Path | Sequence[str | Path], reload: bool = False, main_file_name: str = "SKILL.md", supporting_files: Literal["template", "resources"] = "template", ) -> None: super().__init__() # Normalize to sequence: single path becomes list if isinstance(roots, (str, Path)): roots = [roots] self._roots = [Path(r).resolve() for r in roots] self._reload = reload self._main_file_name = main_file_name self._supporting_files = supporting_files self._discovered = False # Discover skills at init self._discover_skills() def _discover_skills(self) -> None: """Scan root directories and create SkillProvider per valid skill folder.""" # Clear existing providers if reloading self.providers.clear() seen_skill_names: set[str] = set() for root in self._roots: if not root.exists(): logger.debug(f"Skills root does not exist: {root}") continue for skill_dir in root.iterdir(): if not skill_dir.is_dir(): continue main_file = skill_dir / self._main_file_name if not main_file.exists(): continue skill_name = skill_dir.name # Skip if we've already seen this skill name (first wins) if skill_name in seen_skill_names: logger.debug( f"Skipping duplicate skill '{skill_name}' from {root} " f"(already found in earlier root)" ) continue try: provider = SkillProvider( skill_path=skill_dir, main_file_name=self._main_file_name, supporting_files=self._supporting_files, ) self.providers.append(provider) seen_skill_names.add(skill_name) except (FileNotFoundError, PermissionError, OSError): logger.exception(f"Failed to load skill: {skill_dir.name}") self._discovered = True logger.debug( f"SkillsDirectoryProvider loaded {len(self.providers)} skills " f"from {len(self._roots)} root(s)" ) async def _ensure_discovered(self) -> None: """Ensure skills are discovered, rediscovering if reload is enabled.""" if self._reload or not self._discovered: self._discover_skills() # Override list methods to support reload async def _list_resources(self) -> Sequence[Resource]: await self._ensure_discovered() return await super()._list_resources() async def _list_resource_templates(self) -> Sequence[ResourceTemplate]: await self._ensure_discovered() return await super()._list_resource_templates() async def _get_resource( self, uri: str, version: VersionSpec | None = None ) -> Resource | None: await self._ensure_discovered() return await super()._get_resource(uri, version) async def _get_resource_template( self, uri: str, version: VersionSpec | None = None ) -> ResourceTemplate | None: await self._ensure_discovered() return await super()._get_resource_template(uri, version) def __repr__(self) -> str: roots_repr = self._roots[0] if len(self._roots) == 1 else self._roots return ( f"SkillsDirectoryProvider(roots={roots_repr!r}, " f"reload={self._reload}, skills={len(self.providers)})" )