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:
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
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 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 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 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:
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:
rest_backend:
cluster: api
method: POST
path: /v1/users
body_template:
inline_string: |
{ "username": "{{username}}", "email": "{{email}}", "source": "mcp" }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:
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:
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. To turn a whole REST API into tools at once, describe it with an OpenAPI document.
Verify
Initialize a session, then call the tool by its qualified name with the returned mcp-session-id:
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.