Skip to main content
Glama
nazerim

brave-answers-mcp

by nazerim
README.md
# brave-answers-mcp

[![CI](https://github.com/nazerim/brave-answers-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nazerim/brave-answers-mcp/actions/workflows/ci.yml)

TypeScript MCP server (stdio) wrapping the **Brave Answers** API
(`POST https://api.search.brave.com/res/v1/chat/completions`, header
`X-Subscription-Token`). The Brave Answers plan is billed separately from the
Brave Search plan — the key is provided as the `BRAVE_ANSWERS_KEY` env var
(never commit or print it).

Why it exists: no MCP wraps this endpoint (verified 2026-08), and research-mode
calls run ~90–300s — far beyond a blocking agent tool call. This server adds
the missing async **submit → status → result** semantics: the job lives in the
MCP server process, so it outlives bash timeouts and turn boundaries. The SSE
parser was ported from a working production browser implementation (streaming
pass-through only) and extended with the `<answer>` tag and display
normalization.

## Quick start

```sh
npm install
npm run build
export BRAVE_ANSWERS_KEY=...
node dist/index.js    # speaks MCP over stdio
```

Register it in your agent's MCP config (opencode example below).

## Tools

| Tool | Mode | Latency | Cost (all measured 2026-08-16) |
|---|---|---|---|
| `answers` | sync single-search, streaming | ~10–30s | $0.051–0.055/call (citations on/off; entities n/a) |
| `research_submit` | async research, returns `research_id` immediately | ~90–312s (wall can exceed budget — see gotchas) | $0.069–0.084 narrow, **$0.771** for a 10-query broad run — see cost model |
| `research_status` | poll job | instant | free |
| `research_result` | fetch completed job | instant | free |

`answers` params: `query`, `country?`, `language?`, `enable_citations?`
(default true). (`enable_entities` exists in the API but is silently ignored —
verified via raw SSE probe 2026-08-16: zero `<enum_item>` frames — so it is not
exposed; the parser branch is kept in case it ships later.)

`research_submit` params: `query`, `country?`, `language?`,
`research_maximum_number_of_iterations?` (1–5, default 4),
`research_maximum_number_of_seconds?` (1–300, default 180; **soft** target),
`research_allow_thinking?` (default true),
`research_maximum_number_of_queries?` (1–50, default 20),
`research_maximum_number_of_tokens_per_query?` (1024–16384, default 8192),
`research_maximum_number_of_results_per_query?` (1–60, default 60).
Research mode cannot mix with citations — enforced by omitting that key.
`research_status` reports live progress (last `<progress>` frame) while the
job runs; raw SSE is teed for every call.

Request body: `model: "brave"`, exactly one user message, always `stream: true`
(one tested code path). Timeouts: 60s simple / 600s research. Responses are
routed to an internal `brave-pro` model.

## Cost model (measured 2026-08-16)

`<usage>` breaks cost into four components:

| Component | Rate (derived) | Example run |
|---|---|---|
| Input tokens | ~$0.005 per 1K | 9,210 tokens → $0.046 (dominant) |
| Output tokens | ~$0.005 per 1K | 153 tokens → $0.0008 |
| Search queries | ~$0.004 each | 1 → $0.004 |
| Requests | $0 | 1 → $0.00 |

Research cost is driven by **how many queries the engine actually runs**
(~$0.07/query effective, dominated by snippet tokens), not the iteration
caps — those are ceilings, not targets:

| Question type | Queries run | Cost | Wall |
|---|---|---|---|
| Narrow (speculative decoding / MLX) | 1 (of 4-iter cap) | $0.079 | 136s |
| Narrow (forced 1 iteration) | 1 | $0.069 | 91s |
| Broad (solid-state batteries, 3-iter) | **10** | **$0.771** | 312s (240s soft budget) |

So the commonly cited ~$1+/call figure is reasonable for full default runs
(20-query cap — historical multi-query runs measured $1–1.45) while
early-terminating narrow questions are just cheap. Budget rule of thumb:
~$0.07 × expected queries.

Full `<usage>` fields: `X-Request-Requests`, `X-Request-Queries`,
`X-Request-Tokens-In/Out`, `X-Request-Requests-Cost`,
`X-Request-Queries-Cost`, `X-Request-Tokens-In-Cost`,
`X-Request-Tokens-Out-Cost`, `X-Request-Total-Cost`.

SSE tags parsed: `<citation>`, `<enum_item>`, `<usage>`, `<queries>`,
`<analyzing>`, `<thinking>`, `<progress>`, `<blindspots>` plus `<answer>` —
in research mode the final answer arrives as
`<answer>{"answer": "..."}</answer>` (a JSON object; also handled if it's a
JSON string). Everything else accumulates as answer content. `usage` JSON
carries `X-Request-Total-Cost` / `X-Request-Queries` / `X-Request-Requests`.

Display normalization (learned from live runs 2026-08-16): citations are
deduped by URL (Brave emits one frame per inline occurrence — a 10-citation
answer returned 21 frames); repeated `<progress>` frames for the same
iteration are collapsed, last frame per iteration wins.

Job registry: in-memory `Map`, 1h TTL after completion. Raw SSE is teed for
every call to `$TMPDIR/brave-answers-mcp/<research_id>.sse` (research jobs) or
`<uuid>.sse` (simple calls) for debugging. Note `$TMPDIR`, not `/tmp` — hosts
like opencode set a per-user temp dir for child processes. Wiped on reboot.

## Build & test

```sh
npm install        # once
npm run build      # tsc → dist/  (re-run after source changes, then restart opencode)
npm run test:parser   # offline unit tests for the SSE parser (no API calls)
node dist/smoke.js    # end-to-end: tool listing, error path, one LIVE simple call (~$0.05)
```

Node 24, `@modelcontextprotocol/sdk` 1.30.0, zod 3.25. `registerTool` takes a
zod *shape* (not `z.object`). The SDK's `StdioClientTransport` gives child
processes a sanitized env by default (`getDefaultEnvironment`) — pass `env:`
explicitly in any test client; see registration below for the opencode side.

SDK note (2026-08): 1.30.0 is the last **v1-line** release. The **v2 line**
(split `@modelcontextprotocol/server` + `/client` packages) shipped with the
2026-07-28 MCP spec; v1 receives bug fixes for six months after the v2
release, so migration is not urgent.

## opencode registration

In `~/.config/opencode/opencode.json` (global) — replace the path with this
repo's location:

```json
"brave-answers": {
  "type": "local",
  "command": ["node", "<repo-root>/dist/index.js"],
  "environment": { "BRAVE_ANSWERS_KEY": "{env:BRAVE_ANSWERS_KEY}" },
  "enabled": true
}
```

`environment` (not `env`) is the key that works for local MCPs. The `{env:VAR}`
interpolation reads from opencode's own environment. If a call returns 401
after restart, the interpolation didn't apply — delete the `environment` block
and rely on shell-env inheritance instead (opencode launched from the terminal
inherits `~/.zshrc` exports). Restart opencode after any config change (not
hot-reloaded).

A `search` skill for opencode (see `references/` notes) documents the
agent-facing workflow: `answers` for cited one-shot answers,
`research_submit → research_status → research_result` for deep research.

## Artifacts & references

- `artifacts/sse/` — raw SSE streams from the 2026-08-16 test runs (cost
  evidence, dedup evidence, `enable_entities` probe). See `artifacts/README.md`.
- `references/official-brave-skills/` — pinned copy of the official Brave
  answers skill (brave/brave-search-skills @ `62793e0`), the source of truth
  for request parameters and tag formats.

## API gotchas (from Brave docs + live testing)

- Exactly one user message per request.
- `enable_research` requires `stream: true`; incompatible with
  `enable_citations` and `enable_entities` (research has built-in citations).
- `enable_citations` requires streaming; citations arrive as `<citation>`
  frames with `number`/`url` (+ optional `start_index`/`end_index`/`snippet`).
- Progress frames repeat per iteration (multiple frames, same
  `number_of_iterations`); the cost-relevant fields are in the final `<usage>`
  frame — surface it to the user.
- `research_maximum_number_of_seconds` is a **soft** target, not a hard
  wall-clock cap: a job submitted with a 30s budget ran 91s.
- Iteration caps are ceilings, not targets: both observed research runs
  (1-iter-forced and 4-iter-default) executed exactly 1 query / 60 URLs and
  stopped. Cost follows actual queries/tokens — see the cost model above.
- `enable_entities` is silently ignored by the API (no `<enum_item>` frames
  even when set; stream routes to `brave-pro`) — not exposed in the schema.
- The `<progress>` payload's seconds field has been seen spelled
  `elasped_seconds` (upstream typo); the parser handles both.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

The tool set cleanly separates a single-shot direct answer from the multi-step research job lifecycle. The research_submit/status/result trio has clear sequential roles with no overlap, and descriptions reinforce when each should be used.

Naming Consistency4/5

The research_* prefix creates a clear and consistent pattern for the job lifecycle, while 'answers' stands alone as the direct-answer tool. Though not perfectly uniform, the naming is predictable and readable.

Tool Count5/5

With 4 tools, the server is well-scoped for its purpose: one for quick answers, three for asynchronous research jobs. Each tool has a distinct role with no redundancy.

Completeness4/5

The research lifecycle is fully covered with submit, status, and result. The only minor gap is the lack of explicit cancellation or listing of historical jobs, but these are not essential for the server's stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues