Custom Tool
Build and deploy a custom API integration tool end-to-end, from creation with supypowers to use in an agent.
Building a Custom Tool
This walkthrough takes you through building a custom tool from scratch: creating the script with supypowers, implementing the logic, testing it, and using it in an agent. We will build a weather lookup tool that calls an external API.
Step 1: Create the Tool Scaffold
Use supyagent tools new to create a tool from template:
supyagent tools new weatherThis creates powers/weather.py with a Pydantic-based scaffold:
# /// script
# dependencies = ["pydantic"]
# ///
"""
weather - Custom supypowers tool.
"""
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
"""Weather input parameters."""
value: str = Field(..., description="Input value")
class WeatherOutput(BaseModel):
"""Weather output."""
ok: bool
data: str | None = None
error: str | None = None
def weather(input: WeatherInput) -> WeatherOutput:
"""
Describe what this tool does.
Examples:
>>> weather({"value": "test"})
"""
try:
return WeatherOutput(ok=True, data=f"Processed: {input.value}")
except Exception as e:
return WeatherOutput(ok=False, error=str(e))Step 2: Implement the Tool
Replace the scaffold with a real implementation. We will add httpx as a dependency and call the Open-Meteo API (free, no API key required):
# /// script
# dependencies = ["pydantic", "httpx"]
# ///
"""
weather - Get current weather for a location using Open-Meteo API.
"""
import httpx
from pydantic import BaseModel, Field
class GetWeatherInput(BaseModel):
"""Input for get_weather function."""
latitude: float = Field(..., description="Latitude of the location")
longitude: float = Field(..., description="Longitude of the location")
city: str = Field(default="", description="City name (for display purposes)")
class GetWeatherOutput(BaseModel):
"""Weather data output."""
ok: bool
data: dict | None = None
error: str | None = None
def get_weather(input: GetWeatherInput) -> GetWeatherOutput:
"""
Get current weather conditions for a latitude/longitude.
Returns temperature, wind speed, and weather description.
Examples:
>>> get_weather({"latitude": 40.7128, "longitude": -74.0060, "city": "New York"})
"""
try:
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": input.latitude,
"longitude": input.longitude,
"current_weather": True,
"temperature_unit": "celsius",
}
response = httpx.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
current = data.get("current_weather", {})
weather_codes = {
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy",
3: "Overcast", 45: "Foggy", 48: "Depositing rime fog",
51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
80: "Slight rain showers", 81: "Moderate rain showers",
82: "Violent rain showers", 95: "Thunderstorm",
}
result = {
"city": input.city or f"{input.latitude}, {input.longitude}",
"temperature_celsius": current.get("temperature"),
"wind_speed_kmh": current.get("windspeed"),
"wind_direction": current.get("winddirection"),
"description": weather_codes.get(
current.get("weathercode", -1), "Unknown"
),
}
return GetWeatherOutput(ok=True, data=result)
except httpx.HTTPStatusError as e:
return GetWeatherOutput(ok=False, error=f"API error: {e.response.status_code}")
except httpx.RequestError as e:
return GetWeatherOutput(ok=False, error=f"Request failed: {e}")
except Exception as e:
return GetWeatherOutput(ok=False, error=str(e))
class GeolocateInput(BaseModel):
"""Input for geolocate function."""
city: str = Field(..., description="City name to look up")
class GeolocateOutput(BaseModel):
"""Geolocation data output."""
ok: bool
data: dict | None = None
error: str | None = None
def geolocate(input: GeolocateInput) -> GeolocateOutput:
"""
Look up latitude/longitude for a city name using Open-Meteo geocoding.
Examples:
>>> geolocate({"city": "Tokyo"})
"""
try:
url = "https://geocoding-api.open-meteo.com/v1/search"
params = {"name": input.city, "count": 1}
response = httpx.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = data.get("results", [])
if not results:
return GeolocateOutput(ok=False, error=f"City not found: {input.city}")
location = results[0]
return GeolocateOutput(ok=True, data={
"city": location.get("name"),
"country": location.get("country"),
"latitude": location.get("latitude"),
"longitude": location.get("longitude"),
})
except Exception as e:
return GeolocateOutput(ok=False, error=str(e))Key Design Points
- Two functions in one script:
get_weatherandgeolocatebecome two tools (weather__get_weatherandweather__geolocate) - Dependencies declared:
httpxis in the# /// scriptblock --uvinstalls it automatically - Pydantic models: Input and output are typed with Field descriptions for clear tool schemas
- Error handling: Errors are returned in the
ok/errorpattern, never raised - No
print(): Output is pure JSON
Step 3: Test the Tool
Using supypowers Directly
# Test geolocation
supypowers run weather:geolocate '{"city": "Tokyo"}'Output:
{
"ok": true,
"data": {
"city": "Tokyo",
"country": "Japan",
"latitude": 35.6895,
"longitude": 139.6917
}
}# Test weather lookup
supypowers run weather:get_weather '{"latitude": 35.6895, "longitude": 139.6917, "city": "Tokyo"}'Using supyagent tools test
supyagent tools test weather__geolocate '{"city": "London"}'
supyagent tools test weather__get_weather '{"latitude": 51.5074, "longitude": -0.1278, "city": "London"}'Using supypowers test
supypowers test weather:geolocate
supypowers test weather:get_weather --fixture test_weather_input.jsonStep 4: Verify Tool Discovery
Check that your new tools are discovered:
supyagent tools listYou should see:
Name Description Source
weather__get_weather Get current weather conditions... weather.py
weather__geolocate Look up latitude/longitude for a city... weather.pyIf the tools do not appear, try reloading in chat with /reload.
Step 5: Use in an Agent
The tools are automatically available to any agent with matching tool permissions:
supyagent chat myagentYou: What's the weather like in Paris right now?
myagent> Let me look that up for you.
[tool: weather__geolocate] city="Paris" ...
[tool: weather__get_weather] latitude=48.8566, longitude=2.3522 ...
The current weather in Paris:
- Temperature: 12°C
- Conditions: Partly cloudy
- Wind: 15 km/h from the westRestricting Tool Access
If you want only specific agents to use the weather tool:
tools:
allow:
- "weather__*" # Only weather tools
- "web__*" # Plus web toolsStep 6: Add Secrets (If Needed)
If your API requires authentication, use environment variables:
import os
def call_paid_api(input: ApiInput) -> ApiOutput:
api_key = os.environ.get("WEATHER_API_KEY")
if not api_key:
return ApiOutput(ok=False, error="WEATHER_API_KEY not set. Use /creds set WEATHER_API_KEY")
# Use api_key in your requestStore the key:
supyagent config set WEATHER_API_KEYOr declare it in the agent YAML:
credentials:
- name: WEATHER_API_KEY
description: "API key for premium weather service"
required: trueAdvanced: Tools with External Dependencies
For tools that need system packages or complex dependencies:
# /// script
# dependencies = ["pydantic", "httpx", "beautifulsoup4", "lxml"]
# ///All dependencies are resolved by uv at execution time. No manual installation needed.
Advanced: Multi-File Tool Organization
For complex tools, you can organize into subdirectories within powers/:
powers/
weather.py # Simple weather tool
github/
issues.py # GitHub issue management
repos.py # Repository operations
slack/
messages.py # Message sending
channels.py # Channel managementUse supypowers docs --recursive to discover tools in subdirectories.
Related
- Building Tools -- Full guide to the tool system
- supypowers CLI -- Tool execution and testing commands
- Code Assistant -- Agent with
will_create_toolsenabled - Security -- Credential management for tool secrets