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,432 @@
|
|
|
1
|
+
"""Basic skill provider for handling a single skill folder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import mimetypes
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import AnyUrl
|
|
12
|
+
|
|
13
|
+
from fastmcp.resources.resource import Resource, ResourceResult
|
|
14
|
+
from fastmcp.resources.template import ResourceTemplate
|
|
15
|
+
from fastmcp.server.providers.base import Provider
|
|
16
|
+
from fastmcp.server.providers.skills._common import (
|
|
17
|
+
SkillInfo,
|
|
18
|
+
parse_frontmatter,
|
|
19
|
+
scan_skill_files,
|
|
20
|
+
)
|
|
21
|
+
from fastmcp.utilities.logging import get_logger
|
|
22
|
+
from fastmcp.utilities.versions import VersionSpec
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
# Ensure .md is recognized as text/markdown on all platforms (Windows may not have this)
|
|
27
|
+
mimetypes.add_type("text/markdown", ".md")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
# Skill-specific Resource and ResourceTemplate subclasses
|
|
32
|
+
# -----------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SkillResource(Resource):
|
|
36
|
+
"""A resource representing a skill's main file or manifest."""
|
|
37
|
+
|
|
38
|
+
skill_info: SkillInfo
|
|
39
|
+
is_manifest: bool = False
|
|
40
|
+
|
|
41
|
+
async def read(self) -> str | bytes | ResourceResult:
|
|
42
|
+
"""Read the resource content."""
|
|
43
|
+
if self.is_manifest:
|
|
44
|
+
return self._generate_manifest()
|
|
45
|
+
else:
|
|
46
|
+
main_file_path = self.skill_info.path / self.skill_info.main_file
|
|
47
|
+
return main_file_path.read_text()
|
|
48
|
+
|
|
49
|
+
def _generate_manifest(self) -> str:
|
|
50
|
+
"""Generate JSON manifest for the skill."""
|
|
51
|
+
manifest = {
|
|
52
|
+
"skill": self.skill_info.name,
|
|
53
|
+
"files": [
|
|
54
|
+
{"path": f.path, "size": f.size, "hash": f.hash}
|
|
55
|
+
for f in self.skill_info.files
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
return json.dumps(manifest, indent=2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SkillFileTemplate(ResourceTemplate):
|
|
62
|
+
"""A template for accessing files within a skill."""
|
|
63
|
+
|
|
64
|
+
skill_info: SkillInfo
|
|
65
|
+
|
|
66
|
+
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
|
|
67
|
+
"""Read a file from the skill directory."""
|
|
68
|
+
file_path = arguments.get("path", "")
|
|
69
|
+
full_path = self.skill_info.path / file_path
|
|
70
|
+
|
|
71
|
+
# Security: ensure path doesn't escape skill directory
|
|
72
|
+
try:
|
|
73
|
+
full_path = full_path.resolve()
|
|
74
|
+
if not full_path.is_relative_to(self.skill_info.path):
|
|
75
|
+
raise ValueError(f"Path {file_path} escapes skill directory")
|
|
76
|
+
except ValueError as e:
|
|
77
|
+
raise ValueError(f"Invalid path: {e}") from e
|
|
78
|
+
|
|
79
|
+
if not full_path.exists():
|
|
80
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
81
|
+
|
|
82
|
+
if not full_path.is_file():
|
|
83
|
+
raise ValueError(f"Not a file: {file_path}")
|
|
84
|
+
|
|
85
|
+
# Determine if binary or text based on mime type
|
|
86
|
+
mime_type, _ = mimetypes.guess_type(str(full_path))
|
|
87
|
+
if mime_type and mime_type.startswith("text/"):
|
|
88
|
+
return full_path.read_text()
|
|
89
|
+
else:
|
|
90
|
+
return full_path.read_bytes()
|
|
91
|
+
|
|
92
|
+
async def _read( # type: ignore[override]
|
|
93
|
+
self,
|
|
94
|
+
uri: str,
|
|
95
|
+
params: dict[str, Any],
|
|
96
|
+
task_meta: Any = None,
|
|
97
|
+
) -> ResourceResult:
|
|
98
|
+
"""Server entry point - read file directly without creating ephemeral resource.
|
|
99
|
+
|
|
100
|
+
Note: task_meta is ignored - this template doesn't support background tasks.
|
|
101
|
+
"""
|
|
102
|
+
# Call read() directly and convert to ResourceResult
|
|
103
|
+
result = await self.read(arguments=params)
|
|
104
|
+
return self.convert_result(result)
|
|
105
|
+
|
|
106
|
+
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
107
|
+
"""Create a resource for the given URI and parameters.
|
|
108
|
+
|
|
109
|
+
Note: This is not typically used since _read() handles file reading directly.
|
|
110
|
+
Provided for compatibility with the ResourceTemplate interface.
|
|
111
|
+
"""
|
|
112
|
+
file_path = params.get("path", "")
|
|
113
|
+
full_path = (self.skill_info.path / file_path).resolve()
|
|
114
|
+
|
|
115
|
+
# Security: ensure path doesn't escape skill directory
|
|
116
|
+
if not full_path.is_relative_to(self.skill_info.path):
|
|
117
|
+
raise ValueError(f"Path {file_path} escapes skill directory")
|
|
118
|
+
|
|
119
|
+
mime_type, _ = mimetypes.guess_type(str(full_path))
|
|
120
|
+
|
|
121
|
+
# Create a SkillFileResource that can read the file
|
|
122
|
+
return SkillFileResource(
|
|
123
|
+
uri=AnyUrl(uri),
|
|
124
|
+
name=f"{self.skill_info.name}/{file_path}",
|
|
125
|
+
description=f"File from {self.skill_info.name} skill",
|
|
126
|
+
mime_type=mime_type or "application/octet-stream",
|
|
127
|
+
skill_info=self.skill_info,
|
|
128
|
+
file_path=file_path,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class SkillFileResource(Resource):
|
|
133
|
+
"""A resource representing a specific file within a skill."""
|
|
134
|
+
|
|
135
|
+
skill_info: SkillInfo
|
|
136
|
+
file_path: str
|
|
137
|
+
|
|
138
|
+
async def read(self) -> str | bytes | ResourceResult:
|
|
139
|
+
"""Read the file content."""
|
|
140
|
+
full_path = self.skill_info.path / self.file_path
|
|
141
|
+
|
|
142
|
+
# Security check
|
|
143
|
+
full_path = full_path.resolve()
|
|
144
|
+
if not full_path.is_relative_to(self.skill_info.path):
|
|
145
|
+
raise ValueError(f"Path {self.file_path} escapes skill directory")
|
|
146
|
+
|
|
147
|
+
if not full_path.exists():
|
|
148
|
+
raise FileNotFoundError(f"File not found: {self.file_path}")
|
|
149
|
+
|
|
150
|
+
mime_type, _ = mimetypes.guess_type(str(full_path))
|
|
151
|
+
if mime_type and mime_type.startswith("text/"):
|
|
152
|
+
return full_path.read_text()
|
|
153
|
+
else:
|
|
154
|
+
return full_path.read_bytes()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# -----------------------------------------------------------------------------
|
|
158
|
+
# SkillProvider - handles a SINGLE skill folder
|
|
159
|
+
# -----------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class SkillProvider(Provider):
|
|
163
|
+
"""Provider that exposes a single skill folder as MCP resources.
|
|
164
|
+
|
|
165
|
+
Each skill folder must contain a main file (default: SKILL.md) and may
|
|
166
|
+
contain additional supporting files.
|
|
167
|
+
|
|
168
|
+
Exposes:
|
|
169
|
+
- A Resource for the main file (skill://{name}/SKILL.md)
|
|
170
|
+
- A Resource for the synthetic manifest (skill://{name}/_manifest)
|
|
171
|
+
- Supporting files via ResourceTemplate or Resources (configurable)
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
skill_path: Path to the skill directory.
|
|
175
|
+
main_file_name: Name of the main skill file. Defaults to "SKILL.md".
|
|
176
|
+
supporting_files: How supporting files (everything except main file and
|
|
177
|
+
manifest) are exposed to clients:
|
|
178
|
+
- "template": Accessed via ResourceTemplate, hidden from list_resources().
|
|
179
|
+
Clients discover files by reading the manifest first.
|
|
180
|
+
- "resources": Each file exposed as individual Resource in list_resources().
|
|
181
|
+
Full enumeration upfront.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
```python
|
|
185
|
+
from pathlib import Path
|
|
186
|
+
from fastmcp import FastMCP
|
|
187
|
+
from fastmcp.server.providers.skills import SkillProvider
|
|
188
|
+
|
|
189
|
+
mcp = FastMCP("My Skill")
|
|
190
|
+
mcp.add_provider(SkillProvider(
|
|
191
|
+
Path.home() / ".claude/skills/pdf-processing"
|
|
192
|
+
))
|
|
193
|
+
```
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
skill_path: str | Path,
|
|
199
|
+
main_file_name: str = "SKILL.md",
|
|
200
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
201
|
+
) -> None:
|
|
202
|
+
super().__init__()
|
|
203
|
+
self._skill_path = Path(skill_path).resolve()
|
|
204
|
+
self._main_file_name = main_file_name
|
|
205
|
+
self._supporting_files = supporting_files
|
|
206
|
+
self._skill_info: SkillInfo | None = None
|
|
207
|
+
|
|
208
|
+
# Load at init to catch errors early
|
|
209
|
+
self._load_skill()
|
|
210
|
+
|
|
211
|
+
def _load_skill(self) -> None:
|
|
212
|
+
"""Load and parse the skill directory."""
|
|
213
|
+
main_file = self._skill_path / self._main_file_name
|
|
214
|
+
|
|
215
|
+
if not self._skill_path.exists():
|
|
216
|
+
raise FileNotFoundError(f"Skill directory not found: {self._skill_path}")
|
|
217
|
+
|
|
218
|
+
if not main_file.exists():
|
|
219
|
+
raise FileNotFoundError(
|
|
220
|
+
f"Main skill file not found: {main_file}. "
|
|
221
|
+
f"Expected {self._main_file_name} in {self._skill_path}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
content = main_file.read_text()
|
|
225
|
+
frontmatter, body = parse_frontmatter(content)
|
|
226
|
+
|
|
227
|
+
# Get description from frontmatter or first non-empty line
|
|
228
|
+
description = frontmatter.get("description", "")
|
|
229
|
+
if not description:
|
|
230
|
+
for line in body.strip().split("\n"):
|
|
231
|
+
line = line.strip()
|
|
232
|
+
if line and not line.startswith("#"):
|
|
233
|
+
description = line[:200]
|
|
234
|
+
break
|
|
235
|
+
elif line.startswith("#"):
|
|
236
|
+
description = line.lstrip("#").strip()[:200]
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
# Scan all files in the skill directory
|
|
240
|
+
files = scan_skill_files(self._skill_path)
|
|
241
|
+
|
|
242
|
+
self._skill_info = SkillInfo(
|
|
243
|
+
name=self._skill_path.name,
|
|
244
|
+
description=description or f"Skill: {self._skill_path.name}",
|
|
245
|
+
path=self._skill_path,
|
|
246
|
+
main_file=self._main_file_name,
|
|
247
|
+
files=files,
|
|
248
|
+
frontmatter=frontmatter,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
logger.debug(f"SkillProvider loaded skill: {self._skill_info.name}")
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def skill_info(self) -> SkillInfo:
|
|
255
|
+
"""Get the loaded skill info."""
|
|
256
|
+
if self._skill_info is None:
|
|
257
|
+
raise RuntimeError("Skill not loaded")
|
|
258
|
+
return self._skill_info
|
|
259
|
+
|
|
260
|
+
# -------------------------------------------------------------------------
|
|
261
|
+
# Provider interface implementation
|
|
262
|
+
# -------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
async def _list_resources(self) -> Sequence[Resource]:
|
|
265
|
+
"""List skill resources."""
|
|
266
|
+
skill = self.skill_info
|
|
267
|
+
resources: list[Resource] = []
|
|
268
|
+
|
|
269
|
+
# Main skill file
|
|
270
|
+
resources.append(
|
|
271
|
+
SkillResource(
|
|
272
|
+
uri=AnyUrl(f"skill://{skill.name}/{self._main_file_name}"),
|
|
273
|
+
name=f"{skill.name}/{self._main_file_name}",
|
|
274
|
+
description=skill.description,
|
|
275
|
+
mime_type="text/markdown",
|
|
276
|
+
skill_info=skill,
|
|
277
|
+
is_manifest=False,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Synthetic manifest
|
|
282
|
+
resources.append(
|
|
283
|
+
SkillResource(
|
|
284
|
+
uri=AnyUrl(f"skill://{skill.name}/_manifest"),
|
|
285
|
+
name=f"{skill.name}/_manifest",
|
|
286
|
+
description=f"File listing for {skill.name}",
|
|
287
|
+
mime_type="application/json",
|
|
288
|
+
skill_info=skill,
|
|
289
|
+
is_manifest=True,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# If supporting_files="resources", add all supporting files as resources
|
|
294
|
+
if self._supporting_files == "resources":
|
|
295
|
+
for file_info in skill.files:
|
|
296
|
+
# Skip main file and manifest (already added)
|
|
297
|
+
if file_info.path == self._main_file_name:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
mime_type, _ = mimetypes.guess_type(file_info.path)
|
|
301
|
+
resources.append(
|
|
302
|
+
SkillFileResource(
|
|
303
|
+
uri=AnyUrl(f"skill://{skill.name}/{file_info.path}"),
|
|
304
|
+
name=f"{skill.name}/{file_info.path}",
|
|
305
|
+
description=f"File from {skill.name} skill",
|
|
306
|
+
mime_type=mime_type or "application/octet-stream",
|
|
307
|
+
skill_info=skill,
|
|
308
|
+
file_path=file_info.path,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return resources
|
|
313
|
+
|
|
314
|
+
async def _get_resource(
|
|
315
|
+
self, uri: str, version: VersionSpec | None = None
|
|
316
|
+
) -> Resource | None:
|
|
317
|
+
"""Get a resource by URI."""
|
|
318
|
+
skill = self.skill_info
|
|
319
|
+
|
|
320
|
+
# Parse URI: skill://{skill_name}/{file_path}
|
|
321
|
+
if not uri.startswith("skill://"):
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
path_part = uri[len("skill://") :]
|
|
325
|
+
parts = path_part.split("/", 1)
|
|
326
|
+
if len(parts) != 2:
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
skill_name, file_path = parts
|
|
330
|
+
if skill_name != skill.name:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
if file_path == "_manifest":
|
|
334
|
+
return SkillResource(
|
|
335
|
+
uri=AnyUrl(uri),
|
|
336
|
+
name=f"{skill_name}/_manifest",
|
|
337
|
+
description=f"File listing for {skill_name}",
|
|
338
|
+
mime_type="application/json",
|
|
339
|
+
skill_info=skill,
|
|
340
|
+
is_manifest=True,
|
|
341
|
+
)
|
|
342
|
+
elif file_path == self._main_file_name:
|
|
343
|
+
return SkillResource(
|
|
344
|
+
uri=AnyUrl(uri),
|
|
345
|
+
name=f"{skill_name}/{self._main_file_name}",
|
|
346
|
+
description=skill.description,
|
|
347
|
+
mime_type="text/markdown",
|
|
348
|
+
skill_info=skill,
|
|
349
|
+
is_manifest=False,
|
|
350
|
+
)
|
|
351
|
+
elif self._supporting_files == "resources":
|
|
352
|
+
# Check if it's a known supporting file
|
|
353
|
+
for file_info in skill.files:
|
|
354
|
+
if file_info.path == file_path:
|
|
355
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
|
356
|
+
return SkillFileResource(
|
|
357
|
+
uri=AnyUrl(uri),
|
|
358
|
+
name=f"{skill_name}/{file_path}",
|
|
359
|
+
description=f"File from {skill_name} skill",
|
|
360
|
+
mime_type=mime_type or "application/octet-stream",
|
|
361
|
+
skill_info=skill,
|
|
362
|
+
file_path=file_path,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
async def _list_resource_templates(self) -> Sequence[ResourceTemplate]:
|
|
368
|
+
"""List resource templates for accessing files within the skill."""
|
|
369
|
+
# Only expose template if supporting_files="template"
|
|
370
|
+
if self._supporting_files != "template":
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
skill = self.skill_info
|
|
374
|
+
return [
|
|
375
|
+
SkillFileTemplate(
|
|
376
|
+
uri_template=f"skill://{skill.name}/{{path*}}",
|
|
377
|
+
name=f"{skill.name}_files",
|
|
378
|
+
description=f"Access files within {skill.name}",
|
|
379
|
+
mime_type="application/octet-stream",
|
|
380
|
+
parameters={
|
|
381
|
+
"type": "object",
|
|
382
|
+
"properties": {"path": {"type": "string"}},
|
|
383
|
+
"required": ["path"],
|
|
384
|
+
},
|
|
385
|
+
skill_info=skill,
|
|
386
|
+
)
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
async def _get_resource_template(
|
|
390
|
+
self, uri: str, version: VersionSpec | None = None
|
|
391
|
+
) -> ResourceTemplate | None:
|
|
392
|
+
"""Get a resource template that matches the given URI."""
|
|
393
|
+
# Only match if supporting_files="template"
|
|
394
|
+
if self._supporting_files != "template":
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
skill = self.skill_info
|
|
398
|
+
|
|
399
|
+
if not uri.startswith("skill://"):
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
path_part = uri[len("skill://") :]
|
|
403
|
+
parts = path_part.split("/", 1)
|
|
404
|
+
if len(parts) != 2:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
skill_name, file_path = parts
|
|
408
|
+
if skill_name != skill.name:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
# Don't match known resources (main file, manifest)
|
|
412
|
+
if file_path == "_manifest" or file_path == self._main_file_name:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
return SkillFileTemplate(
|
|
416
|
+
uri_template=f"skill://{skill.name}/{{path*}}",
|
|
417
|
+
name=f"{skill.name}_files",
|
|
418
|
+
description=f"Access files within {skill.name}",
|
|
419
|
+
mime_type="application/octet-stream",
|
|
420
|
+
parameters={
|
|
421
|
+
"type": "object",
|
|
422
|
+
"properties": {"path": {"type": "string"}},
|
|
423
|
+
"required": ["path"],
|
|
424
|
+
},
|
|
425
|
+
skill_info=skill,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def __repr__(self) -> str:
|
|
429
|
+
return (
|
|
430
|
+
f"SkillProvider(skill_path={self._skill_path!r}, "
|
|
431
|
+
f"supporting_files={self._supporting_files!r})"
|
|
432
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Vendor-specific skills providers for various AI coding platforms."""
|
|
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 CursorSkillsProvider(SkillsDirectoryProvider):
|
|
12
|
+
"""Cursor skills from ~/.cursor/skills/."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
reload: bool = False,
|
|
17
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
18
|
+
) -> None:
|
|
19
|
+
root = Path.home() / ".cursor" / "skills"
|
|
20
|
+
|
|
21
|
+
super().__init__(
|
|
22
|
+
roots=[root],
|
|
23
|
+
reload=reload,
|
|
24
|
+
main_file_name="SKILL.md",
|
|
25
|
+
supporting_files=supporting_files,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VSCodeSkillsProvider(SkillsDirectoryProvider):
|
|
30
|
+
"""VS Code skills from ~/.copilot/skills/."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
reload: bool = False,
|
|
35
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
36
|
+
) -> None:
|
|
37
|
+
root = Path.home() / ".copilot" / "skills"
|
|
38
|
+
|
|
39
|
+
super().__init__(
|
|
40
|
+
roots=[root],
|
|
41
|
+
reload=reload,
|
|
42
|
+
main_file_name="SKILL.md",
|
|
43
|
+
supporting_files=supporting_files,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CodexSkillsProvider(SkillsDirectoryProvider):
|
|
48
|
+
"""Codex skills from /etc/codex/skills/ and ~/.codex/skills/.
|
|
49
|
+
|
|
50
|
+
Scans both system-level and user-level directories. System skills take
|
|
51
|
+
precedence if duplicates exist.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
reload: bool = False,
|
|
57
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
58
|
+
) -> None:
|
|
59
|
+
system_root = Path("/etc/codex/skills")
|
|
60
|
+
user_root = Path.home() / ".codex" / "skills"
|
|
61
|
+
|
|
62
|
+
# Include both paths (system first, then user)
|
|
63
|
+
roots = [system_root, user_root]
|
|
64
|
+
|
|
65
|
+
super().__init__(
|
|
66
|
+
roots=roots,
|
|
67
|
+
reload=reload,
|
|
68
|
+
main_file_name="SKILL.md",
|
|
69
|
+
supporting_files=supporting_files,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GeminiSkillsProvider(SkillsDirectoryProvider):
|
|
74
|
+
"""Gemini skills from ~/.gemini/skills/."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
reload: bool = False,
|
|
79
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
80
|
+
) -> None:
|
|
81
|
+
root = Path.home() / ".gemini" / "skills"
|
|
82
|
+
|
|
83
|
+
super().__init__(
|
|
84
|
+
roots=[root],
|
|
85
|
+
reload=reload,
|
|
86
|
+
main_file_name="SKILL.md",
|
|
87
|
+
supporting_files=supporting_files,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GooseSkillsProvider(SkillsDirectoryProvider):
|
|
92
|
+
"""Goose skills from ~/.config/agents/skills/."""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
reload: bool = False,
|
|
97
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
98
|
+
) -> None:
|
|
99
|
+
root = Path.home() / ".config" / "agents" / "skills"
|
|
100
|
+
|
|
101
|
+
super().__init__(
|
|
102
|
+
roots=[root],
|
|
103
|
+
reload=reload,
|
|
104
|
+
main_file_name="SKILL.md",
|
|
105
|
+
supporting_files=supporting_files,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CopilotSkillsProvider(SkillsDirectoryProvider):
|
|
110
|
+
"""GitHub Copilot skills from ~/.copilot/skills/."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
reload: bool = False,
|
|
115
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
116
|
+
) -> None:
|
|
117
|
+
root = Path.home() / ".copilot" / "skills"
|
|
118
|
+
|
|
119
|
+
super().__init__(
|
|
120
|
+
roots=[root],
|
|
121
|
+
reload=reload,
|
|
122
|
+
main_file_name="SKILL.md",
|
|
123
|
+
supporting_files=supporting_files,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class OpenCodeSkillsProvider(SkillsDirectoryProvider):
|
|
128
|
+
"""OpenCode skills from ~/.config/opencode/skills/."""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
reload: bool = False,
|
|
133
|
+
supporting_files: Literal["template", "resources"] = "template",
|
|
134
|
+
) -> None:
|
|
135
|
+
root = Path.home() / ".config" / "opencode" / "skills"
|
|
136
|
+
|
|
137
|
+
super().__init__(
|
|
138
|
+
roots=[root],
|
|
139
|
+
reload=reload,
|
|
140
|
+
main_file_name="SKILL.md",
|
|
141
|
+
supporting_files=supporting_files,
|
|
142
|
+
)
|