---
title: "Tool discovery over xDS (TDS)"
url: "/docs/mcp-gateway/tool-discovery-tds/"
kind: "feature"
summary: "Deliver MCP tools, OpenAPI sources, dynamic servers and code-mode toolkits from a control plane: scopes, resource types, and update and remove rules."
---

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

# Tool discovery over xDS (TDS)

Deliver MCP tools, OpenAPI sources, dynamic servers and code-mode toolkits from a control plane: scopes, resource types, and update and remove rules.

TDS, the Tool Discovery Service, lets a control plane add, update and remove MCP tools, OpenAPI tool sources, dynamic MCP servers and code-mode toolkits over xDS. The listener, routes, TLS and the rest of the filter chain stay untouched. Use it for long-lived gateways whose tool catalogs change far more often than their network topology.

## Enable it

Bind the gateway to a TDS stream by setting `tds.config_name` in the filter's `typed_config`. The proxy must be running with an [xDS control plane](@/docs/operate/xds-control-planes.md):

```yaml,arion=fragment
server_info:
  name: tenant-a-mcp
  version: "1.0.0"
tools_mode: {}
tds:
  config_name: main
```

Static `tools`, `open_api_tool_sources` and `dynamic_mcp_servers` can coexist with TDS; delivered resources join them in the same registry.

## Scopes and resource IDs

Each TDS-enabled gateway consumes resources for one scope, formed as `<server_info.name>/<tds.config_name>`, here `tenant-a-mcp/main`. Neither part may contain `/`. A resource ID appends the resource name, which is the resource's identity:

| Resource type | Resource name | Example ID |
|---|---|---|
| `Tool` | The qualified name, `namespace.name` | `tenant-a-mcp/main/weather.get_forecast` |
| `OpenApiToolSource` | The source's namespace | `tenant-a-mcp/main/petstore` |
| `DynamicMcpServer` | The server's namespace | `tenant-a-mcp/main/github` |
| `Toolkit` | The toolkit name (code mode only) | `tenant-a-mcp/main/resolve_support_request` |

TDS uses incremental (delta) xDS with four extension resource types:

```text
type.googleapis.com/arion.extensions.filters.http.mcp.mcp_gateway.v3.Tool
type.googleapis.com/arion.extensions.filters.http.mcp.mcp_gateway.v3.OpenApiToolSource
type.googleapis.com/arion.extensions.filters.http.mcp.mcp_gateway.v3.DynamicMcpServer
type.googleapis.com/arion.extensions.filters.http.mcp.mcp_gateway.v3.Toolkit
```

The resource name always wins. If a payload's own name or namespace differs from it, Arion uses the resource name, and logs a warning for tools and sources, so a later remove targets the same key.

## Update and remove semantics

- **Tool update.** It passes the same validation as static config, then adds or replaces the tool. It can replace a static tool with the same qualified name. It is rejected if an OpenAPI source or a dynamic server owns the namespace.
- **Tool remove.** It deletes the tool, and a static tool it replaced does not come back. Tools derived from an OpenAPI source or discovered from a dynamic server cannot be removed this way.
- **OpenApiToolSource update.** The document is expanded when the resource arrives. The namespace's tools are replaced with the new set, and an invalid document is NACKed with the previous tools still serving.
- **OpenApiToolSource remove.** It evicts every tool derived from the source.
- **DynamicMcpServer update.** It adds or replaces the server and fetches its `tools/list` at once, materializing the tools under the server's namespace. If that fetch fails, the update is NACKed.
- **DynamicMcpServer remove.** It evicts the server and everything discovered from it.
- **Toolkit update and remove.** These add, replace or delete a [code mode](@/docs/mcp-gateway/code-mode.md) toolkit. Gateways in tools mode ignore toolkits and ACK them, so one control plane can serve both kinds of listener.
- **Rejection.** Malformed resource IDs and invalid payloads are NACKed.
- **Early publishing.** Updates for scopes no gateway consumes are ACKed as no-ops, so a control plane can publish resources before the matching listener exists.
- **Embeddings.** If remote [tool search](@/docs/mcp-gateway/tool-search.md) embeddings are configured, a new tool without a supplied vector is embedded on the next search. An embedding failure never rejects an update.

## Scope isolation

Two listeners may share a `config_name` as long as their server names differ. `tenant-a-mcp/main/crm.lookup` updates tenant A only, and tenant B never sees it:

```yaml,arion=fragment
# listener A's gateway
server_info:
  name: tenant-a-mcp
  version: "1.0.0"
tools_mode: {}
tds:
  config_name: main

# listener B's gateway
server_info:
  name: tenant-b-mcp
  version: "1.0.0"
code_mode: {}
tds:
  config_name: main
```

To keep scopes unambiguous, Arion rejects duplicate `server_info.name` values across TDS-enabled gateways in the same runtime.

## Verify

After the control plane publishes a `Tool` resource named `tenant-a-mcp/main/weather.get_forecast`, search for it on that listener:

```bash
curl -s http://127.0.0.1:8080/mcp/search \
  -H 'content-type: application/json' \
  -d '{"query":"forecast","namespace":"weather"}'
```

The answer now includes `weather.get_forecast`, and removing the resource drops it again. On a code-mode listener, run `catalog.tools.list({ namespace: "weather" })` through `execute` instead.

## Limits

- TDS is delta xDS only, and it carries only the four MCP resource types. Listener and route changes still go through their own xDS types.
- Resource names may contain `/`; `server_info.name` and `config_name` may not.
- Each namespace has one owner. A `Tool` cannot be added to a namespace that belongs to an OpenAPI source or a dynamic server.

Payload fields for every resource type are listed in the [TDS resource reference](@/reference/xds/tds-resources.md).
