@dyingc/brave-search-mcp-server 2.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +356 -0
  3. package/dist/BraveAPI/index.js +162 -0
  4. package/dist/BraveAPI/types.js +46 -0
  5. package/dist/config.js +183 -0
  6. package/dist/constants.js +21 -0
  7. package/dist/helpers.js +10 -0
  8. package/dist/index.js +23 -0
  9. package/dist/protocols/http.js +81 -0
  10. package/dist/protocols/index.js +2 -0
  11. package/dist/protocols/stdio.js +11 -0
  12. package/dist/server.js +28 -0
  13. package/dist/tools/images/index.js +61 -0
  14. package/dist/tools/images/schemas/input.js +37 -0
  15. package/dist/tools/images/schemas/output.js +20 -0
  16. package/dist/tools/images/schemas/response.js +54 -0
  17. package/dist/tools/images/types.js +1 -0
  18. package/dist/tools/index.js +14 -0
  19. package/dist/tools/local/index.js +145 -0
  20. package/dist/tools/local/params.js +7 -0
  21. package/dist/tools/local/types.js +1 -0
  22. package/dist/tools/news/index.js +62 -0
  23. package/dist/tools/news/params.js +62 -0
  24. package/dist/tools/news/types.js +1 -0
  25. package/dist/tools/summarizer/index.js +96 -0
  26. package/dist/tools/summarizer/params.js +60 -0
  27. package/dist/tools/summarizer/types.js +1 -0
  28. package/dist/tools/videos/index.js +46 -0
  29. package/dist/tools/videos/params.js +53 -0
  30. package/dist/tools/videos/types.js +1 -0
  31. package/dist/tools/web/index.js +149 -0
  32. package/dist/tools/web/params.js +217 -0
  33. package/dist/tools/web/types.js +1 -0
  34. package/dist/utils/apiKeyManager.js +124 -0
  35. package/dist/utils/retry.js +152 -0
  36. package/dist/utils/retry.test.js +258 -0
  37. package/dist/utils.js +21 -0
  38. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Anthropic, PBC
4
+ Copyright (c) 2025 Brave Software, Inc
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # Brave Search MCP Server
2
+
3
+ An MCP server implementation that integrates the Brave Search API, providing comprehensive search capabilities including web search, local business search, image search, video search, news search, and AI-powered summarization. This project supports both STDIO and HTTP transports, with STDIO as the default mode.
4
+
5
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/brave/brave-search-mcp-server)
6
+
7
+ ## Migration
8
+
9
+ ### 1.x to 2.x
10
+
11
+ #### Default transport now STDIO
12
+
13
+ To follow established MCP conventions, the server now defaults to STDIO. If you would like to continue using HTTP, you will need to set the `BRAVE_MCP_TRANSPORT` environment variable to `http`, or provide the runtime argument `--transport http` when launching the server.
14
+
15
+ #### Response structure of `brave_image_search`
16
+
17
+ Version 1.x of the MCP server would return base64-encoded image data along with image URLs. This dramatically slowed down the response, as well as consumed unnecessarily context in the session. Version 2.x removes the base64-encoded data, and returns a response object that more closely reflects the original Brave Search API response. The updated output schema is defined in [`src/tools/images/schemas/output.ts`](https://github.com/brave/brave-search-mcp-server/blob/main/src/tools/images/schemas/output.ts).
18
+
19
+ ## Tools
20
+
21
+ ### Web Search (`brave_web_search`)
22
+ Performs comprehensive web searches with rich result types and advanced filtering options.
23
+
24
+ **Parameters:**
25
+ - `query` (string, required): Search terms (max 400 chars, 50 words)
26
+ - `country` (string, optional): Country code (default: "US")
27
+ - `search_lang` (string, optional): Search language (default: "en")
28
+ - `ui_lang` (string, optional): UI language (default: "en-US")
29
+ - `count` (number, optional): Results per page (1-20, default: 10)
30
+ - `offset` (number, optional): Pagination offset (max 9, default: 0)
31
+ - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate")
32
+ - `freshness` (string, optional): Time filter ("pd", "pw", "pm", "py", or date range)
33
+ - `text_decorations` (boolean, optional): Include highlighting markers (default: true)
34
+ - `spellcheck` (boolean, optional): Enable spell checking (default: true)
35
+ - `result_filter` (array, optional): Filter result types (default: ["web", "query"])
36
+ - `goggles` (array, optional): Custom re-ranking definitions
37
+ - `units` (string, optional): Measurement units ("metric" or "imperial")
38
+ - `extra_snippets` (boolean, optional): Get additional excerpts (Pro plans only)
39
+ - `summary` (boolean, optional): Enable summary key generation for AI summarization
40
+
41
+ ### Local Search (`brave_local_search`)
42
+ Searches for local businesses and places with detailed information including ratings, hours, and AI-generated descriptions.
43
+
44
+ **Parameters:**
45
+ - Same as `brave_web_search` with automatic location filtering
46
+ - Automatically includes "web" and "locations" in result_filter
47
+
48
+ **Note:** Requires Pro plan for full local search capabilities. Falls back to web search otherwise.
49
+
50
+ ### Video Search (`brave_video_search`)
51
+ Searches for videos with comprehensive metadata and thumbnail information.
52
+
53
+ **Parameters:**
54
+ - `query` (string, required): Search terms (max 400 chars, 50 words)
55
+ - `country` (string, optional): Country code (default: "US")
56
+ - `search_lang` (string, optional): Search language (default: "en")
57
+ - `ui_lang` (string, optional): UI language (default: "en-US")
58
+ - `count` (number, optional): Results per page (1-50, default: 20)
59
+ - `offset` (number, optional): Pagination offset (max 9, default: 0)
60
+ - `spellcheck` (boolean, optional): Enable spell checking (default: true)
61
+ - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate")
62
+ - `freshness` (string, optional): Time filter ("pd", "pw", "pm", "py", or date range)
63
+
64
+ ### Image Search (`brave_image_search`)
65
+ Searches for images with automatic fetching and base64 encoding for direct display.
66
+
67
+ **Parameters:**
68
+ - `query` (string, required): Search terms (max 400 chars, 50 words)
69
+ - `country` (string, optional): Country code (default: "US")
70
+ - `search_lang` (string, optional): Search language (default: "en")
71
+ - `count` (number, optional): Results per page (1-200, default: 50)
72
+ - `safesearch` (string, optional): Content filtering ("off", "strict", default: "strict")
73
+ - `spellcheck` (boolean, optional): Enable spell checking (default: true)
74
+
75
+ ### News Search (`brave_news_search`)
76
+ Searches for current news articles with freshness controls and breaking news indicators.
77
+
78
+ **Parameters:**
79
+ - `query` (string, required): Search terms (max 400 chars, 50 words)
80
+ - `country` (string, optional): Country code (default: "US")
81
+ - `search_lang` (string, optional): Search language (default: "en")
82
+ - `ui_lang` (string, optional): UI language (default: "en-US")
83
+ - `count` (number, optional): Results per page (1-50, default: 20)
84
+ - `offset` (number, optional): Pagination offset (max 9, default: 0)
85
+ - `spellcheck` (boolean, optional): Enable spell checking (default: true)
86
+ - `safesearch` (string, optional): Content filtering ("off", "moderate", "strict", default: "moderate")
87
+ - `freshness` (string, optional): Time filter (default: "pd" for last 24 hours)
88
+ - `extra_snippets` (boolean, optional): Get additional excerpts (Pro plans only)
89
+ - `goggles` (array, optional): Custom re-ranking definitions
90
+
91
+ ### Summarizer Search (`brave_summarizer`)
92
+ Generates AI-powered summaries from web search results using Brave's summarization API.
93
+
94
+ **Parameters:**
95
+ - `key` (string, required): Summary key from web search results (use `summary: true` in web search)
96
+ - `entity_info` (boolean, optional): Include entity information (default: false)
97
+ - `inline_references` (boolean, optional): Add source URL references (default: false)
98
+
99
+ **Usage:** First perform a web search with `summary: true`, then use the returned summary key with this tool.
100
+
101
+ ## Configuration
102
+
103
+ ### Getting an API Key
104
+
105
+ 1. Sign up for a [Brave Search API account](https://brave.com/search/api/)
106
+ 2. Choose a plan:
107
+ - **Free**: 2,000 queries/month, basic web search
108
+ - **Pro**: Enhanced features including local search, AI summaries, extra snippets
109
+ 3. Generate your API key from the [developer dashboard](https://api-dashboard.search.brave.com/app/keys)
110
+
111
+ ### Environment Variables
112
+
113
+ The server supports the following environment variables:
114
+
115
+ **Required:**
116
+ - `BRAVE_API_KEY`: Your Brave Search API key (required)
117
+
118
+ **Optional:**
119
+ - `BRAVE_MCP_TRANSPORT`: Transport mode ("http" or "stdio", default: "stdio")
120
+ - `BRAVE_MCP_PORT`: HTTP server port (default: 8000)
121
+ - `BRAVE_MCP_HOST`: HTTP server host (default: "0.0.0.0")
122
+ - `BRAVE_MCP_LOG_LEVEL`: Desired logging level("debug", "info", "notice", "warning", "error", "critical", "alert", or "emergency", default: "info")
123
+ - `BRAVE_MCP_ENABLED_TOOLS`: When used, specifies a whitelist for supported tools
124
+ - `BRAVE_MCP_DISABLED_TOOLS`: When used, specifies a blacklist for supported tools
125
+ - `BRAVE_MCP_STATELESS`: HTTP stateless mode (default: "false")
126
+
127
+ **Retry Configuration:**
128
+ - `BRAVE_MCP_MAX_RETRIES`: Maximum number of retry attempts for rate-limited requests (default: 5, range: 0-10)
129
+ - `BRAVE_MCP_RETRY_BASE_DELAY`: Base delay for retry in milliseconds (default: 1000, range: 100-60000)
130
+ - `BRAVE_MCP_RETRY_MAX_DELAY`: Maximum delay for retry in milliseconds (default: 30000, range: 1000-300000)
131
+
132
+ ### Command Line Options
133
+
134
+ ```bash
135
+ node dist/index.js [options]
136
+
137
+ Options:
138
+ --brave-api-key <string> Brave API key (required)
139
+ --transport <stdio|http> Transport type (default: stdio)
140
+ --port <number> HTTP server port (default: 8080)
141
+ --host <string> HTTP server host (default: 0.0.0.0)
142
+ --logging-level <string> Desired logging level (one of _debug_, _info_, _notice_, _warning_, _error_, _critical_, _alert_, or _emergency_)
143
+ --enabled-tools Tools whitelist (only the specified tools will be enabled)
144
+ --disabled-tools Tools blacklist (included tools will be disabled)
145
+ --stateless <boolean> HTTP Stateless flag
146
+ --retry-max-attempts <number> Maximum retry attempts (default: 5, range: 0-10)
147
+ --retry-base-delay <number> Base delay in milliseconds (default: 1000, range: 100-60000)
148
+ --retry-max-delay <number> Maximum delay in milliseconds (default: 30000, range: 1000-300000)
149
+ ```
150
+
151
+ ## Installation
152
+
153
+ ### Installing via Smithery
154
+
155
+ To install Brave Search automatically via [Smithery](https://smithery.ai/server/brave):
156
+
157
+ ```bash
158
+ npx -y @smithery/cli install brave
159
+ ```
160
+
161
+ ### Usage with Claude Desktop
162
+
163
+ Add this to your `claude_desktop_config.json`:
164
+
165
+ #### Docker
166
+
167
+ ```json
168
+ {
169
+ "mcpServers": {
170
+ "brave-search": {
171
+ "command": "docker",
172
+ "args": ["run", "-i", "--rm", "-e", "BRAVE_API_KEY", "docker.io/mcp/brave-search"],
173
+ "env": {
174
+ "BRAVE_API_KEY": "YOUR_API_KEY_HERE"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ #### NPX
182
+
183
+ ```json
184
+ {
185
+ "mcpServers": {
186
+ "brave-search": {
187
+ "command": "npx",
188
+ "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "http"],
189
+ "env": {
190
+ "BRAVE_API_KEY": "YOUR_API_KEY_HERE"
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ ### Usage with VS Code
198
+
199
+ For quick installation, use the one-click installation buttons below:
200
+
201
+ [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40brave%2Fbrave-search-mcp-server%22%2C%22--transport%22%2C%22stdio%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40brave%2Fbrave-search-mcp-server%22%2C%22--transport%22%2C%22stdio%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D&quality=insiders)
202
+ [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22BRAVE_API_KEY%22%2C%22mcp%2Fbrave-search%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=brave-search&inputs=%5B%7B%22password%22%3Atrue%2C%22id%22%3A%22brave-api-key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Brave+Search+API+Key%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22BRAVE_API_KEY%22%2C%22mcp%2Fbrave-search%22%5D%2C%22env%22%3A%7B%22BRAVE_API_KEY%22%3A%22%24%7Binput%3Abrave-api-key%7D%22%7D%7D&quality=insiders)
203
+
204
+ For manual installation, add the following to your User Settings (JSON) or `.vscode/mcp.json`:
205
+
206
+ #### Docker
207
+
208
+ ```json
209
+ {
210
+ "inputs": [
211
+ {
212
+ "password": true,
213
+ "id": "brave-api-key",
214
+ "type": "promptString",
215
+ "description": "Brave Search API Key",
216
+ }
217
+ ],
218
+ "servers": {
219
+ "brave-search": {
220
+ "command": "docker",
221
+ "args": ["run", "-i", "--rm", "-e", "BRAVE_API_KEY", "mcp/brave-search"],
222
+ "env": {
223
+ "BRAVE_API_KEY": "${input:brave-api-key}"
224
+ }
225
+ }
226
+ }
227
+ }
228
+ ```
229
+
230
+ #### NPX
231
+
232
+ ```json
233
+ {
234
+ "inputs": [
235
+ {
236
+ "password": true,
237
+ "id": "brave-api-key",
238
+ "type": "promptString",
239
+ "description": "Brave Search API Key",
240
+ }
241
+ ],
242
+ "servers": {
243
+ "brave-search-mcp-server": {
244
+ "command": "npx",
245
+ "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "stdio"],
246
+ "env": {
247
+ "BRAVE_API_KEY": "${input:brave-api-key}"
248
+ }
249
+ }
250
+ }
251
+ }
252
+ ```
253
+
254
+ ## Build
255
+
256
+ ### Docker
257
+
258
+ ```bash
259
+ docker build -t mcp/brave-search:latest .
260
+ ```
261
+
262
+ ### Local Build
263
+
264
+ ```bash
265
+ npm install
266
+ npm run build
267
+ ```
268
+
269
+ ## Development
270
+
271
+ ### Prerequisites
272
+
273
+ - Node.js 22.x or higher
274
+ - npm
275
+ - Brave Search API key
276
+
277
+ ### Setup
278
+
279
+ 1. Clone the repository:
280
+ ```bash
281
+ git clone https://github.com/brave/brave-search-mcp-server.git
282
+ cd brave-search-mcp-server
283
+ ```
284
+
285
+ 2. Install dependencies:
286
+ ```bash
287
+ npm install
288
+ ```
289
+
290
+ 3. Build the project:
291
+ ```bash
292
+ npm run build
293
+ ```
294
+
295
+ ### Testing via Claude Desktop
296
+
297
+ Add a reference to your local build in `claude_desktop_config.json`:
298
+
299
+ ```json
300
+ {
301
+ "mcpServers": {
302
+ "brave-search-dev": {
303
+ "command": "node",
304
+ "args": ["C:\\GitHub\\brave-search-mcp-server\\dist\\index.js"], // Verify your path
305
+ "env": {
306
+ "BRAVE_API_KEY": "YOUR_API_KEY_HERE"
307
+ }
308
+ }
309
+ }
310
+ }
311
+ ```
312
+
313
+ ### Testing via MCP Inspector
314
+
315
+ 1. Build and start the server:
316
+ ```bash
317
+ npm run build
318
+ node dist/index.js
319
+ ```
320
+
321
+ 2. In another terminal, start the MCP Inspector:
322
+ ```bash
323
+ npx @modelcontextprotocol/inspector node dist/index.js
324
+ ```
325
+
326
+ STDIO is the default mode. For HTTP mode testing, add `--transport http` to the arguments in the Inspector UI.
327
+
328
+ ### Testing via Smithery.AI
329
+
330
+ 1. Establish and acquire a smithery.ai account and API key
331
+ 2. Run `npm run install`, `npm run smithery:build`, and lastly `npm run smithery:dev` to begin testing
332
+
333
+ ### Available Scripts
334
+
335
+ - `npm run build`: Build the TypeScript project
336
+ - `npm run watch`: Watch for changes and rebuild
337
+ - `npm run format`: Format code with Prettier
338
+ - `npm run format:check`: Check code formatting
339
+ - `npm run prepare`: Format and build (runs automatically on npm install)
340
+
341
+ - `npm run inspector`: Launch an instance of MCP Inspector
342
+ - `npm run inspector:stdio`: Launch a instance of MCP Inspector, configured for STDIO
343
+ - `npm run smithery:build`: Build the project for smithery.ai
344
+ - `npm run smithery:dev`: Launch the development environment for smithery.ai
345
+
346
+ ### Docker Compose
347
+
348
+ For local development with Docker:
349
+
350
+ ```bash
351
+ docker-compose up --build
352
+ ```
353
+
354
+ ## License
355
+
356
+ This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
@@ -0,0 +1,162 @@
1
+ import config from '../config.js';
2
+ import { withRetry, BraveApiError } from '../utils/retry.js';
3
+ import { extractRetryAfterMs } from './types.js';
4
+ import { getApiKeyManager } from '../utils/apiKeyManager.js';
5
+ const typeToPathMap = {
6
+ images: '/res/v1/images/search',
7
+ localPois: '/res/v1/local/pois',
8
+ localDescriptions: '/res/v1/local/descriptions',
9
+ news: '/res/v1/news/search',
10
+ videos: '/res/v1/videos/search',
11
+ web: '/res/v1/web/search',
12
+ summarizer: '/res/v1/summarizer/search',
13
+ };
14
+ const getDefaultRequestHeaders = () => {
15
+ const manager = getApiKeyManager();
16
+ // Use key manager if available, otherwise fallback to first key
17
+ const keyToUse = manager ? manager.selectBestKey() : config.braveApiKeys[0];
18
+ if (!keyToUse) {
19
+ throw new Error('No valid API key available');
20
+ }
21
+ return {
22
+ Accept: 'application/json',
23
+ 'Accept-Encoding': 'gzip',
24
+ 'X-Subscription-Token': keyToUse,
25
+ };
26
+ };
27
+ const isValidGoggleURL = (url) => {
28
+ try {
29
+ // Only allow HTTPS URLs
30
+ return new URL(url).protocol === 'https:';
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
36
+ /**
37
+ * Perform the actual HTTP request
38
+ * This function is separated from issueRequest to allow retry logic
39
+ *
40
+ * @param urlWithParams - Full URL with query parameters
41
+ * @param headers - Request headers
42
+ * @returns Promise resolving to the response
43
+ * @throws BraveApiError on HTTP errors
44
+ */
45
+ async function performRequest(urlWithParams, headers) {
46
+ const response = await fetch(urlWithParams, { headers });
47
+ // Extract used key for tracking
48
+ const usedApiKey = headers['X-Subscription-Token'];
49
+ const manager = getApiKeyManager();
50
+ // Handle Error
51
+ if (!response.ok) {
52
+ let responseBody;
53
+ try {
54
+ responseBody = await response.json();
55
+ }
56
+ catch {
57
+ // If JSON parsing fails, try to get text
58
+ const text = await response.text();
59
+ responseBody = { message: text };
60
+ }
61
+ // Update key state on error
62
+ if (manager && usedApiKey) {
63
+ if (response.status === 401) {
64
+ manager.markKeyInvalid(usedApiKey, responseBody.message || 'Unauthorized');
65
+ }
66
+ else if (response.status === 429) {
67
+ const retryAfter = extractRetryAfterMs(responseBody);
68
+ manager.markKeyRateLimited(usedApiKey, retryAfter);
69
+ }
70
+ else {
71
+ // Release key for other errors
72
+ manager.releaseKey(usedApiKey);
73
+ }
74
+ }
75
+ // Extract retry-after from rate limit metadata if available
76
+ const retryAfter = extractRetryAfterMs(responseBody);
77
+ // Throw structured error for retry logic to handle
78
+ throw new BraveApiError(response.status, responseBody, retryAfter);
79
+ }
80
+ // Update key state from response headers on success
81
+ if (manager && usedApiKey) {
82
+ manager.updateKeyFromHeaders(usedApiKey, response.headers);
83
+ }
84
+ // Return Response
85
+ const responseBody = await response.json();
86
+ return responseBody;
87
+ }
88
+ async function issueRequest(endpoint, parameters,
89
+ // TODO (Sampson): Implement support for custom request headers (helpful for POIs, etc.)
90
+ requestHeaders = {}) {
91
+ // Determine URL, and setup parameters
92
+ const url = new URL(`https://api.search.brave.com${typeToPathMap[endpoint]}`);
93
+ const queryParams = new URLSearchParams();
94
+ // TODO (Sampson): Move param-construction/validation to modules
95
+ for (const [key, value] of Object.entries(parameters)) {
96
+ // The 'ids' parameter is expected to appear multiple times for multiple IDs
97
+ if (['localPois', 'localDescriptions'].includes(endpoint)) {
98
+ if (key === 'ids') {
99
+ if (Array.isArray(value) && value.length > 0) {
100
+ value.forEach((id) => queryParams.append(key, id));
101
+ }
102
+ else if (typeof value === 'string') {
103
+ queryParams.set(key, value);
104
+ }
105
+ continue;
106
+ }
107
+ }
108
+ // Handle `result_filter` parameter
109
+ if (key === 'result_filter') {
110
+ // Handle special behavior of 'summary' parameter:
111
+ // Requires `result_filter` to be empty, or only contain 'summarizer'
112
+ // see: https://bravesoftware.slack.com/archives/C01NNFM9XMM/p1751654841090929
113
+ if ('summary' in parameters && parameters.summary === true) {
114
+ queryParams.set(key, 'summarizer');
115
+ }
116
+ else if (Array.isArray(value) && value.length > 0) {
117
+ queryParams.set(key, value.join(','));
118
+ }
119
+ continue;
120
+ }
121
+ // Handle `goggles` parameter(s)
122
+ if (key === 'goggles') {
123
+ if (typeof value === 'string') {
124
+ queryParams.set(key, value);
125
+ }
126
+ else if (Array.isArray(value)) {
127
+ for (const url of value.filter(isValidGoggleURL)) {
128
+ queryParams.append(key, url);
129
+ }
130
+ }
131
+ continue;
132
+ }
133
+ if (value !== undefined) {
134
+ queryParams.set(key === 'query' ? 'q' : key, value.toString());
135
+ }
136
+ }
137
+ // Build final URL and headers
138
+ const urlWithParams = url.toString() + '?' + queryParams.toString();
139
+ const headers = { ...getDefaultRequestHeaders(), ...requestHeaders };
140
+ // Special case: Summarizer uses its own polling logic, don't apply retry
141
+ if (endpoint === 'summarizer') {
142
+ return performRequest(urlWithParams, headers);
143
+ }
144
+ // All other endpoints: Use retry logic with exponential backoff
145
+ return withRetry(() => performRequest(urlWithParams, headers), {
146
+ maxRetries: config.retryMaxAttempts,
147
+ baseDelay: config.retryBaseDelay,
148
+ maxDelay: config.retryMaxDelay,
149
+ onRetry: (attempt, error, delay) => {
150
+ // Additional logging can be added here if needed
151
+ if (error instanceof BraveApiError && error.statusCode === 429) {
152
+ // Log specific rate limit info if available
153
+ if (error.retryAfter) {
154
+ console.info(`Rate limit metadata suggests ${error.retryAfter}ms delay`);
155
+ }
156
+ }
157
+ },
158
+ });
159
+ }
160
+ export default {
161
+ issueRequest,
162
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Type guard to check if an error response is a rate limit error
3
+ * @param error - The error to check
4
+ * @returns True if the error is a rate limit error
5
+ */
6
+ export function isRateLimitError(error) {
7
+ return (typeof error === 'object' &&
8
+ error !== null &&
9
+ 'type' in error &&
10
+ error.type === 'ErrorResponse' &&
11
+ 'error' in error &&
12
+ error.error?.code === 'RATE_LIMITED');
13
+ }
14
+ /**
15
+ * Extract the suggested retry delay from a rate limit error response
16
+ * Uses the rate limit metadata to calculate the optimal delay
17
+ * @param response - The error response from the API
18
+ * @returns Suggested delay in milliseconds, or undefined if no metadata available
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const response = {
23
+ * error: {
24
+ * meta: { rate_limit: 1, rate_current: 1.5 }
25
+ * }
26
+ * };
27
+ * const delay = extractRetryAfterMs(response); // Returns 1500
28
+ * ```
29
+ */
30
+ export function extractRetryAfterMs(response) {
31
+ if (!response || typeof response !== 'object') {
32
+ return undefined;
33
+ }
34
+ const meta = response?.error?.meta;
35
+ if (!meta || typeof meta !== 'object') {
36
+ return undefined;
37
+ }
38
+ const rateLimit = meta.rate_limit;
39
+ const rateCurrent = meta.rate_current;
40
+ if (typeof rateLimit === 'number' && typeof rateCurrent === 'number' && rateLimit > 0) {
41
+ // Calculate delay based on current usage rate
42
+ // Example: If rate_limit=1, rate_current=1.5 → 1500ms
43
+ return Math.ceil((rateCurrent / rateLimit) * 1000);
44
+ }
45
+ return undefined;
46
+ }