Metadata-Version: 2.4
Name: brave-api-python
Version: 0.1.0
Summary: An asynchronous Python client for the Brave Ask & Search API.
Author: iqbalmh18
License: MIT
License-File: LICENSE
Keywords: ai,async,brave,client,search
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: curl-cffi>=0.15.0
Requires-Dist: pillow>=12.2.0
Requires-Dist: pydantic>=2.11.0
Provides-Extra: mcp
Requires-Dist: fastmcp>=3.4.3; extra == 'mcp'
Description-Content-Type: text/markdown

# Brave API

<img src="./brave-api.svg" alt="Brave API banner" />

<p align="center">
An async Python client for <a href="https://search.brave.com">Brave Search</a>, providing streaming AI answers and structured web search in a single, typed interface — with a built-in Model Context Protocol (MCP) server.
</p>

---

## Table of Contents

- [Features](#features)
- [Architecture](#architecture)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Ask](#ask)
- [Search](#search)
- [Client Configuration](#client-configuration)
- [Conversation](#conversation)
- [Streaming Events](#streaming-events)
- [StreamResult](#streamresult)
- [Error Handling](#error-handling)
- [MCP Server](#mcp-server)
- [Project Structure](#project-structure)
- [Examples](#examples)
- [License](#license)

---

## Features

**Ask (AI)**

- `client.ask()` — blocking call, returns a complete `StreamResult` with text, infobox, images, videos, web results, and followups
- `client.ask_stream()` — async generator that yields `StreamEvent` objects in real time
- Multi-turn conversation support via `conversation_id` and `symmetric_key`
- Multimodal input: attach images alongside questions (vision)
- Automatic query language detection, with manual override
- Automatic `run_tool` execution for web search, image fetch, and other tool calls

**Search (Web)**

- `client.search()` — scrape structured web and news results with pagination
- `client.suggest()` — autocomplete suggestions with entity detection

**MCP Server**

- Exposes `ask`, `search`, and `suggest` as MCP tools, ready to drop into Claude Desktop, Claude Code, Cursor, or any MCP-compatible client
- Supports both **stdio** (local clients) and **HTTP/SSE** (remote or multi-client deployments) transports
- Configured entirely through environment variables — no code changes required
- Shares the same typed client and error hierarchy as the library

**General**

- Async-native, built on `curl_cffi` with browser fingerprinting (no API key required)
- Full Pydantic models for runtime validation and type safety
- Structured exception hierarchy for predictable error handling
- Configurable language, country, safesearch, geolocation, timeouts, and retries

---

## Architecture

```mermaid
graph TD
    A[Your Application] -->|imports| B[BraveClient]
    C[MCP Client<br/>Claude Desktop / Claude Code / other] -->|stdio| D[Brave API MCP Server]
    G[OpenAI-compatible Client<br/>or remote agent] -->|HTTP / SSE| D
    D -->|ask / search / suggest| B
    B --> E[HTTPClient<br/>curl_cffi]
    E --> F[Brave Search / Brave AI]

    style B fill:#2b2b2b,stroke:#888,color:#fff
    style D fill:#2b2b2b,stroke:#888,color:#fff
    style F fill:#1a1a1a,stroke:#888,color:#fff
```

The library can be used directly in Python code, or indirectly through the MCP server, which wraps the same `BraveClient` and exposes it as tools for LLM-based agents.

---

## Requirements

- Python 3.11+
- Dependencies: `curl-cffi`, `pydantic`, `pillow`
- Optional (MCP server): `fastmcp`

---

## Installation

```bash
uv pip install git+https://github.com/iqbalmh18/brave-api.git
```

From source:

```bash
git clone https://github.com/iqbalmh18/brave-api
cd brave-api
uv pip install -e ".[dev]"
```

With MCP server support:

```bash
uv pip install "brave-api[mcp] @ git+https://github.com/iqbalmh18/brave-api.git"
```

---

## Quick Start

```python
import asyncio
from brave_api import BraveClient

async def main():
    async with BraveClient() as client:
        # AI answer
        result = await client.ask("what is quantum computing?")
        print(result.text)

        # Web search
        search = await client.search("python asyncio tutorial")
        for item in search.web:
            print(item.title, item.url)

asyncio.run(main())
```

---

## Ask

### ask() — blocking, full result

```python
async with BraveClient() as client:
    result = await client.ask("mount bromo indonesia")

print(result.text)           # AI answer text (markdown)

if result.infobox:
    print(result.infobox.title)      # "Mount Bromo"
    print(result.infobox.subtitle)   # "Active volcano in East Java"
    print(result.infobox.url)        # Wikipedia URL
    print(result.infobox.image_url)  # entity image

for img in result.images:
    print(img.url, img.thumbnail)

for vid in result.videos:
    print(vid.title, vid.url)

for web in result.web_results:
    print(web.title, web.url)

for q in result.followups:
    print(q)
```

With an image (vision/multimodal):

```python
from pathlib import Path

result = await client.ask("what is in this image?", image=Path("photo.jpg"))
```

### ask_stream() — real-time streaming

```python
async for event in client.ask_stream("what is Space X?"):
    if event.type is StreamEventType.TEXT_DELTA:
        print(event.delta, end="", flush=True)
    elif event.type is StreamEventType.TEXT_STOP:
        print()
    elif event.type is StreamEventType.FOLLOWUPS:
        for q in event.payload.get("followups", []):
            print(f"-> {q}")
```

### Comparison

| Method | Mode | Returns | Best for |
|---|---|---|---|
| `client.ask()` | Blocking | `StreamResult` | Full result at once (infobox, images, etc.) |
| `client.ask_stream()` | Streaming | `AsyncGenerator[StreamEvent]` | Typewriter output |
| `conversation()` + `collect()` | Blocking | `StreamResult` | Multi-turn, image input, full control |
| `conversation()` + `stream_events()` | Streaming | `AsyncGenerator[StreamEvent]` | Streaming + multi-turn |

---

## Search

### search() — web and news results

```python
async with BraveClient() as client:
    result = await client.search("python asyncio tutorial")

print(result.query)          # original query
print(len(result.web))       # number of web results
print(len(result.news))      # number of news results

for item in result.web:
    print(item.title)
    print(item.url)
    print(item.description)
    print(item.age)          # "2 days ago", etc.

for item in result.news:
    print(item.title, item.source, item.age)

# All unique URLs in a flat list
for url in result.urls:
    print(url)
```

Pagination:

```python
# Page 1 (default)
page1 = await client.search("rust programming", offset=0)

# Page 2
page2 = await client.search("rust programming", offset=1)
```

Disable spellcheck for exact keyword matching:

```python
result = await client.search("pyton tutorial", spellcheck=False)
```

### suggest() — autocomplete

```python
suggestions = await client.suggest("elon")
for s in suggestions:
    print(s.text, s.entity_type, s.is_entity)
    if s.thumbnail:
        print(s.thumbnail)
```

---

## Client Configuration

`ClientConfig` is a frozen Pydantic model. All fields have safe defaults.

```python
from brave_api import BraveClient, ClientConfig

config = ClientConfig(
    # Language and region
    language="id",                   # Response language: "id", "en", etc.
    ui_lang="id-id",                 # UI language: "id-id", "en-us", etc.
    country="id",                    # ISO 3166-1 country code
    geoloc="-6.200x106.816",         # lat x lng (Jakarta)

    # Search
    safesearch="moderate",           # "off", "moderate", or "strict"
    units_of_measurement="metric",   # "metric" or "imperial"

    # Mode
    enable_research=False,           # True = deep research mode

    # HTTP
    request_timeout_seconds=60.0,
    max_retries=3,
    retry_backoff_seconds=1.5,

    # Browser fingerprinting
    impersonate="chrome136",
    extra_headers={"X-Custom": "value"},
)

async with BraveClient(config) as client:
    ...
```

---

## Conversation

```python
async with BraveClient() as client:
    # New conversation
    conv = await client.conversation("explain how DNS works")
    result = await conv.collect()

    # Continue the same conversation
    conv2 = await client.conversation(
        "what is DNSSEC?",
        conversation_id=conv.id,
        symmetric_key=conv.symmetric_key,
    )
    result2 = await conv2.collect()
```

Key `conversation()` parameters:

| Parameter | Type | Description |
|---|---|---|
| `query` | `str` | Question or prompt (required) |
| `conversation_id` | `str \| None` | Continue an existing conversation |
| `symmetric_key` | `str \| None` | Required when `conversation_id` is set |
| `image` | `bytes \| str \| Path \| None` | Image for multimodal input |
| `language` | `str \| None` | Override response language |
| `query_type` | `str` | See `QueryType` enum |
| `auto_tools` | `bool` | Auto-execute tool calls (default: `True`) |
| `context` | `str \| None` | Article/passage context |
| `quote` | `str \| None` | Highlighted text span |

---

## Streaming Events

```python
async for event in conv.stream_events():
    if event.type is StreamEventType.TEXT_DELTA:
        print(event.delta, end="", flush=True)
    elif event.type is StreamEventType.TEXT_STOP:
        print()
    elif event.type is StreamEventType.ERROR:
        print(f"Error: {event.error_message}")
```

Key event types:

```
TEXT_DELTA / TEXT_STOP                    response text tokens
THINKING_DELTA / THINKING_STOP            chain-of-thought reasoning
TOOL_USE                                  server requests a tool call
AUGMENT_WITH_TOOL_USE                     run_tool result (web results, images, etc.)
AUGMENT_WITH_WEB / NEWS / IMAGES / VIDEOS enrichment data
AUGMENT_WITH_INFOBOX                      entity knowledge card
FOLLOWUPS                                 suggested follow-up questions
ERROR                                     server error event
CHALLENGE                                 CAPTCHA required
```

The sequence below shows how these events flow during a single `ask()` call:

```mermaid
sequenceDiagram
    participant App
    participant Client as BraveClient
    participant Brave as Brave AI

    App->>Client: ask(query)
    Client->>Brave: POST /api/tap/v1/new
    Brave-->>Client: SSE stream

    loop until stream ends
        Brave-->>Client: TEXT_DELTA / THINKING_DELTA
        Brave-->>Client: AUGMENT_WITH_WEB / IMAGES / INFOBOX
    end

    Brave-->>Client: FOLLOWUPS
    Client-->>App: StreamResult
```

---

## StreamResult

```python
result = await conv.collect()

result.text            # str - full AI answer (markdown)
result.thinking         # str - chain-of-thought reasoning (if any)
result.urls             # list[str] - unique URLs found
result.images           # list[ImageResult]
result.videos           # list[VideoResult]
result.web_results      # list[WebResult]
result.infobox          # Infobox | None
result.followups        # list[str]
result.citations        # list[dict] - raw tool result payloads
result.inline_entities  # list[dict]
result.raw_events       # list[StreamEvent] - every event for debugging
result.state            # StreamState enum
result.is_complete      # bool
result.has_images       # bool
result.has_videos       # bool
result.has_infobox      # bool
result.has_tool_calls   # bool
```

---

## Error Handling

All exceptions inherit from `BraveAPIError`.

```
BraveAPIError
├── TransportError          network error, timeout, connection reset
├── HTTPStatusError         non-2xx HTTP response (.status_code, .response_text)
├── TokenExtractionError    could not parse auth token from server HTML
├── ConversationError       /api/tap/v1/new did not return a conversation id
├── StreamAbortedError      server sent an error event mid-stream
├── ChallengeRequiredError  server sent a CAPTCHA challenge
└── InvalidResponseError    response was not valid JSON or unexpected shape
```

```python
from brave_api.exceptions import (
    BraveAPIError, ChallengeRequiredError, HTTPStatusError,
    StreamAbortedError, TransportError,
)

try:
    async with BraveClient() as client:
        result = await client.ask("what is rust?")
except ChallengeRequiredError:
    print("CAPTCHA required")
except HTTPStatusError as e:
    print(f"HTTP {e.status_code}: {e.response_text[:200]}")
except TransportError as e:
    print(f"Network error: {e}")
except StreamAbortedError as e:
    print(f"Stream aborted: {e}")
except BraveAPIError as e:
    print(f"Error: {e}")
```

Retry strategy: HTTP 429 and 5xx responses are retried with exponential backoff (`backoff_seconds * 2^attempt`).

The MCP server reuses this same hierarchy: any `BraveAPIError` raised by the client is caught and surfaced to the calling MCP client as a `ToolError`, so agents receive a clean, descriptive message instead of a raw stack trace.

---

## MCP Server

<img src="./brave-api-mcp.svg" alt="Brave API MCP banner" />

Brave API ships with a [Model Context Protocol](https://modelcontextprotocol.io) server built on [FastMCP](https://gofastmcp.com), exposing the client's core capabilities as tools for any MCP-compatible agent (Claude Desktop, Claude Code, Cursor, etc.).

### Tools

| Tool | Description | Read-only |
|---|---|---|
| `ask` | Ask Brave AI a question and receive a complete AI-generated answer with citations, source URLs, images, videos, and follow-up suggestions | Yes |
| `search` | Search Brave and return structured web and news results (raw SERP, no AI answer) | Yes |
| `suggest` | Fetch autocomplete suggestions for a partial query, including rich entity suggestions with thumbnails | Yes |

### Running the server

**stdio** (default — for local clients like Claude Desktop, Claude Code, Cursor):

```bash
python -m brave_api.mcp.server
# or via the CLI entry-point
brave-api-mcp
```

The server communicates over stdio and is meant to be launched by an MCP client, not run standalone in a terminal for interactive use.

**HTTP** (for remote or multi-client deployments, OpenAI-compatible clients):

```bash
brave-api-mcp --http
# bind to a specific host/port
brave-api-mcp --http --host 0.0.0.0 --port 8000
```

Full CLI reference:

```
usage: brave-api-mcp [-h] [--http] [--host HOST] [--port PORT]
                     [--log-level {debug,info,warning,error,critical}]

options:
  --http              Run with HTTP/SSE transport instead of stdio.
  --host HOST         Host address to bind to (HTTP transport only). [default: 127.0.0.1]
  --port PORT         Port to bind to (HTTP transport only). [default: 8000]
  --log-level LEVEL   Logging level. [default: warning]
```

### Configuring an MCP client

**Claude Desktop / Claude Code** (`claude_desktop_config.json` or `~/.claude/claude_code_config.json`):

```json
{
  "mcpServers": {
    "brave-api": {
      "command": "python",
      "args": ["-m", "brave_api.mcp.server"],
      "env": {
        "BRAVE_COUNTRY": "id",
        "BRAVE_LANGUAGE": "id",
        "BRAVE_SAFESEARCH": "moderate"
      }
    }
  }
}
```

Or via the Claude Code CLI:

```bash
claude mcp add brave-api python -- -m brave_api.mcp.server
```

**OpenAI-compatible clients / remote deployments** — start the server in HTTP mode and point the client at the endpoint:

```bash
brave-api-mcp --http --host 0.0.0.0 --port 8000
```

The server exposes a standard MCP-over-HTTP (Streamable HTTP / SSE) endpoint at `http://<host>:<port>/mcp`. Any client that supports the MCP HTTP transport can connect to it directly.

### Environment variables

All server behavior is controlled through environment variables — no code changes required.

| Variable | Default | Description |
|---|---|---|
| `BRAVE_BASE_URL` | `https://search.brave.com` | Base URL for the Brave endpoints |
| `BRAVE_GEOLOC` | library default | Geolocation as `lat x lng` |
| `BRAVE_COUNTRY` | library default | ISO 3166-1 country code |
| `BRAVE_LANGUAGE` | library default | Response language (BCP-47) |
| `BRAVE_UI_LANG` | library default | UI language, e.g. `en-us` |
| `BRAVE_SAFESEARCH` | library default | `off`, `moderate`, or `strict` |
| `BRAVE_ENABLE_RESEARCH` | `false` | `true`/`false`/`1`/`0`/`yes`/`no` — enables deep research mode |
| `BRAVE_REQUEST_TIMEOUT` | library default | Request timeout in seconds |
| `BRAVE_MAX_RETRIES` | library default | Maximum retry attempts on transient failures |
| `BRAVE_MAX_CONCURRENT` | library default | Maximum concurrent requests |

Invalid numeric or boolean values fall back to their defaults, with a warning logged rather than raising an error at startup.

### Error surface

Every tool call is wrapped so that any `BraveAPIError` raised by the underlying client is converted into an MCP `ToolError` with the original message, keeping error handling consistent between direct library use and MCP-based use.

---

## Examples

| File | Description |
|---|---|
| `examples/quick_start.py` | Simplest usage - ask and print |
| `examples/stream_events.py` | Real-time token streaming |
| `examples/web_results_and_urls.py` | Web results, thumbnails, URLs |
| `examples/images_and_videos.py` | Image and video results |
| `examples/multi_turn_conversation.py` | Multi-turn + answer regeneration |
| `examples/client_config.py` | All ClientConfig options |
| `examples/language_override.py` | Auto-detect vs explicit language |
| `examples/multimodal_image_input.py` | Vision - attach image to query |
| `examples/context_and_quote.py` | Context and quote parameters |
| `examples/auto_tools_control.py` | auto_tools=True vs False |
| `examples/exception_handling.py` | All exception types + stream state |
| `examples/inline_entities_and_citations.py` | Inline entities and tool citations |
| `examples/context_manager_vs_manual.py` | Client lifecycle patterns |
| `examples/raw_events_inspection.py` | Inspect every raw event |
| `examples/thinking_mode.py` | Chain-of-thought reasoning |
| `examples/interactive_chat.py` | Terminal REPL chat |
| `examples/ask_method.py` | ask() and ask_stream() demos |
| `examples/search_method.py` | search() and suggest() |

---

## License

This project is licensed under the terms of the license found in the [LICENSE](https://github.com/iqbalmh18/brave-api/blob/main/LICENSE) file.
