- Purpose: expose Brave Search capabilities as an MCP server that works in stdio clients, streamable HTTP clients, MCP Apps hosts, and ChatGPT/OpenAI widget hosts.
- Primary goals:
- Provide reliable, typed access to Brave web, image, news, video, local, and LLM-context search.
- Keep the server transport-agnostic so the same tool layer works in stdio and HTTP modes.
- Keep UI concerns optional and isolated so non-UI MCP clients do not pay a complexity tax.
- Preserve a thin shared SDK layer with one stable package-root import surface so Brave API integration logic does not spread across the app.
- Ensure host widgets can be served safely inside sandboxed iframes as self-contained HTML documents.
- Success criteria:
- New search capabilities can be added as one new tool slice plus optional UI widgets.
- All Brave API calls originate from the shared
packages/brave-searchpackage. - The server can run statelessly over HTTP and simply over stdio with the same tool behavior.
- UI resources remain build-time assets, not runtime server-side rendering concerns.
- Non-goals:
- No persistent application datastore.
- No user/session management inside the server.
- No business workflow orchestration beyond search execution and result shaping.
- No host-specific logic inside core search execution paths.
- Assumptions:
BRAVE_API_KEYis provided at runtime.- HTTP mode is intended for trusted/private deployments unless host protection or auth is added upstream.
- Search freshness, pagination, and limits are bounded by Brave API constraints, not by local storage.
- Chosen style: Layered monorepo with vertical search slices.
- Why this fits:
- The repo already has a clean split between integration code (
packages/brave-search), server orchestration (apps/brave-search-mcp/src), and host UIs (apps/brave-search-mcp/ui). - Each search capability is a vertical slice with its own schema, API call, result formatting, and optional UI.
- Cross-cutting infrastructure such as transports, server registration, logging, and UI resource loading stays centralized.
- The repo already has a clean split between integration code (
- Required layering:
- Layer 1: Brave API wrapper in
packages/brave-search. - Layer 2: MCP server orchestration and tool registration in
apps/brave-search-mcp/src. - Layer 3: Tool slices in
apps/brave-search-mcp/src/tools. - Layer 4: Optional host UI widgets in
apps/brave-search-mcp/ui.
- Layer 1: Brave API wrapper in
- Trade-off:
- This favors clarity and host portability over aggressive abstraction. Some search tools will look similar by design.
Brave Search SDK:- Owns all HTTP calls to Brave endpoints.
- Owns Brave request formatting, polling behavior, response typing, and API error shaping.
- Owns the package-root export surface via
packages/brave-search/src/index.ts. - Compiles as an internal ESM workspace package with
dist/index.jsanddist/index.d.tsas the supported runtime/type entrypoints. - Must not import app/server code.
MCP Server Bootstrap:- Owns process startup, env validation, runtime mode selection, and transport startup.
- Files:
src/index.ts,src/server-utils.ts.
MCP Server Composition:- Owns
McpServercreation, tool instantiation, dependency injection, and registration mode selection. - File:
src/server.ts.
- Owns
Tool Slices:- One class per user-facing tool.
- Own input schema, execution path, structured-content contract, and user-visible tool description.
- Files:
src/tools/Brave*Tool.ts.
Tool Catalog and Names:- Own the canonical tool-name list, manifest tool metadata, and the small shared type surface for tool keys and widget variants.
- Keep one readable TypeScript source of truth for both runtime data and TypeScript types.
- File:
src/tool-catalog.ts.
Tool Shared Helpers:- Own shared result builders, error wrappers, schema helpers, and local fallback contract types.
- File:
src/tools/tool-helpers.ts.
UI Resource Registration:- Owns mapping from tool slices to UI resource URIs, CSP, and host-specific metadata.
- Files:
src/server-ui.ts,src/ui-resources.ts.
Widget Runtime:- Owns client-side rendering, host bridge integration, pagination UX, and context-selection UX.
- Files:
ui/src/lib/**,ui/src/hooks/**. - Delivery rule: each widget entrypoint must compile to a single self-contained HTML file with its JS and CSS bundled/inlined.
/apps/brave-search-mcp- Deployable MCP server package.
/apps/brave-search-mcp/src- Server bootstrap, transport wiring, tool classes, and UI resource registration only.
/apps/brave-search-mcp/src/tool-catalog.ts- Canonical tool metadata and types used by server code, tests, and manifest validation.
/apps/brave-search-mcp/src/tools- Vertical search slices only. New search behavior must start here.
/apps/brave-search-mcp/ui- Build-time widget source only. No server bootstrap logic here.
/apps/brave-search-mcp/test- Unit, integration, and eval coverage for the app package.
/packages/brave-search/src- Shared Brave Search SDK, package-root export surface, and typed models.
/scripts- Repo-level maintenance automation only.
- Rules:
- Brave HTTP integration code must live only in
packages/brave-search. - App and test code must import the SDK from
brave-search, never frompackages/brave-search/dist/*. - MCP registration and transport code must live only in
apps/brave-search-mcp/src. - Server, tool, and test code should import from
apps/brave-search-mcp/src/tool-catalog.ts; UI code should reach the same module through the configured@tool-catalogalias. - UI host bridge code must live only in
apps/brave-search-mcp/ui/src/hooks. - Tool classes may format tool outputs, but they must not read widget bundles directly.
- Widget entrypoints must remain single-file HTML outputs suitable for iframe embedding.
dist/is generated output and never hand-edited.
- Brave HTTP integration code must live only in
- Standard stdio flow:
- Client invokes MCP tool.
src/index.tsvalidates env and createsBraveMcpServer.BraveMcpServerroutes to a tool class.- Tool class validates input with
zod, callsBraveSearch, reshapes results, and returns MCP content.
- Manifest validation flow:
- Canonical manifest tool entries are defined in
src/tool-catalog.ts. test/unit/manifest-tool-names.test.tscomparesmanifest.jsonagainst that canonical array.pnpm -C apps/brave-search-mcp run check:manifest:toolsand the packagecheckscript fail on drift instead of rewritingmanifest.json.
- Canonical manifest tool entries are defined in
- Streamable HTTP flow:
- Hono receives
/mcp. - A fresh
McpServerand transport are created per request. - Tool execution is stateless; no session data is retained between requests.
- Hono receives
- UI-enabled flow:
- Server registers UI resources and tool metadata through
@modelcontextprotocol/ext-apps. - Tool returns minimal text plus structured content.
- Host renders the registered widget inside an iframe.
- Widget consumes structured content and controls what enters model context.
- Because the widget runs in an iframe as a served HTML resource, all runtime code and styles must already be bundled into that HTML file.
- Server registers UI resources and tool metadata through
- Local search special flow:
- Tool performs Brave web search filtered to locations.
- If first-page local results are empty, it calls the web tool core as a fallback.
- POI details and descriptions are fetched through dedicated Brave local endpoints.
- LLM context flow:
- Tool calls Brave LLM context endpoint.
- Compact mode applies local filtering, deduping, and output budgeting before returning text.
- Full mode preserves rawer Brave snippets.
- Boundary rules:
- Only tool classes may call the shared SDK.
- Tool and test code must treat
brave-searchpackage root exports as the only supported SDK boundary. - UI code may call back into server tools through host bridges, but it must not know SDK internals.
- Transport code must not contain tool-specific formatting logic.
- Authn/authz:
- Current model: API key to Brave only; no end-user auth in the MCP server itself.
- HTTP mode relies on bind address and optional
ALLOWED_HOSTSvalidation for basic protection. - Default policy: keep the server read-only and assume upstream auth/containment if exposed beyond localhost.
- Logging and observability:
- Tool logs flow through MCP logging messages.
- Process startup and HTTP failures log to stderr/stdout.
- No metrics backend exists today.
- Default: add structured logging at server/tool boundaries before adding any new observability sink.
- Error handling:
- Shared SDK throws API-focused errors.
- Tools convert failures into MCP-friendly error results, preserving structured UI payloads where practical.
- HTTP transport returns JSON-RPC internal error envelopes on transport failures.
- Configuration and secrets:
- Required:
BRAVE_API_KEY. - Optional:
PORT,HOST,ALLOWED_HOSTS, CLI flags like--httpand--ui. - Secrets must come from env, never from checked-in files or widget code.
- Required:
- Datastores:
- None. The system is stateless and request-scoped.
- External services:
- Brave Search REST API for all search and LLM-context retrieval.
- OpenAI CDN and host widget runtime for ChatGPT/OpenAI widget rendering.
- OpenStreetMap tiles for local-search maps.
- Optional external video/image hosts used by returned content.
- Integration pattern:
- Synchronous request/response for all search endpoints.
- Polling only inside the SDK where Brave summarization requires it.
- UI assets are prebuilt static single-file HTML bundles served as MCP/UI resources.
- Widget code is not assembled dynamically at request time.
- High-level schemas:
- Tool input schemas are defined with
zodper tool. - Tool structured output is the stable contract between tool execution and widgets.
- Brave API response types are owned by
packages/brave-search/src/types.ts.
- Tool input schemas are defined with
- Runtime:
- Node.js 20+ for package execution.
- Docker image also supported for HTTP deployments.
- Deployment modes:
stdio: default for MCP desktop/CLI clients.HTTP: stateless streamable MCP over Hono at/mcp.HTTP + UI: adds app/widget resources for MCP Apps and ChatGPT-compatible hosts.
- Release artifacts:
- NPM package for
brave-search-mcp. - Static iframe-ready UI bundles inside
dist/ui. - Manifest/package metadata for MCP packaging flows.
manifest.jsonremains a checked-in artifact that is validated against source metadata rather than auto-synced by a maintenance script.
- NPM package for
- Environment differences:
- Local development: pnpm workspace with package-local
devtasks; rootpnpm run devstarts SDK and app watch workflows through Turbo. - Test: injected mock
BraveSearchand dedicated test server entrypoint. - Root
pnpm run testdelegates toapps/brave-search-mcpand serves as the repo's documented automated test entrypoint. - Direct app build, check, typecheck, and test scripts rebuild the internal SDK explicitly before consuming it, and the integration test path also rebuilds the app server entrypoint before launching
dist/index.js, so clean-state workflows do not rely on leftoverpackages/brave-search/distorapps/brave-search-mcp/distoutput. - Direct Vitest execution resolves
brave-searchthrough the workspace source entrypoint so package-root imports remain valid even when SDKdist/has been cleaned. - Production: built
distoutput with runtime env vars only. - UI build: Vite plus single-file bundling produces inline HTML assets from
ui/src/lib/*/(mcp-app.html|chatgpt-app.html).
- Local development: pnpm workspace with package-local
- Use a shared SDK package for Brave API access.
- Rationale: centralizes typing and HTTP behavior.
- Trade-off: SDK changes can affect both current and future consumers.
- Treat the shared SDK as an internal ESM workspace package with one supported package-root import surface.
- Rationale: keeps app/tool code off fragile
dist/*deep imports and makes package boundaries explicit. - Trade-off: app scripts and tests must encode workspace-resolution behavior intentionally instead of relying on incidental prebuilt output.
- Rationale: keeps app/tool code off fragile
- Keep one class per MCP tool.
- Rationale: input schema, description, and output contract stay co-located.
- Trade-off: some duplication across similar search tools is acceptable.
- Create fresh HTTP server instances per request.
- Rationale: keeps HTTP mode stateless and simple.
- Trade-off: more object churn than a long-lived session architecture.
- Treat UI as optional structured-content consumers.
- Rationale: non-UI MCP clients stay lightweight.
- Trade-off: UI and non-UI modes need slightly different tool response text.
- Bundle widget assets into standalone HTML entrypoints.
- Rationale: host runtimes load widgets in iframes via registered HTML resources.
- Trade-off: widget bundles must stay lean because there is no follow-up asset graph to load.
- Keep local-search fallback inside the tool layer.
- Rationale: fallback is a product behavior, not a transport concern.
- Trade-off: local tool depends on web-tool core behavior.
- Rebuild the internal SDK explicitly in direct app workflows instead of relying on install-time or cached build output.
- Rationale: documented commands such as
build,build:watch,check,typecheck, andtest:unitmust work from a clean workspace. - Trade-off: some app commands do extra SDK build work up front to keep behavior predictable.
- Rationale: documented commands such as
- Keep one TypeScript source of truth for tool metadata and validate manifest drift explicitly.
- Rationale:
src/tool-catalog.tsnow holds both runtime values and exported types, andcheck:manifest:toolsmakes manifest mismatches visible without hidden mutation. - Trade-off: contributors must update
manifest.jsonintentionally when tool metadata changes, but the workflow is easier to read than a sync script plus handwritten.d.tssplit.
- Rationale:
- Expose a dedicated root
testcommand while keepingcheckseparate.- Rationale:
checkstays a fast static-validation command, whiletestruns the app's executable unit + integration suite from the repo root. - Trade-off: contributors need to remember two validation commands, but each one keeps a clear purpose and predictable runtime cost.
- Rationale:
- Enforce read-only tool annotations.
- Rationale: search tools should never mutate external systems.
- Trade-off: richer workflows belong outside this server.
flowchart LR
User["User / MCP Client"] --> Host["Host Runtime\n(ChatGPT, MCP App host, Claude, CLI)"]
Host --> Server["Brave Search MCP Server"]
Server --> Brave["Brave Search API"]
Host --> Widget["Optional UI Widgets"]
Widget --> Server
Widget --> Ext["External Media / Map Assets"]
flowchart TB
subgraph Monorepo["brave-search-mcp monorepo"]
subgraph App["apps/brave-search-mcp"]
Bootstrap["index.ts + server-utils.ts"]
Compose["server.ts"]
Tools["Tool slices\nBraveWeb/Image/News/Video/Local/LLMContext"]
UiReg["server-ui.ts + ui-resources.ts"]
Widgets["ui/src widgets + hooks"]
end
subgraph SDK["packages/brave-search"]
BraveSdk["BraveSearch SDK"]
BraveTypes["Typed Brave API models"]
end
end
Bootstrap --> Compose
Compose --> Tools
Compose --> UiReg
Tools --> BraveSdk
BraveSdk --> BraveTypes
UiReg --> Widgets
flowchart LR
Request["Tool request"] --> Schema["Zod input schema"]
Schema --> Tool["Tool class"]
Tool --> SDK["BraveSearch SDK"]
SDK --> Brave["Brave API"]
Brave --> SDK
SDK --> Tool
Tool --> Result["MCP text + structured content"]
Result --> Widget["Optional widget rendering"]
- Do not call Brave HTTP endpoints directly from
apps/brave-search-mcp; route all such calls throughpackages/brave-search. - Do not mix transport concerns into tool classes.
- Do not add persistent state, caches, or session stores without first updating this architecture.
- Do not let widget code import server-only modules or Node built-ins.
- Do not make UI mode the source of truth for search behavior; tool classes remain authoritative.
- Do not add host-specific branches inside the shared SDK.
- Do not introduce widget architectures that require separate JS/CSS asset serving at runtime; widgets must stay self-contained HTML resources for iframe hosts.
- Do not hand-edit generated
dist/output. - Do not weaken read-only guarantees by adding mutating tools to this package without a new architecture review.
- Should HTTP mode gain first-class auth or rate limiting for safer internet exposure?
- Should common result-formatting logic across search tools be consolidated further, or is current duplication the clearer trade-off?
- Should widget and MCP App host support continue to share one UI codebase, or eventually split by host runtime?