Skip to content

Add Skills Provider for exposing agent skills as MCP resources#2944

Merged
jlowin merged 7 commits intomainfrom
skills-provider
Jan 19, 2026
Merged

Add Skills Provider for exposing agent skills as MCP resources#2944
jlowin merged 7 commits intomainfrom
skills-provider

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 19, 2026

Agent skills (directories with instruction files like SKILL.md) are used by Claude Code, Cursor, VS Code Copilot, and other AI tools. This PR adds providers that expose these skill directories as MCP resources, enabling skill discovery and sharing across different tools and clients.

Architecture

Two-layer design for flexibility:

from fastmcp import FastMCP
from fastmcp.server.providers.skills import (
    SkillProvider,
    SkillsDirectoryProvider,
    ClaudeSkillsProvider,
)

mcp = FastMCP("Skills")

# Single skill
mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))

# Scan directory (supports multiple roots)
mcp.add_provider(SkillsDirectoryProvider(roots=[
    Path.cwd() / ".claude/skills",      # Project-level
    Path.home() / ".claude/skills",     # User-level
]))

# Platform defaults
mcp.add_provider(ClaudeSkillsProvider())  # ~/.claude/skills/

Each skill exposes:

  • skill://name/SKILL.md - Main instruction file
  • skill://name/_manifest - JSON listing all files with hashes
  • skill://name/{path} - Supporting files (via template or as resources)

Vendor Providers

Platform-specific providers with locked default paths:

Provider Location
ClaudeSkillsProvider ~/.claude/skills/
CursorSkillsProvider ~/.cursor/skills/
VSCodeSkillsProvider ~/.copilot/skills/
CodexSkillsProvider ~/.codex/skills/
GeminiSkillsProvider ~/.gemini/skills/
GooseSkillsProvider ~/.config/agents/skills/
CopilotSkillsProvider ~/.copilot/skills/
OpenCodeSkillsProvider ~/.config/opencode/skills/

Configuration

The supporting_files parameter controls how non-main files are exposed:

  • "template" (default): Hidden from list_resources(), accessed via ResourceTemplate
  • "resources": All files visible as individual Resources

@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality. labels Jan 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57b797af44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +96 to +100

Note: task_meta is ignored - this template doesn't support background tasks.
"""
# Call read() directly and convert to ResourceResult
result = await self.read(arguments=params)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce task routing for skill file templates

The overridden _read explicitly ignores task_meta and directly returns a synchronous ResourceResult. When a client sends task metadata for resources/read (SEP-1686), the server passes that metadata into ResourceTemplate._read, which normally enforces task_config (e.g., raising for mode="forbidden" or returning a CreateTaskResult). Here, those checks are bypassed, so task-augmented requests to skill://…/{path*} will return an immediate result instead of an error or task handle, breaking the task contract and allowing callers to bypass mode="forbidden"/required behavior.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 19, 2026

Warning

Rate limit exceeded

@jlowin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 47 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 884a314 and 863ff05.

⛔ Files ignored due to path filters (2)
  • tests/server/providers/test_skills_provider.py is excluded by none and included by none
  • tests/server/providers/test_skills_vendor_providers.py is excluded by none and included by none
📒 Files selected for processing (16)
  • docs/development/v3-notes/v3-features.mdx
  • docs/docs.json
  • docs/servers/providers/skills.mdx
  • examples/skills/README.md
  • examples/skills/client.py
  • examples/skills/sample_skills/code-review/SKILL.md
  • examples/skills/sample_skills/pdf-processing/SKILL.md
  • examples/skills/sample_skills/pdf-processing/reference.md
  • examples/skills/server.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/skills/__init__.py
  • src/fastmcp/server/providers/skills/_common.py
  • src/fastmcp/server/providers/skills/claude_provider.py
  • src/fastmcp/server/providers/skills/directory_provider.py
  • src/fastmcp/server/providers/skills/skill_provider.py
  • src/fastmcp/server/providers/skills/vendor_providers.py

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This pull request introduces a comprehensive Skills Provider system for FastMCP, enabling exposure of skill directories as MCP resources. The implementation includes a core SkillProvider class for single-skill directories, a SkillsDirectoryProvider for aggregating multiple skills from one or more root directories, vendor-specific providers for Claude, Cursor, VSCode, and other tools, and supporting utilities for frontmatter parsing and file scanning. The change adds new public exports to the providers package and includes documentation and example code demonstrating server and client usage with sample skill definitions for PDF processing and code review tasks.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a Skills Provider to expose agent skills as MCP resources.
Description check ✅ Passed The description is comprehensive and well-structured, covering architecture, vendor providers, and configuration details; however, the required Contributors and Review checklists are incomplete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
examples/skills/server.py (1)

23-46: Make optional provider examples copy‑paste ready.
Uncommenting Options 1/3 currently requires manual imports; a short note avoids NameError surprises.

💡 Optional tweak
-# Option 1: Load a single skill
-# mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))
+# Option 1: Load a single skill (add SkillProvider to the imports if you uncomment)
+# mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))

-# Option 3: Load skills from a platform's default location
-# mcp.add_provider(ClaudeSkillsProvider())  # ~/.claude/skills/
+# Option 3: Load skills from a platform's default location
+# (add ClaudeSkillsProvider to the imports if you uncomment)
+# mcp.add_provider(ClaudeSkillsProvider())  # ~/.claude/skills/
docs/servers/providers/skills.mdx (1)

20-48: Convert Quick Start into a Steps-based, runnable procedure.

This section reads as a procedure but isn’t structured with a <Steps> component and doesn’t show a fully runnable flow with expected outcomes/error handling. Consider converting it to explicit steps and including a minimal runnable example (with basic error handling and expected output). As per coding guidelines, keep procedures in Steps and include runnable examples with outcomes.

src/fastmcp/server/providers/skills/_common.py (1)

86-99: Make manifest ordering deterministic.

rglob() order can vary by filesystem, which can lead to unstable manifests and tests. Consider sorting by relative path before building the list.

Proposed fix
-    for file_path in skill_dir.rglob("*"):
+    for file_path in sorted(skill_dir.rglob("*")):
         if file_path.is_file():
             rel_path = file_path.relative_to(skill_dir)
             files.append(
                 SkillFileInfo(
                     path=str(rel_path),
                     size=file_path.stat().st_size,
                     hash=compute_file_hash(file_path),
                 )
             )
src/fastmcp/server/providers/skills/directory_provider.py (1)

105-114: Consider catching broader exceptions during provider creation.

SkillProvider.__init__ can raise exceptions beyond FileNotFoundError (e.g., PermissionError, OSError for I/O issues, or exceptions from parse_frontmatter). Currently only FileNotFoundError is caught, which could cause a single problematic skill to prevent loading all subsequent skills in the loop.

♻️ Suggested improvement
                 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:
+                except (FileNotFoundError, PermissionError, OSError) as e:
                     logger.exception(f"Failed to load skill: {skill_dir.name}")
src/fastmcp/server/providers/skills/skill_provider.py (2)

68-80: Path traversal protection is correctly implemented.

The security checks using resolve() and is_relative_to() effectively prevent directory traversal attacks. However, the exception handling can be simplified since the ValueError from is_relative_to() is already meaningful.

♻️ Optional simplification
         # Security: ensure path doesn't escape skill directory
-        try:
-            full_path = full_path.resolve()
-            if not full_path.is_relative_to(self.skill_info.path):
-                raise ValueError(f"Path {file_path} escapes skill directory")
-        except ValueError as e:
-            raise ValueError(f"Invalid path: {e}") from e
+        full_path = full_path.resolve()
+        if not full_path.is_relative_to(self.skill_info.path):
+            raise ValueError(f"Path {file_path} escapes skill directory")

131-147: Consider extracting shared file-reading logic.

The path validation and content reading logic is duplicated between SkillFileTemplate.read() (lines 68-87) and SkillFileResource.read() (lines 133-147). A shared helper method could reduce duplication.

Comment thread docs/servers/providers/skills.mdx
Comment thread examples/skills/client.py
Comment thread examples/skills/README.md
Comment on lines +29 to +44
class VSCodeSkillsProvider(SkillsDirectoryProvider):
"""VS Code skills from ~/.copilot/skills/."""

def __init__(
self,
reload: bool = False,
supporting_files: Literal["template", "resources"] = "template",
) -> None:
root = Path.home() / ".copilot" / "skills"

super().__init__(
roots=[root],
reload=reload,
main_file_name="SKILL.md",
supporting_files=supporting_files,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/fastmcp/server/providers/skills/vendor_providers.py

Repository: jlowin/fastmcp

Length of output: 5001


VSCodeSkillsProvider and CopilotSkillsProvider use identical paths.

Both providers point to ~/.copilot/skills/ (lines 37 and 117). If both are added to a server, they'll create duplicate resources for the same directory. Either:

  1. Use a distinct path for VS Code (e.g., ~/.vscode/skills/)
  2. Document explicitly in the docstrings that VS Code shares GitHub Copilot's skills directory (clarify whether this is intentional)

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Windows-specific test failures due to path separator inconsistencies. The code generates Windows-style backslashes (\\) while tests expect POSIX-style forward slashes (/).

Root Cause: In src/fastmcp/server/providers/skills/_common.py:94, the scan_skill_files function uses str(rel_path) which on Windows produces backslash separators (scripts\\helper.py). Tests expect forward slashes (scripts/helper.py).

Suggested Solution:

Normalize path separators to forward slashes in scan_skill_files:

# In src/fastmcp/server/providers/skills/_common.py, line 94
path=str(rel_path).replace("\\\\", "/"),  # Normalize to forward slashes

Or use as_posix() for cleaner code:

path=rel_path.as_posix(),

This will make paths consistent across all platforms since MCP URIs use forward slashes.

Detailed Analysis

Primary Issue: Path Separators

The tests are failing with assertions like:

AssertionError: assert 'my-skill/scripts/helper.py' in {'my-skill/SKILL.md', 'my-skill/_manifest', 'my-skill/reference.md', 'my-skill/scripts\\helper.py'}

Expected: scripts/helper.py
Actual: scripts\\helper.py

The issue originates in _common.py:scan_skill_files():

def scan_skill_files(skill_dir: Path) -> list[SkillFileInfo]:
    files = []
    for file_path in skill_dir.rglob("*"):
        if file_path.is_file():
            rel_path = file_path.relative_to(skill_dir)
            files.append(
                SkillFileInfo(
                    path=str(rel_path),  # <-- Problem: Windows uses backslashes
                    size=file_path.stat().st_size,
                    hash=compute_file_hash(file_path),
                )
            )
    return files

Secondary Issue: Absolute Path Conversion on Windows

Tests that mock Path.home() with POSIX paths fail because Windows converts them:

At index 0 diff: WindowsPath('D:/fake/home/.claude/skills') != WindowsPath('/fake/home/.claude/skills')

However, this is primarily a test issue, not a code issue. The path normalization fix above will resolve the main functionality problems.

Failed Tests

  1. test_list_resources_supporting_files_as_resources - path separator issue
  2. test_read_manifest - path separator issue
  3. test_read_supporting_file_via_template - related AttributeError (secondary)
  4. test_read_supporting_file_via_resource_mode - related AttributeError (secondary)
  5. test_supporting_files_as_resources - path separator issue
  6. All vendor provider path tests - absolute path conversion issue (test-specific)
Related Files
  • src/fastmcp/server/providers/skills/_common.py:94 - Where paths are converted to strings, needs normalization
  • tests/server/providers/test_skills_provider.py:129, 170, 292, 406 - Test assertions expecting forward slashes
  • tests/server/providers/test_skills_vendor_providers.py - Tests affected by Windows path conversion

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/fastmcp/server/providers/skills/vendor_providers.py (1)

29-44: VSCodeSkillsProvider and CopilotSkillsProvider share identical root path.

Both providers point to ~/.copilot/skills/ (lines 37 and 117). If both are added to a server, they'll expose duplicate resources for the same directory.

Either use a distinct path for VS Code (e.g., ~/.vscode/skills/) or document explicitly that this is intentional (VS Code shares GitHub Copilot's skills directory).

Also applies to: 109-124

🧹 Nitpick comments (5)
src/fastmcp/server/providers/skills/_common.py (1)

33-74: Custom YAML parser has limited coverage — acceptable for simple frontmatter but may fail on edge cases.

The parser handles single-level key-value pairs, quoted strings, and inline arrays, which is sufficient for typical frontmatter. However, it doesn't support:

  • Multi-line values
  • Nested objects
  • Block arrays (YAML list syntax with -)
  • Boolean/numeric type coercion

This is acceptable given the comment at line 53 noting "simple key: value parsing, no complex types." Consider documenting these limitations in the docstring or falling back to a YAML library if more complex frontmatter is expected.

src/fastmcp/server/providers/skills/skill_provider.py (2)

38-44: Synchronous file I/O in async method may block the event loop.

main_file_path.read_text() at line 44 performs synchronous file I/O within an async method. For small skill files this is likely acceptable, but for larger files or high-concurrency scenarios, this could block the event loop.

Consider wrapping in asyncio.to_thread() if performance becomes a concern, though this is optional for the typical use case.


307-358: version parameter is accepted but unused.

The version parameter in _get_resource is never used, which the static analysis correctly flags (ARG002). Since versioning may be relevant for future resource management, consider either:

  1. Passing it to the base class method if applicable
  2. Documenting that versioning is not supported for skill resources

This is minor since the base Provider._get_resource signature requires this parameter.

src/fastmcp/server/providers/skills/directory_provider.py (1)

76-120: Synchronous discovery called from async context may block event loop.

_discover_skills() performs synchronous filesystem operations (root.exists(), root.iterdir(), main_file.exists()) and is called from async methods via _ensure_discovered(). With reload=True, this runs on every request and could block the event loop.

For directories with many skills, consider running discovery in a thread:

async def _ensure_discovered(self) -> None:
    if self._reload or not self._discovered:
        await asyncio.to_thread(self._discover_skills)

This is a recommended improvement for production use with reload enabled.

src/fastmcp/server/providers/skills/__init__.py (1)

43-44: Clarify the purpose of the "backwards compatibility" alias.

Since this is a new module being introduced in this PR, the "backwards compatibility alias" comment may be misleading. If SkillsProvider was used during development or in early documentation, consider adding a brief note explaining when/why this alias exists. Otherwise, if this is purely for API convenience (offering both singular and plural naming), consider updating the comment to reflect that intent.

📝 Suggested comment clarification
-# Backwards compatibility alias
+# Convenience alias: SkillsProvider is an alias for SkillsDirectoryProvider
 SkillsProvider = SkillsDirectoryProvider

Comment on lines +89 to +101
async def _read( # type: ignore[override]
self,
uri: str,
params: dict[str, Any],
task_meta: Any = None,
) -> ResourceResult:
"""Server entry point - read file directly without creating ephemeral resource.

Note: task_meta is ignored - this template doesn't support background tasks.
"""
# Call read() directly and convert to ResourceResult
result = await self.read(arguments=params)
return self.convert_result(result)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ignoring task_meta bypasses task routing contract.

The _read override explicitly ignores task_meta (as noted in the docstring), which means requests with task metadata will receive immediate results instead of task handles. This bypasses the task contract where mode="required" should enforce background execution.

If background task support is intentionally unsupported for file templates, consider setting task_config explicitly to mode="forbidden" on the template to signal this clearly, rather than silently ignoring the parameter.

🧰 Tools
🪛 Ruff (0.14.13)

91-91: Unused method argument: uri

(ARG002)


93-93: Unused method argument: task_meta

(ARG002)

Comment thread src/fastmcp/server/providers/skills/skill_provider.py
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Tests are failing on Windows due to (1) not recognizing files as text/markdown on Windows, and (2) path mocking issues with Unix-style paths being converted to Windows drive-letter paths.

Root Cause:

  1. BlobResourceContents instead of TextResourceContents (test_skills_provider.py:319): The mimetypes.guess_type() function doesn't recognize .md files as text/markdown on Windows because the MIME type mappings depend on Windows registry settings, which can vary or be corrupted by applications. When the mime type isn't detected as text/\*, the code returns bytes instead of str, resulting in BlobResourceContents instead of TextResourceContents.

  2. Path comparison failures (test_skills_provider.py:555, test_skills_vendor_providers.py:28, 36): Tests mock Path.home() to return Unix-style paths like /fake/home, but on Windows, Path() constructor automatically converts these to Windows paths with drive letters like D:/fake/home, causing assertion failures.

Suggested Solution:

Fix #1: Ensure .md files are recognized as text/markdown

In src/fastmcp/server/providers/skills/skill_provider.py, add explicit MIME type registration at module init:

import mimetypes

# Ensure .md is recognized as text/markdown on all platforms
mimetypes.add_type('text/markdown', '.md')

This should be added near the top of the file, after the imports (around line 24).

Fix #2: Fix path mocking in tests

For the path comparison tests, use tmp_path fixture instead of mocking Path.home(), or adjust the mock to return a platform-appropriate path:

Option A (Preferred - use tmp_path):

def test_default_root_is_claude_skills_dir(self, tmp_path, monkeypatch):
    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    provider = ClaudeSkillsProvider()
    assert provider._roots == [tmp_path / ".claude" / "skills"]

Option B (Use platform-aware mock):

def test_default_root_is_claude_skills_dir(self, monkeypatch):
    import sys
    if sys.platform == "win32":
        fake_home = Path("C:/fake/home")
    else:
        fake_home = Path("/fake/home")
    monkeypatch.setattr(Path, "home", lambda: fake_home)
    provider = ClaudeSkillsProvider()
    assert provider._roots == [fake_home / ".claude" / "skills"]
Detailed Analysis

Failure 1: BlobResourceContents vs TextResourceContents

From the logs:

E           AssertionError: assert False
E            +  where False = isinstance(BlobResourceContents(...), TextResourceContents)

The issue is in SkillFileTemplate.read() (skill_provider.py:82-87):

mime_type, _ = mimetypes.guess_type(str(full_path))
if mime_type and mime_type.startswith("text/"):
    return full_path.read_text()
else:
    return full_path.read_bytes()  # Returns bytes -> BlobResourceContents

On Windows, mimetypes.guess_type("reference.md") returns (None, None) instead of ('text/markdown', None) due to missing registry entries. This is a known Python limitation (see Issue 39324).

Failure 2: Path Comparison Issues

From the logs:

E       AssertionError: assert [WindowsPath('D:/fake/home/.claude/skills')] == [WindowsPath('/fake/home/.claude/skills')]
E         At index 0 diff: WindowsPath('D:/fake/home/.claude/skills') != WindowsPath('/fake/home/.claude/skills')

The test creates Path("/fake/home") which Windows converts to D:/fake/home (adding drive letter). The assertion compares the mocked return value with the actual path stored in the provider, which has been normalized to Windows format.

Related Files
  • src/fastmcp/server/providers/skills/skill_provider.py:82-87 - Where MIME type detection happens
  • tests/server/providers/test_skills_provider.py:319, 555 - Failing assertions
  • tests/server/providers/test_skills_vendor_providers.py:28, 36 - Path comparison failures

Sources:

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Tests are failing on Windows due to (1) mimetypes.guess_type() not recognizing .md files as text/markdown on Windows, and (2) path mocking issues with Unix-style paths being converted to Windows drive-letter paths.

Root Cause:

  1. BlobResourceContents instead of TextResourceContents (test_skills_provider.py:319): The mimetypes.guess_type() function does not recognize .md files as text/markdown on Windows because the MIME type mappings depend on Windows registry settings, which can vary or be corrupted by applications. When the mime type is not detected as text/*, the code returns bytes instead of str, resulting in BlobResourceContents instead of TextResourceContents.

  2. Path comparison failures (test_skills_provider.py:555, test_skills_vendor_providers.py:28, 36): Tests mock Path.home() to return Unix-style paths like /fake/home, but on Windows, Path() constructor automatically converts these to Windows paths with drive letters like D:/fake/home, causing assertion failures.

Suggested Solution:

Fix 1: Ensure .md files are recognized as text/markdown

In src/fastmcp/server/providers/skills/skill_provider.py, add explicit MIME type registration at module init after line 24:

import mimetypes

# Ensure .md is recognized as text/markdown on all platforms
mimetypes.add_type('text/markdown', '.md')

Fix 2: Fix path mocking in tests

For the path comparison tests, use tmp_path fixture instead of mocking Path.home():

def test_default_root_is_claude_skills_dir(self, tmp_path, monkeypatch):
    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    provider = ClaudeSkillsProvider()
    assert provider._roots == [tmp_path / ".claude" / "skills"]
Detailed Analysis

Failure 1: BlobResourceContents vs TextResourceContents

From the logs, the test expects TextResourceContents but receives BlobResourceContents.

The issue is in SkillFileTemplate.read() (skill_provider.py:82-87):

mime_type, _ = mimetypes.guess_type(str(full_path))
if mime_type and mime_type.startswith("text/"):
    return full_path.read_text()
else:
    return full_path.read_bytes()  # Returns bytes which becomes BlobResourceContents

On Windows, mimetypes.guess_type("reference.md") returns (None, None) instead of ('text/markdown', None) due to missing registry entries. This is a known Python limitation.

Failure 2: Path Comparison Issues

From the logs:

E       AssertionError: assert [WindowsPath('D:/fake/home/.claude/skills')] == [WindowsPath('/fake/home/.claude/skills')]

The test creates Path("/fake/home") which Windows converts to D:/fake/home (adding drive letter). The assertion compares the mocked return value with the actual path stored in the provider, which has been normalized to Windows format.

Related Files
  • src/fastmcp/server/providers/skills/skill_provider.py:82-87 - Where MIME type detection happens
  • tests/server/providers/test_skills_provider.py:319, 555 - Failing assertions
  • tests/server/providers/test_skills_vendor_providers.py:28, 36 - Path comparison failures

Sources:

Two-layer architecture for skill discovery:
- SkillProvider: handles a single skill folder
- SkillsDirectoryProvider: scans directory, creates SkillProvider per folder
- ClaudeSkillsProvider: convenience subclass for ~/.claude/skills/

Skills expose main file + manifest as resources, with supporting files
accessible via ResourceTemplate (default) or as individual Resources
(configurable via supporting_files parameter).
- SkillsDirectoryProvider now accepts roots: Path | list[Path]
- Add vendor providers: Cursor, VS Code, Codex, Gemini, Goose, Copilot, OpenCode
- Add documentation at docs/servers/providers/skills.mdx
- Update examples to showcase vendor providers and multi-directory usage
- Use as_posix() for cross-platform path consistency in manifests
- Sort rglob() results for deterministic ordering
- Catch PermissionError and OSError in addition to FileNotFoundError
- Fix documentation examples to use Path.home() instead of ~ strings
- Fix examples to use roots= parameter instead of root=
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Tests are failing on Windows due to two platform-specific issues: path resolution differences and MIME type detection for text files.

Root Cause:

  1. Path Resolution Issue: On Windows, returns absolute paths with drive letters (e.g., ), but the tests expect Unix-style paths (). When the vendor provider code calls and then , Windows converts the path to include the drive letter, causing path comparison failures in tests.

  2. MIME Type Detection Issue: On Windows, files are being detected as instead of , resulting in being returned instead of . The code in skill_provider.py:148 checks if mime_type.startswith("text/") to decide between text and binary content.

Suggested Solution:

  1. Fix path resolution tests in tests/server/providers/test_skills_vendor_providers.py: Instead of comparing paths directly, resolve both expected and actual paths before comparison, or use .resolve() on the fake_home path in the test assertions.

  2. Fix MIME type detection in src/fastmcp/server/providers/skills/skill_provider.py: Add explicit MIME type handling for common text file extensions (, , etc.) before falling back to mimetypes.guess_type(), since Windows may not have proper MIME type associations.

Detailed Analysis

Failed Tests

Path resolution failures (7 tests):

  • test_skills_provider.py::TestClaudeSkillsProvider::test_default_root_is_claude_skills_dir
  • test_skills_vendor_providers.py::TestVendorProviders::test_cursor_skills_provider_path
  • test_skills_vendor_providers.py::TestVendorProviders::test_vscode_skills_provider_path
  • test_skills_vendor_providers.py::TestVendorProviders::test_gemini_skills_provider_path
  • test_skills_vendor_providers.py::TestVendorProviders::test_goose_skills_provider_path
  • test_skills_vendor_providers.py::TestVendorProviders::test_copilot_skills_provider_path
  • test_skills_vendor_providers.py::TestVendorProviders::test_opencode_skills_provider_path

Error pattern:

AssertionError: assert [WindowsPath('D:/fake/home/.claude/skills')] == [WindowsPath('/fake/home/.claude/skills')]

MIME type detection failures (3 tests):

  • test_skills_provider.py::TestSkillProvider::test_read_supporting_file_via_template
  • test_skills_provider.py::TestSkillProvider::test_read_supporting_file_via_resource_mode
  • test_skills_provider.py::TestSkillsDirectoryProvider::test_read_supporting_file_via_template

Error pattern:

AssertionError: assert isinstance(BlobResourceContents(..., mimeType='application/octet-stream', ...), TextResourceContents)

Code References

Path resolution happens in directory_provider.py:66:

self._roots = [Path(r).resolve() for r in roots]

MIME type detection in skill_provider.py:147-151:

mime_type, _ = mimetypes.guess_type(str(full_path))
if mime_type and mime_type.startswith("text/"):
    return full_path.read_text()
else:
    return full_path.read_bytes()
Related Files
  • src/fastmcp/server/providers/skills/skill_provider.py:147-151 - MIME type detection logic that needs enhancement
  • src/fastmcp/server/providers/skills/directory_provider.py:66 - Path resolution that causes test failures
  • src/fastmcp/server/providers/skills/vendor_providers.py - All vendor providers use Path.home() which behaves differently on Windows
  • tests/server/providers/test_skills_provider.py:310-320 - Tests expecting TextResourceContents
  • tests/server/providers/test_skills_vendor_providers.py:22-89 - Path comparison tests failing on Windows

@jlowin jlowin merged commit 16ffc94 into main Jan 19, 2026
11 checks passed
@jlowin jlowin deleted the skills-provider branch January 19, 2026 23:29
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Jan 30, 2026
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant