Skip to content

Progressive tool disclosure for agents: eager and progressive tools, the tool_search tool, BM25 and remote-embedding ranking, and the search endpoint.

Tool search keeps a large catalog out of the agent's context. Progressive tools are left out of tools/list, and agents find them on demand through a built-in tool_search tool that ranks the registry against a query. Use it when a gateway serves more tools than any single task needs, because sending hundreds of definitions in every tools/list wastes the context window.

Eager and progressive tools

Every tool has a disclosure_mode:

  • PROGRESSIVE (the default): the tool is not listed. Agents find it with tool_search, and can call it by its qualified name at any time.
  • EAGER: the tool appears in every tools/list.

Mark the handful of tools most tasks need as eager, and leave the long tail progressive. Dynamic MCP servers and OpenAPI sources set the mode for all their tools with tool_disclosure_mode:

tools:
  - namespace: weather
    name: get_forecast
    description: Get forecast, temperature and sky conditions for a city.
    disclosure_mode: EAGER
    # input_schema and rest_backend as usual
dynamic_mcp_servers:
  - namespace: github
    transport: StreamableHttp
    url: http://127.0.0.1:5678/mcp
    tool_disclosure_mode: PROGRESSIVE

What tools/list returns depends only on the tools the caller is authorized for:

The caller's permitted toolstools/list returns
All eagerThe eager tools
Some progressiveThe eager tools and tool_search
All progressiveOnly tool_search

Listing is stateless. A search does not change later tools/list results, and there is no list-changed notification to handle.

tool_search takes a query (required), an optional namespace to search within, and an optional limit. It returns the best-matching progressive tools as full MCP tool definitions, a JSON array in the result's text content, with name, description, inputSchema and outputSchema. The agent then calls a tool by the exact name it received:

curl -s http://127.0.0.1:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "mcp-session-id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"tool_search","arguments":{"query":"open a pull request","namespace":"github","limit":3}}}'

The description of tool_search itself lists the names of up to 256 progressive tools the caller may use, so a model can often pick a name without searching at all.

Tune the results

search.max_allowable_results is the most tools one search returns. A caller's limit defaults to it and is clamped to it:

search:
  max_allowable_results: 8

Without a search block, ranking uses the built-in BM25 scorer and returns up to 5 tools. BM25 needs no external service. It scores the qualified name's terms at double weight and the description's terms at single weight, and ignores the input schema so that argument names do not dominate intent matching.

Remote embeddings

For vector ranking instead of BM25, point the gateway at an OpenAI-compatible embeddings endpoint behind a normal Arion cluster:

search:
  max_allowable_results: 5
  embeddings:
    cluster: embeddings
    model_id: text-embedding-model
    path: /v1/embeddings
    timeout: 5s
    dimensions: 384
    max_batch_size: 64

cluster, model_id and dimensions are required, and every returned vector is checked against dimensions. Arion embeds each tool from its qualified name, its description, and its input-schema property names and descriptions, capped at 2000 bytes. Tools are embedded lazily, on the next list or search, in batches of up to max_batch_size. A failed batch is retried later and counted in embedding_failures_total.

Cosine ranking is used only when the query embeds successfully and every candidate has a vector. Otherwise the whole candidate set falls back to BM25, so a search never fails because the embeddings endpoint is down.

Precomputed vectors

A tool can ship its own vector instead of being embedded at runtime, which is useful when a control plane computes embeddings offline:

tools:
  - namespace: weather
    name: get_forecast
    description: Get forecast, temperature and sky conditions for a city.
    embedding: [0.123, -0.456, 0.789]

Supplied vectors must be finite and match the configured dimensions; Arion normalizes them on load.

Search over HTTP

POST /mcp/search runs the same search without an MCP session. It is useful for a UI, or for a client that selects tools before it starts an agent:

curl -s http://127.0.0.1:8080/mcp/search \
  -H 'content-type: application/json' \
  -d '{"query":"forecast for a city","limit":3}'

It answers {"tools": [...]} with full tool definitions, eager and progressive alike, filtered by the same RBAC as every other path. A malformed body gets 400, and a method other than POST gets 405.

In code mode

A code-mode gateway serves neither tool_search nor /mcp/search. Scripts call catalog.tools.search({ query, namespace?, limit? }) instead. It uses the same ranking and max_allowable_results over every permitted tool, whatever its disclosure mode, and returns compact entries to pass to catalog.tools.describe.

Verify

The repository ships a runnable demo that starts Arion, four REST backends and a gateway whose tools are progressive, ranked with BM25:

cargo build -p arion-proxy
cargo run -p arion-e2e-tests --example mcp_semantic_search

It prints the MCP endpoint, a sample tool_search result, and a curl flow you can paste that reuses the real mcp-session-id. Add -- --max-allowable-results 1 to see the clamp. To test remote embeddings against the bundled FastEmbed sidecar, run:

cargo run -p arion-e2e-tests --features fastembed-service --example fastembed_embedding_service

Limits

  • Search ranks only tools the caller is authorized to see; tools hidden by RBAC never appear.
  • There is no minimum score. A search returns up to limit tools even when the matches are weak.
  • tools/list is not paginated, so keep the eager set small.
  • tool_search is offered only when the caller has at least one permitted progressive tool.

The search field table and disclosure_mode values are in the filter reference.

Continue

Start typing to search the site.

↑ ↓ to move Enter to open Esc to close