# KeenableTools adds keyless web search and page reading to Agno agents

> Agno's KeenableTools gives an agent web_search and fetch_url_content through the Keenable Search API, with no signup and no package to install.

- Published: 2026-10-19
- Author: Ilya Bogin
- Categories: Changelog
- Canonical: https://www.agno.com/blog/keenabletools-adds-keyless-web-search-and-page-reading-to-agno-agents
- Markdown: https://www.agno.com/blog/keenabletools-adds-keyless-web-search-and-page-reading-to-agno-agents.md

Agno's `KeenableTools` registers `web_search` by default, and `fetch_url_content` joins it with `enable_fetch=True` or `all=True`. Search returns ranked results as markdown or JSON, each snippet folded onto one line and capped at `max_snippet_chars` (500 by default). Fetch returns a page's main content as markdown under its title.

With no `KEENABLE_API_KEY` set, the toolkit talks to Keenable's public endpoints. A key moves it to the authenticated endpoints, which lift rate limits. Requests go through `httpx`, already an Agno dependency, and `agent.arun()` picks up the async variants `aweb_search` and `afetch_url_content`.

```python
from agno.agent import Agent
from agno.tools.keenable import KeenableTools

agent = Agent(tools=[KeenableTools(all=True)])
```

We called both tools against the live API with no key set. A search, trimmed to two results:

```text
>>> tools = KeenableTools(max_results=2, max_snippet_chars=120)
>>> print(tools.web_search("RFC 6761 special-use domain names"))
# RFC 6761 special-use domain names

### [RFC 6761: Special-Use Domain Names](https://iana.org/go/rfc6761)
Special-Use Domain Names Abstract This document describes what it means to say that a Domain Name (DNS name) is reserved…

### [RFC 6761: Special-Use Domain Names | RFC Editor](https://rfc-editor.org/info/rfc6761)
Special-Use Domain Names Abstract This document describes what it means to say that a Domain Name (DNS name) is reserved…
```

A page fetch, trimmed after the first lines:

```text
>>> print(tools.fetch_url_content("https://docs.agno.com/introduction"))
## Welcome to Agno | Agno

Welcome

# Welcome to Agno

Build your own agent platform.
...
```

A fetch carries the full URL to Keenable, so the toolkit resolves the hostname on your machine first. It refuses anything that is not http or https, special-use names such as `localhost` and `.internal`, hostnames that do not resolve, and any host whose addresses are loopback, private, link-local or otherwise not public. These calls return at once and send nothing:

```text
http://127.0.0.1:8000/admin              -> Error: refusing to fetch a private/internal host: '127.0.0.1'
http://169.254.169.254/latest/meta-data/ -> Error: refusing to fetch a private/internal host: '169.254.169.254'
http://10.0.0.5/                         -> Error: refusing to fetch a private/internal host: '10.0.0.5'
http://localhost:7777                    -> Error: refusing to fetch a private/internal host: 'localhost'
file:///etc/passwd                       -> Error: refusing to fetch a non-http(s) URL: 'file:///etc/passwd'
```

API errors come back as text the agent can act on. A 401, 402 or 429 from Keenable becomes a labelled message carrying the backend's own detail.

See the [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/91_tools/keenable_tools.py), and learn more about [toolkits](https://docs.agno.com/tools/toolkits/overview) in the documentation.
