Skip to main content
Glama

agent-tools

agent-tools is a multi-function MCP tool aggregation server. It exposes a single Streamable HTTP MCP endpoint hosting multiple tool families. The first family is web search (aggregating Brave → Exa → SearXNG with multi-key rotation and cross-provider fallback). Additional families (URL fetch, code search, image search, RAG retrieval, etc.) can be added under src/agent_tools/families/<name>/ without changing the server shell, auth, or observability layer.

agent-tools is written in Python 3.12 using the official Python MCP SDK (mcp==1.27.2) and FastMCP. It is consumed by Open WebUI 0.11.0 over Streamable HTTP only — no stdio, no legacy SSE.

  • License: MIT

  • Default endpoint: http://0.0.0.0:8765/mcp

  • Container bind: 0.0.0.0:8765 (host-side loopback isolation is enforced by docker run -p 127.0.0.1:8765:8765)

  • Logs: JSONL to stderr only (stdout is reserved for MCP protocol frames)


Install

docker buildx build --load -f Dockerfile -t agent-tools:latest .
docker run -d --name agent-tools \
  -p 127.0.0.1:8765:8765 \
  --env-file config/agent-tools/tools.env \
  --env-file config/agent-tools/tools.secrets.env \
  -v "$PWD/examples/backends.yaml:/app/config/backends.yaml:ro" \
  agent-tools:latest

The image listens on 0.0.0.0:8765/mcp; bind to loopback on the host with -p 127.0.0.1:8765:8765.

Run from source

Requires Python 3.12 and uv.

uv sync --frozen
.venv/bin/python -m agent_tools

Related MCP server: MCP Server Metasearch

Configuration

Configuration is split into two layers:

  1. Environment variables — bind settings and secrets.

  2. backends.yaml — operator-tunable per-backend policy (chain order, cooldowns, regex safety gate). No secrets live here.

Environment variables

Variable

Required

Default

Description

HOST

no

0.0.0.0

Bind address (container-internal; publish via -p).

PORT

no

8765

Bind port.

MCP_PATH

no

/mcp

MCP Streamable HTTP mount path.

LOG_LEVEL

no

INFO

Python log level.

BRAVE_API_KEYS

yes*

Comma-separated Brave Search API keys.

EXA_API_KEYS

yes*

Comma-separated Exa API keys.

SEARXNG_URLS

yes*

http://localhost:8888

Comma-separated SearXNG instance base URLs.

TOOLS_AUTH_ENABLED

no

false

Enable bearer-token auth on the MCP endpoint.

TOOLS_AUTH_TOKEN

no

Bearer token when TOOLS_AUTH_ENABLED=true.

AGENT_TOOLS_BACKENDS_CONFIG

no

/app/config/backends.yaml

Path to the backends policy file.

* At least one backend must have at least one key/URL configured for the server to serve traffic; the server itself starts regardless so the failure is visible to MCP clients rather than as a crash loop.

backends.yaml

See examples/backends.yaml for the full annotated sample. Key fields:

chain_order: [brave, exa, searxng]
fallback_on_empty: false         # healthy empty result is a success by default
request_timeout_seconds: 10
max_provider_attempts: 3

backends:
  brave:
    rate_limit_patterns: ["429", "rate.?limit", "quota"]
    cooldown_seconds: 300
    suspend_on_monthly_exhausted: true
  exa:
    rate_limit_patterns: ["429", "rate.?limit"]
    cooldown_seconds: 60
  searxng:
    rate_limit_patterns: []      # no rate-limit body patterns by default
    cooldown_seconds: 30
    partial_on_unresponsive_engines: true

Safety gate (invariant). rate_limit_patterns is the only signal used to classify an error as retryable. An unmatched error (for example a provider returning isError: true with body "no results") is terminal — it does not trigger key rotation or cross-backend fallback. This prevents infinite retry loops.

Secrets hygiene

The repository ships no secrets. The example backends file contains no keys. Put API keys only in an --env-file or your secret manager. Anything matching *.secrets.env is gitignored.


Tool reference

All four tools share the same input schema and return the same WebSearchOutput. They differ only in which backends are consulted.

Canonical aggregated web search. Walks chain_order (brave → exa → searxng by default), rotating keys within each backend and falling over to the next backend on hard failures. Set backend: "brave" | "exa" | "searxng" in the input to force a single provider; the default is backend: "auto".

web_search_brave

Force Brave Search only. Bypasses the fallback chain but still uses the Brave token pool for multi-key rotation.

web_search_exa

Force Exa only. Bypasses the fallback chain but still uses the Exa token pool. 402 TEAM_BUDGET_EXCEEDED suspends the whole pool; 402 NO_MORE_CREDITS / API_KEY_BUDGET_EXCEEDED suspends only the current key.

web_search_searxng

Force a self-hosted SearXNG instance only. Bypasses the fallback chain. The "key" in the pool is the instance base URL; SearXNG has no API-key contract. The instance must be configured with search.formats: [html, json] or agent-tools will classify the response as config_error.

Input schema (WebSearchInput)

Field

Type

Default

Notes

query

str

required

Max 400 chars (Brave clamps).

num_results

int [1, 100]

10

Brave clamps to 20; Exa to 100; SearXNG ignores (instance controls page size).

page

int >= 1

1

Brave treats this as a 0-indexed page index (offset 0-9); Exa ignores beyond page 1; SearXNG maps to pageno.

language

str

"en"

BCP-47-ish language tag.

time_range

"day" | "week" | "month" | "year" | "all"

"all"

SearXNG supports day/month/year; "week" maps to "month" and is recorded in applied_filters.

safe_search

"off" | "moderate" | "strict"

"moderate"

Mapped per provider.

include_domains

list[str]

[]

Exa only; Brave/SearXNG record as applied=false.

exclude_domains

list[str]

[]

Exa only; Brave/SearXNG record as applied=false.

backend

"auto" | "brave" | "exa" | "searxng"

"auto"

Force a single backend.

fallback_on_empty

bool | null

null

If true, chain to the next backend when the selected one returns a healthy empty; null reads backends.yaml (default false).

Output schema (WebSearchOutput)

Field

Type

Notes

query

str

Echoes the input query.

backend_used

"brave" | "exa" | "searxng"

The backend that returned the result.

results

SearchResult[]

Canonical result list.

total_results

int | null

Provider-reported total when available.

search_time_ms

int

Wall-clock elapsed for the full chain.

fallback_chain_used

str[]

Backends attempted in order.

partial

bool

True when at least one backend failed before another succeeded (or all failed).

applied_filters

dict

Per-backend notes about filters that could not be applied verbatim (page size clamp, unsupported domains, etc.).

errors

dict[]

Structured per-backend errors: {backend, class, message, details?}.

SearchResult fields: title, url, snippet, published_date, author, score, highlights, engine, provider_metadata.

Scores are not comparable across providers. score is preserved provider-native and provider_metadata carries the type context (e.g. Exa similarity, SearXNG instance-local score). There is no cross-provider normalization.


Development

Running tests

uv sync --frozen --extra dev
.venv/bin/pytest -q

Or in Docker (fully network-isolated):

docker buildx build --load --target test -f Dockerfile -t agent-tools:test .
docker run --rm --network=none agent-tools:test

All HTTP is mocked with respx; tests pass with --network=none.

Adding a new backend

  1. Add src/agent_tools/families/websearch/backends/<name>.py implementing BackendAdapter.search(key, request) -> BackendResult.

  2. Add a normalize_<name>(raw) function in normalizer.py that maps the raw payload to SearchResult (preserve every native field under provider_metadata.<name>).

  3. Register the adapter and a TokenPool in families/websearch/tool.py (WebSearchFamily.__init__).

  4. Add the backend name to BackendName in schemas.py and to the chain validator in config.py.

  5. Add tests/test_<name>_adapter.py using respx to mock success / empty / rate-limit / auth / quota / 5xx paths.

Adding a new tool family

Tool families live under src/agent_tools/families/<name>/ and are self-contained: schemas, adapters (if any), and tool registration.

  1. Create the family package. mkdir -p src/agent_tools/families/<name> and add __init__.py.

  2. Define pydantic schemas. Create schemas.py with an input model and an output model. These drive the MCP inputSchema/outputSchema automatically.

  3. Implement the tool functions. Create a module (e.g. tool.py) exposing one async callable per MCP tool, typed with the pydantic models from step 2. Group shared state (HTTP clients, pools) in a class.

  4. Register in server.py. Construct the family in create_app() and attach its tools to the shared FastMCP instance, typically via a register(mcp, family) hook.

  5. Add tests. Create tests/test_<name>.py with mocked external I/O. If the family calls HTTP services, mock them with respx so the suite remains --network=none clean.

The server shell (auth, JSONL logging, bind/mount, error envelope) does not change when a family is added.


Project layout

src/agent_tools/
  __init__.py
  __main__.py             # python -m agent_tools entrypoint
  server.py               # FastMCP app construction
  config.py               # env + backends.yaml loading
  logging.py              # JSONL -> stderr
  http_client.py          # shared httpx.AsyncClient factory
  pool.py                 # TokenPool: health-aware weighted round-robin
  families/
    websearch/
      schemas.py          # WebSearchInput / WebSearchOutput / SearchResult
      fallback.py         # cross-backend state machine
      normalizer.py       # per-provider canonical mapping
      tool.py             # FastMCP tool registration
      backends/
        base.py           # BackendAdapter ABC + BackendResult + ErrorClass
        brave.py
        exa.py
        searxng.py
tests/                    # pytest + respx (all network mocked)
examples/backends.yaml    # documented sample policy
Dockerfile                # multi-stage: builder + runtime + test target

Operational notes

  • Bearer auth is optional. Loopback + tailnet is the default boundary; set TOOLS_AUTH_ENABLED=true and TOOLS_AUTH_TOKEN for defense-in-depth.

  • Quarantine is sticky. A key that returns 401/403 is quarantined until an operator calls the pool's clear_quarantine (v1: restart the server to clear all state). This prevents a known-bad credential from being retried on every request.

  • Team suspension (Exa). 402 TEAM_BUDGET_EXCEEDED suspends the entire Exa pool for one hour by default (override via backends.yaml); individual key suspensions last until the process restarts.

  • No caching in v1. Per-query caching and SearXNG-native cache control are deferred to a future release.

A
license - permissive license
-
quality - not tested
C
maintenance

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • AI Reasoning Cache & Consensus Layer with 11 MCP tools via Streamable HTTP.

  • Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.

  • Local-first RAG engine with MCP server for AI agent integration.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/garnetlyx/agent-tools'

If you have feedback or need assistance with the MCP directory API, please join our Discord server