Skip to main content

Batch Requests

Neva supports JSON-RPC 2.0 batch requests — a way to send multiple requests to the server in a single round trip and receive all responses at once. This is useful when you need to fetch several independent pieces of information (tools list, resources, prompt results, etc.) and want to minimize latency.

Building a Batch​

Use client.batch() to obtain a BatchBuilder, chain the desired requests, then call .send():

use neva::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt.with_default_http());

client.connect().await?;

let responses = client
.batch()
.list_tools()
.list_resources()
.call_tool("add", [("a", 40_i32), ("b", 2_i32)])
.send()
.await?;

println!("{responses:?}");

client.disconnect().await
}

send() returns Vec<Response> in the same order as the requests were added.

Available Batch Methods​

MethodEquivalent single call
.list_tools()client.tools().list(None)
.call_tool(name, args)client.tools().call(name, args)
.list_resources()client.resources().list(None)
.read_resource(uri)client.resources().read(uri)
.list_resource_templates()client.resources().templates(None)
.list_prompts()client.prompts().list(None)
.get_prompt(name, args)client.prompts().get(name, args)
.notify(method, params)fire-and-forget notification
ping is gone

MCP 2026-07-28 removed the ping method, so BatchBuilder::ping and Client::ping no longer exist. Use a custom handler under your own method name if you need a liveness probe. They come back under legacy-spec.

Batches and Multi Round-Trip Requests​

A batched request that elicits is driven through the MRTR retry loop in lock-step rounds: one transport write per round, and slots come back in input order regardless of how many rounds each one needed. Cap re-issues per slot with McpOptions::with_max_mrtr_rounds.

Routing headers on a batch are rejected

No single method or name describes a batch, so Mcp-Method / Mcp-Name / Mcp-Param-{name} must not be sent with one — a batch carrying them is rejected outright. Neva's client omits them for you; the constraint matters if you sit behind a proxy that injects headers.

The mandatory _meta keys still apply, and requests inside a batch are checked one by one.

Processing Responses​

Each element in the returned Vec<Response> corresponds to a request in order. Use into_result::<T>() to deserialize a response into the expected type:

let responses = client
.batch()
.list_tools()
.call_tool("add", [("a", 40_i32), ("b", 2_i32)])
.send()
.await?;

let tools = responses[0].clone().into_result::<ListToolsResult>()?;
let add = responses[1].clone().into_result::<CallToolResponse>()?;

println!("Tools: {:?}", tools.tools);
println!("add(40, 2) = {:?}", add.content);

Pattern Destructuring​

For a fixed-size batch you can destructure the slice directly:

let responses = client
.batch()
.list_tools()
.list_resources()
.list_prompts()
.call_tool("add", [("a", 40_i32), ("b", 2_i32)])
.read_resource("notes://daily")
.get_prompt("greeting", [("name", "Neva")])
.send()
.await?;

let [tools, resources, prompts, add_result, daily, greeting] =
responses.as_slice() else {
return Err(Error::new(ErrorCode::InternalError, "unexpected number of responses"));
};

let tools = tools.clone().into_result::<ListToolsResult>()?;
let add = add_result.clone().into_result::<CallToolResponse>()?;
let daily = daily.clone().into_result::<ReadResourceResult>()?;
let greeting = greeting.clone().into_result::<GetPromptResult>()?;

Notifications in a Batch​

Notifications are fire-and-forget — they are included in the wire payload but do not produce a response slot in the returned Vec:

use serde_json::json;

let responses = client
.batch()
.notify("notifications/message", Some(json!({ "level": "info", "data": "hello" })))
.list_tools()
.send()
.await?;

// responses has 1 element (only list_tools produced a response)
let tools = responses[0].clone().into_result::<ListToolsResult>()?;

Hand-Built Batches​

client.call_batch(items) sends a Vec<MessageEnvelope> you assembled yourself — BatchBuilder is built on it. The client numbers every request it sends, these too: on the wire each one carries an id the client generated, and each response comes back carrying the id you gave it. An id of your own could repeat one still owed an answer — a request given up on, another batch in flight — and that answer would then reach the wrong waiter.

Like any request, a batch the caller stops waiting for is cancelled, every request in it.

Server Side​

No additional server configuration is required to support batch requests. Any Neva server — on both stdio and HTTP transports — handles JSON-RPC 2.0 batches automatically. A standard setup is sufficient:

use neva::prelude::*;

#[tokio::main]
async fn main() {
App::new()
.with_options(|opt| opt.with_default_http())
.run()
.await;
}

Learn By Example​

Here you may find the full example.