fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Skills providers for exposing agent skills as MCP resources.
|
|
2
|
+
|
|
3
|
+
This module provides a two-layer architecture for skill discovery:
|
|
4
|
+
|
|
5
|
+
- **SkillProvider**: Handles a single skill folder, exposing its files as resources.
|
|
6
|
+
- **SkillsDirectoryProvider**: Scans a directory, creates a SkillProvider per folder.
|
|
7
|
+
- **Vendor providers**: Platform-specific providers for Claude, Cursor, VS Code, Codex,
|
|
8
|
+
Gemini, Goose, Copilot, and OpenCode.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
from fastmcp.server.providers.skills import ClaudeSkillsProvider, SkillProvider
|
|
15
|
+
|
|
16
|
+
mcp = FastMCP("Skills Server")
|
|
17
|
+
|
|
18
|
+
# Load a single skill
|
|
19
|
+
mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))
|
|
20
|
+
|
|
21
|
+
# Or load all skills in a directory
|
|
22
|
+
mcp.add_provider(ClaudeSkillsProvider()) # Uses ~/.claude/skills/
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
# Import providers
|
|
29
|
+
from fastmcp.server.providers.skills.claude_provider import ClaudeSkillsProvider
|
|
30
|
+
from fastmcp.server.providers.skills.directory_provider import SkillsDirectoryProvider
|
|
31
|
+
from fastmcp.server.providers.skills.skill_provider import SkillProvider
|
|
32
|
+
from fastmcp.server.providers.skills.vendor_providers import (
|
|
33
|
+
CodexSkillsProvider,
|
|
34
|
+
CopilotSkillsProvider,
|
|
35
|
+
CursorSkillsProvider,
|
|
36
|
+
GeminiSkillsProvider,
|
|
37
|
+
GooseSkillsProvider,
|
|
38
|
+
OpenCodeSkillsProvider,
|
|
39
|
+
VSCodeSkillsProvider,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Backwards compatibility alias
|
|
44
|
+
SkillsProvider = SkillsDirectoryProvider
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"ClaudeSkillsProvider",
|
|
49
|
+
"CodexSkillsProvider",
|
|
50
|
+
"CopilotSkillsProvider",
|
|
51
|
+
"CursorSkillsProvider",
|
|
52
|
+
"GeminiSkillsProvider",
|
|
53
|
+
"GooseSkillsProvider",
|
|
54
|
+
"OpenCodeSkillsProvider",
|
|
55
|
+
"SkillProvider",
|
|
56
|
+
"SkillsDirectoryProvider",
|
|
57
|
+
"SkillsProvider", # Backwards compatibility alias
|
|
58
|
+
"VSCodeSkillsProvider",
|
|
59
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared utilities and data structures for skills providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SkillFileInfo:
|
|
14
|
+
"""Information about a file within a skill."""
|
|
15
|
+
|
|
16
|
+
path: str # Relative path within skill directory
|
|
17
|
+
size: int
|
|
18
|
+
hash: str # sha256 hash
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SkillInfo:
|
|
23
|
+
"""Parsed information about a skill."""
|
|
24
|
+
|
|
25
|
+
name: str # Directory name (canonical identifier)
|
|
26
|
+
description: str # From frontmatter or first line
|
|
27
|
+
path: Path # Absolute path to skill directory
|
|
28
|
+
main_file: str # Name of main file (e.g., "SKILL.md")
|
|
29
|
+
files: list[SkillFileInfo] = field(default_factory=list)
|
|
30
|
+
frontmatter: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
|
|
34
|
+
"""Parse YAML frontmatter from markdown content.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
content: Markdown content potentially starting with ---
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Tuple of (frontmatter dict, remaining content)
|
|
41
|
+
"""
|
|
42
|
+
if not content.startswith("---"):
|
|
43
|
+
return {}, content
|
|
44
|
+
|
|
45
|
+
# Find the closing ---
|
|
46
|
+
end_match = re.search(r"\n---\s*\n", content[3:])
|
|
47
|
+
if not end_match:
|
|
48
|
+
return {}, content
|
|
49
|
+
|
|
50
|
+
frontmatter_text = content[3 : 3 + end_match.start()]
|
|
51
|
+
remaining = content[3 + end_match.end() :]
|
|
52
|
+
|
|
53
|
+
# Parse YAML (simple key: value parsing, no complex types)
|
|
54
|
+
frontmatter: dict[str, Any] = {}
|
|
55
|
+
for line in frontmatter_text.strip().split("\n"):
|
|
56
|
+
if ":" in line:
|
|
57
|
+
key, _, value = line.partition(":")
|
|
58
|
+
key = key.strip()
|
|
59
|
+
value = value.strip()
|
|
60
|
+
|
|
61
|
+
# Handle quoted strings
|
|
62
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
63
|
+
value.startswith("'") and value.endswith("'")
|
|
64
|
+
):
|
|
65
|
+
value = value[1:-1]
|
|
66
|
+
|
|
67
|
+
# Handle lists [a, b, c]
|
|
68
|
+
if value.startswith("[") and value.endswith("]"):
|
|
69
|
+
items = value[1:-1].split(",")
|
|
70
|
+
value = [item.strip().strip("\"'") for item in items if item.strip()]
|
|
71
|
+
|
|
72
|
+
frontmatter[key] = value
|
|
73
|
+
|
|
74
|
+
return frontmatter, remaining
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_file_hash(path: Path) -> str:
|
|
78
|
+
"""Compute SHA256 hash of a file."""
|
|
79
|
+
sha256 = hashlib.sha256()
|
|
80
|
+
with open(path, "rb") as f:
|
|
81
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
82
|
+
sha256.update(chunk)
|
|
83
|
+
return f"sha256:{sha256.hexdigest()}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def scan_skill_files(skill_dir: Path) -> list[SkillFileInfo]:
|
|
87
|
+
"""Scan a skill directory for all files."""
|
|
88
|
+
files = []
|
|
89
|
+
# Sort for deterministic ordering across platforms
|
|
90
|
+
for file_path in sorted(skill_dir.rglob("*")):
|
|
91
|
+
if file_path.is_file():
|
|
92
|
+
rel_path = file_path.relative_to(skill_dir)
|
|
93
|
+
files.append(
|
|
94
|
+
SkillFileInfo(
|
|
95
|
+
# Use POSIX paths for cross-platform URI consistency
|
|
96
|
+
path=rel_path.as_posix(),
|
|
97
|
+
size=file_path.stat().st_size,
|
|
98
|
+
hash=compute_file_hash(file_path),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return files
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Claude-specific skills provider for Claude Code skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from fastmcp.server.providers.skills.directory_provider import SkillsDirectoryProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaudeSkillsProvider(SkillsDirectoryProvider):
|
|
12
|
+
"""Provider for Claude Code skills from ~/.claude/skills/.
|
|
13
|
+
|
|
14
|
+
A convenience subclass that sets the default root to Claude's skills location.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
reload: If True, re-scan on every request. Defaults to False.
|
|
18
|
+
supporting_files: How supporting files are exposed:
|
|
19
|
+
- "template": Accessed via ResourceTemplate, hidden from list_resources().
|
|
20
|
+
- "resources": Each file exposed as individual Resource in list_resources().
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
from fastmcp import FastMCP
|
|
25
|
+
from fastmcp.server.providers.skills import ClaudeSkillsProvider
|
|
26
|
+
|
|
27
|
+
mcp = FastMCP("Claude Skills")
|
|
28
|
+
mcp.add_provider(ClaudeSkillsProvider()) # Uses default location
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
reload: bool = False,
|
|
35
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
36
|
+
) -> None:
|
|
37
|
+
root = Path.home() / ".claude" / "skills"
|
|
38
|
+
|
|
39
|
+
super().__init__(
|
|
40
|
+
roots=[root],
|
|
41
|
+
reload=reload,
|
|
42
|
+
main_file_name="SKILL.md",
|
|
43
|
+
supporting_files=supporting_files,
|
|
44
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Directory scanning provider for discovering multiple skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from fastmcp.resources.resource import Resource
|
|
10
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
11
|
+
from fastmcp.server.providers.aggregate import AggregateProvider
|
|
12
|
+
from fastmcp.server.providers.skills.skill_provider import SkillProvider
|
|
13
|
+
from fastmcp.utilities.logging import get_logger
|
|
14
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SkillsDirectoryProvider(AggregateProvider):
|
|
20
|
+
"""Provider that scans directories and creates a SkillProvider per skill folder.
|
|
21
|
+
|
|
22
|
+
This extends AggregateProvider to combine multiple SkillProviders into one.
|
|
23
|
+
Each subdirectory containing a main file (default: SKILL.md) becomes a skill.
|
|
24
|
+
Can scan multiple root directories - if a skill name appears in multiple roots,
|
|
25
|
+
the first one found wins.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
roots: Root directory(ies) containing skill folders. Can be a single path
|
|
29
|
+
or a sequence of paths.
|
|
30
|
+
reload: If True, re-discover skills on each request. Defaults to False.
|
|
31
|
+
main_file_name: Name of the main skill file. Defaults to "SKILL.md".
|
|
32
|
+
supporting_files: How supporting files are exposed in child SkillProviders:
|
|
33
|
+
- "template": Accessed via ResourceTemplate, hidden from list_resources().
|
|
34
|
+
- "resources": Each file exposed as individual Resource in list_resources().
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from fastmcp import FastMCP
|
|
40
|
+
from fastmcp.server.providers.skills import SkillsDirectoryProvider
|
|
41
|
+
|
|
42
|
+
mcp = FastMCP("Skills")
|
|
43
|
+
# Single directory
|
|
44
|
+
mcp.add_provider(SkillsDirectoryProvider(
|
|
45
|
+
roots=Path.home() / ".claude" / "skills",
|
|
46
|
+
reload=True, # Re-scan on each request
|
|
47
|
+
))
|
|
48
|
+
# Multiple directories
|
|
49
|
+
mcp.add_provider(SkillsDirectoryProvider(
|
|
50
|
+
roots=[Path("/etc/skills"), Path.home() / ".local" / "skills"],
|
|
51
|
+
))
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
roots: str | Path | Sequence[str | Path],
|
|
58
|
+
reload: bool = False,
|
|
59
|
+
main_file_name: str = "SKILL.md",
|
|
60
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__()
|
|
63
|
+
# Normalize to sequence: single path becomes list
|
|
64
|
+
if isinstance(roots, (str, Path)):
|
|
65
|
+
roots = [roots]
|
|
66
|
+
|
|
67
|
+
self._roots = [Path(r).resolve() for r in roots]
|
|
68
|
+
self._reload = reload
|
|
69
|
+
self._main_file_name = main_file_name
|
|
70
|
+
self._supporting_files = supporting_files
|
|
71
|
+
self._discovered = False
|
|
72
|
+
|
|
73
|
+
# Discover skills at init
|
|
74
|
+
self._discover_skills()
|
|
75
|
+
|
|
76
|
+
def _discover_skills(self) -> None:
|
|
77
|
+
"""Scan root directories and create SkillProvider per valid skill folder."""
|
|
78
|
+
# Clear existing providers if reloading
|
|
79
|
+
self.providers.clear()
|
|
80
|
+
|
|
81
|
+
seen_skill_names: set[str] = set()
|
|
82
|
+
|
|
83
|
+
for root in self._roots:
|
|
84
|
+
if not root.exists():
|
|
85
|
+
logger.debug(f"Skills root does not exist: {root}")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
for skill_dir in root.iterdir():
|
|
89
|
+
if not skill_dir.is_dir():
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
main_file = skill_dir / self._main_file_name
|
|
93
|
+
if not main_file.exists():
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
skill_name = skill_dir.name
|
|
97
|
+
# Skip if we've already seen this skill name (first wins)
|
|
98
|
+
if skill_name in seen_skill_names:
|
|
99
|
+
logger.debug(
|
|
100
|
+
f"Skipping duplicate skill '{skill_name}' from {root} "
|
|
101
|
+
f"(already found in earlier root)"
|
|
102
|
+
)
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
provider = SkillProvider(
|
|
107
|
+
skill_path=skill_dir,
|
|
108
|
+
main_file_name=self._main_file_name,
|
|
109
|
+
supporting_files=self._supporting_files,
|
|
110
|
+
)
|
|
111
|
+
self.providers.append(provider)
|
|
112
|
+
seen_skill_names.add(skill_name)
|
|
113
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
114
|
+
logger.exception(f"Failed to load skill: {skill_dir.name}")
|
|
115
|
+
|
|
116
|
+
self._discovered = True
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"SkillsDirectoryProvider loaded {len(self.providers)} skills "
|
|
119
|
+
f"from {len(self._roots)} root(s)"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def _ensure_discovered(self) -> None:
|
|
123
|
+
"""Ensure skills are discovered, rediscovering if reload is enabled."""
|
|
124
|
+
if self._reload or not self._discovered:
|
|
125
|
+
self._discover_skills()
|
|
126
|
+
|
|
127
|
+
# Override list methods to support reload
|
|
128
|
+
async def _list_resources(self) -> Sequence[Resource]:
|
|
129
|
+
await self._ensure_discovered()
|
|
130
|
+
return await super()._list_resources()
|
|
131
|
+
|
|
132
|
+
async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
|
|
133
|
+
await self._ensure_discovered()
|
|
134
|
+
return await super()._list_resource_templates()
|
|
135
|
+
|
|
136
|
+
async def _get_resource(
|
|
137
|
+
self, uri: str, version: VersionSpec | None = None
|
|
138
|
+
) -> Resource | None:
|
|
139
|
+
await self._ensure_discovered()
|
|
140
|
+
return await super()._get_resource(uri, version)
|
|
141
|
+
|
|
142
|
+
async def _get_resource_template(
|
|
143
|
+
self, uri: str, version: VersionSpec | None = None
|
|
144
|
+
) -> ResourceTemplate | None:
|
|
145
|
+
await self._ensure_discovered()
|
|
146
|
+
return await super()._get_resource_template(uri, version)
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
roots_repr = self._roots[0] if len(self._roots) == 1 else self._roots
|
|
150
|
+
return (
|
|
151
|
+
f"SkillsDirectoryProvider(roots={roots_repr!r}, "
|
|
152
|
+
f"reload={self._reload}, skills={len(self.providers)})"
|
|
153
|
+
)
|