@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.
Files changed (3) hide show
  1. package/README.md +46 -0
  2. package/dist/index.js +275 -0
  3. 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
+ }