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

Call a tool

typescript
import { callTool } from '@net-mesh/sdk';
 
// The tool surface hangs off the RPC handle; `node.rpc()` bridges the two.
// Hold it — each call builds a new handle with its own reference to the
// mesh, and an outstanding reference blocks `shutdown()`.
const rpc = node.rpc();
 
const resp = await callTool<{ query: string }, { results: string[] }>(
  rpc,
  'web_search',
  { query: 'how does the capability fold work' },
  { deadlineMs: 500 },
);

callTool(rpc, toolId, req, opts) — the first argument is the TypedMeshRpc, not the MeshNode. This is the same handle serveTool takes on the announce page, and the same asymmetry: listTools wants the native handle, the tool call wants the typed RPC over it.

Serve and call over nRPC directly

typescript
interface SummarizeReq  { text: string }
interface SummarizeResp { summary: string }
 
const serverRpc = server.rpc();
const handle = serverRpc.serve<SummarizeReq, SummarizeResp>(
  'summarize',
  async (req) => ({ summary: req.text.slice(0, 40) }),
);
 
const clientRpc = client.rpc();
const reply = await clientRpc.call<SummarizeReq, SummarizeResp>(
  server.nodeId(),
  'summarize',
  { text: '…' },
  { deadlineMs: 500 },
);
 
await handle.close();   // MUST close — Node finalizers are non-deterministic

call pins a node id (a bigint). callService(service, req, opts) lets the mesh resolve the service through the capability index, which is what makes failover to a standby possible.

The deadline is deadlineMs — plain milliseconds, unlike Rust's absolute Instant. opts.signal accepts an AbortSignal for caller-side cancellation.

A request that will not decode

The handler's request decode failure is not your exception to catch — it is converted into a typed bad-request status the caller sees:

typescript
import { classifyError, RpcServerError } from '@net-mesh/core/errors';
import { NRPC_TYPED_BAD_REQUEST } from '@net-mesh/core/mesh_rpc';
 
try {
  await clientRpc.call(nodeId, 'summarize', { wrong: 'shape' }, { deadlineMs: 500 });
} catch (e) {
  const typed = classifyError(e);
  if (typed instanceof RpcServerError && typed.status === NRPC_TYPED_BAD_REQUEST) {
    // the provider rejected the request shape — a bug in the caller, not a retry
  }
}

Verify it worked

typescript
const resp = await callTool<{ query: string }, { results: string[] }>(
  rpc, 'web_search', { query: 'ping' },
);
if (resp.results.length === 0) throw new Error('the provider answered, but with nothing');
console.log(`invoked: ${resp.results.length} result(s)`);

Next: Watch the event stream.

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