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 — Python

Call a tool

python
from net.mesh_rpc import TypedMeshRpc
from net_sdk import call_tool
 
rpc = TypedMeshRpc.from_mesh(node._native)
 
resp = call_tool(rpc, "web_search", {"query": "how does the capability fold work"})
print(resp)

call_tool(rpc, tool_id, request, opts=None) — the first argument is the TypedMeshRpc, not the MeshNode and not the native handle. Three different objects appear on this spine and only this one is right here; see Announce for how they relate.

Async and streaming variants take the same first argument:

python
from net_sdk import call_tool_async, call_tool_streaming
 
resp = await call_tool_async(rpc, "web_search", {"query": "…"})
 
for chunk in call_tool_streaming(rpc, "tail", {"tail": "events"}):
    handle(chunk)

Serve and call over nRPC directly

python
handle = rpc.serve("summarize", lambda req: {"summary": req["text"][:40]})
 
reply = rpc.call(provider_node_id, "summarize", {"text": "…"}, {"deadline_ms": 500})

rpc.serve(service, handler) takes a sync callable; an async def handler works transparently when registered against AsyncTypedMeshRpc. rpc.call_service(...) resolves by service name through the capability index rather than pinning a node.

A request that will not decode

A handler that cannot decode its request does not raise into your code. The caller receives a canonical typed bad-request — status NRPC_TYPED_BAD_REQUEST (0x8000) with body {"error": "invalid_request", "detail": ...}:

python
import re
from net.mesh_rpc import RpcServerError, NRPC_TYPED_BAD_REQUEST
 
try:
    rpc.call(node_id, "summarize", {"wrong": "shape"}, {"deadline_ms": 500})
except RpcServerError as e:
    m = re.search(r"status\s*=?\s*0x([0-9a-fA-F]+)", str(e))
    if m and int(m.group(1), 16) == NRPC_TYPED_BAD_REQUEST:
        ...   # the provider rejected the request shape — a caller bug

RpcServerError has no status attribute in Python. The status rides inside the message text as status=0xNNNN, and the parser for it (_parse_status_from_message) is private, so branching on a status means the regex above. Every message the binding raises starts with a stable nrpc:<kind>: prefix, which is the more robust thing to match on when you only need the kind.

Check the classes are real before branching on them at all. When the extension is built without the nRPC feature, every one of them is aliased to RpcError, which is itself aliased to Exception — so except RpcServerError silently becomes except Exception and swallows everything. See Errors.

Verify it worked

python
resp = call_tool(rpc, "web_search", {"query": "ping"})
assert resp["results"], "the provider answered, but with nothing"
print(f"invoked: {len(resp['results'])} result(s)")

Next: Watch the event stream.

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