MESH ONLINECODENAME:
v0.34

Invoke a Capability

Discovery returns candidate providers visible to the caller. Invocation addresses one provider or a service name, applies provider-side admission, and returns a typed result or failure.

Two ways to address a call

By node id. You pin a specific provider. Use this when the provider holds a session or other state the caller already selected.

By service or tool name. The mesh selects from providers that advertise it. This allows another eligible provider to be selected after the current provider becomes unavailable.

Prefer name-addressed calls unless you have a reason to pin. The reason is usually state — a provider holding a session you already started.

The tool call and the RPC call are the same call

call_tool and its equivalents are sugar over nRPC. A tool call is a name-addressed, JSON-coded nRPC request whose service name is the tool id. When you want request/response without the tool abstraction — your own service name, your own codec — use nRPC directly. Nothing is lost by dropping down and nothing extra is gained by staying up.

Both paths carry deadlines. Both surface the same typed failures.

Deadlines are a caller-side promise about waiting, not a cancel

A deadline bounds how long you wait. When it elapses the caller does emit a cancel for that call id, so a cooperating provider can drop the in-flight handler — but "can drop" is not "did not run." When a deadline elapses you learn that no answer arrived in time. You do not learn whether the work happened.

That distinction is the whole of ambiguous execution, and it is the reason a retry is not free: retrying a call whose deadline elapsed may run the work a second time. Make the operation idempotent, or carry an idempotency key, or accept the duplicate. This is not a Net peculiarity; it is what a network deadline means everywhere, stated here because the typed API makes it easy to forget.

Provider admission happens at invocation

Seeing a capability does not grant the right to invoke it. Discovery may itself be scoped, but provider admission is still evaluated for the call.

The provider enforces scope at call time, against the authenticated caller origin. An owner-only capability refuses a caller outside its scope regardless of who can see it in the fold. A grant that has expired or been revoked stops working at the next call, not at the next announcement — there is no cached permission to go stale.

What a caller gets back is a typed denial, not a silence and not a generic error: a distinct failure that says the authority check refused, so your code can tell "you may not" apart from "nobody answered." The four bindings name it differently; Errors has each one and demonstrates the refusal.

For wrapped MCP tools this is the owner-scope and consent model in Wrap an MCP Server and Expose Net as MCP. Deadlines, cancellation and streaming in depth are in Typed RPC with nRPC.

Invoke it — Rust

Call a tool

rust
#[derive(serde::Serialize)]
struct WebSearchReq { query: String }
#[derive(serde::Deserialize, Debug)]
struct WebSearchResp { results: Vec<String> }
 
let resp: WebSearchResp = agent
    .call_tool("web_search", &WebSearchReq {
        query: "how does the capability fold work".into(),
    })
    .await?;
println!("{resp:?}");

call_tool is a method on the Mesh node. It finds a provider for the named tool, sends the typed request, and deserializes the reply. Request and response are your own types; the wire is JSON over the encrypted transport.

Serve and call over nRPC directly

rust
use net_sdk::mesh_rpc::{CallOptionsTyped, Codec};
use std::time::{Duration, Instant};
 
// Provider: three arguments — service name, codec, handler.
let _h = provider.serve_rpc_typed::<SummarizeReq, SummarizeResp, _, _>(
    "summarize",
    Codec::default(),
    |req: SummarizeReq| async move {
        Ok(SummarizeResp { summary: summarize(&req.text) })
    },
)?;
 
// Caller: typed call with a deadline.
let mut opts = CallOptionsTyped::default();
opts.raw.deadline = Some(Instant::now() + Duration::from_millis(500));
 
let resp: SummarizeResp = caller
    .call_typed(provider_node_id, "summarize", &SummarizeReq { text: "…".into() }, opts)
    .await?;

Two shapes worth reading twice. serve_rpc_typed takes a Codec between the service name and the handler — it is not a two-argument call. And the deadline is an Instant, not a Duration: it lives on CallOptionsTyped::raw.deadline and is an absolute point in time, so it is Instant::now() + d rather than d. There is no with_deadline builder on CallOptions.

_h matters as much here as on the announce page — the handle deregisters on drop, so binding it to _ unserves the handler immediately.

Let the mesh pick the provider

rust
let resp: SummarizeResp = caller
    .call_service_typed("summarize", &req, opts)
    .await?;

call_service_typed routes by service name through the capability index, which is what makes failover to a standby possible. CallOptions::routing_policy chooses between round-robin and lowest-latency selection.

Verify it worked

rust
let resp: WebSearchResp = agent.call_tool("web_search", &req).await?;
assert!(!resp.results.is_empty(), "the provider answered, but with nothing");
println!("invoked: {} result(s)", resp.results.len());

A successful typed invocation is the strongest claim on this page: it proves the handshake, the announcement, the fold, the route and the codec all worked.

The end-to-end version is sdk/examples/tool_calling.rs, which runs in CI.

Next: Watch the event stream.

§ parity · nRPC — typed request/response + streamingfrom the capability record
Rust supportedNode / TS supportedPython supportedGo supportedC supported