Brave

NAME

MCP::Server::Tool::Web::Provider::Brave - Brave Search adapter for web_search

SYNOPSIS


use MCP::Server::Tool::Web::Provider::Brave;

# BRAVE_API_KEY set in the environment:
my $brave = MCP::Server::Tool::Web::Provider::Brave.new;
my @rows = $brave.search('raku json::fast api', 5);
# @rows[0]<title> / <url> / <snippet> / <age>?

# Or explicit, e.g. a key vaulted under a different name, or a
# self-hosted Brave-compatible proxy:
my $brave2 = MCP::Server::Tool::Web::Provider::Brave.new(
    api-key     => $vault.fetch('brave-search'),
    api-url     => 'https://search.internal.example.com/res/v1/web/search',
    country     => 'GB',
    search-lang => 'en',
);

DESCRIPTION

Default MCP::Server::Tool::Web::SearchProvider implementation, talking to the Brave Search API's web/search endpoint. This is what MCP::Server::Tool::Web's web_search tool uses unless a different provider is configured or an alternative MCP::Server::Tool::Web::SearchProvider is injected for testing.

KEY RESOLUTION

The API key is resolved at call time, inside .search, never at construction. This matters: a MCP::Server::Tool::Web instance with no key configured anywhere must still construct and register all four web_* tools โ€” web_fetch, web_crawl and web_grep don't need a search key at all, and only web_search should fail, with a teaching message, the moment it's actually called without one.

Resolution order, first match wins:

  • $.api-key โ€” used verbatim when set to a non-empty string.

  • %*ENV{$.api-key-env} โ€” $.api-key-env defaults to 'BRAVE_API_KEY' but names whatever environment variable the operator points it at; used when non-empty.

  • Neither is set (or both are empty) โ€” .search dies with a message naming the environment variable actually in play (i.e. the current value of $.api-key-env, not always the literal string "BRAVE_API_KEY") and both config keys that would supply it.

SSRF GUARD โ€” DELIBERATELY NOT APPLIED HERE

This provider does NOT go through the pack's SSRF Guard. That guard exists to stop a model-supplied URL (from web_fetch / web_crawl / web_grep) from being used to probe the operator's own network โ€” the URL in those cases comes from untrusted model output. $.api-url here is the opposite: it is set once, by whoever deploys MCP::Server::Tool::Web, in the same config that also chooses the provider class. Refusing loopback/private addresses on it would break the entirely legitimate case of a self-hosted search backend (a SearXNG instance, an internal Brave-compatible proxy, ...) running on the operator's own LAN or even localhost. If $.api-url is ever made settable from an untrusted source, THAT call site is responsible for guarding it โ€” this class intentionally does not.

REQUEST SHAPE

GET $.api-url with query parameters q (the query), count, safesearch, and country / search_lang only when those attributes are set. Headers: X-Subscription-Token (the resolved key), Accept: application/json, and Cache-Control: no-cache โ€” the last of these is not cosmetic: Brave has been observed in the field to answer a request that omits it with a 422 that has nothing to do with the query, so it is sent on every call. Accept-Encoding: gzip is deliberately not sent: Cro's body-blob does not decompress a gzipped response unless the optional Compress::Zlib module happens to be installed, and this class hand-decodes body-blob itself (see !blob-text below) โ€” an environment-dependent decompression step is not something this dist can rely on, so the request stays identity-encoded and Brave answers in kind.

ERRORS

Every failure mode .search can hit is turned into a die with a teaching first line, per the MCP::Server::Tool::Web::SearchProvider contract:

  • 401 / 403 โ€” names the environment variable / config key the key was actually read from.

  • 422 โ€” "Brave refused the request as invalid", Brave's own error message when the body carries one, and a reminder of the 400-character / 50-word query limit (the most common cause).

  • 429 โ€” "Brave rate-limited this search", the X-RateLimit-Reset seconds when Brave sent that header, and a note that only successful requests count against quota (so retrying after the reset is safe).

  • Other 5xx โ€” described as transient; safe to retry.

  • Timeout โ€” names the seconds waited and the timeout config key that controls it.

  • Connection failure (refused, reset, DNS, ...) โ€” names the host and a one-line cause.

  • An unparseable (non-JSON) response body โ€” quotes its first 200 characters, which is usually enough to recognise a captive portal or an HTML error page from an intermediary proxy.

  • A gzip-compressed response body (the gzip magic bytes 1f 8b at the start of body-blob) โ€” this client cannot decompress it (see REQUEST SHAPE above on why Accept-Encoding: gzip is never sent) and dies with a diagnostic that says so plainly, rather than feeding a latin-1 mangling of the compressed bytes to from-json and reporting a confusing "not JSON" error instead.

AUTHOR

Matt Doughty

COPYRIGHT AND LICENSE

Copyright 2026 Matt Doughty

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

MCP::Server::Tool::Web v0.1.1

web search, fetch, crawl and grep for MCP::Server

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

MCP::Server:auth<zef:apogee>:ver<0.6.0+>Cro::HTTP:auth<zef:cro>:ver<0.8.11+>Cro::Core:auth<zef:cro>:ver<0.8.10+>IO::Socket::Async::SSL:auth<zef:raku-community-modules>:ver<0.8.2+>JSON::Fast:ver<0.19>:auth<cpan:TIMOTIMO>

Test Dependencies

Provides

  • MCP::Server::Tool::Web
  • MCP::Server::Tool::Web::Addr
  • MCP::Server::Tool::Web::Budget
  • MCP::Server::Tool::Web::Crawl
  • MCP::Server::Tool::Web::Extract
  • MCP::Server::Tool::Web::Fetcher
  • MCP::Server::Tool::Web::Guard
  • MCP::Server::Tool::Web::Provider::Brave
  • MCP::Server::Tool::Web::Robots
  • MCP::Server::Tool::Web::SearchProvider
  • MCP::Server::Tool::Web::Transport
  • MCP::Server::Tool::Web::Url
  • MCP::Server::Tool::Web::X

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite โ€” the markup and publishing tools behind this site.