Announce a Capability
A capability is a typed unit of work a node can do. An announcement places its descriptor and provider properties into the distributed fold. Callers with the required discovery authority can then find it. Invocation is a separate decision.
Two things you can announce
Tags describe what a node is — gpu, region:eu-west, a model it hosts, an
accelerator it has. Nothing is callable; you are populating a placement index so
somebody can find a machine that fits.
Tools describe what a node can do — a named, schema'd operation an agent can invoke. A tool is the thing an LLM's tool-call resolves to.
Most agent code wants tools. Most placement code wants tags. They travel in the same capability set and are announced by the same call.
Serving a tool and announcing a tool are different acts
Most bindings expose these as separate operations:
- Serving registers a handler on an RPC surface, so that a call addressed to that tool name reaches your function.
- Announcing puts the tool's descriptor into your capability set, so that peers folding your announcement learn the tool exists.
Do the first without the second and you have a working tool nobody can find:
discover returns nothing, invoke fails to route, and neither error mentions
announcing. In Rust the two are fused — registering a tool inserts it into the
node's tool registry and the next announce carries it. In TypeScript, Python and
Go they are not, and each fragment below shows the explicit merge step its
binding needs.
The RPC handle
The tool API does not hang off the node. It hangs off a typed RPC surface constructed over the node — serve, call, list and watch are all operations on that surface.
Rust is the exception, and it is the reason this is worth spelling out: in Rust the mesh node is that surface, so the tool methods are node methods. Documentation written from Rust first tends to project that shape onto the other three, where it is wrong. Take the constructor from your own fragment.
Announcement mechanics
Announcements expire. The default TTL is five minutes. Re-announce before it elapses or peers garbage-collect your entry and you quietly stop being discoverable.
Re-announcing is cheap. The mesh diffs against your last announced set, so a steady-state re-announce costs tens of bytes rather than a full rebroadcast. There is no reason to hand-roll change detection.
Announcements travel multi-hop, bounded by a hop count, so a peer several hops away can fold your capability without ever having connected to you directly.
Announcing to nobody succeeds. If the node has no connected, started peers, the announce call returns cleanly and reaches no one. If discovery remains empty, confirm the node has joined a peer before changing the capability filter. See the handshake.
Discovery and invocation have separate authority
Announcing does not make a capability visible to every participant or open to invocation.
Visibility and invocability are separate decisions. Organization-scoped discovery can hide or encrypt a descriptor for callers outside its audience. The provider then makes the final admission decision when a caller invokes it. See Invoke and Errors.
The full tag and axis model — hardware, software, model, tool, resource-limit projections — is in Capabilities and Capability Schema.
Announce it — TypeScript
Tags
import { MeshNode } from '@net-mesh/sdk';
const node = await MeshNode.create({ bindAddr: '127.0.0.1:9001', psk });
await node.announceCapabilities({
tags: ['gpu', 'inference', 'region:eu-west'],
});announceCapabilities takes a CapabilitySet — tags plus optional hardware,
models, tools and limits fields. It self-indexes, so findNodes on this same
node matches its own announcement.
Serve a tool
The tool API takes a TypedMeshRpc, not the node:
import { serveTool, descriptorFrom, addToolCapabilitiesToAnnounce } from '@net-mesh/sdk';
// The tool surface — serve, call, list, watch — hangs off the RPC handle
// rather than the node. `node.rpc()` is the bridge.
//
// Hold the handle rather than calling `rpc()` per tool: each call builds a
// new one with its own reference to the mesh, and an outstanding reference
// makes `shutdown()` fail. Release it with `rpc.raw.close()` when done.
const rpc = node.rpc();
const options = {
name: 'web_search',
description: 'Search the web for relevant pages.',
tags: ['web', 'research'],
};
const handle = serveTool(rpc, options, async (req: { query: string }) => {
return { results: [`first hit for '${req.query}'`] };
});serveTool(rpc, options, handler) — first argument is the RPC surface. Passing the
node here is the most common mistake on this page, and it is a type error rather
than a runtime one, so the compiler will catch it.
Announce the tool — the step serveTool does not do
await node.announceCapabilities(
addToolCapabilitiesToAnnounce({ tags: [] }, [descriptorFrom(options)]),
);addToolCapabilitiesToAnnounce adds an ai-tool:<toolId> tag and a tools[]
entry to the capability set you are about to announce. Without it the handler is
served and invisible — listTools on a peer returns nothing, and a callTool
against the name fails to route.
handle.close() when you are done. Node's finalizers are non-deterministic, so an
unclosed handle keeps serving for an unspecified length of time.
Verify it worked
From a peer that folded the announcement:
import { listTools } from '@net-mesh/sdk';
const deadline = Date.now() + 3000;
while (Date.now() < deadline && listTools(agent).length === 0) {
await new Promise((r) => setTimeout(r, 20));
}
if (listTools(agent).length === 0) {
throw new Error('the announcement did not fold');
}Folding is asynchronous, so the loop is the point — an immediate listTools
after a peer announces will usually be empty, and that is not a failure.
Two surfaces, and the difference is not cosmetic: announceCapabilities and
listTools work on the MeshNode, while serveTool takes the
TypedMeshRpc that node.rpc() returns. Reach for the wrong one and
TypeScript will tell you, which is the good case — the same mistake in Python
surfaces as an AttributeError at call time.
Next: Discover a capability.