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.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {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
+ )