> Canonical guide: https://developers.foxlight.ai/build/descriptors/
> Contract snapshots: Skulk 2.0.0 (b0af39c79b6b7102b2478062904f1d7cc8619975); SDK 0.4.0 (31bb090b8f64689f87514e48385e22b6ad94a6c2). Check the installed runtime when versions differ.

# Capability _descriptors_

A descriptor is the interface a caller discovers: an exact id and semantic version, a useful description, JSON Schemas, and an I/O mode. Its content revision binds discovery to invocation.

## Choose the I/O contract

| Mode               | Initial request | Caller chunks        | Provider chunks       | Final result             |
| ------------------ | --------------- | -------------------- | --------------------- | ------------------------ |
| `unary`            | `input_schema`  | —                    | —                     | `output_schema`          |
| `server_streaming` | `input_schema`  | —                    | `output_chunk_schema` | Optional `output_schema` |
| `client_streaming` | `input_schema`  | `input_chunk_schema` | —                     | Optional `output_schema` |
| `bidirectional`    | `input_schema`  | `input_chunk_schema` | `output_chunk_schema` | Optional `output_schema` |

Declare chunk schemas precisely for directions that exist; other modes forbid them. Use `additionalProperties: false` when the contract should reject unknown fields. Descriptions should explain units, formats, limits and failure conditions to both people and generative callers.

```python
from skulk.extensions import CapabilityDescriptor

ECHO_DESCRIPTOR = CapabilityDescriptor(
    id="echo", version="1.0.0", title="Echo",
    description="Return the supplied text unchanged.",
    io_mode="unary",
    input_schema={
        "type": "object",
        "properties": {"text": {"type": "string"}},
        "required": ["text"], "additionalProperties": False,
    },
    output_schema={
        "type": "object",
        "properties": {"text": {"type": "string"}},
        "required": ["text"], "additionalProperties": False,
    },
)
```

Managed children use the SDK's equivalent [`Descriptor`](https://developers.foxlight.ai/build/sdk/reference/contracts/#descriptor). Keep provider identity, transport-node identity, and descriptor identity separate.

## Discover, then pin

1. Find a candidate provider through the cluster's capability tags.
2. Describe that node using `context.describe_node(node_id)` or `GET /v1/capabilities?node_id=...`.
3. Select the exact `id@version` and retain its descriptor revision.
4. Send that revision with the call. A changed contract returns `revision_mismatch`; rediscover before deciding whether to retry.

The HTTP response maps every `id@version` to its revision in `revisions`. Do not invent a digest, assume that a name has only one version, or treat a cached descriptor as proof of current admission capacity. A revision does not grant permission.

## Evolve the interface

A breaking behavior or shape change needs a new semantic version. Any descriptor-content change also changes its revision, even when the semantic version remains unchanged. Existing callers must negotiate again. Input and output validation never fetch remote schema references.

[Explore example or live descriptors](https://developers.foxlight.ai/build/reference/capabilities/) · [Calling and streaming](https://developers.foxlight.ai/build/calling-and-streaming/) · [Normative provider contract](https://docs.foxlight.ai/skulk/next/extensions/#serving-a-capability-providers)
