Capability Discovery¶
Liveliness tells you a peer is up. .istos/capabilities tells you what it
exposes — which @handle / @stream / @channel / @publish / @subscribe
keys, plus JSON Schema from type hints where they exist. On by default.
from istos import Istos
app = Istos() # enable_discovery=True
# app = Istos(enable_discovery=False) # turn it off
In-process:
Over the fabric, one service at a time:
Or the whole fleet:
fleet = await app.discover_capabilities()
# {"clients": {...}, "cdc": {...}}
for service, manifest in fleet.items():
for tool in manifest["capabilities"]:
print(service, tool["prefix"], tool.get("params_schema"))
That is the tool catalogue an agent needs: teach the fleet a new key and the agent can find it with nothing but a schema.
Two keys, and why¶
| Key | Answers |
|---|---|
.istos/capabilities/<service> |
that service |
.istos/capabilities |
one node, whichever Zenoh picked |
The bare key is the same on every node, and @handle declares its queryable
complete=True, meaning one responder can answer the whole key. Zenoh takes that
at its word and asks exactly one node; the others are never reached. A wildcard
does not help, because the key is byte-identical everywhere and still resolves to
that one key expression. It is kept for callers written against it, and it is fine
for a single node — but it cannot describe a fleet.
Per-service keys are distinct, which is the only thing Zenoh fans out over: the
same reason */health reaches both a/health and b/health.
Name your services
The key is the service name, so two services sharing one name share a key and
only one of them answers. Istos(service_name="clients"), not the default.
Replicas of a single service are meant to share a key: the manifest
describes the service, not the process.
Fan-out needs consolidate_replies=False
Zenoh consolidates replies by default and drops some even when they arrive on
different keys. discover_capabilities() handles this, but a hand-rolled
wildcard needs it too:
Shape¶
Each entry has prefix, kind, optional description (docstring), and
params_schema / return_schema when types exist (same schemas AsyncAPI uses).
kind |
Source |
|---|---|
handle |
@handle |
stream |
@stream |
channel |
@channel — may also include websocket (path) when ws= is set |
publish |
@publish |
subscribe |
@subscribe |
.istos/* built-ins are hidden. Client-only decorators (@query,
@stream_client, @channel_client), liveliness callbacks, and persist()
roles are not listed.
Non-Python peers can serve the same key — see Wire Protocol.
The endpoint uses the app-wide authorizer, same as health/metrics. Lock it
down if you don't want strangers enumerating your tools.
MCP is a different surface
enable_mcp=True exposes @handle endpoints as MCP tools over HTTP.
Capability discovery lists streams and channels too; MCP does not — see
HTTP Gateway.
See also: Liveliness, Channels, HTTP Gateway, Authorization.