Building Your First Agent Skill: A Practical Guide

In my previous post about MCP servers, I showed how to build an MCP (Model Context Protocol) server for exposing APIs to Claude. Today, we'll explore an alternative approach: Agent Skills - a simpler, more flexible format also created by Anthropic.
While MCP provides strong process isolation and security boundaries, Agent Skills offers a lightweight, filesystem-based approach that's perfect for rapid prototyping and personal workflows. In this tutorial, we'll build a practical weather forecast skill that demonstrates the core concepts.
What Are Agent Skills?
Agent Skills are modular capabilities packaged as directories containing:
- A
SKILL.mdfile with YAML frontmatter and instructions - Optional Python/JavaScript scripts
- Reference documentation and assets
The key difference from MCP: Skills are discovered and loaded from the filesystem, not run as separate processes. This makes them:
- Simpler to create: No SDK or protocol knowledge required
- Easier to debug: Just markdown and scripts
- Portable: Work across Claude Code, VS Code Copilot, and other Agent Skills-compatible clients
- Flexible: The AI can even write and modify skills on-the-fly
Let's build one.
Project Setup
The complete code for this tutorial is available on GitHub.
We'll create a weather forecast skill that fetches real-time weather data from the National Weather Service API (no API key required for US locations).
Directory Structure
mkdir -p ~/.claude/skills/weather-forecast
cd ~/.claude/skills/weather-forecast
The standard skill locations are:
- Personal skills:
~/.claude/skills/or~/.copilot/skills/ - Project skills:
.claude/skills/or.github/skills/in your repo
Claude Code and VS Code Copilot scan these directories automatically at startup.
Creating the SKILL.md File
Create ~/.claude/skills/weather-forecast/SKILL.md:
---
name: weather-forecast
description: Fetch real-time weather forecasts for US locations using the National Weather Service API. Use when the user asks about weather, temperature, or conditions for a specific US city or coordinates.
license: MIT
metadata:
author: Nils Friedrichs
version: "1.0.0"
capabilities:
- weather-data
- temperature-alerts
- forecast-summary
---
# Weather Forecast Skill
This skill provides access to real-time weather data from the National Weather Service (NWS) API.
## Capabilities
- **Current conditions**: Temperature, humidity, wind speed
- **7-day forecast**: Daily summaries with high/low temperatures
- **Weather alerts**: Active warnings and watches
- **Hourly forecast**: Detailed hour-by-hour predictions
## Usage
When the user asks about weather, follow this workflow:
1. **Get coordinates**:
- If the user provides a city name, use the geocoding script to get lat/lon
- If they provide coordinates directly, use those
2. **Fetch forecast**:
- Run `scripts/get_forecast.py --lat <latitude> --lon <longitude>`
- The script returns JSON with forecast data
3. **Present results**:
- Summarize the current conditions clearly
- Include the 7-day forecast if requested
- Highlight any active weather alerts
## Scripts
### `scripts/get_forecast.py`
Fetches weather data from NWS API. Requires latitude and longitude.
**Usage**:
```bash
python scripts/get_forecast.py --lat 40.7128 --lon -74.0060
```
**Output**: JSON with current conditions and forecast
### `scripts/geocode.py`
Converts city names to coordinates using Nominatim OpenStreetMap API.
**Usage**:
```bash
python scripts/geocode.py "New York, NY"
```
**Output**: JSON with latitude and longitude
## Error Handling
- **Invalid coordinates**: NWS API only covers US territories. Return helpful message if coordinates are outside coverage.
- **API errors**: If API is down, inform user and suggest trying again later.
- **Missing city**: If geocoding fails, ask user for coordinates directly.
## Examples
**User**: "What's the weather in Seattle?"
**Response**:
1. Geocode "Seattle, WA" → lat: 47.6062, lon: -122.3321
2. Fetch forecast for those coordinates
3. Present: "In Seattle, it's currently 52°F and partly cloudy. High of 58°F today with a 30% chance of rain this evening."
**User**: "Will it rain in Chicago this week?"
**Response**:
1. Geocode "Chicago, IL"
2. Fetch 7-day forecast
3. Analyze precipitation chances
4. Present: "Looking at Chicago's 7-day forecast, rain is likely on Thursday (80% chance) and Saturday (60% chance). The rest of the week looks clear."
Creating the Python Scripts
Install Dependencies
Create a virtual environment (recommended)
python -m venv ~/.claude/skills/weather-forecast/venv
source ~/.claude/skills/weather-forecast/venv/bin/activate
Install required packages
pip install requests
Geocoding Script
Create ~/.claude/skills/weather-forecast/scripts/geocode.py:
#!/usr/bin/env python3
"""
Geocode city names to coordinates using Nominatim OpenStreetMap API.
"""
import sys
import json
import requests
def geocode_city(city_name):
"""
Convert city name to coordinates.
Args:
city_name: City name (e.g., "New York, NY" or "Seattle")
Returns:
dict: {"lat": float, "lon": float, "display_name": str}
"""
url = "https://nominatim.openstreetmap.org/search"
params = {
"q": city_name,
"format": "json",
"limit": 1,
"countrycodes": "us" # Limit to US since NWS is US-only
}
headers = {
"User-Agent": "WeatherForecastSkill/1.0"
}
try:
response = requests.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
results = response.json()
if not results:
return {"error": f"City not found: {city_name}"}
location = results[0]
return {
"lat": float(location["lat"]),
"lon": float(location["lon"]),
"display_name": location["display_name"]
}
except requests.RequestException as e:
return {"error": f"Geocoding failed: {str(e)}"}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(json.dumps({"error": "Usage: geocode.py <city name>"}))
sys.exit(1)
city = " ".join(sys.argv[1:])
result = geocode_city(city)
print(json.dumps(result, indent=2))
Make it executable:
chmod +x ~/.claude/skills/weather-forecast/scripts/geocode.py
Weather Forecast Script
Create ~/.claude/skills/weather-forecast/scripts/get_forecast.py:
#!/usr/bin/env python3
"""
Fetch weather forecast from National Weather Service API.
"""
import sys
import json
import argparse
import requests
def get_forecast(latitude, longitude):
"""
Fetch weather forecast for given coordinates.
Args:
latitude: Latitude (decimal)
longitude: Longitude (decimal)
Returns:
dict: Weather data including current conditions and forecast
"""
try:
# Step 1: Get grid point data
points_url = f"https://api.weather.gov/points/{latitude},{longitude}"
headers = {"User-Agent": "WeatherForecastSkill/1.0"}
points_response = requests.get(points_url, headers=headers, timeout=10)
points_response.raise_for_status()
points_data = points_response.json()
# Step 2: Get forecast URL
forecast_url = points_data["properties"]["forecast"]
forecast_hourly_url = points_data["properties"]["forecastHourly"]
# Step 3: Fetch forecast
forecast_response = requests.get(forecast_url, headers=headers, timeout=10)
forecast_response.raise_for_status()
forecast_data = forecast_response.json()
# Step 4: Fetch hourly forecast
hourly_response = requests.get(forecast_hourly_url, headers=headers, timeout=10)
hourly_response.raise_for_status()
hourly_data = hourly_response.json()
# Step 5: Check for alerts
alerts_url = f"https://api.weather.gov/alerts/active?point={latitude},{longitude}"
alerts_response = requests.get(alerts_url, headers=headers, timeout=10)
alerts_response.raise_for_status()
alerts_data = alerts_response.json()
# Extract key information
current_period = forecast_data["properties"]["periods"][0]
result = {
"location": {
"lat": latitude,
"lon": longitude,
"city": points_data["properties"]["relativeLocation"]["properties"]["city"],
"state": points_data["properties"]["relativeLocation"]["properties"]["state"]
},
"current": {
"temperature": current_period["temperature"],
"temperatureUnit": current_period["temperatureUnit"],
"windSpeed": current_period["windSpeed"],
"windDirection": current_period["windDirection"],
"shortForecast": current_period["shortForecast"],
"detailedForecast": current_period["detailedForecast"]
},
"forecast": {
"periods": forecast_data["properties"]["periods"][:7] # 7-day forecast
},
"hourly": {
"periods": hourly_data["properties"]["periods"][:24] # Next 24 hours
},
"alerts": [
{
"event": alert["properties"]["event"],
"headline": alert["properties"]["headline"],
"severity": alert["properties"]["severity"],
"urgency": alert["properties"]["urgency"]
}
for alert in alerts_data.get("features", [])
]
}
return result
except requests.HTTPError as e:
if e.response.status_code == 404:
return {
"error": "Location not covered by National Weather Service. "
"This API only supports US territories."
}
return {"error": f"API error: {str(e)}"}
except requests.RequestException as e:
return {"error": f"Network error: {str(e)}"}
except (KeyError, IndexError) as e:
return {"error": f"Unexpected API response format: {str(e)}"}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Get weather forecast")
parser.add_argument("--lat", type=float, required=True, help="Latitude")
parser.add_argument("--lon", type=float, required=True, help="Longitude")
args = parser.parse_args()
result = get_forecast(args.lat, args.lon)
print(json.dumps(result, indent=2))
Make it executable:
chmod +x ~/.claude/skills/weather-forecast/scripts/get_forecast.py
Testing the Scripts
Before using the skill with Claude, test the scripts independently:
Test geocoding
python ~/.claude/skills/weather-forecast/scripts/geocode.py "San Francisco, CA"
Expected output:
{
"lat": 37.7879363,
"lon": -122.4075201,
"display_name": "San Francisco, California, United States"
}
Test weather forecast
python ~/.claude/skills/weather-forecast/scripts/get_forecast.py \
--lat 37.7879363 --lon -122.4075201
Expected output: JSON with current conditions, forecast, and alerts
{
"location": {
"lat": 37.7879363,
"lon": -122.4075201,
"city": "Sausalito",
"state": "CA"
},
"current": {
"temperature": 67,
"temperatureUnit": "F",
"windSpeed": "10 mph",
"windDirection": "NNE",
"shortForecast": "Sunny",
"detailedForecast": "Sunny. High near 67, with temperatures falling to around 64 in the afternoon. North northeast wind around 10 mph."
},
"forecast": {
...
}
}
Using the Skill with Claude
Option 1: Claude Code
If you're using Claude Code, the skill is automatically available:
- Restart Claude Code to pick up new skills
- Ask: "What's the weather in Portland, Oregon?"
- Claude will:
- Recognize the weather query matches the skill description
- Load
SKILL.mdinstructions - Execute
geocode.pyto get coordinates - Run
get_forecast.pywith those coordinates - Present the results in natural language
Option 2: VS Code with GitHub Copilot
Enable Agent Skills in VS Code:
- Open Settings (Cmd/Ctrl + ,)
- Search for
chat.useAgentSkills - Enable the setting
- Restart VS Code
- In Copilot Chat, ask about weather
Option 3: Claude API
You can also use skills via the Claude API by uploading them:
import anthropic
import os
# Upload the skill
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# Create skill from directory
with open("~/.claude/skills/weather-forecast/SKILL.md", "r") as f:
skill_content = f.read()
# Upload skill (requires beta headers)
skill = client.beta.skills.create(
name="weather-forecast",
content=skill_content,
# scripts are uploaded separately as resources
)
print(f"Skill uploaded: {skill.skill_id}")
Comparing to MCP
Let's compare this Agent Skill to an equivalent MCP server:
| Aspect | Agent Skill | MCP Server |
|---|---|---|
| Setup complexity | Create SKILL.md + scripts | Implement SDK, handle JSON-RPC |
| Execution model | Scripts run in client's context | Separate process with stdio transport |
| Security | Shares client's environment | Process isolation |
| Distribution | Copy directory | npm package or server binary |
| Discoverability | Filesystem scanning | Registry + config file |
| AI modification | Can write/edit skills | Cannot modify server code |
When to use Agent Skills:
- Rapid prototyping
- Personal workflows
- Trusted scripts you control
- Dynamic capability expansion
When to use MCP:
- Production deployments
- Third-party integrations
- Credential isolation needed
- Organizational policies require sandboxing
Advanced: Adding Reference Documentation
Skills support progressive disclosure - loading detailed docs only when needed:
Create ~/.claude/skills/weather-forecast/references/NWS_API.md:
# National Weather Service API Reference
## Endpoints
### Points API
`GET /points/{lat},{lon}`
Returns metadata about a location, including:
- Grid coordinates for forecast URLs
- Time zone
- Radar station
- Forecast office
### Forecast API
`GET /gridpoints/{office}/{gridX},{gridY}/forecast`
Returns 7-day forecast with:
- Day/night periods
- Temperature and wind
- Detailed text forecast
### Alerts API
`GET /alerts/active?point={lat},{lon}`
Returns active weather alerts:
- Warnings (severe conditions)
- Watches (conditions possible)
- Advisories (less urgent)
## Rate Limits
- No API key required
- Please use a descriptive User-Agent
- Recommended: 1 request per second
Now Claude can reference this document when it needs detailed API information, without loading it into every conversation.
Best Practices
1. Clear Activation Criteria
Make the description field specific about when to use the skill:
# ❌ Too vague
description: Weather information
# ✅ Clear activation trigger
description: Fetch real-time weather forecasts for US locations using the National Weather Service API. Use when the user asks about weather, temperature, or conditions for a specific US city or coordinates.
2. Handle Errors Gracefully
Your scripts should return structured errors:
# Good error handling
try:
result = api_call()
except APIError:
return {"error": "API temporarily unavailable. Please try again."}
3. Use Progressive Disclosure
Don't put everything in SKILL.md. Use references for:
- API documentation
- Domain-specific guides
- Large examples or templates
4. Keep Scripts Self-Contained
Make scripts runnable independently for testing:
if __name__ == "__main__":
# CLI interface for testing
args = parse_arguments()
result = main_function(args)
print(json.dumps(result))
5. Document Dependencies
Include a requirements.txt or note dependencies in SKILL.md:
## Setup
This skill requires:
- Python 3.8+
- `requests` library: `pip install requests`
Next Steps
Now that you've built a weather forecast skill, try:
- Add caching: Store recent forecasts to reduce API calls
- Support international locations: Integrate additional weather APIs
- Create visualizations: Generate weather charts using matplotlib
- Add notifications: Alert script that runs on a schedule
For more skill examples, check out:
Conclusion
Agent Skills provide a lightweight, flexible way to extend AI agents without the complexity of full protocol implementations. By packaging instructions and scripts in a simple directory structure, you can rapidly build specialized capabilities that work across multiple AI platforms.
The weather forecast skill we built demonstrates the core concepts: clear activation criteria, well-structured scripts, and progressive disclosure of documentation. This pattern scales from simple utilities to complex domain-specific workflows.
For production deployments with third-party code or strict security requirements, MCP's process isolation model is more appropriate. But for trusted, personal workflows and rapid iteration, Agent Skills offer an compelling alternative that puts AI-assisted development front and center.
Ready to explore both approaches? Check out my new post about comparison of Agent Skills vs MCP security architectures to understand which model fits your use case.