MESH ONLINECODENAME:
v0.34

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 isgpu, 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:

  1. Serving registers a handler on an RPC surface, so that a call addressed to that tool name reaches your function.
  2. 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 — Go

Tags

go
node, err := net.NewMeshNode(net.MeshConfig{BindAddr: "127.0.0.1:9001", PskHex: psk})
if err != nil {
    log.Fatal(err)
}
defer node.Shutdown()
 
err = node.AnnounceCapabilities(net.CapabilitySet{
    Tags: []string{"gpu", "inference", "region:eu-west"},
})
if err != nil {
    log.Fatal(err)
}

CapabilitySet carries Tags plus Hardware, Software, Models, Tools and Limits. AnnounceCapabilities returns an error like every call that crosses the cgo boundary.

Serve a tool

The tool API takes a *TypedMeshRpc, and getting one is two constructors:

go
raw, err := net.NewMeshRpc(node)          // *MeshRpc
if err != nil {
    log.Fatal(err)
}
rpc := net.NewTypedMeshRpc(raw)           // *TypedMeshRpc — what the tool API wants
 
type WebSearchReq struct {
    Query string `json:"query"`
}
type WebSearchResp struct {
    Results []string `json:"results"`
}
 
desc, err := net.DescriptorFor(net.ToolOptions{
    Name:        "web_search",
    Description: "Search the web for relevant pages.",
    Tags:        []string{"web", "research"},
})
if err != nil {
    log.Fatal(err)
}
 
handle, err := net.RegisterTool[WebSearchReq, WebSearchResp](
    rpc,
    desc,
    func(req WebSearchReq) (WebSearchResp, error) {
        return WebSearchResp{Results: []string{"first hit for '" + req.Query + "'"}}, nil
    },
)
if err != nil {
    log.Fatal(err)
}
defer handle.Close()

NewMeshRpc returns *MeshRpc; every generic tool function (RegisterTool, CallTool, WatchTools) wants *TypedMeshRpc. Passing the raw one is a compile error, and it is the most common way this page goes wrong.

Announcing the tool — and where Go currently stops

AddToolCapabilitiesToAnnounce builds the merged announcement:

go
wire := net.AddToolCapabilitiesToAnnounce(
    net.CapabilitySetWire{}, []net.ToolDescriptor{desc},
)

Then there is a seam. wire is a CapabilitySetWireTags plus a flat Metadata map[string]string holding the tool::<id>::input_schema keys peers hydrate schemas from. AnnounceCapabilities takes a CapabilitySet, which has no Metadata field, and nothing else in the binding consumes a CapabilitySetWire.

So what you can do today from Go is announce the discovery tags:

go
if err := node.AnnounceCapabilities(net.CapabilitySet{Tags: wire.Tags}); err != nil {
    log.Fatal(err)
}

That carries ai-tool:web_search, so peers can find the tool by tag and route a call to it. What it does not carry is the schema metadata, so a peer's ListTools() sees the tool without its input and output schemas — enough to invoke if the caller already knows the shape, not enough to hand an LLM a tool definition.

This page states that rather than showing a call that does not compile. The Go binding has no live-mesh RegisterTool + CallTool test yet; the descriptor and merge helpers are unit-tested, the round trip is not.

Verify it worked

From a peer that folded the announcement:

go
peers, err := agent.FindNodes(net.CapabilityFilter{
    RequireTags: []string{"ai-tool:web_search"},
})
if err != nil {
    log.Fatal(err)
}
if len(peers) == 0 {
    log.Fatal("the announcement did not fold")
}

Checking the tag rather than ListTools() is deliberate: the tag is what Go actually announced, so it is what Go can actually verify.

Next: Discover a capability.

§ parity · Capability announcefrom the capability record
Rust supportedNode / TS supportedPython supportedGo supportedC supported