In this tutorial you will build weather-tool from scratch — a WASM tool that fetches current conditions, a 5-day forecast, and air quality data using the free Open-Meteo API (no API key required).By the end you will have a working tool your agent can call like this:
“What’s the weather in Tokyo right now?”
The complete source code for this tool is available on GitHub:
weather-tool source
Browse the full implementation — lib.rs, Cargo.toml, and weather-tool.capabilities.json.
[package]name = "weather-tool"version = "0.1.0"edition = "2021"description = "Weather information tool for IronClaw (WASM component)"[lib]crate-type = ["cdylib"][dependencies]wit-bindgen = "=0.36"serde = { version = "1", features = ["derive"] }serde_json = "1"[profile.release]opt-level = "s"lto = truestrip = truecodegen-units = 1[workspace]
crate-type = ["cdylib"] tells Cargo to produce a dynamic library — the format WASM components require. [workspace] stops Cargo from merging this crate into a parent workspace.
Every IronClaw tool is a WASM component that implements a WIT interface. The host provides HTTP, logging, and workspace capabilities; your tool exports execute, schema, and description.Replace src/lib.rs with the following skeleton:
src/lib.rs
wit_bindgen::generate!({ world: "sandboxed-tool", path: "../../wit/tool.wit", // path relative to your Cargo.toml});use serde::{Deserialize, Serialize};struct WeatherTool;impl exports::near::agent::tool::Guest for WeatherTool { fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response { match execute_inner(&req.params) { Ok(result) => exports::near::agent::tool::Response { output: Some(result), error: None, }, Err(e) => exports::near::agent::tool::Response { output: None, error: Some(e), }, } } fn schema() -> String { SCHEMA.to_string() } fn description() -> String { "Get weather information using Open-Meteo (no API key required). \ Supports three actions: 'get_current' returns current weather conditions \ for a city; 'get_forecast' returns a 5-day daily forecast; \ 'get_air_quality' returns air pollution data for given coordinates." .to_string() }}export!(WeatherTool);
execute_inner is where the real logic lives — you will fill it in next.
The wit/tool.wit file ships with IronClaw. If you are building inside the IronClaw repo (e.g. under tools-src/my-tool/), the path ../../wit/tool.wit is correct. If you are building in a standalone directory, copy wit/tool.wit from the repo root and adjust the path accordingly.
If your tool uses private credentials (API keys, OAuth tokens), you still keep the same WIT interface. Secret handling is declared in *.capabilities.json and injected by the host at runtime. Your WASM tool should not ask the model for secrets in params.
The tool will receive parameters provided by the LLM in JSON format, then execute the right logic based on those parameters and return a result also in JSON format.
Remember to match the action names and parameter structure in the JSON schema you will define later. The LLM relies on that schema to know what JSON to send, so if your Rust code expects country_code but the schema calls it country, the LLM won’t know to include it and you’ll get errors at runtime.
We will now implement the three actions: get_current, get_forecast, and get_air_quality. Each action will call the appropriate Open-Meteo API endpoint, parse the response, and return a JSON string with the relevant information.
Handling authenticated APIs
If your API needs a secret (for example a bearer token), you do not inject it in these Rust functions manually.Instead you will declare them in the capabilities file and let the host inject them at runtime.Your Rust code just calls api_get(...) with the right URL and headers, and the host adds credentials automatically for allowlisted hosts.You can still check for the presence of secrets if you want to return a custom error message when credentials are missing:
Open-Meteo needs coordinates, not city names. Add a helper that calls the free geocoding API:
src/lib.rs
#[derive(Debug, Deserialize)]struct GeoResult { latitude: f64, longitude: f64, name: String, country: String,}fn geocode(city: &str, country_code: Option<&str>) -> Result<GeoResult, String> { let mut url = format!( "https://geocoding-api.open-meteo.com/v1/search?name={}&count=1&language=en&format=json", url_encode(city) ); if let Some(cc) = country_code { if !cc.is_empty() { url.push_str(&format!("&countryCode={}", url_encode(cc))); } } near::agent::host::log( near::agent::host::LogLevel::Info, &format!("Geocoding: {city}"), ); let resp = api_get(&url)?; let data: serde_json::Value = serde_json::from_str(&resp).map_err(|e| format!("Failed to parse geocoding: {e}"))?; let results = data["results"] .as_array() .ok_or_else(|| format!("City not found: {city}"))?; if results.is_empty() { return Err(format!("City not found: {city}")); } let r = &results[0]; Ok(GeoResult { latitude: r["latitude"].as_f64().unwrap_or(0.0), longitude: r["longitude"].as_f64().unwrap_or(0.0), name: r["name"].as_str().unwrap_or(city).to_string(), country: r["country"].as_str().unwrap_or("").to_string(), })}
near::agent::host::log emits a structured log line visible in ironclaw output. The host collects all log entries and flushes them after the call completes.
The weather tool needs three hosts because get_current and get_forecast make two requests each: one to geocode the city name and one to fetch the weather data.
7. Add secrets and auth (for tools that need credentials)
This weather tool uses Open-Meteo, so it does not need a secret. If your tool calls an API that needs a token, declare that in the capabilities file so IronClaw can inject it at request time.Example capability sections (pattern used in tools-src/* on the IronClaw repo):