Skip to content

iqbalmh18/brave-api

Repository files navigation

Brave API

Brave API banner

An async Python client for Brave Search, providing streaming AI answers and structured web search in a single, typed interface — with a built-in Model Context Protocol (MCP) server.


Table of Contents


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

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
Loading

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

uv pip install brave-api-python

From source:

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

With MCP server support:

uv pip install "brave-api-python[mcp]"

Quick Start

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

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):

from pathlib import Path

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

ask_stream() — real-time streaming

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

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:

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

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

suggest() — autocomplete

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.

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

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

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:

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
Loading

StreamResult

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

Brave API MCP banner

Brave API ships with a Model Context Protocol server built on FastMCP, 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):

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):

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):

{
  "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:

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:

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

About

Brave API is totally free. No login. No API key. No bullshit.

Topics

Resources

License

Stars

40 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages