@modelcontextprotocol/server-brave-search 0.1.0
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.
- package/README.md +46 -0
- package/dist/index.js +275 -0
- package/package.json +30 -0
package/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Brave Search MCP Server
|
2
|
+
|
3
|
+
An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Web Search**: General queries, news, articles, with pagination and freshness controls
|
8
|
+
- **Local Search**: Find businesses, restaurants, and services with detailed information
|
9
|
+
- **Flexible Filtering**: Control result types, safety levels, and content freshness
|
10
|
+
- **Smart Fallbacks**: Local search automatically falls back to web when no results are found
|
11
|
+
|
12
|
+
## Tools
|
13
|
+
|
14
|
+
- **brave_web_search**
|
15
|
+
- Execute web searches with pagination and filtering
|
16
|
+
- Inputs:
|
17
|
+
- `query` (string): Search terms
|
18
|
+
- `count` (number, optional): Results per page (max 20)
|
19
|
+
- `offset` (number, optional): Pagination offset (max 9)
|
20
|
+
|
21
|
+
- **brave_local_search**
|
22
|
+
- Search for local businesses and services
|
23
|
+
- Inputs:
|
24
|
+
- `query` (string): Local search terms
|
25
|
+
- `count` (number, optional): Number of results (max 20)
|
26
|
+
- Automatically falls back to web search if no local results found
|
27
|
+
|
28
|
+
|
29
|
+
## Configuration
|
30
|
+
|
31
|
+
### Getting an API Key
|
32
|
+
1. Sign up for a [Brave Search API account](https://brave.com/search/api/)
|
33
|
+
2. Choose a plan (Free tier available with 2,000 queries/month)
|
34
|
+
3. Generate your API key [from the developer dashboard](https://api.search.brave.com/app/keys)
|
35
|
+
|
36
|
+
### Usage with Claude Desktop
|
37
|
+
Add this to your `claude_desktop_config.json`:
|
38
|
+
|
39
|
+
```json
|
40
|
+
"mcp-server-brave-search": {
|
41
|
+
"command": "mcp-server-brave-search",
|
42
|
+
"env": {
|
43
|
+
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
|
44
|
+
}
|
45
|
+
}
|
46
|
+
```
|
package/dist/index.js
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
5
|
+
import fetch from "node-fetch";
|
6
|
+
const WEB_SEARCH_TOOL = {
|
7
|
+
name: "brave_web_search",
|
8
|
+
description: "Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +
|
9
|
+
"Use this for broad information gathering, recent events, or when you need diverse web sources. " +
|
10
|
+
"Supports pagination, content filtering, and freshness controls. " +
|
11
|
+
"Maximum 20 results per request, with offset for pagination. ",
|
12
|
+
inputSchema: {
|
13
|
+
type: "object",
|
14
|
+
properties: {
|
15
|
+
query: {
|
16
|
+
type: "string",
|
17
|
+
description: "Search query (max 400 chars, 50 words)"
|
18
|
+
},
|
19
|
+
count: {
|
20
|
+
type: "number",
|
21
|
+
description: "Number of results (1-20, default 10)",
|
22
|
+
default: 10
|
23
|
+
},
|
24
|
+
offset: {
|
25
|
+
type: "number",
|
26
|
+
description: "Pagination offset (max 9, default 0)",
|
27
|
+
default: 0
|
28
|
+
},
|
29
|
+
},
|
30
|
+
required: ["query"],
|
31
|
+
},
|
32
|
+
};
|
33
|
+
const LOCAL_SEARCH_TOOL = {
|
34
|
+
name: "brave_local_search",
|
35
|
+
description: "Searches for local businesses and places using Brave's Local Search API. " +
|
36
|
+
"Best for queries related to physical locations, businesses, restaurants, services, etc. " +
|
37
|
+
"Returns detailed information including:\n" +
|
38
|
+
"- Business names and addresses\n" +
|
39
|
+
"- Ratings and review counts\n" +
|
40
|
+
"- Phone numbers and opening hours\n" +
|
41
|
+
"Use this when the query implies 'near me' or mentions specific locations. " +
|
42
|
+
"Automatically falls back to web search if no local results are found.",
|
43
|
+
inputSchema: {
|
44
|
+
type: "object",
|
45
|
+
properties: {
|
46
|
+
query: {
|
47
|
+
type: "string",
|
48
|
+
description: "Local search query (e.g. 'pizza near Central Park')"
|
49
|
+
},
|
50
|
+
count: {
|
51
|
+
type: "number",
|
52
|
+
description: "Number of results (1-20, default 5)",
|
53
|
+
default: 5
|
54
|
+
},
|
55
|
+
},
|
56
|
+
required: ["query"]
|
57
|
+
}
|
58
|
+
};
|
59
|
+
// Server implementation
|
60
|
+
const server = new Server({
|
61
|
+
name: "example-servers/brave-search",
|
62
|
+
version: "0.1.0",
|
63
|
+
}, {
|
64
|
+
capabilities: {
|
65
|
+
tools: {},
|
66
|
+
},
|
67
|
+
});
|
68
|
+
// Check for API key
|
69
|
+
const BRAVE_API_KEY = process.env.BRAVE_API_KEY;
|
70
|
+
if (!BRAVE_API_KEY) {
|
71
|
+
console.error("Error: BRAVE_API_KEY environment variable is required");
|
72
|
+
process.exit(1);
|
73
|
+
}
|
74
|
+
const RATE_LIMIT = {
|
75
|
+
perSecond: 1,
|
76
|
+
perMonth: 15000
|
77
|
+
};
|
78
|
+
let requestCount = {
|
79
|
+
second: 0,
|
80
|
+
month: 0,
|
81
|
+
lastReset: Date.now()
|
82
|
+
};
|
83
|
+
function checkRateLimit() {
|
84
|
+
const now = Date.now();
|
85
|
+
if (now - requestCount.lastReset > 1000) {
|
86
|
+
requestCount.second = 0;
|
87
|
+
requestCount.lastReset = now;
|
88
|
+
}
|
89
|
+
if (requestCount.second >= RATE_LIMIT.perSecond ||
|
90
|
+
requestCount.month >= RATE_LIMIT.perMonth) {
|
91
|
+
throw new Error('Rate limit exceeded');
|
92
|
+
}
|
93
|
+
requestCount.second++;
|
94
|
+
requestCount.month++;
|
95
|
+
}
|
96
|
+
function isBraveWebSearchArgs(args) {
|
97
|
+
return (typeof args === "object" &&
|
98
|
+
args !== null &&
|
99
|
+
"query" in args &&
|
100
|
+
typeof args.query === "string");
|
101
|
+
}
|
102
|
+
function isBraveLocalSearchArgs(args) {
|
103
|
+
return (typeof args === "object" &&
|
104
|
+
args !== null &&
|
105
|
+
"query" in args &&
|
106
|
+
typeof args.query === "string");
|
107
|
+
}
|
108
|
+
async function performWebSearch(query, count = 10, offset = 0) {
|
109
|
+
checkRateLimit();
|
110
|
+
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
111
|
+
url.searchParams.set('q', query);
|
112
|
+
url.searchParams.set('count', Math.min(count, 20).toString()); // API limit
|
113
|
+
url.searchParams.set('offset', offset.toString());
|
114
|
+
const response = await fetch(url, {
|
115
|
+
headers: {
|
116
|
+
'Accept': 'application/json',
|
117
|
+
'Accept-Encoding': 'gzip',
|
118
|
+
'X-Subscription-Token': BRAVE_API_KEY
|
119
|
+
}
|
120
|
+
});
|
121
|
+
if (!response.ok) {
|
122
|
+
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
123
|
+
}
|
124
|
+
const data = await response.json();
|
125
|
+
// Extract just web results
|
126
|
+
const results = (data.web?.results || []).map(result => ({
|
127
|
+
title: result.title || '',
|
128
|
+
description: result.description || '',
|
129
|
+
url: result.url || ''
|
130
|
+
}));
|
131
|
+
return results.map(r => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n');
|
132
|
+
}
|
133
|
+
async function performLocalSearch(query, count = 5) {
|
134
|
+
checkRateLimit();
|
135
|
+
// Initial search to get location IDs
|
136
|
+
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search');
|
137
|
+
webUrl.searchParams.set('q', query);
|
138
|
+
webUrl.searchParams.set('search_lang', 'en');
|
139
|
+
webUrl.searchParams.set('result_filter', 'locations');
|
140
|
+
webUrl.searchParams.set('count', Math.min(count, 20).toString());
|
141
|
+
const webResponse = await fetch(webUrl, {
|
142
|
+
headers: {
|
143
|
+
'Accept': 'application/json',
|
144
|
+
'Accept-Encoding': 'gzip',
|
145
|
+
'X-Subscription-Token': BRAVE_API_KEY
|
146
|
+
}
|
147
|
+
});
|
148
|
+
if (!webResponse.ok) {
|
149
|
+
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`);
|
150
|
+
}
|
151
|
+
const webData = await webResponse.json();
|
152
|
+
const locationIds = webData.locations?.results?.filter((r) => r.id != null).map(r => r.id) || [];
|
153
|
+
if (locationIds.length === 0) {
|
154
|
+
return performWebSearch(query, count); // Fallback to web search
|
155
|
+
}
|
156
|
+
// Get POI details and descriptions in parallel
|
157
|
+
const [poisData, descriptionsData] = await Promise.all([
|
158
|
+
getPoisData(locationIds),
|
159
|
+
getDescriptionsData(locationIds)
|
160
|
+
]);
|
161
|
+
return formatLocalResults(poisData, descriptionsData);
|
162
|
+
}
|
163
|
+
async function getPoisData(ids) {
|
164
|
+
checkRateLimit();
|
165
|
+
const url = new URL('https://api.search.brave.com/res/v1/local/pois');
|
166
|
+
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
167
|
+
const response = await fetch(url, {
|
168
|
+
headers: {
|
169
|
+
'Accept': 'application/json',
|
170
|
+
'Accept-Encoding': 'gzip',
|
171
|
+
'X-Subscription-Token': BRAVE_API_KEY
|
172
|
+
}
|
173
|
+
});
|
174
|
+
if (!response.ok) {
|
175
|
+
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
176
|
+
}
|
177
|
+
const poisResponse = await response.json();
|
178
|
+
return poisResponse;
|
179
|
+
}
|
180
|
+
async function getDescriptionsData(ids) {
|
181
|
+
checkRateLimit();
|
182
|
+
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions');
|
183
|
+
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
184
|
+
const response = await fetch(url, {
|
185
|
+
headers: {
|
186
|
+
'Accept': 'application/json',
|
187
|
+
'Accept-Encoding': 'gzip',
|
188
|
+
'X-Subscription-Token': BRAVE_API_KEY
|
189
|
+
}
|
190
|
+
});
|
191
|
+
if (!response.ok) {
|
192
|
+
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
193
|
+
}
|
194
|
+
const descriptionsData = await response.json();
|
195
|
+
return descriptionsData;
|
196
|
+
}
|
197
|
+
function formatLocalResults(poisData, descData) {
|
198
|
+
return (poisData.results || []).map(poi => {
|
199
|
+
const address = [
|
200
|
+
poi.address?.streetAddress ?? '',
|
201
|
+
poi.address?.addressLocality ?? '',
|
202
|
+
poi.address?.addressRegion ?? '',
|
203
|
+
poi.address?.postalCode ?? ''
|
204
|
+
].filter(part => part !== '').join(', ') || 'N/A';
|
205
|
+
return `Name: ${poi.name}
|
206
|
+
Address: ${address}
|
207
|
+
Phone: ${poi.phone || 'N/A'}
|
208
|
+
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
|
209
|
+
Price Range: ${poi.priceRange || 'N/A'}
|
210
|
+
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
|
211
|
+
Description: ${descData.descriptions[poi.id] || 'No description available'}
|
212
|
+
`;
|
213
|
+
}).join('\n---\n') || 'No local results found';
|
214
|
+
}
|
215
|
+
// Tool handlers
|
216
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
217
|
+
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL],
|
218
|
+
}));
|
219
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
220
|
+
try {
|
221
|
+
const { name, arguments: args } = request.params;
|
222
|
+
if (!args) {
|
223
|
+
throw new Error("No arguments provided");
|
224
|
+
}
|
225
|
+
switch (name) {
|
226
|
+
case "brave_web_search": {
|
227
|
+
if (!isBraveWebSearchArgs(args)) {
|
228
|
+
throw new Error("Invalid arguments for brave_web_search");
|
229
|
+
}
|
230
|
+
const { query, count = 10 } = args;
|
231
|
+
const results = await performWebSearch(query, count);
|
232
|
+
return {
|
233
|
+
content: [{ type: "text", text: results }],
|
234
|
+
isError: false,
|
235
|
+
};
|
236
|
+
}
|
237
|
+
case "brave_local_search": {
|
238
|
+
if (!isBraveLocalSearchArgs(args)) {
|
239
|
+
throw new Error("Invalid arguments for brave_local_search");
|
240
|
+
}
|
241
|
+
const { query, count = 5 } = args;
|
242
|
+
const results = await performLocalSearch(query, count);
|
243
|
+
return {
|
244
|
+
content: [{ type: "text", text: results }],
|
245
|
+
isError: false,
|
246
|
+
};
|
247
|
+
}
|
248
|
+
default:
|
249
|
+
return {
|
250
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
251
|
+
isError: true,
|
252
|
+
};
|
253
|
+
}
|
254
|
+
}
|
255
|
+
catch (error) {
|
256
|
+
return {
|
257
|
+
content: [
|
258
|
+
{
|
259
|
+
type: "text",
|
260
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
261
|
+
},
|
262
|
+
],
|
263
|
+
isError: true,
|
264
|
+
};
|
265
|
+
}
|
266
|
+
});
|
267
|
+
async function runServer() {
|
268
|
+
const transport = new StdioServerTransport();
|
269
|
+
await server.connect(transport);
|
270
|
+
console.error("Brave Search MCP Server running on stdio");
|
271
|
+
}
|
272
|
+
runServer().catch((error) => {
|
273
|
+
console.error("Fatal error running server:", error);
|
274
|
+
process.exit(1);
|
275
|
+
});
|
package/package.json
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"name": "@modelcontextprotocol/server-brave-search",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"description": "MCP server for Brave Search API integration",
|
5
|
+
"license": "MIT",
|
6
|
+
"author": "Anthropic, PBC (https://anthropic.com)",
|
7
|
+
"homepage": "https://modelcontextprotocol.io",
|
8
|
+
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
9
|
+
"type": "module",
|
10
|
+
"bin": {
|
11
|
+
"mcp-server-brave-search": "dist/index.js"
|
12
|
+
},
|
13
|
+
"files": [
|
14
|
+
"dist"
|
15
|
+
],
|
16
|
+
"scripts": {
|
17
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
18
|
+
"prepare": "npm run build",
|
19
|
+
"watch": "tsc --watch"
|
20
|
+
},
|
21
|
+
"dependencies": {
|
22
|
+
"@modelcontextprotocol/sdk": "0.5.0",
|
23
|
+
"node-fetch": "^3.3.2"
|
24
|
+
},
|
25
|
+
"devDependencies": {
|
26
|
+
"@types/node": "^20.10.0",
|
27
|
+
"shx": "^0.3.4",
|
28
|
+
"typescript": "^5.6.2"
|
29
|
+
}
|
30
|
+
}
|