@modelcontextprotocol/server-brave-search 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }