diff --git a/package-lock.json b/package-lock.json index 7730c30e..94a2c794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "src/*" ], "dependencies": { + "@modelcontextprotocol/server-brave-search": "*", "@modelcontextprotocol/server-duckduckgo": "*", "@modelcontextprotocol/server-everything": "*", "@modelcontextprotocol/server-filesystem": "*", @@ -162,6 +163,10 @@ "zod": "^3.23.8" } }, + "node_modules/@modelcontextprotocol/server-brave-search": { + "resolved": "src/brave-search", + "link": true + }, "node_modules/@modelcontextprotocol/server-duckduckgo": { "resolved": "src/duckduckgo", "link": true @@ -3607,6 +3612,62 @@ "zod": "^3.23.3" } }, + "src/brave-search": { + "name": "@modelcontextprotocol/server-brave-search", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.5.0", + "jsdom": "^24.1.3", + "node-fetch": "^3.3.2" + }, + "bin": { + "mcp-server-brave-search": "dist/index.js" + }, + "devDependencies": { + "@types/jsdom": "^21.1.6", + "@types/node": "^20.10.0", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/brave-search/node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "src/brave-search/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "src/brave-search/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "src/duckduckgo": { "name": "@modelcontextprotocol/server-duckduckgo", "version": "0.1.0", diff --git a/src/brave-search/README.md b/src/brave-search/README.md index 2c1661d9..0c118e29 100644 --- a/src/brave-search/README.md +++ b/src/brave-search/README.md @@ -1,6 +1,6 @@ # Brave Search MCP Server -An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities through the Model Context Protocol. +An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities. ## Features @@ -9,86 +9,38 @@ An MCP server implementation that integrates the Brave Search API, providing bot - **Flexible Filtering**: Control result types, safety levels, and content freshness - **Smart Fallbacks**: Local search automatically falls back to web when no results are found -## Configuration +## Tools -### Client Configuration -Add this to your MCP client config: +- **brave_web_search** + - Execute web searches with pagination and filtering + - Inputs: + - `query` (string): Search terms + - `count` (number, optional): Results per page (max 20) + - `offset` (number, optional): Pagination offset (max 9) -```json -"brave-search": { - "command": "mcp-server-brave-search", - "env": { - "BRAVE_API_KEY": "YOUR_API_KEY_HERE" - } -} -``` +- **brave_local_search** + - Search for local businesses and services + - Inputs: + - `query` (string): Local search terms + - `count` (number, optional): Number of results (max 20) + - Automatically falls back to web search if no local results found -Alternatively, you can set the API key as an environment variable: -```bash -export BRAVE_API_KEY='your_actual_api_key_here' -``` +## Configuration ### Getting an API Key -1. Sign up for a Brave Search API account -2. Choose a plan (Free tier available) -3. Generate your API key from the developer dashboard - -## Tools +1. Sign up for a [Brave Search API account](https://brave.com/search/api/) +2. Choose a plan (Free tier available with 2,000 queries/month) +3. Generate your API key [from the developer dashboard](https://api.search.brave.com/app/keys) -### brave_web_search -Performs general web searches: - -```javascript -{ - "name": "brave_web_search", - "arguments": { - "query": "latest AI developments", - "count": 10, - "freshness": "pw", // Past week - "safesearch": "moderate" - } -} -``` +### Usage with Claude Desktop +Add this to your `claude_desktop_config.json`: -### brave_local_search -Finds local businesses and services: - -```javascript -{ - "name": "brave_local_search", - "arguments": { - "query": "pizza near Central Park", - "count": 5, - "units": "imperial" +```json +"mcp-server-brave-search": { + "command": "mcp-server-brave-search", + "env": { + "BRAVE_API_KEY": "YOUR_API_KEY_HERE" } } ``` - -## Key Implementation Details - -- Rate limiting to respect API quotas (1 request/second, 15000/month) -- Parallel fetching of POI details and descriptions for local search -- Type-safe argument validation -- Comprehensive error handling and logging - -## Development - -```bash -# Install dependencies -npm install - -# Build the server -npm run build - -# Run the server -mcp-server-brave-search -``` - -## Contributing - -Contributions welcome! Please check the issues tab or submit a PR. - -## License - -MIT - see [LICENSE](LICENSE) file for details. diff --git a/src/brave-search/index.ts b/src/brave-search/index.ts index 1f814dbe..596dc9f7 100644 --- a/src/brave-search/index.ts +++ b/src/brave-search/index.ts @@ -4,26 +4,18 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, - ListResourcesRequestSchema, ListToolsRequestSchema, - ReadResourceRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; -// Define tool schemas 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. " + - "Additional features:\n" + - "- Safesearch: moderate (default), strict, or off\n" + - "- Freshness: filter by recency (past day/week/month/year)\n" + - "- Result types: web, news, videos, discussions\n" + - "- Spell check and query alteration support", + "Maximum 20 results per request, with offset for pagination. ", inputSchema: { type: "object", properties: { @@ -41,37 +33,6 @@ const WEB_SEARCH_TOOL: Tool = { description: "Pagination offset (max 9, default 0)", default: 0 }, - freshness: { - type: "string", - description: "Filter by recency: pd (past day), pw (past week), pm (past month), or custom date range", - enum: ["pd", "pw", "pm", "py"] - }, - safesearch: { - type: "string", - description: "Content filtering level", - enum: ["off", "moderate", "strict"], - default: "moderate" - }, - country: { - type: "string", - description: "2-letter country code for localized results", - default: "US" - }, - search_lang: { - type: "string", - description: "Search language (2+ char code)", - default: "en" - }, - ui_lang: { - type: "string", - description: "UI language preference", - default: "en-US" - }, - result_filter: { - type: "string", - description: "Comma-separated result types: web, news, videos, discussions, locations", - default: null - } }, required: ["query"], }, @@ -86,7 +47,6 @@ const LOCAL_SEARCH_TOOL: Tool = { "- Business names and addresses\n" + "- Ratings and review counts\n" + "- Phone numbers and opening hours\n" + - "- AI-generated descriptions\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: { @@ -101,26 +61,6 @@ const LOCAL_SEARCH_TOOL: Tool = { description: "Number of results (1-20, default 5)", default: 5 }, - units: { - type: "string", - description: "Measurement system for distances", - enum: ["metric", "imperial"] - }, - country: { - type: "string", - description: "2-letter country code for localized results", - default: "US" - }, - search_lang: { - type: "string", - description: "Search language (2+ char code)", - default: "en" - }, - ui_lang: { - type: "string", - description: "UI language preference", - default: "en-US" - } }, required: ["query"] } @@ -135,7 +75,6 @@ const server = new Server( { capabilities: { tools: {}, - resources: {}, }, }, ); @@ -221,7 +160,6 @@ interface BraveDescription { descriptions: {[id: string]: string}; } -// Type guard functions for arguments function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } { return ( typeof args === "object" && @@ -240,19 +178,12 @@ function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: ); } -// API functions 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('search_lang', 'en'); url.searchParams.set('count', Math.min(count, 20).toString()); // API limit url.searchParams.set('offset', offset.toString()); - url.searchParams.set('result_filter', 'web'); - url.searchParams.set('text_decorations', '0'); - url.searchParams.set('spellcheck', '0'); - url.searchParams.set('safesearch', 'moderate'); - url.searchParams.set('freshness', 'pw'); // Past week results const response = await fetch(url, { headers: { @@ -377,32 +308,6 @@ Description: ${descData.descriptions[poi.id] || 'No description available'} }).join('\n---\n') || 'No local results found'; } -// Resource handlers -server.setRequestHandler(ListResourcesRequestSchema, async () => ({ - resources: [ - { - uri: "brave://search", - mimeType: "text/plain", - name: "Brave Search Interface", - }, - ], -})); - -server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - if (request.params.uri.toString() === "brave://search") { - return { - contents: [ - { - uri: "brave://search", - mimeType: "text/plain", - text: "Brave Search API interface", - }, - ], - }; - } - throw new Error("Resource not found"); -}); - // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL],