---
title: "Tools and REST backends"
url: "/docs/mcp-gateway/tools-and-rest-backends/"
kind: "feature"
summary: "Declare static MCP tools and map tools/call onto HTTP requests: namespaces, path templates, query parameters, body mapping and response handling."
---

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

# Tools and REST backends

Declare static MCP tools and map tools/call onto HTTP requests: namespaces, path templates, query parameters, body mapping and response handling.

Static tools are declared in the MCP gateway's `tools` list. Each gives agents a qualified name, a description and an input schema, and maps a call onto a backend. Use a REST backend to expose an existing HTTP API as a tool without writing any server code.

## Declare a tool

Each tool needs a `namespace`, a `name`, a `description`, an `input_schema` (JSON Schema, given as a data source such as `inline_string`) and exactly one backend. This fragment sits under the filter's `typed_config`, and `cluster` must name a cluster from `static_resources`:

{% raw %}
```yaml,arion=fragment
tools:
  - namespace: weather
    name: get_forecast
    description: Get current weather and forecast for a city.
    disclosure_mode: EAGER
    input_schema:
      inline_string: |
        {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "City name" },
            "days": { "type": "integer", "description": "Forecast days" }
          },
          "required": ["city"]
        }
    rest_backend:
      cluster: api
      method: GET
      path: /v1/weather/{{city | encode_path_segment}}
      query_params:
        - name: days
          source: days
```
{% endraw %}

Agents see this tool as `weather.get_forecast`: the namespace, a dot, then the local name. Namespaces use letters, digits, `_` and `-`, and the local name may also contain dots. The qualified name must be unique and at most 128 bytes. Group related tools under one namespace, since agents and [code mode](@/docs/mcp-gateway/code-mode.md) scripts browse the catalog by namespace.

`disclosure_mode: EAGER` lists the tool in every `tools/list`. Without it, the tool is progressive: agents find it through [tool search](@/docs/mcp-gateway/tool-search.md) and can still call it by name.

Arion validates call arguments against `input_schema` before dispatch; an empty schema means no validation. The `path` is a template: double-braced placeholders are replaced with argument values, and a leading `/` is added if missing. The `encode_path_segment` filter percent-encodes a scalar value so that it stays inside one path segment. Use it for any argument a model supplies, so that a value such as `../admin` cannot change the path. A placeholder with no matching argument fails the render, so mark every templated argument as `required` in the schema.

Each `query_params` entry maps a URL parameter `name` to an argument path in `source`. Dot notation reaches nested fields, and values are percent-encoded. An array value becomes a repeated parameter, as in `?tag=a&tag=b`, and a parameter whose source is absent is omitted.

Dispatch is direct: the gateway resolves `rest_backend.cluster` itself rather than going through the route table. A per-tool `upstream_policy` can set an explicit `authority` (required for `ORIGINAL_DST` clusters), a `timeout` and a `retry_policy`. See the [filter reference](@/reference/filters/http/mcp-gateway.md#rest-backend) for the full field table.

## Map a request body

`body_json` sends one argument as the JSON body, unchanged. It is the simplest mapping when the upstream expects a JSON document the agent can build:

```yaml,arion=fragment
input_schema:
  inline_string: |
    {
      "type": "object",
      "properties": {
        "user": {
          "type": "object",
          "properties": { "username": { "type": "string" }, "email": { "type": "string" } },
          "required": ["username", "email"]
        }
      },
      "required": ["user"]
    }
rest_backend:
  cluster: api
  method: POST
  path: /v1/users
  body_json: user
```

`body_json` is a dot path into the arguments. If the argument is absent, no body is sent. For a body that mixes fixed text with arguments, use `body_template` instead. It renders a template from the arguments and is also sent as `content-type: application/json`. A tool sets one or the other, never both:

{% raw %}
```yaml,arion=fragment
rest_backend:
  cluster: api
  method: POST
  path: /v1/users
  body_template:
    inline_string: |
      { "username": "{{username}}", "email": "{{email}}", "source": "mcp" }
```
{% endraw %}

## Validate the response

Without `output_schema`, the upstream body is returned to the agent as plain text content. With one, Arion parses successful responses as JSON, validates them, and returns the payload as structured MCP content. Code mode scripts then receive it as parsed JSON:

```yaml,arion=fragment
output_schema:
  inline_string: |
    {
      "type": "object",
      "properties": { "id": { "type": "string" }, "status": { "type": "string" } },
      "required": ["id", "status"]
    }
```

Failed calls come back as a structured tool error with a code, the HTTP status and a retryable flag. The codes are `upstream_error`, `upstream_timeout`, `response_too_large`, `invalid_output` and `overflow`. Responses are capped by the gateway's `max_upstream_response_bytes` (default 1 MiB), and calls by `upstream_timeout` (default 15s).

## Forward a tool to an MCP server

A static tool can instead pin one tool of an upstream MCP server. The local `name` is sent upstream as the tool name, and the `namespace` exists only on the gateway side. The upstream stays authoritative for schemas, so `input_schema` and `output_schema` are ignored for this backend:

```yaml,arion=fragment
tools:
  - namespace: utils
    name: echo
    description: Echo back the supplied message.
    mcp_server_backend:
      transport: StreamableHttp
      url: http://127.0.0.1:5678/mcp
```

To discover an upstream server's whole catalog instead, use [federation](@/docs/mcp-gateway/federation.md). To turn a whole REST API into tools at once, describe it with an [OpenAPI document](@/docs/mcp-gateway/openapi-tools.md).

## Verify

Initialize a session, then call the tool by its qualified name with the returned `mcp-session-id`:

```bash
curl -is http://127.0.0.1:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

SESSION_ID=<session-id-from-the-response-header>

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":2,"method":"tools/call","params":{"name":"weather.get_forecast","arguments":{"city":"Dublin","days":3}}}'
```

The first response carries the session id header. The second returns a `result` with the upstream payload as `content`, or as `structuredContent` when an output schema is set.

## Limits

- Exactly one backend per tool, and qualified names must be unique within a gateway.
- A namespace used by static tools cannot also be used by an OpenAPI source or a dynamic server.
- Schemas are enforced for REST backends only; MCP server backends ignore them.
- Responses are buffered, size-capped and returned whole rather than streamed.
- A missing template argument fails the call at render time; guard it with `required`.

Whole runnable configs are in the [MCP gateway guide](@/guides/mcp/mcp-gateway.md).
