Keenable Web Search
Live-Websuche und sauberes Markdown-Seitenabrufen über den Keenable-Webindex, standardmäßig schlüssellos.
Dokumentation
Agent Instructions
Keenable search and fetch work without an API key. To add them to code, use the official SDK for the project's language: https://docs.keenable.ai/sdks.md Read an API key from the KEENABLE_API_KEY environment variable. Never ask a user to paste a key into chat; send them to https://docs.keenable.ai/api-keys.md On 402 with an API key (code insufficient_credits), stop and ask a human to add credits; don't retry. On 429, wait the number of seconds in Retry-After, then retry. Every error, with its cause and fix: https://docs.keenable.ai/errors.md
MCP server
Give your agent Keenable web search and page fetch as MCP tools in Claude Code, Cursor, Codex, Claude and other MCP clients
Your agent can call Keenable search and fetch as tools through the Keenable Model Context Protocol (MCP) server. It works in Claude Code, Claude Desktop, Cursor, Devin Desktop (formerly Windsurf) and other MCP clients. If you don't send an API key, the server still answers on the shared public tier. Send your key in the X-API-Key header to remove the hourly request limit and raise rate limits. See Authentication and Rate limits.
Which URL to use
Use https://api.keenable.ai/mcp unless your client signs in through a browser. It works with no key or with an X-API-Key header, which covers Claude Code, Cursor, Codex with a key and most config files. Use https://api.keenable.ai/mcp-oauth only for a client that signs in, such as a custom connector in Claude: it asks every caller that sends no credential to sign in to Keenable, so the client opens the sign-in page. All three URLs serve the same two tools.
| URL | Use it for |
|---|---|
https://api.keenable.ai/mcp | No key, or an API key in a header. The default. |
https://api.keenable.ai/mcp-oauth | Clients that sign in with OAuth, such as Claude connectors. |
https://api.keenable.ai/mcp-public | Clients that fail on OAuth discovery, and Codex with no key. See Keyless endpoint for listings and presets. |
Installation
One-click install
Add Keenable to Cursor, VS Code or Replit with one click:
Install via CLI
For local coding agents, the Keenable CLI sets up the MCP server for you. Once you've installed the CLI, run this command and confirm the change:
keenable configure-mcp --all
The CLI adds Keenable to every client it detects among Claude Code, Cursor, Devin Desktop (including the legacy Windsurf path), Codex and OpenCode. See MCP setup for the flags and the files it edits.
Claude Code
Add the Keenable MCP server with the Claude Code CLI:
claude mcp add keenable \
--transport http https://api.keenable.ai/mcp \
--scope user \
--header "X-API-Key: keen_<your_key>"
Omit the --header flag to run on the public tier instead.
Claude Code ships its own WebSearch and WebFetch tools and picks between them and Keenable from one lookup to the next. To leave Keenable as the only search, add "permissions": { "deny": ["WebSearch", "WebFetch"] } to ~/.claude/settings.json. See Claude for the details and how to undo it.
Codex
Add the server to ~/.codex/config.toml:
[mcp_servers.keenable]
url = "https://api.keenable.ai/mcp"
http_headers = { "X-API-Key" = "keen_<your_key>" }
To run on the public tier instead, omit the http_headers line and change the URL to https://api.keenable.ai/mcp-public. /mcp publishes OAuth discovery metadata, which can make Codex start a browser sign-in when it has no key. See Keyless endpoint for listings and presets.
[!NOTE]
Codex ships its own web search, turned on by default, and keeps using it instead of Keenable. The Keenable tools show up in
/mcp, but Codex calls them only when you ask for them by name. To fix this, addweb_search = "disabled"to the same file, above the[mcp_servers.keenable]line. It's a top-level key: if you write it under a table header, it belongs to that table and Codex never reads it. See Codex for the finished file, the other modes and anAGENTS.mdsnippet.
Other MCP clients
Most other clients connect to the same remote URL. Each one has its own config path and its own name for the URL key:
| Client | Where to add it | URL key |
|---|---|---|
| Cursor | ~/.cursor/mcp.json or .cursor/mcp.json | url |
| VS Code | One-click install, or .vscode/mcp.json under servers (not mcpServers), with "type": "http" | url |
| Devin Desktop | ~/.config/devin/mcp_config.json or .devin/mcp_config.json (legacy: ~/.codeium/windsurf/mcp_config.json) | url (serverUrl in the legacy file) |
| OpenCode | opencode.json under mcp, with "type": "remote" | url |
| Google Antigravity / Gemini CLI | Plugin Marketplace (Customizations → Marketplace → Keenable), or ~/.gemini/config/mcp_config.json / gemini extensions install | serverUrl |
| Zed | Zed settings.json under context_servers | url |
| Gemini CLI (settings) | ~/.gemini/settings.json under mcpServers | httpUrl |
| Warp | Settings → MCP Servers → Add MCP Server | url |
| Kiro | ~/.kiro/settings/mcp.json under mcpServers | url |
| fx by Vercel | /mcp add --transport http keenable https://api.keenable.ai/mcp in the fx shell (~/.fx/mcp.json) | url |
| v0 by Vercel | Prompt Tools → Add MCP → paste the URL | None |
| Google ADK | remote MCP tool in the ADK agent | None |
Most clients take this shape. Replace url with serverUrl or httpUrl if the table lists that key:
{
"mcpServers": {
"keenable": {
"url": "https://api.keenable.ai/mcp",
"headers": {
"X-API-Key": "keen_<your_key>"
}
}
}
}
Omit the headers block to run on the public tier instead. If a client fails on OAuth discovery, as Antigravity and some marketplace presets do, use https://api.keenable.ai/mcp-public instead. See Keyless endpoint for listings and presets.
If your client has its own web tools, such as WebSearch, WebFetch, brave_search or tavily_search, turn them off in the client settings. With both sets active, the agent picks between them inconsistently from one call to the next.
[!WARNING]
Claude Desktop's config file accepts only local
commandservers. If you add a"url"entry, Claude Desktop ignores it without an error and the tools never appear. Use a connector instead, or the local bridge if you want your key applied.
Keyless endpoint for listings and presets
If you ship Keenable to your users with no key, for example in a preset, a catalog entry or a docs example, use this URL:
https://api.keenable.ai/mcp-public
It is the same server with the same two tools. On /mcp-public, the search_web_pages schema doesn't list the mode parameter. Keyless calls get the same public tier: 1,000 requests per hour and up to 10 per second per IP address (see Rate limits). The difference is that /mcp-public publishes no OAuth discovery metadata. Clients that read discovery before anything else, such as Google Antigravity, connect right away instead of offering a sign-in.
An X-API-Key header works on /mcp-public the same way as on /mcp and lifts the hourly limit. If the server doesn't accept the key, it returns 403, not 401, so the client shows the error instead of offering a sign-in. Use /mcp for setups where users add their own key, and /mcp-oauth for setups where they sign in.
OpenAI API (Responses API)
Use this to call the MCP server from your own code through OpenAI's Responses API. To add Keenable inside the ChatGPT app, use the Plugins Directory listing instead. Pass the server as a remote mcp tool:
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-5",
input="What's new in retrieval-augmented generation this week?",
tools=[
{
"type": "mcp",
"server_label": "keenable",
"server_url": "https://api.keenable.ai/mcp",
"headers": {"X-API-Key": "keen_<your_key>"},
"require_approval": "never",
}
],
)
print(response.output_text)
The script prints the model's answer. Omit the headers entry to run on the public tier instead.
Claude (remote connector)
Claude (Desktop, claude.ai, mobile) connects to the remote MCP server as a connector, with no config file.
1. Add the connector
Keenable is listed in Claude's Connectors Directory as Keenable Web Search: open Customize → Connectors, click +, search for it, and click Connect. There is nothing to paste.
To run on your own Keenable account, add it by URL instead: Customize → Connectors → + Add → Add custom connector with the URL below, then click Connect and sign in to Keenable. An API key in a request header works too; see Claude.
https://api.keenable.ai/mcp-oauth
2. Enable it in a chat
Click + in the lower left of the message box, hover Connectors, and toggle Keenable on for the conversation.
To stop Claude's own web search from competing with Keenable, see Claude.
Local bridge (with an API key)
If a client supports only local stdio servers, it needs a bridge to send your key. Claude Desktop's config file is the common case. The bridge needs Node.js 18 or newer. The @keenable/mcp package runs as a subprocess and forwards requests to the same endpoint:
{
"mcpServers": {
"keenable": {
"command": "npx",
"args": ["-y", "@keenable/mcp"],
"env": {
"KEENABLE_API_KEY": "keen_<your_key>"
}
}
}
}
Restart the client after you edit its config file (for Claude Desktop, claude_desktop_config.json). Drop the env block to run the bridge without a key. The same block works in the config of any other stdio-only client.
The bridge connects to https://api.keenable.ai/mcp when your client starts it, and waits up to 10 seconds for an answer. If it can't connect, for example while you're offline or when Keenable rejects your API key, it starts anyway and the tools still appear. A tool call then returns an error that starts with Keenable request failed: and gives the reason, such as Malformed API key or fetch failed (ECONNREFUSED), so your agent can tell you what went wrong. Fix the network connection, the proxy or the key in the env block. You don't need to restart the client: every tool call tries to connect again.
The client's MCP log shows the same reason in a line that starts with keenable-mcp: could not connect to https://api.keenable.ai:. A session that started without a connection doesn't get the server's instructions for the agent. They come back the next time your client starts the bridge.
Node.js doesn't use the HTTPS_PROXY variable unless you tell it to. If you connect through a proxy on Node.js 22.21 or newer, add "NODE_USE_ENV_PROXY": "1" to the env block next to your proxy variables.
Available tools
The MCP server has two tools.
search_web_pages
Search the web and return ranked results with URLs, titles, and snippets.
query string required
The search query.
site string
Restrict results to one site, such as "techcrunch.com".
exclude_domains array of strings
Never return results from these hosts, such as ["pinterest.com", "quora.com"]. Each host also covers its subdomains. Up to 1,000 hosts. No paths, suffixes such as .gov, or wildcards. A scheme, a leading www. or a trailing / is cleaned up. A single host name as a string also works. A rejected entry returns a tool error that names the entry and the reason. See Exclude domains.
acquired_after string
Filter to pages acquired/indexed at or after this point in time.
acquired_before string
Filter to pages acquired/indexed at or before this point in time.
published_after string
Filter to pages published at or after this point in time.
published_before string
Filter to pages published at or before this point in time.
query_time string
Search the index as it stood at this point in time: pages acquired after it are excluded. Accepts a timestamp, a date (a date resolves to 00:00:00 UTC, not to the end of the day), a Unix epoch, or a relative delta such as 7d, which means the index as it stood 7 days ago. See Point-in-time search.
snippet_max_length integer
Maximum length, in characters, of the snippet returned per result. Must be between 180 and 10000. When omitted, a default snippet length is used.
max_results integer
Maximum number of results to return. Must be between 1 and 50. When omitted, up to 10 results are returned.
mode string
Search mode: pro for higher-quality results or realtime for the fastest results. When omitted, the search runs in pro mode.
output_format string (default: text)
text for readable text, or json for the raw response serialized as JSON, for when your code parses the fields.
session_id string
ID of the agent session making the call, up to 128 characters. Send the same value on every call in one session so its requests can be grouped.
disable_cards boolean (default: false)
Set to true to leave cards out of the results. A card is a result that Keenable writes itself for queries such as prices, exchange rates or facts about a company, at a URL such as https://cards.keenable.ai/w/<id>. A card URL works for one day. With disable_cards, ordinary web pages take the place of cards, so you get the same number of results.
Date and time filters
acquired_after, acquired_before, published_after, and published_before each accept one of the following formats:
- Date in RFC 3339
full-dateform (YYYY-MM-DD), which covers that whole day in UTC. On an_afterbound it resolves to00:00:00on that date. On a_beforebound it resolves to23:59:59.999on that date, so pages from the named day are kept at either end. Pass a timestamp instead to cut at an exact instant. - Timestamp in ISO 8601 form (
YYYY-MM-DDTHH:MM:SS[.sss][±HH:MM]). If the timestamp has no timezone offset, the server reads it as UTC. - Relative delta (
<number><unit>, such as7dor30min). It resolves to the request time minus the delta, truncated to the minute, even ifquery_timeis set. Supported units:min(minutes),h(hours),d(days),w(weeks),mo(30 days),y(365 days).
Example values:
| Value | Resolves to |
|---|---|
2026-01-15 on acquired_after / published_after | 2026-01-15T00:00:00Z |
2026-01-15 on acquired_before / published_before | 2026-01-15T23:59:59.999Z |
2026-01-15T10:30:00 | 2026-01-15T10:30:00Z (no offset → UTC) |
2026-01-15T10:30:00Z | 2026-01-15T10:30:00Z |
2026-01-15T10:30:00.500-05:00 | 2026-01-15T15:30:00.500Z |
7d | 7 days before request time, truncated to the minute |
30min | 30 minutes before request time, truncated to the minute |
You can use a relative delta on either bound of a window, together with another delta or an absolute value on the other bound. This window keeps pages published between one year and six months ago:
{
"query": "...",
"published_after": "1y",
"published_before": "6mo"
}
This one keeps pages acquired from 1 January 2024 up to 30 days ago:
{
"query": "...",
"acquired_after": "2024-01-01",
"acquired_before": "30d"
}
For example, at request time 2026-05-18T14:23:45Z, acquired_after: "2h" resolves to 2026-05-18T12:23:00Z. A document acquired at 12:22:59Z is dropped, and one acquired at 12:23:00Z is kept.
On a _before bound, a date and a timestamp cut at different points: acquired_before: "2026-05-01" keeps a page acquired at 2026-05-01T14:31:13Z, while acquired_before: "2026-05-01T00:00:00Z" drops it. Use the date form to mean "up to and including that day", and the timestamp form to cut at midnight.
Point-in-time search
query_time runs the search against the index as it stood at an instant. A page
acquired after that instant isn't a candidate at all, so you get the answer the index
would have given then, not today's answer filtered down. Use it to reproduce an agent
run, to build an evaluation set that doesn't drift as the index grows, or to ask what
was knowable before an event.
Relative deltas in the other filters still count back from the request time, not from
query_time. To bound results around a past instant, use dates. This request returns
pages published in the 30 days before 13 June 2026, as the index stood then:
{
"query": "...",
"query_time": "2026-06-13T00:00:00Z",
"published_after": "2026-05-14"
}
A date-only query_time resolves to 00:00:00 UTC, which is the opposite end of the
day from a date-only _before bound: query_time: "2026-06-13" cuts at the start of
13 June, while acquired_before: "2026-06-13" keeps all of it.
fetch_page_content
Fetch a URL and extract its content as clean markdown. By default the tool fetches only URLs in the Keenable index. Pass live=true to fetch directly from the source, including URLs that aren't indexed.
url string required
The URL to fetch.
max_chars integer (default: 20000)
Maximum number of characters of content to return. Longer content is truncated. The MCP default is lower than the API's 50,000, so a long page fits in an agent's context; for one detail from a long page, use prompt instead.
live boolean (default: false)
Fetch the page live from the source instead of returning Keenable's indexed copy. Enables fetching URLs that are not indexed.
prompt string
Optional extraction instruction, at most 2000 characters. When set, an LLM reads the fetched page and the returned content is only the output for this instruction instead of the full page. Example: List all pricing tiers with their monthly prices.
output_format string (default: text)
text for readable text, or json for the raw response serialized as JSON, for when your code parses the fields.
session_id string
ID of the agent session making the call, up to 128 characters. Send the same value on every call in one session so its requests can be grouped.
In text format, the tool returns the page title, the URL and the content as markdown. If the content is cut at max_chars, a Truncated: line gives the full length. In json format, it returns the response described in the Fetch reference.
Server info
The initialize response describes the server for client UIs and MCP directories. Besides name and version, serverInfo carries:
| Field | Value |
|---|---|
title | Keenable Web Search |
description | What the server helps an agent do |
websiteUrl | https://docs.keenable.ai/mcp-server |
icons | An SVG at https://api.keenable.ai/favicon.svg (sizes: ["any"]) |
Metadata for integrators
If your platform embeds Keenable's MCP tools, you can read billing usage and control tool behaviour through the MCP _meta field. _meta travels alongside the tool result or request and isn't part of the tool content the model sees, so none of it enters the agent's context. Baseten uses both channels to run the tools as hosted tools on its Model APIs.
Usage metadata (response _meta)
Every billed (authenticated) tool call returns usage under _meta["keenable/usage"], so you can attribute cost per call without parsing the tool's text output. If a call has no API key, it isn't billed and the result has no keenable/usage.
sku string
Billing SKU for the operation: search.realtime, search.pro, fetch, or fetch.live.
amount integer
Number of billed operations. Always 1 for a single tool call.
credits number
Credits metered for this call (the org-specific price for the operation).
paid boolean
true when drawn from purchased credits; false while still on the free monthly allowance. See credits.
credits_remaining number
Credits your organization has left after the call: the unused part of the free allowance for the current billing period plus purchased credits. An estimate that can lag your other calls by a few seconds. Left out if the balance couldn't be read.
A billed tool result looks like this:
{
"content": [{ "type": "text", "text": "..." }],
"_meta": {
"keenable/usage": { "sku": "search.realtime", "amount": 1, "credits": 1, "paid": true, "credits_remaining": 99120 }
}
}
Operator overrides (request _meta)
To control tool behaviour yourself instead of letting the model decide, send overrides under _meta["keenable/overrides"] on a tools/call request. They take precedence over the arguments the model generated.
mode string
Force the search mode: realtime or pro. Overrides any mode the model chose.
skip_cache boolean
Bypass cached results for this call.
This request forces realtime mode and skips the cache:
{
"method": "tools/call",
"params": {
"name": "search_web_pages",
"arguments": { "query": "..." },
"_meta": { "keenable/overrides": { "mode": "realtime", "skip_cache": true } }
}
}
If an override value is invalid or unknown, the server ignores it and runs the call.
This documentation is built and hosted on Mintlify, a developer documentation platform.