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

This commit is contained in:
Jeremiah Lowin 2026-01-19 18:29:16 -05:00 committed by GitHub
commit 16ffc9432f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2398 additions and 0 deletions

104
examples/skills/README.md Normal file
View file

@ -0,0 +1,104 @@
# Skills Provider Example
This example demonstrates how to expose agent skills (like Claude Code skills) as MCP resources.
## Structure
```
skills/
├── README.md # This file
├── server.py # MCP server that exposes skills
├── client.py # Example client that discovers and reads skills
└── sample_skills/ # Example skills directory
├── pdf-processing/
│ ├── SKILL.md # Main skill file
│ └── reference.md # Supporting documentation
└── code-review/
└── SKILL.md # Main skill file
```
## Running the Example
1. Start the server:
```bash
uv run python examples/skills/server.py
```
2. In another terminal, run the client:
```bash
uv run python examples/skills/client.py
```
## How It Works
The skills provider system has a two-layer architecture:
- **`SkillProvider`** - Handles a single skill folder, exposing its files as resources
- **`SkillsDirectoryProvider`** - Scans a directory, creates a `SkillProvider` per folder
- **`ClaudeSkillsProvider`** - Convenience subclass for Claude Code skills (~/.claude/skills/)
For each skill, the provider exposes:
- A **Resource** for the main file (`skill://{name}/SKILL.md`)
- A **Resource** for a synthetic manifest (`skill://{name}/_manifest`)
- Supporting files via **ResourceTemplate** or **Resources** (configurable)
### Progressive Disclosure
When a client lists resources, they see skill names and descriptions (from frontmatter) without fetching the full content. This keeps the discovery cost low.
By default, supporting files are exposed via ResourceTemplate (hidden from `list_resources()`). Set `supporting_files="resources"` to make them visible:
```python
SkillsDirectoryProvider(roots=skills_dir, supporting_files="resources")
```
### The Manifest
The `_manifest` resource provides a JSON listing of all files in a skill:
```json
{
"skill": "pdf-processing",
"files": [
{"path": "SKILL.md", "size": 1234, "hash": "sha256:abc..."},
{"path": "reference.md", "size": 5678, "hash": "sha256:def..."}
]
}
```
This enables clients to download entire skills for local use.
## Usage Examples
### Single Skill
```python
from pathlib import Path
from fastmcp import FastMCP
from fastmcp.server.providers.skills import SkillProvider
mcp = FastMCP("My Skill")
mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))
mcp.run()
```
### All Skills in a Directory
```python
from fastmcp.server.providers.skills import SkillsDirectoryProvider
mcp = FastMCP("Skills")
mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills"))
mcp.run()
```
### Claude Code Skills (default location)
```python
from fastmcp import FastMCP
from fastmcp.server.providers.skills import ClaudeSkillsProvider
mcp = FastMCP("My Skills")
mcp.add_provider(ClaudeSkillsProvider()) # Uses ~/.claude/skills/
mcp.run()
```

66
examples/skills/client.py Normal file
View file

@ -0,0 +1,66 @@
"""Example: Skills Client
This example shows how to discover and download skills from a skills server.
Run this client (it starts its own server internally):
uv run python examples/skills/client.py
"""
import asyncio
import json
from pathlib import Path
from fastmcp import Client
from fastmcp.server.providers.skills import SkillsDirectoryProvider
async def main():
# Create a skills provider pointing at our sample skills
skills_dir = Path(__file__).parent / "sample_skills"
provider = SkillsDirectoryProvider(roots=skills_dir)
# Connect to a FastMCP server with this provider
from fastmcp import FastMCP
mcp = FastMCP("Skills Server")
mcp.add_provider(provider)
async with Client(mcp) as client:
print("Connected to skills server\n")
# List available resources
print("=== Available Resources ===")
resources = await client.list_resources()
for r in resources:
print(f" {r.uri}")
if r.description:
print(f" Description: {r.description}")
print()
# List resource templates
print("=== Resource Templates ===")
templates = await client.list_resource_templates()
for t in templates:
print(f" {t.uriTemplate}")
print()
# Read a skill's main file
print("=== Reading pdf-processing/SKILL.md ===")
result = await client.read_resource("skill://pdf-processing/SKILL.md")
print(result[0].text[:500] + "...\n")
# Read the manifest to see all files
print("=== Reading pdf-processing/_manifest ===")
result = await client.read_resource("skill://pdf-processing/_manifest")
manifest = json.loads(result[0].text)
print(json.dumps(manifest, indent=2))
print()
# Read a supporting file via template
print("=== Reading pdf-processing/reference.md ===")
result = await client.read_resource("skill://pdf-processing/reference.md")
print(result[0].text[:500] + "...\n")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,40 @@
---
description: Review code for quality, maintainability, and correctness
version: "1.0.0"
tags: [code, review, quality]
---
# Code Review Skill
This skill guides you through conducting thorough code reviews.
## Review Checklist
When reviewing code, consider:
### Correctness
- Does the code do what it's supposed to do?
- Are edge cases handled?
- Are there any obvious bugs?
### Maintainability
- Is the code easy to understand?
- Are variable and function names descriptive?
- Is there appropriate documentation?
### Performance
- Are there any obvious performance issues?
- Are expensive operations cached when appropriate?
- Are database queries efficient?
### Security
- Is user input validated?
- Are there any injection vulnerabilities?
- Are secrets properly managed?
## Giving Feedback
- Be specific and actionable
- Explain *why* something should change
- Suggest alternatives, don't just criticize
- Acknowledge good work too

View file

@ -0,0 +1,28 @@
---
description: Extract text from PDFs, fill forms, and merge documents
version: "1.0.0"
tags: [document, pdf, extraction]
---
# PDF Processing Skill
This skill helps you work with PDF documents.
## Capabilities
- Extract text content from PDF files
- Fill form fields in PDF documents
- Merge multiple PDFs into one
- Split PDFs into separate pages
## Usage
When working with PDFs, use the appropriate tools:
1. For text extraction: Read the PDF and parse its content
2. For forms: Identify form fields and fill them programmatically
3. For merging: Combine multiple documents in the desired order
## Additional Resources
See [reference.md](reference.md) for detailed API documentation.

View file

@ -0,0 +1,47 @@
# PDF Processing Reference
## Text Extraction
To extract text from a PDF:
```python
from pypdf import PdfReader
reader = PdfReader("document.pdf")
for page in reader.pages:
text = page.extract_text()
print(text)
```
## Form Filling
PDF forms can be filled using the form fields API:
```python
from pypdf import PdfReader, PdfWriter
reader = PdfReader("form.pdf")
writer = PdfWriter()
writer.append(reader)
writer.update_page_form_field_values(
writer.pages[0],
{"field_name": "field_value"}
)
with open("filled_form.pdf", "wb") as output:
writer.write(output)
```
## Merging PDFs
```python
from pypdf import PdfWriter
writer = PdfWriter()
writer.append("doc1.pdf")
writer.append("doc2.pdf")
with open("merged.pdf", "wb") as output:
writer.write(output)
```

49
examples/skills/server.py Normal file
View file

@ -0,0 +1,49 @@
"""Example: Skills Provider Server
This example shows how to expose agent skills as MCP resources.
Skills can be discovered, browsed, and downloaded by any MCP client.
Run this server:
uv run python examples/skills/server.py
Then use the client example to interact with it:
uv run python examples/skills/client.py
"""
from pathlib import Path
from fastmcp import FastMCP
from fastmcp.server.providers.skills import (
SkillsDirectoryProvider,
)
# Create server
mcp = FastMCP("Skills Server")
# Option 1: Load a single skill
# mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))
# Option 2: Load all skills from a custom directory
skills_dir = Path(__file__).parent / "sample_skills"
mcp.add_provider(SkillsDirectoryProvider(roots=skills_dir, reload=True))
# Option 3: Load skills from a platform's default location
# mcp.add_provider(ClaudeSkillsProvider()) # ~/.claude/skills/
# Option 4: Load from multiple directories (in precedence order)
# mcp.add_provider(SkillsDirectoryProvider(roots=[
# Path.cwd() / ".claude/skills", # Project-level first
# Path.home() / ".claude/skills", # User-level fallback
# ]))
# Other vendor providers available:
# - CursorSkillsProvider() → ~/.cursor/skills/
# - VSCodeSkillsProvider() → ~/.copilot/skills/
# - CodexSkillsProvider() → ~/.codex/skills/
# - GeminiSkillsProvider() → ~/.gemini/skills/
# - GooseSkillsProvider() → ~/.config/agents/skills/
# - CopilotSkillsProvider() → ~/.copilot/skills/
# - OpenCodeSkillsProvider() → ~/.config/opencode/skills/
if __name__ == "__main__":
mcp.run()