Actions
Each window-level action becomes an MCP tool named {winId}__{action}. Build one with win.actions().add(name, title), refine it, and finish with .on(handler).
Builder options
| Method | Effect |
|---|---|
.description(s) | Human-readable description in tools/list |
.params::<T>() | Generates inputSchema from T: JsonSchema + DeserializeOwned |
.returns::<R>() | Contributes the result sub-schema of outputSchema |
.read_only() .destructive() .idempotent() .open_world() | MCP annotation hints |
.available(bool) | Initial visibility in tools/list |
Handler forms
.on(..) accepts six shapes. Pick by two questions: does it need parameters? and does it need the host context &mut Cx?
| Form | Runs on | Use for |
|---|---|---|
on_ui(|| …) | Main thread | Pure UI logic, no args |
on_ui(|p| …) | Main thread | Pure UI logic with args |
on_ui(|cx| …) | Main thread | Touching real widgets, no args |
on_ui(|p, cx| …) | Main thread | Touching real widgets with args |
|| async { … } | tokio | Async work, no args |
|p| async { … } | tokio | Async work with args |
#[derive(Deserialize, JsonSchema)]
struct Add { text: String }
#[derive(Serialize, JsonSchema)]
struct Stats { total: usize, done: usize }
win.actions()
.add("add", "Add a todo")
.description("Append a new open item.")
.params::<Add>() // → inputSchema
.returns::<Stats>() // → outputSchema
.on(on_ui(move |p: Add, _cx: &mut Cx| {
let text = p.text.trim().to_string();
if text.is_empty() {
return Err(ActionError::field("text", "must not be empty"));
}
// …mutate real state, then let the read side mirror it…
Ok(Outcome::message("added").with(Stats { total: 1, done: 0 }))
}));Returning results
Return &str, String, or an Outcome. Outcome::message(..).with(value) attaches structured data the agent can parse instead of re-reading the whole snapshot.
Async handlers and the main thread
Important
An async handler runs on tokio and therefore receives no &mut Cx. To touch the UI after awaiting, capture a Ui<Cx> (from app.ui()) and hop back:
let ui = app.ui();
win.actions().add("report", "Build a report").on(move || {
let ui = ui.clone();
async move {
let data = fetch_from_network().await; // off-thread work
let n = ui.run(move |cx| apply(cx, data)).await; // back on the main thread
Ok::<_, ActionError>(Outcome::message(format!("{n} rows")))
}
});Changing actions at runtime
.on(..) returns an ActionHandle. Keep it to hide a tool the agent should not currently call — the tool list is recomputed and clients are notified:
let clear = win.actions().add("clear_done", "Clear completed")
.available(false) // hidden until useful
.on(on_ui(|| Ok::<_, ActionError>("cleared")));
// In sync(), as state changes:
clear.set_available(self.items.iter().any(|t| t.done));ActionHandle also has set_title, set_description and remove.
Why bother
An agent that only ever sees callable tools does not need to guess, and cannot waste a turn on an action that would fail.