> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keenable.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP

> Connect Keenable APIs as tools to clients that support MCP

The Keenable tools are also available as a Model Context Protocol server, so agents like Claude Code, Claude Desktop, Cursor, and Windsurf can call them directly. Pass your API key in the `X-API-Key` header to remove the hourly request limit and raise rate limits — see [Authentication](/authentication) and [Rate limits](/rate-limits). Drop the header and the server still answers, on the shared public tier.

## Installation

### One-click install

Add Keenable to Cursor or VS Code with a single click:

<div className="install-buttons">
  <a className="install-button" href="cursor://anysphere.cursor-deeplink/mcp/install?name=keenable&config=eyJ1cmwiOiJodHRwczovL2FwaS5rZWVuYWJsZS5haS9tY3AifQ==">
    <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
      <path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" />
    </svg>

    Add to Cursor
  </a>

  <a className="install-button" href="vscode:mcp/install?%7B%22name%22%3A%22keenable%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.keenable.ai%2Fmcp%22%7D">
    <svg viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" />
    </svg>

    Add to VS Code
  </a>

  <a className="install-button" href="vscode-insiders:mcp/install?%7B%22name%22%3A%22keenable%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.keenable.ai%2Fmcp%22%7D">
    <svg viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" />
    </svg>

    Add to VS Code Insiders
  </a>
</div>

### Install via CLI

The easiest way to set up the Keenable MCP server for local coding agents is via the [Keenable CLI](/cli). Once you've installed the CLI, run the following to be guided through the MCP server setup.

```shellscript theme={"system"}
keenable configure-mcp
```

### Claude Code

Add the Keenable MCP server with the Claude Code CLI:

```bash theme={"system"}
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.

### Codex

Add the following to `~/.codex/config.toml`

```toml theme={"system"}
[mcp_servers.keenable]
url = "https://api.keenable.ai/mcp"
http_headers = { "X-API-Key" = "keen_<your_key>" }
```

Omit the `http_headers` line to run on the public tier instead.

<Note>
  **Codex needs one more line.** It ships its own web search, enabled by default, and keeps reaching for that instead — so the tools show up in `/mcp` but never get called unless you ask for them by name. Add `web_search = "disabled"` to the same file, **above** the `[mcp_servers.keenable]` line — it's a top-level key, and a bare key written under a table header belongs to that table instead, where Codex never reads it. See [Codex](/integrations/codex) for the finished file, the other modes, and an `AGENTS.md` snippet.
</Note>

### Other MCP clients

For Cursor and other clients that accept a remote MCP URL in their config file:

```json theme={"system"}
{
  "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.

<Warning>
  **Claude Desktop is not one of these clients.** Its config file accepts only local `command` servers, so a `"url"` entry is ignored without an error and the tools never appear. Use a [connector](#claude-remote-connector) instead, or the [local bridge](#local-bridge-with-an-api-key) if you want your key applied.
</Warning>

### OpenAI API (Responses API)

This is for calling the MCP server from your own code against OpenAI's API — different from adding Keenable inside the ChatGPT app itself, which is the [Plugins Directory listing](/integrations/chatgpt). Pass it as a remote `mcp` tool:

```python theme={"system"}
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)
```

Omit the `headers` entry to run on the public tier instead.

<Note>
  Keenable's search and fetch cover the same ground as a client's own built-in tools
  (`WebSearch`, `WebFetch`, `brave_search`, `tavily_search`). With both sets active an agent
  picks between them inconsistently from one call to the next, so most people turn the
  built-in ones off in client settings once Keenable is in place.
</Note>

### Claude (remote connector)

Claude (Desktop, claude.ai, mobile) connects to the remote MCP server through its **custom connector** UI — no config file needed. The connection is made from Anthropic's cloud, so your server just needs to be reachable at its public URL.

<Note>
  **[ChatGPT](/integrations/chatgpt) installs from the Plugins Directory** — Keenable is published there, so it comes from the catalog and signs you in to your Keenable account. [Codex](/integrations/codex) can install from the same listing, but has a `config.toml` route of its own.
</Note>

<Steps>
  <Step title="Add the connector">
    Keenable is a listed connector in Claude, so open **Settings → Connectors**, find **Keenable**, and there is nothing to paste.

    Otherwise add it by URL — **Settings → Connectors → Add custom connector** — leaving OAuth Client ID / Secret empty:

    ```
    https://api.keenable.ai/mcp
    ```
  </Step>

  <Step title="Connect">
    A newly added connector isn't active until you connect it. Open **Manage connectors**, find **Keenable**, and click **Connect** — the tools appear immediately. The connector UI has no field for a request header, so it runs on the public tier; use the [local bridge](#local-bridge-with-an-api-key) if you want your own key applied.
  </Step>

  <Step title="Enable it in a chat">
    In the composer, click **+ → Connectors** and toggle **Keenable** on for the conversation.
  </Step>
</Steps>

### Local bridge (with an API key)

Any client that only speaks local stdio — Claude Desktop's config file is the common case, since its connector UI has no field for a header — needs a bridge to apply your own key. `@keenable/mcp-server` runs as a subprocess and forwards to the same endpoint:

```json theme={"system"}
{
  "mcpServers": {
    "keenable": {
      "command": "npx",
      "args": ["-y", "@keenable/mcp-server"],
      "env": {
        "KEENABLE_API_KEY": "keen_<your_key>"
      }
    }
  }
}
```

Needs Node.js on the machine, and the client has to be restarted after editing its config file (for Claude Desktop, `claude_desktop_config.json`). Drop the `env` block to run it without a key. The same block works verbatim in any other stdio-only client's config, not just Claude Desktop's.

## Available tools

Two tools are exposed by the MCP server.

### `search_web_pages`

Search the web and return ranked results with URLs, titles, and descriptions.

<ParamField body="query" type="string" required>
  The search query.
</ParamField>

<ParamField body="site" type="string">
  Restrict results to a specific site (e.g. `"techcrunch.com"`).
</ParamField>

<ParamField body="acquired_after" type="string">
  Filter to pages acquired/indexed at or after this point in time.
</ParamField>

<ParamField body="acquired_before" type="string">
  Filter to pages acquired/indexed at or before this point in time.
</ParamField>

<ParamField body="published_after" type="string">
  Filter to pages published at or after this point in time.
</ParamField>

<ParamField body="published_before" type="string">
  Filter to pages published at or before this point in time.
</ParamField>

<ParamField body="query_time" type="string">
  Search the index as it stood at this point in time: pages acquired after it are excluded. Accepts a timestamp or a date (a date resolves to `00:00:00` UTC, not to the end of the day). See [Point-in-time search](#point-in-time-search).
</ParamField>

<ParamField body="snippet_max_length" type="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.
</ParamField>

<ParamField body="max_results" type="integer">
  Maximum number of results to return. Must be between 1 and 50. When omitted, up to 10 results are returned.
</ParamField>

#### 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-date` form (`YYYY-MM-DD`) — covers that whole day in UTC. On an `_after` bound it resolves to `00:00:00` on that date; on a `_before` bound it resolves to `23:59:59.999` on 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]`). When a timezone offset is not provided, the timezone is interpreted as UTC.
* **Relative delta** (`<number><unit>`, e.g. `7d`, `30min`) — resolves to the request time minus the delta, truncated to minute precision, or to [`query_time`](#point-in-time-search) minus the delta when that is set. Supported units: `min` (minutes), `h` (hours), `d` (days), `mo` (months), `y` (years).

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 |

Relative deltas may be combined with absolute values across the two bounds of a window:

```json theme={"system"}
{
  "query": "...",
  "published_after": "1y",
  "published_before": "6mo"
}
```

```json theme={"system"}
{
  "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, one acquired at `12:23:00Z` is kept.

Mind the difference between a date and a timestamp on a `_before` bound: `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` moves the whole search back to an instant: a page acquired after it is
not a candidate at all, so the answer is the one the index would have given then
rather than today's answer filtered down. Reach for it to reproduce an agent run, to
build an evaluation set that does not drift as the index grows, or to ask what was
knowable before an event.

It also **re-bases every relative delta on this request**. A delta resolves against
the request time only when `query_time` is absent; with it, `published_after: "30d"`
means thirty days before `query_time`:

```json theme={"system"}
{
  "query": "...",
  "query_time": "2026-06-13T00:00:00Z",
  "published_after": "30d"
}
```

resolves to pages published between `2026-05-14` and `2026-06-13` — not to the last
thirty days. Absolute bounds are unaffected.

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 content as clean markdown. By default only URLs from the index are supported; this is not a general web scraper. Pass `live=true` to fetch directly from the source, including URLs that are not indexed.

<ParamField body="url" type="string" required>
  The URL to fetch.
</ParamField>

<ParamField body="max_chars" type="integer" default="50000">
  Maximum number of characters of content to return. Longer content is truncated.
</ParamField>

<ParamField body="live" type="boolean" default="false">
  Fetch the page live from the source instead of returning Keenable's indexed copy. Enables fetching URLs that are not indexed.
</ParamField>

<ParamField body="prompt" type="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`.
</ParamField>

Returns `url`, `title`, and `content` (markdown). See the [Fetch reference](/api-reference/fetch) for the response shape.

## Metadata for integrators

Platforms embedding Keenable's MCP tools can read billing usage and control tool behaviour through the MCP `_meta` side channel. `_meta` travels alongside the tool result or request and is **not** part of the model-visible tool `content`, so none of it enters the agent's context.

### 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. Unauthenticated calls are unbilled and omit it.

<ParamField body="sku" type="string">
  Billing SKU for the operation: `search.realtime`, `search.pro`, `fetch`, or `fetch.live`.
</ParamField>

<ParamField body="amount" type="integer">
  Number of billed operations — always `1` for a single tool call.
</ParamField>

<ParamField body="credits" type="number">
  Credits metered for this call (the org-specific price for the operation).
</ParamField>

<ParamField body="paid" type="boolean">
  `true` when drawn from purchased credits; `false` while still on the free monthly allowance. See [credits](/credits).
</ParamField>

Example tool result:

```json theme={"system"}
{
  "content": [{ "type": "text", "text": "..." }],
  "_meta": {
    "keenable/usage": { "sku": "search.realtime", "amount": 1, "credits": 1, "paid": true }
  }
}
```

### 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.

<ParamField body="mode" type="string">
  Force the search mode: `realtime` or `pro`. Overrides any `mode` the model chose.
</ParamField>

<ParamField body="skip_cache" type="boolean">
  Bypass cached results for this call.
</ParamField>

Example request:

```json theme={"system"}
{
  "method": "tools/call",
  "params": {
    "name": "search_web_pages",
    "arguments": { "query": "..." },
    "_meta": { "keenable/overrides": { "mode": "realtime", "skip_cache": true } }
  }
}
```

Invalid or unknown override values are ignored rather than erroring the call.
