KeenableTools adds keyless web search and page reading to Agno agents
October 19, 20261 min read
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.
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:
>>> 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:
>>> 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:
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, and learn more about toolkits in the documentation.
Frequently asked questions
No. With no KEENABLE_API_KEY set, KeenableTools calls Keenable's public endpoints. Set the key, or pass api_key, to switch to the authenticated endpoints, which lift rate limits.
Only web_search. Pass enable_fetch=True to add fetch_url_content, or all=True for both. Each has an async variant that agent.arun() uses.
fetch_url_content refuses URLs that are not http or https, localhost, names ending in .local, .internal, .localhost or .home.arpa, hostnames that do not resolve, and any host that resolves to a loopback, private, link-local, reserved, multicast or other non-public address. It returns an error string and sends no request.
No. The toolkit sends its requests through httpx, which Agno already depends on.




