# Directory Structure
```
├── .dxtignore
├── .github
│ └── workflows
│ └── publish.yml
├── .gitignore
├── DEBUGGING.md
├── Dockerfile
├── eslint.config.js
├── LICENSE
├── manifest.json
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── index.ts
│ ├── server.ts
│ ├── tools
│ │ ├── BaseTool.ts
│ │ ├── BraveImageSearchTool.ts
│ │ ├── BraveLocalSearchTool.ts
│ │ ├── BraveNewsSearchTool.ts
│ │ ├── BraveVideoSearchTool.ts
│ │ └── BraveWebSearchTool.ts
│ └── utils.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | build
3 | dist
4 | .env
5 | notes.txt
6 | .vscode
```
--------------------------------------------------------------------------------
/.dxtignore:
--------------------------------------------------------------------------------
```
1 | notes.txt
2 | Dockerfile
3 | eslint.config.js
4 | smithery.yaml
5 | tsconfig.json
6 | DEBUGGING.md
7 | ./src
8 | ./.vscode/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Brave Search MCP Server
2 |
3 | An MCP Server implementation that integrates the [Brave Search API](https://brave.com/search/api/), providing, Web Search, Local Points of Interest Search, Video Search, Image Search and News Search capabilities
4 |
5 | <a href="https://glama.ai/mcp/servers/@mikechao/brave-search-mcp">
6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@mikechao/brave-search-mcp/badge" alt="Brave Search MCP server" />
7 | </a>
8 |
9 | ## Features
10 |
11 | - **Web Search**: Perform a regular search on the web
12 | - **Image Search**: Search the web for images. Image search results will be available as a Resource
13 | - **News Search**: Search the web for news
14 | - **Video Search**: Search the web for videos
15 | - **Local Points of Interest Search**: Search for local physical locations, businesses, restaurants, services, etc
16 |
17 | ## Tools
18 |
19 | - **brave_web_search**
20 |
21 | - Execute web searches using Brave's API
22 | - Inputs:
23 | - `query` (string): The term to search the internet for
24 | - `count` (number, optional): The number of results to return (max 20, default 10)
25 | - `offset` (number, optional, default 0): The offset for pagination
26 | - `freshness` (enum, optional): Filters search results by when they were discovered
27 | - The following values are supported
28 | - pd: Discovered within the last 24 hours.
29 | - pw: Discovered within the last 7 Days.
30 | - pm: Discovered within the last 31 Days.
31 | - py: Discovered within the last 365 Days
32 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
33 |
34 | - **brave_image_search**
35 |
36 | - Get images from the web relevant to the query
37 | - Inputs:
38 | - `query` (string): The term to search the internet for images of
39 | - `count` (number, optional): The number of images to return (max 3, default 1)
40 |
41 | - **brave_news_search**
42 |
43 | - Searches the web for news
44 | - Inputs:
45 | - `query` (string): The term to search the internet for news articles, trending topics, or recent events
46 | - `count` (number, optional): The number of results to return (max 20, default 10)
47 | - `freshness` (enum, optional): Filters search results by when they were discovered
48 | - The following values are supported
49 | - pd: Discovered within the last 24 hours.
50 | - pw: Discovered within the last 7 Days.
51 | - pm: Discovered within the last 31 Days.
52 | - py: Discovered within the last 365 Days
53 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
54 |
55 | - **brave_local_search**
56 |
57 | - Search for local businesses, services and points of interest
58 | - **REQUIRES** subscription to the Pro api plan for location results
59 | - Falls back to brave_web_search if no location results are found
60 | - Inputs:
61 | - `query` (string): Local search term
62 | - `count` (number, optional): The number of results to return (max 20, default 5)
63 |
64 | - **brave_video_search**
65 |
66 | - Search the web for videos
67 | - Inputs:
68 | - `query`: (string): The term to search for videos
69 | - `count`: (number, optional): The number of videos to return (max 20, default 10)
70 | - `freshness` (enum, optional): Filters search results by when they were discovered
71 | - The following values are supported
72 | - pd: Discovered within the last 24 hours.
73 | - pw: Discovered within the last 7 Days.
74 | - pm: Discovered within the last 31 Days.
75 | - py: Discovered within the last 365 Days
76 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
77 |
78 | ## Configuration
79 |
80 | ### Getting an API Key
81 |
82 | 1. Sign up for a [Brave Search API account](https://brave.com/search/api/)
83 | 2. Choose a plan (Free tier available with 2,000 queries/month)
84 | 3. Generate your API key [from the developer dashboard](https://api.search.brave.com/app/keys)
85 |
86 | ### Usage with Claude Code
87 |
88 | For [Claude Code](https://claude.ai/code) users, run this command:
89 |
90 | **Windows:**
91 |
92 | ```bash
93 | claude mcp add-json brave-search '{"command":"cmd","args":["/c","npx","-y","brave-search-mcp"],"env":{"BRAVE_API_KEY":"YOUR_API_KEY_HERE"}}'
94 | ```
95 |
96 | **Linux/macOS:**
97 |
98 | ```bash
99 | claude mcp add-json brave-search '{"command":"npx","args":["-y","brave-search-mcp"],"env":{"BRAVE_API_KEY":"YOUR_API_KEY_HERE"}}'
100 | ```
101 |
102 | Replace `YOUR_API_KEY_HERE` with your actual Brave Search API key.
103 |
104 | ### Usage with Claude Desktop
105 |
106 | ## Desktop Extension (DXT)
107 |
108 | 1. Download the `dxt` file from the [Releases](https://github.com/mikechao/brave-search-mcp/releases)
109 | 2. Open it with Claude Desktop
110 | or
111 | Go to File -> Settings -> Extensions and drag the .DXT file to the window to install it
112 |
113 | ## Docker
114 |
115 | 1. Clone the repo
116 | 2. Docker build
117 |
118 | ```bash
119 | docker build -t brave-search-mcp:latest -f ./Dockerfile .
120 | ```
121 |
122 | 3. Add this to your `claude_desktop_config.json`:
123 |
124 | ```json
125 | {
126 | "mcp-servers": {
127 | "brave-search": {
128 | "command": "docker",
129 | "args": [
130 | "run",
131 | "-i",
132 | "--rm",
133 | "-e",
134 | "BRAVE_API_KEY",
135 | "brave-search-mcp"
136 | ],
137 | "env": {
138 | "BRAVE_API_KEY": "YOUR API KEY HERE"
139 | }
140 | }
141 | }
142 | }
143 | ```
144 |
145 | ### NPX
146 |
147 | Add this to your `claude_desktop_config.json`:
148 |
149 | ```json
150 | {
151 | "mcp-servers": {
152 | "brave-search": {
153 | "command": "npx",
154 | "args": [
155 | "-y",
156 | "brave-search-mcp"
157 | ],
158 | "env": {
159 | "BRAVE_API_KEY": "YOUR API KEY HERE"
160 | }
161 | }
162 | }
163 | }
164 | ```
165 |
166 | ### Usage with LibreChat
167 |
168 | Add this to librechat.yaml
169 |
170 | ```yaml
171 | brave-search:
172 | command: sh
173 | args:
174 | - -c
175 | - BRAVE_API_KEY=API KEY npx -y brave-search-mcp
176 | ```
177 |
178 | ## Contributing
179 |
180 | Contributions are welcome! Please feel free to submit a Pull Request.
181 |
182 | ## Desktop Extensions (DXT)
183 |
184 | Anthropic recently released [Desktop Extensions](https://github.com/anthropics/dxt) allowing installation of local MCP Servers with one click.
185 |
186 | Install the CLI tool to help generate both `manifest.json` and final `.dxt` file.
187 |
188 | ```sh
189 | npm install -g @anthropic-ai/dxt
190 | ```
191 |
192 | ### Creating the manifest.json file
193 |
194 | 1. In this folder/directory which contains the local MCP Server, run `dxt init`. The command will start an interactive CLI to help create the `manifest.json`.
195 |
196 | ### Creating the `dxt` file
197 |
198 | 1. First install dev dependencies and build
199 |
200 | ```sh
201 | npm install
202 | npm run build
203 | ```
204 |
205 | 2. Then install only the production dependencies, generate a smaller nodule_modules directory
206 |
207 | ```sh
208 | npm install --omit=dev
209 | ```
210 |
211 | 3. Run `dxt pack` to create a `dxt` file. This will also validate the manifest.json that was created. The `dxt` is essentially a zip file and will contain everything in this directory.
212 |
213 | ## Disclaimer
214 |
215 | This library is not officially associated with Brave Software. It is a third-party implementation of the Brave Search API with a MCP Server.
216 |
217 | ## License
218 |
219 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
220 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import antfu from '@antfu/eslint-config';
2 |
3 | export default antfu({
4 | formatters: true,
5 | typescript: {
6 | overrides: {
7 | 'no-console': 'off',
8 | },
9 | },
10 | stylistic: {
11 | semi: true,
12 | indent: 2,
13 | quotes: 'single',
14 | },
15 | });
16 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "rootDir": "./src",
5 | "module": "Node16",
6 | "moduleResolution": "Node16",
7 | "strict": true,
8 | "outDir": "./dist",
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "skipLibCheck": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | startCommand:
2 | type: stdio
3 | configSchema:
4 | type: object
5 | require:
6 | - braveapikey
7 | properties:
8 | braveapikey:
9 | type: string
10 | description: The API key for Brave Search.
11 | commandFunction:
12 | |-
13 | (config) => ({ command: 'node', args: ['dist/index.js'], env: { BRAVE_API_KEY: config.braveapikey } })
14 | exampleConfig:
15 | braveapikey: YOUR_API_KEY_HERE
16 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import process from 'node:process';
4 | import { BraveMcpServer } from './server.js';
5 |
6 | // Check for API key
7 | const BRAVE_API_KEY = process.env.BRAVE_API_KEY;
8 | if (!BRAVE_API_KEY) {
9 | console.error('Error: BRAVE_API_KEY environment variable is required');
10 | process.exit(1);
11 | }
12 |
13 | const braveMcpServer = new BraveMcpServer(BRAVE_API_KEY);
14 | braveMcpServer.start().catch((error) => {
15 | console.error('Error starting server:', error);
16 | process.exit(1);
17 | });
18 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM node:23.11-alpine AS builder
2 |
3 | # Must be entire project because `prepare` script is run during `npm install` and requires all files.
4 | COPY . /app
5 |
6 | WORKDIR /app
7 |
8 | RUN --mount=type=cache,target=/root/.npm npm install
9 |
10 | FROM node:23.11-alpine AS release
11 |
12 | WORKDIR /app
13 |
14 | COPY --from=builder /app/dist /app/dist
15 | COPY --from=builder /app/package.json /app/package.json
16 | COPY --from=builder /app/package-lock.json /app/package-lock.json
17 |
18 | ENV NODE_ENV=production
19 |
20 | RUN npm ci --ignore-scripts --omit-dev
21 |
22 | ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/src/tools/BaseTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { z } from 'zod';
2 |
3 | export abstract class BaseTool<T extends z.ZodType, R> {
4 | public abstract readonly name: string;
5 | public abstract readonly description: string;
6 | public abstract readonly inputSchema: T;
7 |
8 | protected constructor() {}
9 |
10 | public abstract executeCore(input: z.infer<T>): Promise<R>;
11 |
12 | public async execute(input: z.infer<T>) {
13 | try {
14 | return await this.executeCore(input);
15 | }
16 | catch (error) {
17 | console.error(`Error executing ${this.name}:`, error);
18 | return {
19 | content: [{
20 | type: 'text' as const,
21 | text: `Error in ${this.name}: ${error}`,
22 | }],
23 | isError: true,
24 | };
25 | }
26 | }
27 | }
28 |
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish to NPM
2 | on:
3 | release:
4 | types: [created]
5 | jobs:
6 | publish-to-npm:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v4
11 | with:
12 | node-version: 20.19.0
13 | registry-url: 'https://registry.npmjs.org/'
14 | cache: npm
15 | - name: Install dependencies and build 🔧
16 | run: |
17 | npm ci
18 | npm run build || (echo "Build failed" && exit 1)
19 | - name: Publish to NPM 📦
20 | run: npm publish
21 | env:
22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
23 | permissions:
24 | contents: read
25 | id-token: write
26 |
27 | create-dxt-release:
28 | needs: publish-to-npm
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: actions/checkout@v4
32 | - uses: actions/setup-node@v4
33 | with:
34 | node-version: 20.19.0
35 | cache: npm
36 | - name: Install dependencies and build 🔧
37 | run: |
38 | npm ci
39 | npm run build || (echo "Build failed" && exit 1)
40 | - name: Install Anthropic DXT CLI
41 | run: npm install -g @anthropic-ai/dxt
42 | - name: Create DXT file
43 | run: |
44 | npm install --omit=dev
45 | dxt pack
46 | - name: Debug DXT file
47 | run: |
48 | ls -la *.dxt || echo "No .dxt files found"
49 | find . -name "*.dxt" -type f || echo "No .dxt files found in subdirectories"
50 | - name: Upload DXT to GitHub Release
51 | uses: softprops/action-gh-release@v2
52 | with:
53 | files: ./brave-search-mcp.dxt
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | permissions:
57 | contents: write
58 | id-token: write
59 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "brave-search-mcp",
3 | "type": "module",
4 | "version": "0.8.0",
5 | "description": "MCP Server that uses Brave Search API to search for images, general web search, video, news and points of interest.",
6 | "author": "[email protected]",
7 | "license": "GPL-3.0-or-later",
8 | "homepage": "https://github.com/mikechao/brave-search-mcp",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/mikechao/brave-search-mcp.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/mikechao/brave-search-mcp/issues"
15 | },
16 | "keywords": [
17 | "brave",
18 | "mcp",
19 | "web-search",
20 | "image-search",
21 | "news-search",
22 | "brave-search-api",
23 | "brave-search",
24 | "brave-search-mcp"
25 | ],
26 | "main": "dist/index.js",
27 | "bin": {
28 | "brave-search-mcp": "dist/index.js"
29 | },
30 | "files": [
31 | "LICENSE",
32 | "README.md",
33 | "dist"
34 | ],
35 | "scripts": {
36 | "clean": "shx rm -rf dist && shx mkdir dist",
37 | "build": "npm run clean && tsc && shx chmod +x dist/*.js",
38 | "build:watch": "tsc --sourceMap -p tsconfig.json -w",
39 | "lint": "eslint . --ext .ts,.js,.mjs,.cjs --fix",
40 | "lint:check": "eslint . --ext .ts,.js,.mjs,.cjs",
41 | "typecheck": "tsc --noEmit",
42 | "check": "npm run lint:check && npm run typecheck"
43 | },
44 | "dependencies": {
45 | "@modelcontextprotocol/sdk": "^1.8.0",
46 | "brave-search": "^0.9.0",
47 | "image-to-base64": "^2.2.0",
48 | "zod": "^3.24.3"
49 | },
50 | "devDependencies": {
51 | "@antfu/eslint-config": "^4.11.0",
52 | "@modelcontextprotocol/inspector": "^0.14.1",
53 | "@types/express": "^5.0.1",
54 | "@types/image-to-base64": "^2.1.2",
55 | "@types/node": "^22.13.14",
56 | "eslint": "^9.23.0",
57 | "eslint-plugin-format": "^1.0.1",
58 | "shx": "^0.4.0",
59 | "typescript": "^5.8.2"
60 | },
61 | "overrides": {
62 | "form-data": "^4.0.4"
63 | }
64 | }
65 |
```
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "dxt_version": "0.1",
3 | "name": "brave-search-mcp",
4 | "version": "0.8.0",
5 | "description": "MCP Server that uses Brave Search API to search for images, general web search, video, news and points of interest.",
6 | "author": {
7 | "name": "Mike Chao",
8 | "email": "[email protected]",
9 | "url": "https://github.com/mikechao"
10 | },
11 | "homepage": "https://github.com/mikechao/brave-search-mcp",
12 | "documentation": "https://github.com/mikechao/brave-search-mcp/blob/main/README.md",
13 | "server": {
14 | "type": "node",
15 | "entry_point": "dist/index.js",
16 | "mcp_config": {
17 | "command": "node",
18 | "args": [
19 | "${__dirname}/dist/index.js"
20 | ],
21 | "env": {
22 | "BRAVE_API_KEY": "${user_config.brave_api_key}"
23 | }
24 | }
25 | },
26 | "tools": [
27 | {
28 | "name": "brave_web_search",
29 | "description": "Execute web searches using Brave's API"
30 | },
31 | {
32 | "name": "brave_image_search",
33 | "description": "Get images from the web relevant to the query"
34 | },
35 | {
36 | "name": "brave_news_search",
37 | "description": "Searches the web for news"
38 | },
39 | {
40 | "name": "brave_local_search",
41 | "description": "Search for local businesses, services and points of interest"
42 | },
43 | {
44 | "name": "brave_video_search",
45 | "description": "Search the web for videos"
46 | }
47 | ],
48 | "keywords": [
49 | "brave search",
50 | "web search",
51 | "image search",
52 | "video search"
53 | ],
54 | "license": "GPL-3.0-or-later",
55 | "user_config": {
56 | "brave_api_key": {
57 | "type": "string",
58 | "title": "Brave Search API Key",
59 | "description": "Your Brave Search API key. You can get one from https://search.brave.com/settings/api-keys",
60 | "sensitive": true,
61 | "required": true
62 | }
63 | },
64 | "repository": {
65 | "type": "git",
66 | "url": "https://github.com/mikechao/brave-search-mcp.git"
67 | }
68 | }
69 |
```
--------------------------------------------------------------------------------
/src/tools/BraveImageSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { BraveSearch } from 'brave-search';
2 | import type { BraveMcpServer } from '../server.js';
3 | import { SafeSearchLevel } from 'brave-search/dist/types.js';
4 | import imageToBase64 from 'image-to-base64';
5 | import { z } from 'zod';
6 | import { BaseTool } from './BaseTool.js';
7 |
8 | const imageSearchInputSchema = z.object({
9 | searchTerm: z.string().describe('The term to search the internet for images of'),
10 | count: z.number().min(1).max(3).optional().default(1).describe('The number of images to search for, minimum 1, maximum 3'),
11 | });
12 |
13 | export class BraveImageSearchTool extends BaseTool<typeof imageSearchInputSchema, any> {
14 | public readonly name = 'brave_image_search';
15 | public readonly description = 'A tool for searching the web for images using the Brave Search API.';
16 | public readonly inputSchema = imageSearchInputSchema;
17 | public readonly imageByTitle = new Map<string, string>();
18 |
19 | constructor(private server: BraveMcpServer, private braveSearch: BraveSearch) {
20 | super();
21 | }
22 |
23 | public async executeCore(input: z.infer<typeof imageSearchInputSchema>) {
24 | const { searchTerm, count } = input;
25 | this.server.log(`Searching for images of "${searchTerm}" with count ${count}`, 'debug');
26 |
27 | const imageResults = await this.braveSearch.imageSearch(searchTerm, {
28 | count,
29 | safesearch: SafeSearchLevel.Strict,
30 | });
31 | this.server.log(`Found ${imageResults.results.length} images for "${searchTerm}"`, 'debug');
32 | const base64Strings = [];
33 | const titles = [];
34 | for (const result of imageResults.results) {
35 | const base64 = await imageToBase64(result.properties.url);
36 | this.server.log(`Image base64 length: ${base64.length}`, 'debug');
37 | titles.push(result.title);
38 | base64Strings.push(base64);
39 | this.imageByTitle.set(result.title, base64);
40 | }
41 | const results = [];
42 | for (const [index, title] of titles.entries()) {
43 | results.push({
44 | type: 'text',
45 | text: `${title}`,
46 | });
47 | results.push({
48 | type: 'image',
49 | data: base64Strings[index],
50 | mimeType: 'image/png',
51 | });
52 | }
53 | this.server.resourceChangedNotification();
54 | return { content: results };
55 | }
56 | }
57 |
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { LocalDescriptionsSearchApiResponse, LocalPoiSearchApiResponse, OpeningHours } from 'brave-search/dist/types.js';
2 | import type { BraveVideoResult } from './tools/BraveVideoSearchTool.js';
3 |
4 | export function formatPoiResults(poiData: LocalPoiSearchApiResponse, poiDesc: LocalDescriptionsSearchApiResponse) {
5 | return (poiData.results || []).map((poi) => {
6 | const description = poiDesc.results.find(locationDescription => locationDescription.id === poi.id);
7 | return `Name: ${poi.title}\n`
8 | + `${poi.serves_cuisine ? `Cuisine: ${poi.serves_cuisine.join(', ')}\n` : ''}`
9 | + `Address: ${poi.postal_address.displayAddress}\n`
10 | + `Phone: ${poi.contact?.telephone || 'No phone number found'}\n`
11 | + `Email: ${poi.contact?.email || 'No email found'}\n`
12 | + `Price Range: ${poi.price_range || 'No price range found'}\n`
13 | + `Ratings: ${poi.rating?.ratingValue || 'N/A'} (${poi.rating?.reviewCount}) reviews\n`
14 | + `Hours:\n ${(poi.opening_hours) ? formatOpeningHours(poi.opening_hours) : 'No opening hours found'}\n`
15 | + `Description: ${(description) ? description.description : 'No description found'}\n`;
16 | }).join('\n---\n');
17 | }
18 |
19 | export function formatVideoResults(results: BraveVideoResult[]) {
20 | return (results || []).map((video) => {
21 | return `Title: ${video.title}\n`
22 | + `URL: ${video.url}\n`
23 | + `Description: ${video.description}\n`
24 | + `Age: ${video.age}\n`
25 | + `Duration: ${video.video.duration}\n`
26 | + `Views: ${video.video.views}\n`
27 | + `Creator: ${video.video.creator}\n`
28 | + `${('requires_subscription' in video.video)
29 | ? (video.video.requires_subscription ? 'Requires subscription\n' : 'No subscription\n')
30 | : ''} `
31 | + `${('tags' in video.video && video.video.tags)
32 | ? (`Tags: ${video.video.tags.join(', ')}`)
33 | : ''} `
34 | ;
35 | }).join('\n---\n');
36 | }
37 |
38 | function formatOpeningHours(data: OpeningHours): string {
39 | const today = data.current_day.map((day) => {
40 | return `${day.full_name} ${day.opens} - ${day.closes}\n`;
41 | });
42 | const weekly = data.days.map((daySlot) => {
43 | return daySlot.map((day) => {
44 | return `${day.full_name} ${day.opens} - ${day.closes}`;
45 | });
46 | });
47 | return `Today: ${today}\nWeekly:\n${weekly.join('\n')}`;
48 | }
49 |
```
--------------------------------------------------------------------------------
/src/tools/BraveNewsSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { BraveSearch } from 'brave-search';
2 | import type { BraveMcpServer } from '../server.js';
3 | import { z } from 'zod';
4 | import { BaseTool } from './BaseTool.js';
5 |
6 | const newsSearchInputSchema = z.object({
7 | query: z.string().describe('The term to search the internet for news articles, trending topics, or recent events'),
8 | count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
9 | freshness: z.union([
10 | z.enum(['pd', 'pw', 'pm', 'py']),
11 | z.string().regex(/^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$/, 'Date range must be in format YYYY-MM-DDtoYYYY-MM-DD')
12 | ])
13 | .optional()
14 | .describe(
15 | `Filters search results by when they were discovered.
16 | The following values are supported:
17 | - pd: Discovered within the last 24 hours.
18 | - pw: Discovered within the last 7 Days.
19 | - pm: Discovered within the last 31 Days.
20 | - py: Discovered within the last 365 Days.
21 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
22 | ),
23 | });
24 |
25 | export class BraveNewsSearchTool extends BaseTool<typeof newsSearchInputSchema, any> {
26 | public readonly name = 'brave_news_search';
27 | public readonly description = 'Searches for news articles using the Brave Search API. '
28 | + 'Use this for recent events, trending topics, or specific news stories. '
29 | + 'Returns a list of articles with titles, URLs, and descriptions. '
30 | + 'Maximum 20 results per request.';
31 |
32 | public readonly inputSchema = newsSearchInputSchema;
33 |
34 | constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch) {
35 | super();
36 | }
37 |
38 | public async executeCore(input: z.infer<typeof newsSearchInputSchema>) {
39 | const { query, count, freshness } = input;
40 | const newsResult = await this.braveSearch.newsSearch(query, {
41 | count,
42 | ...(freshness ? { freshness } : {}),
43 | });
44 | if (!newsResult.results || newsResult.results.length === 0) {
45 | this.braveMcpServer.log(`No news results found for "${query}"`);
46 | const text = `No news results found for "${query}"`;
47 | return { content: [{ type: 'text' as const, text }] };
48 | }
49 |
50 | const text = newsResult.results
51 | .map(result =>
52 | `Title: ${result.title}\n`
53 | + `URL: ${result.url}\n`
54 | + `Age: ${result.age}\n`
55 | + `Description: ${result.description}\n`,
56 | )
57 | .join('\n\n');
58 | return { content: [{ type: 'text' as const, text }] };
59 | }
60 | }
61 |
```
--------------------------------------------------------------------------------
/src/tools/BraveWebSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { BraveSearch } from 'brave-search';
2 | import type { BraveMcpServer } from '../server.js';
3 | import { SafeSearchLevel } from 'brave-search/dist/types.js';
4 | import { z } from 'zod';
5 | import { BaseTool } from './BaseTool.js';
6 |
7 | const webSearchInputSchema = z.object({
8 | query: z.string().describe('The term to search the internet for'),
9 | count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
10 | offset: z.number().min(0).default(0).optional().describe('The offset for pagination, minimum 0'),
11 | freshness: z.union([
12 | z.enum(['pd', 'pw', 'pm', 'py']),
13 | z.string().regex(/^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$/, 'Date range must be in format YYYY-MM-DDtoYYYY-MM-DD')
14 | ])
15 | .optional()
16 | .describe(
17 | `Filters search results by when they were discovered.
18 | The following values are supported:
19 | - pd: Discovered within the last 24 hours.
20 | - pw: Discovered within the last 7 Days.
21 | - pm: Discovered within the last 31 Days.
22 | - py: Discovered within the last 365 Days.
23 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
24 | ),
25 | });
26 |
27 | export class BraveWebSearchTool extends BaseTool<typeof webSearchInputSchema, any> {
28 | public readonly name = 'brave_web_search';
29 | public readonly description = 'Performs a web search using the Brave Search API, ideal for general queries, and online content. '
30 | + 'Use this for broad information gathering, recent events, or when you need diverse web sources. '
31 | + 'Maximum 20 results per request ';
32 |
33 | public readonly inputSchema = webSearchInputSchema;
34 |
35 | constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch) {
36 | super();
37 | }
38 |
39 | public async executeCore(input: z.infer<typeof webSearchInputSchema>) {
40 | const { query, count, offset, freshness } = input;
41 | const results = await this.braveSearch.webSearch(query, {
42 | count,
43 | offset,
44 | safesearch: SafeSearchLevel.Strict,
45 | ...(freshness ? { freshness } : {}),
46 | });
47 | if (!results.web || results.web?.results.length === 0) {
48 | this.braveMcpServer.log(`No results found for "${query}"`);
49 | const text = `No results found for "${query}"`;
50 | return { content: [{ type: 'text' as const, text }] };
51 | }
52 | const text = results.web.results.map(result => `Title: ${result.title}\nURL: ${result.url}\nDescription: ${result.description}`).join('\n\n');
53 | return { content: [{ type: 'text' as const, text }] };
54 | }
55 | }
56 |
```
--------------------------------------------------------------------------------
/DEBUGGING.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Debugging
2 |
3 | 1. Clone the repo
4 |
5 | 2. Install Dependencies and build it
6 |
7 | ```bash
8 | npm install
9 | ```
10 |
11 | 3. Build the app
12 |
13 | ```bash
14 | npm run build
15 | ```
16 |
17 | ### Use the VS Code Run and Debug Function
18 |
19 | ⚠ Does not seem to work on Windows 10/11, but works in WSL2
20 |
21 | Use the VS Code
22 | [Run and Debug launcher](https://code.visualstudio.com/docs/debugtest/debugging#_start-a-debugging-session) with fully
23 | functional breakpoints in the code:
24 |
25 | 1. Locate and select the run debug.
26 | 2. Select the configuration labeled "`MCP Server Launcher`" in the dropdown.
27 | 3. Select the run/debug button.
28 | We can debug the various tools using [MCP Inspector](https://github.com/modelcontextprotocol/inspector) and VS Code.
29 |
30 | ### VS Code Debug setup
31 |
32 | To set up local debugging with breakpoints:
33 |
34 | 1. Store Brave API Key in the VS Code
35 |
36 | - Open the Command Palette (Cmd/Ctrl + Shift + P).
37 | - Type `Preferences: Open User Settings (JSON)`.
38 | - Add the following snippet:
39 |
40 | ```json
41 | {
42 | "brave.search.api.key": "your-api-key-here"
43 | }
44 | ```
45 |
46 | 2. Create or update `.vscode/launch.json`:
47 |
48 | ```json
49 | {
50 | "version": "0.2.0",
51 | "configurations": [
52 | {
53 | "type": "node",
54 | "request": "launch",
55 | "name": "MCP Server Launcher",
56 | "skipFiles": ["<node_internals>/**"],
57 | "program": "${workspaceFolder}/node_modules/@modelcontextprotocol/inspector/cli/build/cli.js",
58 | "outFiles": ["${workspaceFolder}/dist/**/*.js"],
59 | "env": {
60 | "BRAVE_API_KEY": "${config:brave.search.api.key}",
61 | "DEBUG": "true"
62 | },
63 | "args": ["dist/index.js"],
64 | "sourceMaps": true,
65 | "console": "integratedTerminal",
66 | "internalConsoleOptions": "neverOpen",
67 | "preLaunchTask": "npm: build:watch"
68 | },
69 | {
70 | "type": "node",
71 | "request": "attach",
72 | "name": "Attach to Debug Hook Process",
73 | "port": 9332,
74 | "skipFiles": ["<node_internals>/**"],
75 | "sourceMaps": true,
76 | "outFiles": ["${workspaceFolder}/dist/**/*.js"]
77 | },
78 | {
79 | "type": "node",
80 | "request": "attach",
81 | "name": "Attach to REPL Process",
82 | "port": 9333,
83 | "skipFiles": ["<node_internals>/**"],
84 | "sourceMaps": true,
85 | "outFiles": ["${workspaceFolder}/dist/**/*.js"]
86 | }
87 | ],
88 | "compounds": [
89 | {
90 | "name": "Attach to MCP Server",
91 | "configurations": ["Attach to Debug Hook Process", "Attach to REPL Process"]
92 | }
93 | ]
94 | }
95 | ```
96 |
97 | 3. Create `.vscode/tasks.json`:
98 |
99 | ```json
100 | {
101 | "version": "2.0.0",
102 | "tasks": [
103 | {
104 | "type": "npm",
105 | "script": "build:watch",
106 | "group": {
107 | "kind": "build",
108 | "isDefault": true
109 | },
110 | "problemMatcher": ["$tsc"]
111 | }
112 | ]
113 | }
114 | ```
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4 | import { BraveSearch } from 'brave-search';
5 | import { BraveImageSearchTool } from './tools/BraveImageSearchTool.js';
6 | import { BraveLocalSearchTool } from './tools/BraveLocalSearchTool.js';
7 | import { BraveNewsSearchTool } from './tools/BraveNewsSearchTool.js';
8 | import { BraveVideoSearchTool } from './tools/BraveVideoSearchTool.js';
9 | import { BraveWebSearchTool } from './tools/BraveWebSearchTool.js';
10 |
11 | export class BraveMcpServer {
12 | private server: McpServer;
13 | private braveSearch: BraveSearch;
14 | private imageSearchTool: BraveImageSearchTool;
15 | private webSearchTool: BraveWebSearchTool;
16 | private localSearchTool: BraveLocalSearchTool;
17 | private newsSearchTool: BraveNewsSearchTool;
18 | private videoSearchTool: BraveVideoSearchTool;
19 |
20 | constructor(private braveSearchApiKey: string) {
21 | this.server = new McpServer(
22 | {
23 | name: 'Brave Search MCP Server',
24 | description: 'A server that provides tools for searching the web, images, videos, and local businesses using the Brave Search API.',
25 | version: '0.6.0',
26 | },
27 | {
28 | capabilities: {
29 | resources: {},
30 | tools: {},
31 | logging: {},
32 | },
33 | },
34 | );
35 | this.braveSearch = new BraveSearch(braveSearchApiKey);
36 | this.imageSearchTool = new BraveImageSearchTool(this, this.braveSearch);
37 | this.webSearchTool = new BraveWebSearchTool(this, this.braveSearch);
38 | this.localSearchTool = new BraveLocalSearchTool(this, this.braveSearch, this.webSearchTool, braveSearchApiKey);
39 | this.newsSearchTool = new BraveNewsSearchTool(this, this.braveSearch);
40 | this.videoSearchTool = new BraveVideoSearchTool(this, this.braveSearch, braveSearchApiKey);
41 | this.setupTools();
42 | this.setupResourceListener();
43 | }
44 |
45 | private setupTools(): void {
46 | this.server.tool(
47 | this.imageSearchTool.name,
48 | this.imageSearchTool.description,
49 | this.imageSearchTool.inputSchema.shape,
50 | this.imageSearchTool.execute.bind(this.imageSearchTool),
51 | );
52 | this.server.tool(
53 | this.webSearchTool.name,
54 | this.webSearchTool.description,
55 | this.webSearchTool.inputSchema.shape,
56 | this.webSearchTool.execute.bind(this.webSearchTool),
57 | );
58 | this.server.tool(
59 | this.localSearchTool.name,
60 | this.localSearchTool.description,
61 | this.localSearchTool.inputSchema.shape,
62 | this.localSearchTool.execute.bind(this.localSearchTool),
63 | );
64 | this.server.tool(
65 | this.newsSearchTool.name,
66 | this.newsSearchTool.description,
67 | this.newsSearchTool.inputSchema.shape,
68 | this.newsSearchTool.execute.bind(this.newsSearchTool),
69 | );
70 | this.server.tool(
71 | this.videoSearchTool.name,
72 | this.videoSearchTool.description,
73 | this.videoSearchTool.inputSchema.shape,
74 | this.videoSearchTool.execute.bind(this.videoSearchTool),
75 | );
76 | }
77 |
78 | private setupResourceListener(): void {
79 | this.server.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
80 | resources: [
81 | ...Array.from(this.imageSearchTool.imageByTitle.keys()).map(title => ({
82 | uri: `brave-image://${title}`,
83 | mimeType: 'image/png',
84 | name: `${title}`,
85 | })),
86 | ],
87 | }));
88 |
89 | this.server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
90 | const uri = request.params.uri.toString();
91 | if (uri.startsWith('brave-image://')) {
92 | const title = uri.split('://')[1];
93 | const image = this.imageSearchTool.imageByTitle.get(title);
94 | if (image) {
95 | return {
96 | contents: [{
97 | uri,
98 | mimeType: 'image/png',
99 | blob: image,
100 | }],
101 | };
102 | }
103 | }
104 | return {
105 | content: [{ type: 'text', text: `Resource not found: ${uri}` }],
106 | isError: true,
107 | };
108 | });
109 | }
110 |
111 | public async start() {
112 | const transport = new StdioServerTransport();
113 | await this.server.connect(transport);
114 | this.log('Server is running with Stdio transport');
115 | }
116 |
117 | public resourceChangedNotification() {
118 | this.server.server.notification({
119 | method: 'notifications/resources/list_changed',
120 | });
121 | }
122 |
123 | public log(
124 | message: string,
125 | level: 'error' | 'debug' | 'info' | 'notice' | 'warning' | 'critical' | 'alert' | 'emergency' = 'info',
126 | ): void {
127 | this.server.server.sendLoggingMessage({
128 | level,
129 | message,
130 | });
131 | }
132 | }
133 |
```
--------------------------------------------------------------------------------
/src/tools/BraveVideoSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { BraveSearch } from 'brave-search';
2 | import type { BraveSearchOptions, Profile, Query, VideoData, VideoResult } from 'brave-search/dist/types.js';
3 | import type { BraveMcpServer } from '../server.js';
4 | import axios from 'axios';
5 | import { SafeSearchLevel } from 'brave-search/dist/types.js';
6 | import { z } from 'zod';
7 | import { formatVideoResults } from '../utils.js';
8 | import { BaseTool } from './BaseTool.js';
9 |
10 | // workaround for https://github.com/erik-balfe/brave-search/pull/4
11 | // not being merged yet into brave-search
12 | export interface BraveVideoData extends VideoData {
13 | /**
14 | * Whether the video requires a subscription.
15 | * @type {boolean}
16 | */
17 | requires_subscription?: boolean;
18 | /**
19 | * A list of tags relevant to the video.
20 | * @type {string[]}
21 | */
22 | tags?: string[];
23 | /**
24 | * A profile associated with the video.
25 | * @type {Profile}
26 | */
27 | author?: Profile;
28 | }
29 |
30 | export interface BraveVideoResult extends Omit<VideoResult, 'video'> {
31 | video: BraveVideoData;
32 | }
33 |
34 | export interface VideoSearchApiResponse {
35 | /**
36 | * The type of search API result. The value is always video.
37 | * @type {string}
38 | */
39 | type: 'video';
40 | /**
41 | * Video search query string.
42 | * @type {Query}
43 | */
44 | query: Query;
45 | /**
46 | * The list of video results for the given query.
47 | * @type {BraveVideoResult[]}
48 | */
49 | results: BraveVideoResult[];
50 | }
51 |
52 | export interface VideoSearchOptions extends Pick<BraveSearchOptions, 'country' | 'search_lang' | 'ui_lang' | 'count' | 'offset' | 'spellcheck' | 'safesearch' | 'freshness'> {
53 | }
54 | // end workaround
55 |
56 | const videoSearchInputSchema = z.object({
57 | query: z.string().describe('The term to search the internet for videos of'),
58 | count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
59 | freshness: z.union([
60 | z.enum(['pd', 'pw', 'pm', 'py']),
61 | z.string().regex(/^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$/, 'Date range must be in format YYYY-MM-DDtoYYYY-MM-DD')
62 | ])
63 | .optional()
64 | .describe(
65 | `Filters search results by when they were discovered.
66 | The following values are supported:
67 | - pd: Discovered within the last 24 hours.
68 | - pw: Discovered within the last 7 Days.
69 | - pm: Discovered within the last 31 Days.
70 | - py: Discovered within the last 365 Days.
71 | - YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
72 | ),
73 | });
74 |
75 | export class BraveVideoSearchTool extends BaseTool<typeof videoSearchInputSchema, any> {
76 | public readonly name = 'brave_video_search';
77 | public readonly description = 'Searches for videos using the Brave Search API. '
78 | + 'Use this for video content, tutorials, or any media-related queries. '
79 | + 'Returns a list of videos with titles, URLs, and descriptions. '
80 | + 'Maximum 20 results per request.';
81 |
82 | public readonly inputSchema = videoSearchInputSchema;
83 |
84 | private baseUrl = 'https://api.search.brave.com/res/v1';
85 |
86 | constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch, private apiKey: string) {
87 | super();
88 | }
89 |
90 | public async executeCore(input: z.infer<typeof videoSearchInputSchema>) {
91 | const { query, count, freshness } = input;
92 | const videoSearchResults = await this.videoSearch(query, {
93 | count,
94 | safesearch: SafeSearchLevel.Strict,
95 | ...(freshness ? { freshness } : {}),
96 | });
97 | if (!videoSearchResults.results || videoSearchResults.results.length === 0) {
98 | this.braveMcpServer.log(`No video results found for "${query}"`);
99 | const text = `No video results found for "${query}"`;
100 | return { content: [{ type: 'text' as const, text }] };
101 | }
102 |
103 | const text = formatVideoResults(videoSearchResults.results);
104 | return { content: [{ type: 'text' as const, text }] };
105 | }
106 |
107 | // workaround for https://github.com/erik-balfe/brave-search/pull/4
108 | // not being merged yet into brave-search
109 | private async videoSearch(
110 | query: string,
111 | options: VideoSearchOptions = {},
112 | ): Promise<VideoSearchApiResponse> {
113 | const response = await axios.get<VideoSearchApiResponse>(
114 | `${this.baseUrl}/videos/search?`,
115 | {
116 | params: {
117 | q: query,
118 | ...this.formatOptions(options),
119 | },
120 | headers: this.getHeaders(),
121 | },
122 | );
123 | return response.data;
124 | }
125 |
126 | private formatOptions(options: Record<string, any>): Record<string, string> {
127 | return Object.entries(options).reduce(
128 | (acc, [key, value]) => {
129 | if (value !== undefined) {
130 | acc[key] = value.toString();
131 | }
132 | return acc;
133 | },
134 | {} as Record<string, string>,
135 | );
136 | }
137 |
138 | private getHeaders() {
139 | return {
140 | 'Accept': 'application/json',
141 | 'Accept-Encoding': 'gzip',
142 | 'X-Subscription-Token': this.apiKey,
143 | };
144 | }
145 | // end workaround
146 | }
147 |
```
--------------------------------------------------------------------------------
/src/tools/BraveLocalSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { BraveSearch, LocalDescriptionsSearchApiResponse, LocalPoiSearchApiResponse } from 'brave-search';
2 | import type { BraveMcpServer } from '../server.js';
3 | import type { BraveWebSearchTool } from './BraveWebSearchTool.js';
4 | import { SafeSearchLevel } from 'brave-search/dist/types.js';
5 | import { z } from 'zod';
6 | import { formatPoiResults } from '../utils.js';
7 | import { BaseTool } from './BaseTool.js';
8 |
9 | const localSearchInputSchema = z.object({
10 | query: z.string().describe('Local search query (e.g. \'pizza near Central Park\')'),
11 | count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
12 | });
13 |
14 | export class BraveLocalSearchTool extends BaseTool<typeof localSearchInputSchema, any> {
15 | public readonly name = 'brave_local_search';
16 | public readonly description = 'Searches for local businesses and places using Brave\'s Local Search API. '
17 | + 'Best for queries related to physical locations, businesses, restaurants, services, etc. '
18 | + 'Returns detailed information including:\n'
19 | + '- Business names and addresses\n'
20 | + '- Ratings and review counts\n'
21 | + '- Phone numbers and opening hours\n'
22 | + 'Use this when the query implies \'near me\' or mentions specific locations. '
23 | + 'Automatically falls back to web search if no local results are found.';
24 |
25 | public readonly inputSchema = localSearchInputSchema;
26 |
27 | private baseUrl = 'https://api.search.brave.com/res/v1';
28 |
29 | constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch, private webSearchTool: BraveWebSearchTool, private apiKey: string) {
30 | super();
31 | }
32 |
33 | public async executeCore(input: z.infer<typeof localSearchInputSchema>) {
34 | const { query, count } = input;
35 | const results = await this.braveSearch.webSearch(query, {
36 | count,
37 | safesearch: SafeSearchLevel.Strict,
38 | result_filter: 'locations',
39 | });
40 | // it looks like the count parameter is only good for web search results
41 | if (!results.locations || results.locations?.results.length === 0) {
42 | this.braveMcpServer.log(`No location results found for "${query}" falling back to web search. Make sure your API Plan is at least "Pro"`);
43 | return this.webSearchTool.executeCore({ query, count, offset: 0 });
44 | }
45 | const allIds = results.locations.results.map(result => result.id);
46 | // count is restricted to 20 in the schema, and the location api supports up to 20 at a time
47 | // so we can just use the count parameter to limit the number of ids, take the first "count" ids
48 | const ids = allIds.slice(0, count);
49 | this.braveMcpServer.log(`Using ${ids.length} of ${allIds.length} location IDs for "${query}"`, 'debug');
50 | const formattedText = [];
51 |
52 | const localPoiSearchApiResponse = await this.localPoiSearch(ids);
53 | // the call to localPoiSearch does not return the id of the pois
54 | // add them here, they should be in the same order as the ids
55 | // and the same order of id in localDescriptionsSearchApiResponse
56 | localPoiSearchApiResponse.results.forEach((result, index) => {
57 | (result as any).id = ids[index];
58 | });
59 | const localDescriptionsSearchApiResponse = await this.localDescriptionsSearch(ids);
60 | const text = formatPoiResults(localPoiSearchApiResponse, localDescriptionsSearchApiResponse);
61 | formattedText.push(text);
62 | const finalText = formattedText.join('\n\n');
63 | return { content: [{ type: 'text' as const, text: finalText }] };
64 | }
65 |
66 | // workaround for https://github.com/erik-balfe/brave-search/pull/3
67 | // not being merged yet into brave-search
68 | private async localPoiSearch(ids: string[]) {
69 | try {
70 | const qs = ids.map(id => `ids=${encodeURIComponent(id)}`).join('&');
71 | const url = `${this.baseUrl}/local/pois?${qs}`;
72 | this.braveMcpServer.log(`Fetching local POI data from ${url}`, 'debug');
73 | const res = await fetch(url, {
74 | method: 'GET',
75 | headers: this.getHeaders(),
76 | redirect: 'follow',
77 | });
78 | if (!res.ok) {
79 | throw new Error(`Error fetching local POI data Status:${res.status} Status Text:${res.statusText} Headers:${JSON.stringify(res.headers)}`);
80 | }
81 | const data = (await res.json()) as LocalPoiSearchApiResponse;
82 | return data;
83 | }
84 | catch (error) {
85 | this.handleError(error);
86 | throw error;
87 | }
88 | }
89 |
90 | private async localDescriptionsSearch(ids: string[]) {
91 | try {
92 | const qs = ids.map(id => `ids=${encodeURIComponent(id)}`).join('&');
93 | const url = `${this.baseUrl}/local/descriptions?${qs}`;
94 | const res = await fetch(url, {
95 | method: 'GET',
96 | headers: this.getHeaders(),
97 | redirect: 'follow',
98 | });
99 | if (!res.ok) {
100 | const responseText = await res.text();
101 | this.braveMcpServer.log(`Error response body: ${responseText}`, 'error');
102 | this.braveMcpServer.log(`Response headers: ${JSON.stringify(Object.fromEntries(res.headers.entries()))}`, 'error');
103 | this.braveMcpServer.log(`Request URL: ${url}`, 'error');
104 | this.braveMcpServer.log(`Request headers: ${JSON.stringify(this.getHeaders())}`, 'error');
105 | if (res.status === 429) {
106 | this.braveMcpServer.log('429 Rate limit exceeded, consider adding delay between requests', 'error');
107 | }
108 | else if (res.status === 403) {
109 | this.braveMcpServer.log('403 Authentication error - check your API key', 'error');
110 | }
111 | else if (res.status === 500) {
112 | this.braveMcpServer.log('500 Internal server error - might be an issue with request format or API temporary issues', 'error');
113 | }
114 | // return an empty response instead of error so we can at least return the pois results
115 | return {
116 | type: 'local_descriptions',
117 | results: [],
118 | } as LocalDescriptionsSearchApiResponse;
119 | }
120 | const data = (await res.json()) as LocalDescriptionsSearchApiResponse;
121 | return data;
122 | }
123 | catch (error) {
124 | this.handleError(error);
125 | throw error;
126 | }
127 | }
128 |
129 | private handleError(error: any) {
130 | this.braveMcpServer.log(`${error}`, 'error');
131 | }
132 |
133 | private getHeaders() {
134 | return {
135 | 'Accept': '*/*',
136 | 'Accept-Encoding': 'gzip, deflate, br',
137 | 'Connection': 'keep-alive',
138 | 'X-Subscription-Token': this.apiKey,
139 | 'User-Agent': 'BraveSearchMCP/1.0',
140 | 'Content-Type': 'application/json',
141 | 'Cache-Control': 'no-cache',
142 | };
143 | }
144 | // end workaround
145 | }
146 |
```