---
title: "Tools and MCP"
subtitle: "Put MCP servers and built-in tool vendors behind the gateway, and let any model use their tools."
---

Each `[tools.<name>]` table is a tool server. It is one of three things:

- a **tool vendor**: an HTTP API, such as Linkup search, that the gateway serves as MCP tools itself;
- a remote **MCP server** over HTTP;
- a local **MCP server** run as a command (stdio).

The gateway serves each one over MCP at `/mcp/{name}`, and all of them together at `/mcp`. With the [tool loop](#tool-loop), any chat model can use them in a normal chat request.

```toml
[tools.search]
vendor = "linkup"
key = "env:LINKUP_API_KEY"

[plugins.tool_loop]
servers = ["search"]
```

Now any model, called from any SDK, can search the web. The client sends an ordinary chat request and gets an ordinary reply.

## Tool vendors

A tool vendor is an HTTP API that is not an MCP server. With `vendor`, the gateway turns its endpoints into MCP tools, in process.

```toml
[tools.search]
vendor = "tavily"              # key from TAVILY_API_KEY

[tools.web]
vendor = "tinyfish"
key = "env:MY_TINYFISH_KEY"    # default: TINYFISH_API_KEY
# base_url = "https://..."     # default: the vendor's own
```

A `vendor` entry takes only `key`, `base_url` and `proxy`.

| `vendor` | For | Key variable | Tools |
| --- | --- | --- | --- |
| `apify` | Web scraping | `APIFY_TOKEN` or `APIFY_API_TOKEN` | `search_actors`, `run_actor`, `get_run_items`, `browse_web` |
| `browserbase` | Browser automation | `BROWSERBASE_API_KEY` | `search`, `fetch`, `run_agent`, `get_agent_run`, `stop_agent_run`, `live_view` |
| `cloudinary` | Image and video | `CLOUDINARY_URL` | `upload`, `search`, `get_asset`, `delivery_url`, `delete_asset` |
| `context7` | Coding docs | `CONTEXT7_API_KEY` | `resolve_library_id`, `query_docs`, `search_docs` |
| `daytona` | Code sandbox | `DAYTONA_API_KEY` | `create_sandbox`, `list_sandboxes`, `delete_sandbox`, `run_command`, `run_code`, `write_file` |
| `linkup` | Search | `LINKUP_API_KEY` | `search`, `answer`, `fetch` |
| `reducto` | Document parsing | `REDUCTO_API_KEY` | `parse_document`, `extract_fields`, `split_document` |
| `tavily` | Search | `TAVILY_API_KEY` | `search`, `extract`, `crawl`, `map_site` |
| `tinyfish` | Search and browser agent | `TINYFISH_API_KEY` | `search`, `fetch`, `run_agent` |
| `zep` | Memory | `ZEP_API_KEY` | `search`, `get_graph_context`, `get_thread_context`, `add_messages`, `add_data` |
| `e2b` | Code execution | `E2B_API_KEY` | `run_code`, `list_sandboxes`, `kill_sandbox` |
| `elevenlabs` | Voice | `ELEVENLABS_API_KEY` | `text_to_speech`, `search_voices`, `speech_to_text`, `sound_effect` |
| `exa` | Search | `EXA_API_KEY` | `search`, `get_contents` |

The first ten are verified end to end against the live API. E2B, ElevenLabs and Exa are maintained but not yet verified that way. Most of these calls are billed by the vendor. Each call's result reports what it used in its `_meta`, under `respan.ai/usage`.

Without a `gateway.toml`, each tool vendor whose key is in the environment becomes a server named after it, served at `/mcp`. The tool loop stays off until you configure it.

## MCP servers

The fields match an MCP client's `mcp.json`, so entries copy across.

<Tabs>
<Tab title="Remote (HTTP)">
```toml
[tools.github]
url  = "https://api.githubcopilot.com/mcp/"
auth = { bearer = "env:GITHUB_TOKEN" }
```

A server that takes its key in a header of its own uses `headers` instead:

```toml
[tools.docs]
url     = "https://mcp.example.com/mcp"
headers = { "x-api-key" = "env:DOCS_KEY" }
```
</Tab>
<Tab title="Local (stdio)">
```toml
[tools.files]
command = "npx"
args    = ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
# env   = { SOME_TOKEN = "env:SOME_TOKEN" }
# cwd   = "/srv/files"
```

The gateway runs one child process per client session. The child gets only `PATH`, the gateway's proxy settings, and the `env` you give it.
</Tab>
</Tabs>

`auth` on an HTTP server is one of:

| `auth` | Meaning |
| --- | --- |
| `{ bearer = "env:KEY" }` | A static key, sent as a bearer token. |
| `{ oauth = { token_url, client_id, client_secret, scopes, refresh_token } }` | The gateway's own OAuth client. |
| `"login"` | A login saved by `respan-gateway mcp login <name>`, for servers a person must approve once. |
| `"forward"` | Each client's own credentials, passed on. |

`respan-gateway mcp` lists the servers and whether each is authenticated. `mcp login` opens your browser; on a headless machine, paste the callback address back instead.

## One endpoint for every server

`/mcp` merges every tool server into one MCP server, so an MCP client needs one URL and one key:

```json
{
  "mcpServers": {
    "gateway": {
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer YOUR_CLIENT_KEY" }
    }
  }
}
```

Tools and prompts are named `{server}__{tool}`, such as `search__search` or `github__create_issue`. `/mcp` fronts every server except those with `auth = "forward"`, since those need each client's own credentials. Those are reached at `/mcp/{name}` only.

`/mcp/{name}` serves one server as it is, with all of its tools.

## Tool loop

With `[plugins.tool_loop]`, the gateway adds the tool servers' tools to chat requests, runs the calls the model makes, and calls the model again with the results. The client sees one reply, with usage summed over the steps. This works for every chat format and provider, and in realtime sessions too.

```toml
[plugins.tool_loop]
servers   = ["search", "github"]      # default: every server /mcp fronts
allow     = ["search__*", "github__search_*"]
deny      = ["github__delete_*"]      # wins over allow
max_steps = 8
# models  = ["gpt-", "claude-"]       # model name prefixes; default: every chat model
```

| Key | Default | Meaning |
| --- | --- | --- |
| `servers` | every server `/mcp` fronts | The tool servers whose tools are offered. |
| `allow` | all | Tools offered, as `server__tool` globs. |
| `deny` | none | Tools never offered. Wins over `allow`. |
| `models` | every chat model | Model name prefixes the loop applies to. |
| `max_steps` | `8` | The most model calls for one request. The last one forbids tools. |
| `timeout` | `"5m"` | The time limit for one request's loop. Past it, the request fails with 504 `tool_loop_timeout`. |
| `tool_timeout` | `"60s"` | The time limit for one tool call. |
| `parallel` | `4` | Tool calls of one step that run at once. |
| `max_result` | `"64KiB"` | The most of one tool's answer the model sees. |
| `max_results` | `"256KiB"` | The most of all tool answers the model sees in one request. |
| `tool_list_ttl` | `"5m"` | How long a server's tool list is cached. |

If the client sends tools of its own and the model calls one, the loop stops and returns that call to the client as usual. A client can turn the loop off for one request with the header `x-respan-tool-loop: off`.

<Warning>
The tool loop runs tools on the gateway's machine, with the gateway's credentials, for any client that can call the gateway. A tool's description and output are untrusted input to the model. Offer only tools from servers you trust, use `allow` to offer only what you would let any client do, and use `deny` to keep destructive tools out.
</Warning>

<Note>
`allow` and `deny` apply to the tool loop, not to `/mcp`. To limit what a client can reach at `/mcp`, give its [client key](/docs/oss-gateway/client-keys#limit-a-key-to-some-tools) a `tools` list.
</Note>

A request the tool loop runs goes to its route's first candidate only, without the route's fallback, retries or hedge.

## Turn tools off at runtime

The web UI's Tools page turns a server or a single tool off and on while the gateway runs. The admin API does the same:

```bash
curl -X PUT http://localhost:8080/ui/api/mcp/github \
  -H "content-type: application/json" -d '{"enabled": false}'
curl -X PUT http://localhost:8080/ui/api/mcp/github/tools/delete_repo \
  -H "content-type: application/json" -d '{"enabled": false}'
```

A tool turned off is neither offered nor run by `/mcp` and the tool loop. A server turned off also refuses clients at `/mcp/{server}` with 403 `tool_server_off`. These switches live in memory: a config reload keeps them, a restart turns everything back on.
