---
title: "Tool search"
url: "/docs/mcp-gateway/tool-search/"
kind: "feature"
summary: "Progressive tool disclosure for agents: eager and progressive tools, the tool_search tool, BM25 and remote-embedding ranking, and the search endpoint."
---

> Arion site index: [/llms.txt](/llms.txt)

# Tool search

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`:

```yaml,arion=fragment
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:

```bash
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:

```yaml,arion=fragment
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:

```yaml,arion=fragment
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:

```yaml,arion=fragment
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:

```bash
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](@/docs/mcp-gateway/code-mode.md) 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:

```bash
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:

```bash
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](@/reference/filters/http/mcp-gateway.md#search).
