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 withtool_search, and can call it by its qualified name at any time.EAGER: the tool appears in everytools/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 tools | tools/list returns |
|---|---|
| All eager | The eager tools |
| Some progressive | The eager tools and tool_search |
| All progressive | Only tool_search |
Listing is stateless. A search does not change later tools/list results, and there is no list-changed notification to handle.
Search with tool_search
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_serviceLimits
- 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
limittools even when the matches are weak. tools/listis not paginated, so keep the eager set small.tool_searchis 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.