配置 Brave Search MCP Server
什么是 Brave Search MCP
Brave Search MCP 作为 Claude Desktop 和 Brave search 搜索引擎之间的连接。这种集成将Claude 与 MCP 的结构化连接无缝对接,让用户能够轻松访问网络和本地搜索。
Brave Search MCP 提供多种功能,包括:
- 实时网络信息检索
- 基于上下文的本地搜索,以提供更优质的结果
- 尊重隐私的 AI 辅助研究
翻译的这个文章中的段落内容:自己看完 MCP Integration: How Brave Search and Claude Desktop Enhance AI Assistant Agentic Capabilities
1. 前提
- 保留 filesystem
- filesystem 使用的是 网络驱动器
2. 新建 Brave search 文件夹
原因:
- filesystem server 与 Brave search server 是两个不同的 MCP 服务实现
- 它们使用各个独立的依赖
- 它们的配件不同
- 它们使用不同的工具
创建 Brave search 文件夹
我使用: C:\Users\dave\AppData\Roaming\Claude\Brave_search
我推荐使用本地目录,如果是映射驱动器或网路路径,要用 UNC 表达
C:\Users\<your ID>\AppData\Roaming\Claude\Brave_search
放这里,只是为了不离着近,不会删除目录。
3. 三个 node.js 配置文件放到上面的目录里面
Github 项目文件:servers/src/brave-search at main · modelcontextprotocol/servers · GitHub
自己用的,放到这里做为参考:
1)index.ts
这个文件没有变化
#!/usr/bin/env nodeimport { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { CallToolRequestSchema, ListToolsRequestSchema, Tool,} from "@modelcontextprotocol/sdk/types.js";const WEB_SEARCH_TOOL: Tool = { name: "brave_web_search", description:"Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +"Use this for broad information gathering, recent events, or when you need diverse web sources. " +"Supports pagination, content filtering, and freshness controls. " +"Maximum 20 results per request, with offset for pagination. ", inputSchema: {type: "object", properties: { query: {type: "string", description: "Search query (max 400 chars, 50 words)" },count: {type: "number", description: "Number of results (1-20, default 10)",default: 10 }, offset: {type: "number", description: "Pagination offset (max 9, default 0)",default: 0 }, }, required: ["query"], },};const LOCAL_SEARCH_TOOL: Tool = { name: "brave_local_search", description:"Searches for local businesses and places using Brave's Local Search API. " +"Best for queries related to physical locations, businesses, restaurants, services, etc. " +"Returns detailed information including:\n" +"- Business names and addresses\n" +"- Ratings and review counts\n" +"- Phone numbers and opening hours\n" +"Use this when the query implies 'near me' or mentions specific locations. " +"Automatically falls back to web search if no local results are found.", inputSchema: {type: "object", properties: { query: {type: "string", description: "Local search query (e.g. 'pizza near Central Park')" },count: {type: "number", description: "Number of results (1-20, default 5)",default: 5 }, }, required: ["query"] }};// Server implementationconst server = new Server( { name: "example-servers/brave-search", version: "0.1.0", }, { capabilities: { tools: {}, }, },);// Check for API keyconst BRAVE_API_KEY = process.env.BRAVE_API_KEY!;if (!BRAVE_API_KEY) { console.error("Error: BRAVE_API_KEY environment variable is required"); process.exit(1);}const RATE_LIMIT = { perSecond: 1, perMonth: 15000};let requestCount = { second: 0, month: 0, lastReset: Date.now()};function checkRateLimit() { const now = Date.now();if (now - requestCount.lastReset > 1000) { requestCount.second = 0; requestCount.lastReset = now; }if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) { throw new Error('Rate limit exceeded'); } requestCount.second++; requestCount.month++;}interface BraveWeb { web?: { results?: Array<{ title: string; description: string; url: string; language?: string; published?: string; rank?: number; }>; }; locations?: { results?: Array<{ id: string; // Required by API title?: string; }>; };}interface BraveLocation { id: string; name: string;address: { streetAddress?: string; addressLocality?: string; addressRegion?: string; postalCode?: string; }; coordinates?: { latitude: number; longitude: number; }; phone?: string; rating?: { ratingValue?: number; ratingCount?: number; }; openingHours?: string[]; priceRange?: string;}interface BravePoiResponse { results: BraveLocation[];}interface BraveDescription { descriptions: {[id: string]: string};}function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {return ( typeof args === "object" && args !== null &&"query" in args && typeof (args as { query: string }).query === "string" );}function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {return ( typeof args === "object" && args !== null &&"query" in args && typeof (args as { query: string }).query === "string" );}async function performWebSearch(query: string, count: number = 10, offset: number = 0) { checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/web/search'); url.searchParams.set('q', query); url.searchParams.set('count', Math.min(count, 20).toString()); // API limit url.searchParams.set('offset', offset.toString()); const response = await fetch(url, { headers: {'Accept': 'application/json','Accept-Encoding': 'gzip','X-Subscription-Token': BRAVE_API_KEY } });if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const data = await response.json() as BraveWeb;// Extract just web results const results = (data.web?.results || []).map(result => ({ title: result.title || '', description: result.description || '', url: result.url || '' }));return results.map(r => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}` ).join('\n\n');}async function performLocalSearch(query: string, count: number = 5) { checkRateLimit();// Initial search to get location IDs const webUrl = new URL('https://api.search.brave.com/res/v1/web/search'); webUrl.searchParams.set('q', query); webUrl.searchParams.set('search_lang', 'en'); webUrl.searchParams.set('result_filter', 'locations'); webUrl.searchParams.set('count', Math.min(count, 20).toString()); const webResponse = await fetch(webUrl, { headers: {'Accept': 'application/json','Accept-Encoding': 'gzip','X-Subscription-Token': BRAVE_API_KEY } });if (!webResponse.ok) { throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`); } const webData = await webResponse.json() as BraveWeb; const locationIds = webData.locations?.results?.filter((r): r is {id: string; title?: string} => r.id != null).map(r => r.id) || [];if (locationIds.length === 0) {return performWebSearch(query, count); // Fallback to web search }// Get POI details and descriptions in parallel const [poisData, descriptionsData] = await Promise.all([ getPoisData(locationIds), getDescriptionsData(locationIds) ]);return formatLocalResults(poisData, descriptionsData);}async function getPoisData(ids: string[]): Promise<BravePoiResponse> { checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/local/pois'); ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id)); const response = await fetch(url, { headers: {'Accept': 'application/json','Accept-Encoding': 'gzip','X-Subscription-Token': BRAVE_API_KEY } });if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const poisResponse = await response.json() as BravePoiResponse;return poisResponse;}async function getDescriptionsData(ids: string[]): Promise<BraveDescription> { checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/local/descriptions'); ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id)); const response = await fetch(url, { headers: {'Accept': 'application/json','Accept-Encoding': 'gzip','X-Subscription-Token': BRAVE_API_KEY } });if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const descriptionsData = await response.json() as BraveDescription;return descriptionsData;}function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {return (poisData.results || []).map(poi => { const address = [ poi.address?.streetAddress ?? '', poi.address?.addressLocality ?? '', poi.address?.addressRegion ?? '', poi.address?.postalCode ?? '' ].filter(part => part !== '').join(', ') || 'N/A';return `Name: ${poi.name}Address: ${address}Phone: ${poi.phone || 'N/A'}Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)Price Range: ${poi.priceRange || 'N/A'}Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}Description: ${descData.descriptions[poi.id] || 'No description available'}`; }).join('\n---\n') || 'No local results found';}// Tool handlersserver.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL],}));server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params;if (!args) { throw new Error("No arguments provided"); } switch (name) { case "brave_web_search": {if (!isBraveWebSearchArgs(args)) { throw new Error("Invalid arguments for brave_web_search"); } const { query, count = 10 } = args; const results = await performWebSearch(query, count);return {content: [{ type: "text", text: results }], isError: false, }; } case "brave_local_search": {if (!isBraveLocalSearchArgs(args)) { throw new Error("Invalid arguments for brave_local_search"); } const { query, count = 5 } = args; const results = await performLocalSearch(query, count);return {content: [{ type: "text", text: results }], isError: false, }; }default:return {content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) {return {content: [ {type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; }});async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Brave Search MCP Server running on stdio");}runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1);});
2)package.json
{"name": "@modelcontextprotocol/server-brave-search","version": "0.6.2","description": "MCP server for Brave Search API integration","type": "module","bin": {"mcp-server-brave-search": "dist/index.js" },"scripts": {"build": "tsc && shx chmod +x dist/*.js","prepare": "npm run build","watch": "tsc --watch" },"dependencies": {"@modelcontextprotocol/sdk": "1.0.1" },"devDependencies": {"@types/node": "^20.10.0","shx": "^0.3.4","typescript": "^5.6.2" }}
3)tsconfig.json
{"compilerOptions": {"outDir": "./dist","rootDir": ".","target": "ES2022","module": "NodeNext","moduleResolution": "NodeNext","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true },"include": ["./**/*.ts" ],"exclude": ["node_modules" ]}
4. claude_desktop_config.json 配置文件 (含 filesystem MCP)
{"mcpServers": {"filesystem": {"command": "npx","args": [ "-y", "@modelcontextprotocol/server-filesystem", "\\\\davens\\Multimedia\\2024-MyProgramFiles" ] },"brave-search": {"command": "npx","args": ["-y","@modelcontextprotocol/server-brave-search" ],"env": {"BRAVE_API_KEY": "用自己的KEY" } } }}
5. 申请 Brave Search Free KEY
这个在 Github 里有给链接:brave.com/search/api/
* 验证 Brave Search
1. 网上搜索
prompt: Can you search for recent trends in generative AI?
2 搜索本地内容, 因为我用 VXN 所在带上地名 p
prompt: 北京最古老的清真寺
{ `query`: `北京最古老的清真寺 历史 牛街`}{ `query`: `北京最古老的清真寺 历史 牛街`}Title: 牛街礼拜寺 - 维基百科,自由的百科全书Description: 据说,牛街礼拜寺始建于辽朝统和十四年(996年),由阿拉伯学者纳苏鲁丁(那速鲁定)创建。《北京牛街冈儿上礼拜寺志》(简称《冈志》)刘仲泉补志记载:URL: https://zh.wikipedia.org/zh-hans/%E7%89%9B%E8%A1%97%E7%A4%BC%E6%8B%9C%E5%AF%BATitle: 牛街 - 维基百科,自由的百科全书Description: 牛街是中国北京市西城区南部的一条南北走向的大街。北起广安门内大街南至南横街,因为在这条街上多穆斯林聚居而闻名,亦因为在这条街上拥有牛街礼拜寺而闻名于世。 · 牛街这个名称的历史远远没有这条街悠久,明...URL: https://zh.wikipedia.org/zh/%E7%89%9B%E8%A1%97Title: “牛街”的由来与北京最大的清真寺_建筑Description: <strong>牛街礼拜寺</strong>位于北京市西城区广安门内牛街,占地6000平方米,是北京规模最大、历史最久的清真寺,也是世界上著名的清真寺之一。URL: https://www.sohu.com/a/419716451_168296Title: 牛街Description: 牛街是北京市西城区牛街...京规模最大、历史最久的清真寺,<strong>始建于北宋至道二年(996年),明正统七年(1442年)重修,清康三十五年(1696年)大修,近年又有修缮装饰</strong>。寺院面积不大,但建筑集中、对称。...URL: https://baike.baidu.com/item/%E7%89%9B%E8%A1%97/22993Title: 牛街清真寺Description: 牛街清真寺位于广安门内牛街。是北京规模最大、历史最久的一座清真寺,<strong>创建于辽圣宗十三年(966),宋太宗至道元年(995)、明正统七年(1442)重修</strong>。清康熙三十五年(1696)又按原样进行大规模修葺。主要建筑有礼拜殿、梆歌楼...URL: https://s.visitbeijing.com.cn/attraction/101846Title: 北京最大的回民区,被称为吃货天堂,拥有北京最古老的清真寺_牛街Description: 作为北京最大的回民聚集区,牛街历史悠久,这里聚居着以回族为主的20多个少数民族,建筑风格和颜色具有民族特色,北京规模最大、历史最久的国家重点文物之一牛街礼拜寺就坐落其中。URL: https://www.sohu.com/a/440856633_100143624Title: 牛街,绝对是北京吃货的天堂_豆汁_红豆_红枣Description: 在宣南有块独特的宝地,那就是牛街。 · 别看牛街只是一条不长的街道,可这里却有二十多个民族的人生活在这里。牛街地区胡同连着胡同,小胡同大约60余条,其中回族住户相对集中的胡同有30余条。URL: https://www.sohu.com/a/615474822_121117451Title: Niujie Mosque, BeijingDescription: This is the version of our website addressed to speakers of English in the United States. If you are a resident of another country or region, please select the appropriate version of Tripadvisor for your country or region in the drop-down menu. moreURL: https://www.tripadvisor.com/Attraction_Review-g294212-d1979771-Reviews-Niujie_Mosque-Beijing.htmlTitle: 牛街礼拜寺_牛街礼拜寺_首都之窗_北京市人民政府门户网站Description: 牛街礼拜寺,是回族伊斯兰建筑,居北京四大清真寺之首。牛街清真寺的总平面布局很有特点。寺在牛街东侧,大殿必须坐西向东,入口就只能设在殿的后面。寺门以望月楼代替,楼前有木牌楼三间,隔街为照壁,以强调入口。URL: https://www.beijing.gov.cn/renwen/rwzyd/gdwh/njlbj/202107/t20210726_2448729.htmlTitle: 探秘北京牛街礼拜寺_赏心悦目_首都之窗_北京市人民政府门户网站Description: 牛街礼拜寺位于北京广安门内牛街,是北京历史最悠久、规模最大的清真寺,也是世界上著名的清真寺之一。喜欢它中国传统的木结构式以及浓厚的伊斯兰风格,神秘而静谧,正因古寺游客少,可以静心细看这明清古建,...URL: https://www.beijing.gov.cn/tsbj/sxym/202111/t20211130_2549950.html
可以看到搜索源是中文世界,还有英文的搜索词。可能是我用了 data for AI / suggestion
** 验证 Filesystem
总结:
Brave Search 使用 Free 订购要链接付款账号。
测试时,关闭 Claude Desktop App 需要在任务管理器结束 task (Claud Node.js如果有),再启动 Claude Desktop App。
Brave Search 没有计数器, 稍微上点儿隐就要送 刀乐。
有闲心从推荐从这三个里,找一个练练手。
- Google Maps - Location services, directions, and place details
- Memory - Knowledge graph-based persistent memory system
- PostgreSQL - Read-only database access with schema inspection