agent-tools
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@agent-toolssearch the web for the latest AI agent frameworks news"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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/mcpContainer bind:
0.0.0.0:8765(host-side loopback isolation is enforced bydocker run -p 127.0.0.1:8765:8765)Logs: JSONL to stderr only (stdout is reserved for MCP protocol frames)
Install
Run with Docker (recommended)
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:latestThe 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_toolsRelated MCP server: MCP Server Metasearch
Configuration
Configuration is split into two layers:
Environment variables — bind settings and secrets.
backends.yaml— operator-tunable per-backend policy (chain order, cooldowns, regex safety gate). No secrets live here.
Environment variables
Variable | Required | Default | Description |
| no |
| Bind address (container-internal; publish via |
| no |
| Bind port. |
| no |
| MCP Streamable HTTP mount path. |
| no |
| Python log level. |
| yes* | — | Comma-separated Brave Search API keys. |
| yes* | — | Comma-separated Exa API keys. |
| yes* |
| Comma-separated SearXNG instance base URLs. |
| no |
| Enable bearer-token auth on the MCP endpoint. |
| no | — | Bearer token when |
| no |
| 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: trueSafety 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.
web_search
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 |
|
| required | Max 400 chars (Brave clamps). |
|
|
| Brave clamps to 20; Exa to 100; SearXNG ignores (instance controls page size). |
|
|
| Brave treats this as a 0-indexed page index (offset 0-9); Exa ignores beyond page 1; SearXNG maps to |
|
|
| BCP-47-ish language tag. |
|
|
| SearXNG supports day/month/year; |
|
|
| Mapped per provider. |
|
|
| Exa only; Brave/SearXNG record as |
|
|
| Exa only; Brave/SearXNG record as |
|
|
| Force a single backend. |
|
|
| If true, chain to the next backend when the selected one returns a healthy empty; |
Output schema (WebSearchOutput)
Field | Type | Notes |
|
| Echoes the input query. |
|
| The backend that returned the result. |
|
| Canonical result list. |
|
| Provider-reported total when available. |
|
| Wall-clock elapsed for the full chain. |
|
| Backends attempted in order. |
|
| True when at least one backend failed before another succeeded (or all failed). |
|
| Per-backend notes about filters that could not be applied verbatim (page size clamp, unsupported domains, etc.). |
|
| Structured per-backend errors: |
SearchResult fields: title, url, snippet, published_date, author, score, highlights, engine, provider_metadata.
Scores are not comparable across providers.
scoreis preserved provider-native andprovider_metadatacarries 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 -qOr in Docker (fully network-isolated):
docker buildx build --load --target test -f Dockerfile -t agent-tools:test .
docker run --rm --network=none agent-tools:testAll HTTP is mocked with respx; tests pass with --network=none.
Adding a new backend
Add
src/agent_tools/families/websearch/backends/<name>.pyimplementingBackendAdapter.search(key, request) -> BackendResult.Add a
normalize_<name>(raw)function innormalizer.pythat maps the raw payload toSearchResult(preserve every native field underprovider_metadata.<name>).Register the adapter and a
TokenPoolinfamilies/websearch/tool.py(WebSearchFamily.__init__).Add the backend name to
BackendNameinschemas.pyand to the chain validator inconfig.py.Add
tests/test_<name>_adapter.pyusingrespxto 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.
Create the family package.
mkdir -p src/agent_tools/families/<name>and add__init__.py.Define pydantic schemas. Create
schemas.pywith an input model and an output model. These drive the MCPinputSchema/outputSchemaautomatically.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.Register in
server.py. Construct the family increate_app()and attach its tools to the sharedFastMCPinstance, typically via aregister(mcp, family)hook.Add tests. Create
tests/test_<name>.pywith mocked external I/O. If the family calls HTTP services, mock them withrespxso the suite remains--network=noneclean.
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 targetOperational notes
Bearer auth is optional. Loopback + tailnet is the default boundary; set
TOOLS_AUTH_ENABLED=trueandTOOLS_AUTH_TOKENfor 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_EXCEEDEDsuspends 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.
Related MCP Connectors
AI Reasoning Cache & Consensus Layer with 11 MCP tools via Streamable HTTP.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
Docs: https://docs.keenable.ai/mcp-server Keenable is a free, remote MCP server that gives agents access to the web index. Search the web with ranked results and date/site filters, then fetch any indexed page as clean markdown. Works out of the box with no account or API key.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA lightweight MCP server that enables LLMs to search the web via DuckDuckGo, search GitHub code repositories, and extract clean content from web pages in LLM-friendly formats.8-
- AlicenseNot gradedqualityDmaintenanceA unified MCP server aggregating 15 web search and extraction tools across 5 providers (Jina, Tavily, Exa, Firecrawl, Bocha) with automatic API key validation and plugin architecture.MIT
- AlicenseNot gradedqualityAmaintenanceAggregates search results from multiple providers (web, academic, code, finance) with a unified JSON schema, providing both an HTTP API and an MCP server for AI agent tooling.55MIT
- AlicenseNot gradedqualityAmaintenanceA minimal MCP server that exposes a private SearXNG instance as a search tool over streamable-HTTP, enabling web search from the llama.cpp WebUI or any compatible MCP client.3MIT