MCP & Agent Skills
Agent Skills Framework
4 min read
As agents gain more capabilities, managing them becomes challenging. A skills framework organizes capabilities into discoverable, self-documenting modules.
The Problem with Flat Tool Lists
# This becomes unmanageable at scale
tools = [
"search_web", "search_files", "search_database",
"read_file", "write_file", "delete_file",
"run_python", "run_bash", "run_javascript",
"send_email", "send_slack", "send_sms",
"create_pr", "merge_pr", "review_pr",
# ... 50 more tools
]
The model sees 60+ tools and wastes tokens deciding which to use.
Skills: Grouped Capabilities
Organize tools into logical skill groups:
skills/
├── web/
│ ├── SKILL.md
│ └── tools: [search, fetch, scrape]
├── filesystem/
│ ├── SKILL.md
│ └── tools: [read, write, list, delete]
├── coding/
│ ├── SKILL.md
│ └── tools: [run_python, run_bash, lint]
└── communication/
├── SKILL.md
└── tools: [email, slack, sms]
The SKILL.md Pattern
Each skill has a manifest describing what it does:
# Filesystem Skill
## Purpose
Read, write, and manage files in the workspace.
## When to Use
- User asks to read or edit files
- Need to save output to disk
- Managing project structure
## When NOT to Use
- Reading from URLs (use web skill)
- Database operations (use database skill)
## Tools
- `read_file`: Read file contents
- `write_file`: Create or overwrite files
- `list_directory`: List files in a directory
- `delete_file`: Remove a file
## Examples
"Read the config file" → read_file("config.json")
"Save this to output.txt" → write_file("output.txt", content)
Progressive Disclosure
Don't load all skills at once. Load based on context:
class SkillManager:
def __init__(self):
self.available_skills = self.discover_skills()
self.active_skills = set()
def discover_skills(self) -> dict:
"""Find all SKILL.md files."""
skills = {}
for path in Path("skills").glob("*/SKILL.md"):
skill_name = path.parent.name
skills[skill_name] = self.parse_skill(path)
return skills
def get_relevant_skills(self, user_message: str) -> list[str]:
"""Determine which skills might be needed."""
relevant = []
# Simple keyword matching (use embeddings for production)
keywords = {
"filesystem": ["file", "read", "write", "save", "directory"],
"web": ["search", "url", "website", "fetch", "http"],
"coding": ["run", "execute", "python", "script", "code"],
"communication": ["email", "slack", "send", "message", "notify"]
}
message_lower = user_message.lower()
for skill, words in keywords.items():
if any(word in message_lower for word in words):
relevant.append(skill)
return relevant or ["filesystem"] # Default skill
def activate_skills(self, skill_names: list[str]):
"""Load only the needed skills."""
self.active_skills = set(skill_names)
return self.get_active_tools()
def get_active_tools(self) -> list[dict]:
"""Get tool definitions for active skills only."""
tools = []
for skill_name in self.active_skills:
skill = self.available_skills[skill_name]
tools.extend(skill["tools"])
return tools
Context-Aware Skill Loading
async def process_message(user_message: str):
# Step 1: Determine relevant skills
relevant = skill_manager.get_relevant_skills(user_message)
# Step 2: Activate only those skills
tools = skill_manager.activate_skills(relevant)
# Step 3: Call LLM with focused tool set
response = await llm.chat(
messages=[{"role": "user", "content": user_message}],
tools=tools # Only 5-10 tools instead of 60
)
return response
Benefits
| Approach | Tools Visible | Token Cost | Decision Quality |
|---|---|---|---|
| Flat list (60 tools) | 60 | High | Poor (overwhelmed) |
| Skills (3 active) | 15 | Low | Good (focused) |
Skill Composition
Skills can depend on other skills:
# skills/research/SKILL.md
name: research
description: Deep research on topics
depends_on:
- web # For searching
- filesystem # For saving notes
tools:
- research_topic
- summarize_sources
Nerd Note: Think of skills like VS Code extensions. Users don't install all 30,000. They pick what they need.
Next: Loading skills dynamically at runtime. :::