Tools
In the Basics chapter, we learned how to invoke a simple tool. In this section, we’ll explore in more detail how to call tools, pass arguments, handle structured results, and validate outputs against the tool schemas provided by the MCP server.
Calling a Tool
To call a tool, use client.tools().call().
It requires the tool name and optional arguments.
use neva::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt
.with_stdio(
"cargo",
["run", "--manifest-path", "./neva-mcp-server/Cargo.toml"]));
client.connect().await?;
let args = ("name", "John");
let result = client.tools().call("hello", args).await?;
println!("{:?}", result.content);
client.disconnect().await
}
Passing Arguments
If a tool accepts a single parameter, pass a tuple containing the parameter name and its value:
let args = ("name", "John");
let result = client.tools().call("hello", args).await?;
If a tool has multiple parameters, pass them as an array, Vec, or HashMap:
let args = [
("name", "John"),
("say", "Hi"),
];
let result = client.tools().call("hello", args).await?;
If a tool is parameterless, pass the unit type ():
let result = client.tools().call("hello", ()).await?;
Structured Content
Some tools return structured JSON data (see MCP Structured Content spec).
You can access it directly through the struct_content field:
let result = client.tools().call("weather-forecast", args).await?;
println!("{:?}", result.struct_content);
Or, you can deserialize it into a typed structure using as_json():
#[derive(Debug, serde::Deserialize)]
struct Weather {
conditions: String,
temperature: f32,
humidity: f32,
}
let args = ("location", "London");
let result = client.tools().call("weather-forecast", args).await?;
let weather: Weather = result.as_json()?;
Validating Structured Results
It’s a good practice to validate structured responses against the output schema that every MCP server should provide.
When you list tools with client.tools().list(), you receive metadata for each tool, including input and output schemas.
#[json_schema(de, debug)]
struct Weather {
conditions: String,
temperature: f32,
humidity: f32,
}
// Get the list of available tools
let tools = client.tools().list(None).await?;
// Find a specific tool
let tool = tools.get("weather-forecast")
.expect("No weather-forecast tool found");
// Call the tool
let args = ("location", "London");
let result = client.tools().call(&tool.name, args).await?;
// Validate and deserialize the result
let weather: Weather = tool
.validate(&result)
.and_then(|res| res.as_json())?;
The json_schema macro automatically derives JSON schema metadata from your Rust structures, ensuring compatibility with serde.
You can configure its behavior using attributes such as:
de- derive deserialization onlyser- derive serialization onlyserde- derive both serialization and deserializationdebug- include debug metadata in the generated schema
Raw Calls
call_raw() takes fully formed
CallToolRequestParams
and answers with the raw JSON-RPC Response, an error response included. The
_meta the params carry goes out as given — a traceparent, say — except the
progress token, which is the client's, since progress notifications find their
call by it:
let params = CallToolRequestParams::new("add").with_args([("a", 1), ("b", 2)]);
let response = client.tools().call_raw(params).await?;
Tools with a UI
A tool may carry MCP Apps metadata naming an HTML document a host
renders for it. tool.ui() reads the block back, and tool.is_model_visible()
answers whether the agent may see the tool at all — a server lists app-only
tools like any other, so filtering them out is the host's job:
for tool in tools.tools.iter() {
if !tool.is_model_visible() {
continue; // the iframe may call it; the model must not see it
}
}
Learn By Example
Here you may find the full example