# 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:
--------------------------------------------------------------------------------
```
node_modules
build
dist
.env
notes.txt
.vscode
```
--------------------------------------------------------------------------------
/.dxtignore:
--------------------------------------------------------------------------------
```
notes.txt
Dockerfile
eslint.config.js
smithery.yaml
tsconfig.json
DEBUGGING.md
./src
./.vscode/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Brave Search MCP Server
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
<a href="https://glama.ai/mcp/servers/@mikechao/brave-search-mcp">
<img width="380" height="200" src="https://glama.ai/mcp/servers/@mikechao/brave-search-mcp/badge" alt="Brave Search MCP server" />
</a>
## Features
- **Web Search**: Perform a regular search on the web
- **Image Search**: Search the web for images. Image search results will be available as a Resource
- **News Search**: Search the web for news
- **Video Search**: Search the web for videos
- **Local Points of Interest Search**: Search for local physical locations, businesses, restaurants, services, etc
## Tools
- **brave_web_search**
- Execute web searches using Brave's API
- Inputs:
- `query` (string): The term to search the internet for
- `count` (number, optional): The number of results to return (max 20, default 10)
- `offset` (number, optional, default 0): The offset for pagination
- `freshness` (enum, optional): Filters search results by when they were discovered
- The following values are supported
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
- **brave_image_search**
- Get images from the web relevant to the query
- Inputs:
- `query` (string): The term to search the internet for images of
- `count` (number, optional): The number of images to return (max 3, default 1)
- **brave_news_search**
- Searches the web for news
- Inputs:
- `query` (string): The term to search the internet for news articles, trending topics, or recent events
- `count` (number, optional): The number of results to return (max 20, default 10)
- `freshness` (enum, optional): Filters search results by when they were discovered
- The following values are supported
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
- **brave_local_search**
- Search for local businesses, services and points of interest
- **REQUIRES** subscription to the Pro api plan for location results
- Falls back to brave_web_search if no location results are found
- Inputs:
- `query` (string): Local search term
- `count` (number, optional): The number of results to return (max 20, default 5)
- **brave_video_search**
- Search the web for videos
- Inputs:
- `query`: (string): The term to search for videos
- `count`: (number, optional): The number of videos to return (max 20, default 10)
- `freshness` (enum, optional): Filters search results by when they were discovered
- The following values are supported
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)
## Configuration
### Getting an API Key
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)
### Usage with Claude Code
For [Claude Code](https://claude.ai/code) users, run this command:
**Windows:**
```bash
claude mcp add-json brave-search '{"command":"cmd","args":["/c","npx","-y","brave-search-mcp"],"env":{"BRAVE_API_KEY":"YOUR_API_KEY_HERE"}}'
```
**Linux/macOS:**
```bash
claude mcp add-json brave-search '{"command":"npx","args":["-y","brave-search-mcp"],"env":{"BRAVE_API_KEY":"YOUR_API_KEY_HERE"}}'
```
Replace `YOUR_API_KEY_HERE` with your actual Brave Search API key.
### Usage with Claude Desktop
## Desktop Extension (DXT)
1. Download the `dxt` file from the [Releases](https://github.com/mikechao/brave-search-mcp/releases)
2. Open it with Claude Desktop
or
Go to File -> Settings -> Extensions and drag the .DXT file to the window to install it
## Docker
1. Clone the repo
2. Docker build
```bash
docker build -t brave-search-mcp:latest -f ./Dockerfile .
```
3. Add this to your `claude_desktop_config.json`:
```json
{
"mcp-servers": {
"brave-search": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"BRAVE_API_KEY",
"brave-search-mcp"
],
"env": {
"BRAVE_API_KEY": "YOUR API KEY HERE"
}
}
}
}
```
### NPX
Add this to your `claude_desktop_config.json`:
```json
{
"mcp-servers": {
"brave-search": {
"command": "npx",
"args": [
"-y",
"brave-search-mcp"
],
"env": {
"BRAVE_API_KEY": "YOUR API KEY HERE"
}
}
}
}
```
### Usage with LibreChat
Add this to librechat.yaml
```yaml
brave-search:
command: sh
args:
- -c
- BRAVE_API_KEY=API KEY npx -y brave-search-mcp
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Desktop Extensions (DXT)
Anthropic recently released [Desktop Extensions](https://github.com/anthropics/dxt) allowing installation of local MCP Servers with one click.
Install the CLI tool to help generate both `manifest.json` and final `.dxt` file.
```sh
npm install -g @anthropic-ai/dxt
```
### Creating the manifest.json file
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`.
### Creating the `dxt` file
1. First install dev dependencies and build
```sh
npm install
npm run build
```
2. Then install only the production dependencies, generate a smaller nodule_modules directory
```sh
npm install --omit=dev
```
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.
## Disclaimer
This library is not officially associated with Brave Software. It is a third-party implementation of the Brave Search API with a MCP Server.
## License
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import antfu from '@antfu/eslint-config';
export default antfu({
formatters: true,
typescript: {
overrides: {
'no-console': 'off',
},
},
stylistic: {
semi: true,
indent: 2,
quotes: 'single',
},
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"rootDir": "./src",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
startCommand:
type: stdio
configSchema:
type: object
require:
- braveapikey
properties:
braveapikey:
type: string
description: The API key for Brave Search.
commandFunction:
|-
(config) => ({ command: 'node', args: ['dist/index.js'], env: { BRAVE_API_KEY: config.braveapikey } })
exampleConfig:
braveapikey: YOUR_API_KEY_HERE
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import process from 'node:process';
import { BraveMcpServer } from './server.js';
// Check for API key
const 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 braveMcpServer = new BraveMcpServer(BRAVE_API_KEY);
braveMcpServer.start().catch((error) => {
console.error('Error starting server:', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:23.11-alpine AS builder
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
COPY . /app
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
FROM node:23.11-alpine AS release
WORKDIR /app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
RUN npm ci --ignore-scripts --omit-dev
ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/src/tools/BaseTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { z } from 'zod';
export abstract class BaseTool<T extends z.ZodType, R> {
public abstract readonly name: string;
public abstract readonly description: string;
public abstract readonly inputSchema: T;
protected constructor() {}
public abstract executeCore(input: z.infer<T>): Promise<R>;
public async execute(input: z.infer<T>) {
try {
return await this.executeCore(input);
}
catch (error) {
console.error(`Error executing ${this.name}:`, error);
return {
content: [{
type: 'text' as const,
text: `Error in ${this.name}: ${error}`,
}],
isError: true,
};
}
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish to NPM
on:
release:
types: [created]
jobs:
publish-to-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.0
registry-url: 'https://registry.npmjs.org/'
cache: npm
- name: Install dependencies and build 🔧
run: |
npm ci
npm run build || (echo "Build failed" && exit 1)
- name: Publish to NPM 📦
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
contents: read
id-token: write
create-dxt-release:
needs: publish-to-npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.0
cache: npm
- name: Install dependencies and build 🔧
run: |
npm ci
npm run build || (echo "Build failed" && exit 1)
- name: Install Anthropic DXT CLI
run: npm install -g @anthropic-ai/dxt
- name: Create DXT file
run: |
npm install --omit=dev
dxt pack
- name: Debug DXT file
run: |
ls -la *.dxt || echo "No .dxt files found"
find . -name "*.dxt" -type f || echo "No .dxt files found in subdirectories"
- name: Upload DXT to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: ./brave-search-mcp.dxt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
id-token: write
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "brave-search-mcp",
"type": "module",
"version": "0.8.0",
"description": "MCP Server that uses Brave Search API to search for images, general web search, video, news and points of interest.",
"author": "[email protected]",
"license": "GPL-3.0-or-later",
"homepage": "https://github.com/mikechao/brave-search-mcp",
"repository": {
"type": "git",
"url": "https://github.com/mikechao/brave-search-mcp.git"
},
"bugs": {
"url": "https://github.com/mikechao/brave-search-mcp/issues"
},
"keywords": [
"brave",
"mcp",
"web-search",
"image-search",
"news-search",
"brave-search-api",
"brave-search",
"brave-search-mcp"
],
"main": "dist/index.js",
"bin": {
"brave-search-mcp": "dist/index.js"
},
"files": [
"LICENSE",
"README.md",
"dist"
],
"scripts": {
"clean": "shx rm -rf dist && shx mkdir dist",
"build": "npm run clean && tsc && shx chmod +x dist/*.js",
"build:watch": "tsc --sourceMap -p tsconfig.json -w",
"lint": "eslint . --ext .ts,.js,.mjs,.cjs --fix",
"lint:check": "eslint . --ext .ts,.js,.mjs,.cjs",
"typecheck": "tsc --noEmit",
"check": "npm run lint:check && npm run typecheck"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"brave-search": "^0.9.0",
"image-to-base64": "^2.2.0",
"zod": "^3.24.3"
},
"devDependencies": {
"@antfu/eslint-config": "^4.11.0",
"@modelcontextprotocol/inspector": "^0.14.1",
"@types/express": "^5.0.1",
"@types/image-to-base64": "^2.1.2",
"@types/node": "^22.13.14",
"eslint": "^9.23.0",
"eslint-plugin-format": "^1.0.1",
"shx": "^0.4.0",
"typescript": "^5.8.2"
},
"overrides": {
"form-data": "^4.0.4"
}
}
```
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
```json
{
"dxt_version": "0.1",
"name": "brave-search-mcp",
"version": "0.8.0",
"description": "MCP Server that uses Brave Search API to search for images, general web search, video, news and points of interest.",
"author": {
"name": "Mike Chao",
"email": "[email protected]",
"url": "https://github.com/mikechao"
},
"homepage": "https://github.com/mikechao/brave-search-mcp",
"documentation": "https://github.com/mikechao/brave-search-mcp/blob/main/README.md",
"server": {
"type": "node",
"entry_point": "dist/index.js",
"mcp_config": {
"command": "node",
"args": [
"${__dirname}/dist/index.js"
],
"env": {
"BRAVE_API_KEY": "${user_config.brave_api_key}"
}
}
},
"tools": [
{
"name": "brave_web_search",
"description": "Execute web searches using Brave's API"
},
{
"name": "brave_image_search",
"description": "Get images from the web relevant to the query"
},
{
"name": "brave_news_search",
"description": "Searches the web for news"
},
{
"name": "brave_local_search",
"description": "Search for local businesses, services and points of interest"
},
{
"name": "brave_video_search",
"description": "Search the web for videos"
}
],
"keywords": [
"brave search",
"web search",
"image search",
"video search"
],
"license": "GPL-3.0-or-later",
"user_config": {
"brave_api_key": {
"type": "string",
"title": "Brave Search API Key",
"description": "Your Brave Search API key. You can get one from https://search.brave.com/settings/api-keys",
"sensitive": true,
"required": true
}
},
"repository": {
"type": "git",
"url": "https://github.com/mikechao/brave-search-mcp.git"
}
}
```
--------------------------------------------------------------------------------
/src/tools/BraveImageSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { BraveSearch } from 'brave-search';
import type { BraveMcpServer } from '../server.js';
import { SafeSearchLevel } from 'brave-search/dist/types.js';
import imageToBase64 from 'image-to-base64';
import { z } from 'zod';
import { BaseTool } from './BaseTool.js';
const imageSearchInputSchema = z.object({
searchTerm: z.string().describe('The term to search the internet for images of'),
count: z.number().min(1).max(3).optional().default(1).describe('The number of images to search for, minimum 1, maximum 3'),
});
export class BraveImageSearchTool extends BaseTool<typeof imageSearchInputSchema, any> {
public readonly name = 'brave_image_search';
public readonly description = 'A tool for searching the web for images using the Brave Search API.';
public readonly inputSchema = imageSearchInputSchema;
public readonly imageByTitle = new Map<string, string>();
constructor(private server: BraveMcpServer, private braveSearch: BraveSearch) {
super();
}
public async executeCore(input: z.infer<typeof imageSearchInputSchema>) {
const { searchTerm, count } = input;
this.server.log(`Searching for images of "${searchTerm}" with count ${count}`, 'debug');
const imageResults = await this.braveSearch.imageSearch(searchTerm, {
count,
safesearch: SafeSearchLevel.Strict,
});
this.server.log(`Found ${imageResults.results.length} images for "${searchTerm}"`, 'debug');
const base64Strings = [];
const titles = [];
for (const result of imageResults.results) {
const base64 = await imageToBase64(result.properties.url);
this.server.log(`Image base64 length: ${base64.length}`, 'debug');
titles.push(result.title);
base64Strings.push(base64);
this.imageByTitle.set(result.title, base64);
}
const results = [];
for (const [index, title] of titles.entries()) {
results.push({
type: 'text',
text: `${title}`,
});
results.push({
type: 'image',
data: base64Strings[index],
mimeType: 'image/png',
});
}
this.server.resourceChangedNotification();
return { content: results };
}
}
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
import type { LocalDescriptionsSearchApiResponse, LocalPoiSearchApiResponse, OpeningHours } from 'brave-search/dist/types.js';
import type { BraveVideoResult } from './tools/BraveVideoSearchTool.js';
export function formatPoiResults(poiData: LocalPoiSearchApiResponse, poiDesc: LocalDescriptionsSearchApiResponse) {
return (poiData.results || []).map((poi) => {
const description = poiDesc.results.find(locationDescription => locationDescription.id === poi.id);
return `Name: ${poi.title}\n`
+ `${poi.serves_cuisine ? `Cuisine: ${poi.serves_cuisine.join(', ')}\n` : ''}`
+ `Address: ${poi.postal_address.displayAddress}\n`
+ `Phone: ${poi.contact?.telephone || 'No phone number found'}\n`
+ `Email: ${poi.contact?.email || 'No email found'}\n`
+ `Price Range: ${poi.price_range || 'No price range found'}\n`
+ `Ratings: ${poi.rating?.ratingValue || 'N/A'} (${poi.rating?.reviewCount}) reviews\n`
+ `Hours:\n ${(poi.opening_hours) ? formatOpeningHours(poi.opening_hours) : 'No opening hours found'}\n`
+ `Description: ${(description) ? description.description : 'No description found'}\n`;
}).join('\n---\n');
}
export function formatVideoResults(results: BraveVideoResult[]) {
return (results || []).map((video) => {
return `Title: ${video.title}\n`
+ `URL: ${video.url}\n`
+ `Description: ${video.description}\n`
+ `Age: ${video.age}\n`
+ `Duration: ${video.video.duration}\n`
+ `Views: ${video.video.views}\n`
+ `Creator: ${video.video.creator}\n`
+ `${('requires_subscription' in video.video)
? (video.video.requires_subscription ? 'Requires subscription\n' : 'No subscription\n')
: ''} `
+ `${('tags' in video.video && video.video.tags)
? (`Tags: ${video.video.tags.join(', ')}`)
: ''} `
;
}).join('\n---\n');
}
function formatOpeningHours(data: OpeningHours): string {
const today = data.current_day.map((day) => {
return `${day.full_name} ${day.opens} - ${day.closes}\n`;
});
const weekly = data.days.map((daySlot) => {
return daySlot.map((day) => {
return `${day.full_name} ${day.opens} - ${day.closes}`;
});
});
return `Today: ${today}\nWeekly:\n${weekly.join('\n')}`;
}
```
--------------------------------------------------------------------------------
/src/tools/BraveNewsSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { BraveSearch } from 'brave-search';
import type { BraveMcpServer } from '../server.js';
import { z } from 'zod';
import { BaseTool } from './BaseTool.js';
const newsSearchInputSchema = z.object({
query: z.string().describe('The term to search the internet for news articles, trending topics, or recent events'),
count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
freshness: z.union([
z.enum(['pd', 'pw', 'pm', 'py']),
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')
])
.optional()
.describe(
`Filters search results by when they were discovered.
The following values are supported:
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days.
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
),
});
export class BraveNewsSearchTool extends BaseTool<typeof newsSearchInputSchema, any> {
public readonly name = 'brave_news_search';
public readonly description = 'Searches for news articles using the Brave Search API. '
+ 'Use this for recent events, trending topics, or specific news stories. '
+ 'Returns a list of articles with titles, URLs, and descriptions. '
+ 'Maximum 20 results per request.';
public readonly inputSchema = newsSearchInputSchema;
constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch) {
super();
}
public async executeCore(input: z.infer<typeof newsSearchInputSchema>) {
const { query, count, freshness } = input;
const newsResult = await this.braveSearch.newsSearch(query, {
count,
...(freshness ? { freshness } : {}),
});
if (!newsResult.results || newsResult.results.length === 0) {
this.braveMcpServer.log(`No news results found for "${query}"`);
const text = `No news results found for "${query}"`;
return { content: [{ type: 'text' as const, text }] };
}
const text = newsResult.results
.map(result =>
`Title: ${result.title}\n`
+ `URL: ${result.url}\n`
+ `Age: ${result.age}\n`
+ `Description: ${result.description}\n`,
)
.join('\n\n');
return { content: [{ type: 'text' as const, text }] };
}
}
```
--------------------------------------------------------------------------------
/src/tools/BraveWebSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { BraveSearch } from 'brave-search';
import type { BraveMcpServer } from '../server.js';
import { SafeSearchLevel } from 'brave-search/dist/types.js';
import { z } from 'zod';
import { BaseTool } from './BaseTool.js';
const webSearchInputSchema = z.object({
query: z.string().describe('The term to search the internet for'),
count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
offset: z.number().min(0).default(0).optional().describe('The offset for pagination, minimum 0'),
freshness: z.union([
z.enum(['pd', 'pw', 'pm', 'py']),
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')
])
.optional()
.describe(
`Filters search results by when they were discovered.
The following values are supported:
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days.
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
),
});
export class BraveWebSearchTool extends BaseTool<typeof webSearchInputSchema, any> {
public readonly name = 'brave_web_search';
public readonly description = 'Performs a web search using the Brave Search API, ideal for general queries, and online content. '
+ 'Use this for broad information gathering, recent events, or when you need diverse web sources. '
+ 'Maximum 20 results per request ';
public readonly inputSchema = webSearchInputSchema;
constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch) {
super();
}
public async executeCore(input: z.infer<typeof webSearchInputSchema>) {
const { query, count, offset, freshness } = input;
const results = await this.braveSearch.webSearch(query, {
count,
offset,
safesearch: SafeSearchLevel.Strict,
...(freshness ? { freshness } : {}),
});
if (!results.web || results.web?.results.length === 0) {
this.braveMcpServer.log(`No results found for "${query}"`);
const text = `No results found for "${query}"`;
return { content: [{ type: 'text' as const, text }] };
}
const text = results.web.results.map(result => `Title: ${result.title}\nURL: ${result.url}\nDescription: ${result.description}`).join('\n\n');
return { content: [{ type: 'text' as const, text }] };
}
}
```
--------------------------------------------------------------------------------
/DEBUGGING.md:
--------------------------------------------------------------------------------
```markdown
## Debugging
1. Clone the repo
2. Install Dependencies and build it
```bash
npm install
```
3. Build the app
```bash
npm run build
```
### Use the VS Code Run and Debug Function
⚠ Does not seem to work on Windows 10/11, but works in WSL2
Use the VS Code
[Run and Debug launcher](https://code.visualstudio.com/docs/debugtest/debugging#_start-a-debugging-session) with fully
functional breakpoints in the code:
1. Locate and select the run debug.
2. Select the configuration labeled "`MCP Server Launcher`" in the dropdown.
3. Select the run/debug button.
We can debug the various tools using [MCP Inspector](https://github.com/modelcontextprotocol/inspector) and VS Code.
### VS Code Debug setup
To set up local debugging with breakpoints:
1. Store Brave API Key in the VS Code
- Open the Command Palette (Cmd/Ctrl + Shift + P).
- Type `Preferences: Open User Settings (JSON)`.
- Add the following snippet:
```json
{
"brave.search.api.key": "your-api-key-here"
}
```
2. Create or update `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "MCP Server Launcher",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/node_modules/@modelcontextprotocol/inspector/cli/build/cli.js",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"env": {
"BRAVE_API_KEY": "${config:brave.search.api.key}",
"DEBUG": "true"
},
"args": ["dist/index.js"],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"preLaunchTask": "npm: build:watch"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Debug Hook Process",
"port": 9332,
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
},
{
"type": "node",
"request": "attach",
"name": "Attach to REPL Process",
"port": 9333,
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
],
"compounds": [
{
"name": "Attach to MCP Server",
"configurations": ["Attach to Debug Hook Process", "Attach to REPL Process"]
}
]
}
```
3. Create `.vscode/tasks.json`:
```json
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build:watch",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$tsc"]
}
]
}
```
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { BraveSearch } from 'brave-search';
import { BraveImageSearchTool } from './tools/BraveImageSearchTool.js';
import { BraveLocalSearchTool } from './tools/BraveLocalSearchTool.js';
import { BraveNewsSearchTool } from './tools/BraveNewsSearchTool.js';
import { BraveVideoSearchTool } from './tools/BraveVideoSearchTool.js';
import { BraveWebSearchTool } from './tools/BraveWebSearchTool.js';
export class BraveMcpServer {
private server: McpServer;
private braveSearch: BraveSearch;
private imageSearchTool: BraveImageSearchTool;
private webSearchTool: BraveWebSearchTool;
private localSearchTool: BraveLocalSearchTool;
private newsSearchTool: BraveNewsSearchTool;
private videoSearchTool: BraveVideoSearchTool;
constructor(private braveSearchApiKey: string) {
this.server = new McpServer(
{
name: 'Brave Search MCP Server',
description: 'A server that provides tools for searching the web, images, videos, and local businesses using the Brave Search API.',
version: '0.6.0',
},
{
capabilities: {
resources: {},
tools: {},
logging: {},
},
},
);
this.braveSearch = new BraveSearch(braveSearchApiKey);
this.imageSearchTool = new BraveImageSearchTool(this, this.braveSearch);
this.webSearchTool = new BraveWebSearchTool(this, this.braveSearch);
this.localSearchTool = new BraveLocalSearchTool(this, this.braveSearch, this.webSearchTool, braveSearchApiKey);
this.newsSearchTool = new BraveNewsSearchTool(this, this.braveSearch);
this.videoSearchTool = new BraveVideoSearchTool(this, this.braveSearch, braveSearchApiKey);
this.setupTools();
this.setupResourceListener();
}
private setupTools(): void {
this.server.tool(
this.imageSearchTool.name,
this.imageSearchTool.description,
this.imageSearchTool.inputSchema.shape,
this.imageSearchTool.execute.bind(this.imageSearchTool),
);
this.server.tool(
this.webSearchTool.name,
this.webSearchTool.description,
this.webSearchTool.inputSchema.shape,
this.webSearchTool.execute.bind(this.webSearchTool),
);
this.server.tool(
this.localSearchTool.name,
this.localSearchTool.description,
this.localSearchTool.inputSchema.shape,
this.localSearchTool.execute.bind(this.localSearchTool),
);
this.server.tool(
this.newsSearchTool.name,
this.newsSearchTool.description,
this.newsSearchTool.inputSchema.shape,
this.newsSearchTool.execute.bind(this.newsSearchTool),
);
this.server.tool(
this.videoSearchTool.name,
this.videoSearchTool.description,
this.videoSearchTool.inputSchema.shape,
this.videoSearchTool.execute.bind(this.videoSearchTool),
);
}
private setupResourceListener(): void {
this.server.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
...Array.from(this.imageSearchTool.imageByTitle.keys()).map(title => ({
uri: `brave-image://${title}`,
mimeType: 'image/png',
name: `${title}`,
})),
],
}));
this.server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri.startsWith('brave-image://')) {
const title = uri.split('://')[1];
const image = this.imageSearchTool.imageByTitle.get(title);
if (image) {
return {
contents: [{
uri,
mimeType: 'image/png',
blob: image,
}],
};
}
}
return {
content: [{ type: 'text', text: `Resource not found: ${uri}` }],
isError: true,
};
});
}
public async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
this.log('Server is running with Stdio transport');
}
public resourceChangedNotification() {
this.server.server.notification({
method: 'notifications/resources/list_changed',
});
}
public log(
message: string,
level: 'error' | 'debug' | 'info' | 'notice' | 'warning' | 'critical' | 'alert' | 'emergency' = 'info',
): void {
this.server.server.sendLoggingMessage({
level,
message,
});
}
}
```
--------------------------------------------------------------------------------
/src/tools/BraveVideoSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { BraveSearch } from 'brave-search';
import type { BraveSearchOptions, Profile, Query, VideoData, VideoResult } from 'brave-search/dist/types.js';
import type { BraveMcpServer } from '../server.js';
import axios from 'axios';
import { SafeSearchLevel } from 'brave-search/dist/types.js';
import { z } from 'zod';
import { formatVideoResults } from '../utils.js';
import { BaseTool } from './BaseTool.js';
// workaround for https://github.com/erik-balfe/brave-search/pull/4
// not being merged yet into brave-search
export interface BraveVideoData extends VideoData {
/**
* Whether the video requires a subscription.
* @type {boolean}
*/
requires_subscription?: boolean;
/**
* A list of tags relevant to the video.
* @type {string[]}
*/
tags?: string[];
/**
* A profile associated with the video.
* @type {Profile}
*/
author?: Profile;
}
export interface BraveVideoResult extends Omit<VideoResult, 'video'> {
video: BraveVideoData;
}
export interface VideoSearchApiResponse {
/**
* The type of search API result. The value is always video.
* @type {string}
*/
type: 'video';
/**
* Video search query string.
* @type {Query}
*/
query: Query;
/**
* The list of video results for the given query.
* @type {BraveVideoResult[]}
*/
results: BraveVideoResult[];
}
export interface VideoSearchOptions extends Pick<BraveSearchOptions, 'country' | 'search_lang' | 'ui_lang' | 'count' | 'offset' | 'spellcheck' | 'safesearch' | 'freshness'> {
}
// end workaround
const videoSearchInputSchema = z.object({
query: z.string().describe('The term to search the internet for videos of'),
count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
freshness: z.union([
z.enum(['pd', 'pw', 'pm', 'py']),
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')
])
.optional()
.describe(
`Filters search results by when they were discovered.
The following values are supported:
- pd: Discovered within the last 24 hours.
- pw: Discovered within the last 7 Days.
- pm: Discovered within the last 31 Days.
- py: Discovered within the last 365 Days.
- YYYY-MM-DDtoYYYY-MM-DD: Custom date range (e.g., 2022-04-01to2022-07-30)`,
),
});
export class BraveVideoSearchTool extends BaseTool<typeof videoSearchInputSchema, any> {
public readonly name = 'brave_video_search';
public readonly description = 'Searches for videos using the Brave Search API. '
+ 'Use this for video content, tutorials, or any media-related queries. '
+ 'Returns a list of videos with titles, URLs, and descriptions. '
+ 'Maximum 20 results per request.';
public readonly inputSchema = videoSearchInputSchema;
private baseUrl = 'https://api.search.brave.com/res/v1';
constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch, private apiKey: string) {
super();
}
public async executeCore(input: z.infer<typeof videoSearchInputSchema>) {
const { query, count, freshness } = input;
const videoSearchResults = await this.videoSearch(query, {
count,
safesearch: SafeSearchLevel.Strict,
...(freshness ? { freshness } : {}),
});
if (!videoSearchResults.results || videoSearchResults.results.length === 0) {
this.braveMcpServer.log(`No video results found for "${query}"`);
const text = `No video results found for "${query}"`;
return { content: [{ type: 'text' as const, text }] };
}
const text = formatVideoResults(videoSearchResults.results);
return { content: [{ type: 'text' as const, text }] };
}
// workaround for https://github.com/erik-balfe/brave-search/pull/4
// not being merged yet into brave-search
private async videoSearch(
query: string,
options: VideoSearchOptions = {},
): Promise<VideoSearchApiResponse> {
const response = await axios.get<VideoSearchApiResponse>(
`${this.baseUrl}/videos/search?`,
{
params: {
q: query,
...this.formatOptions(options),
},
headers: this.getHeaders(),
},
);
return response.data;
}
private formatOptions(options: Record<string, any>): Record<string, string> {
return Object.entries(options).reduce(
(acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value.toString();
}
return acc;
},
{} as Record<string, string>,
);
}
private getHeaders() {
return {
'Accept': 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': this.apiKey,
};
}
// end workaround
}
```
--------------------------------------------------------------------------------
/src/tools/BraveLocalSearchTool.ts:
--------------------------------------------------------------------------------
```typescript
import type { BraveSearch, LocalDescriptionsSearchApiResponse, LocalPoiSearchApiResponse } from 'brave-search';
import type { BraveMcpServer } from '../server.js';
import type { BraveWebSearchTool } from './BraveWebSearchTool.js';
import { SafeSearchLevel } from 'brave-search/dist/types.js';
import { z } from 'zod';
import { formatPoiResults } from '../utils.js';
import { BaseTool } from './BaseTool.js';
const localSearchInputSchema = z.object({
query: z.string().describe('Local search query (e.g. \'pizza near Central Park\')'),
count: z.number().min(1).max(20).default(10).optional().describe('The number of results to return, minimum 1, maximum 20'),
});
export class BraveLocalSearchTool extends BaseTool<typeof localSearchInputSchema, any> {
public readonly name = 'brave_local_search';
public readonly 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.';
public readonly inputSchema = localSearchInputSchema;
private baseUrl = 'https://api.search.brave.com/res/v1';
constructor(private braveMcpServer: BraveMcpServer, private braveSearch: BraveSearch, private webSearchTool: BraveWebSearchTool, private apiKey: string) {
super();
}
public async executeCore(input: z.infer<typeof localSearchInputSchema>) {
const { query, count } = input;
const results = await this.braveSearch.webSearch(query, {
count,
safesearch: SafeSearchLevel.Strict,
result_filter: 'locations',
});
// it looks like the count parameter is only good for web search results
if (!results.locations || results.locations?.results.length === 0) {
this.braveMcpServer.log(`No location results found for "${query}" falling back to web search. Make sure your API Plan is at least "Pro"`);
return this.webSearchTool.executeCore({ query, count, offset: 0 });
}
const allIds = results.locations.results.map(result => result.id);
// count is restricted to 20 in the schema, and the location api supports up to 20 at a time
// so we can just use the count parameter to limit the number of ids, take the first "count" ids
const ids = allIds.slice(0, count);
this.braveMcpServer.log(`Using ${ids.length} of ${allIds.length} location IDs for "${query}"`, 'debug');
const formattedText = [];
const localPoiSearchApiResponse = await this.localPoiSearch(ids);
// the call to localPoiSearch does not return the id of the pois
// add them here, they should be in the same order as the ids
// and the same order of id in localDescriptionsSearchApiResponse
localPoiSearchApiResponse.results.forEach((result, index) => {
(result as any).id = ids[index];
});
const localDescriptionsSearchApiResponse = await this.localDescriptionsSearch(ids);
const text = formatPoiResults(localPoiSearchApiResponse, localDescriptionsSearchApiResponse);
formattedText.push(text);
const finalText = formattedText.join('\n\n');
return { content: [{ type: 'text' as const, text: finalText }] };
}
// workaround for https://github.com/erik-balfe/brave-search/pull/3
// not being merged yet into brave-search
private async localPoiSearch(ids: string[]) {
try {
const qs = ids.map(id => `ids=${encodeURIComponent(id)}`).join('&');
const url = `${this.baseUrl}/local/pois?${qs}`;
this.braveMcpServer.log(`Fetching local POI data from ${url}`, 'debug');
const res = await fetch(url, {
method: 'GET',
headers: this.getHeaders(),
redirect: 'follow',
});
if (!res.ok) {
throw new Error(`Error fetching local POI data Status:${res.status} Status Text:${res.statusText} Headers:${JSON.stringify(res.headers)}`);
}
const data = (await res.json()) as LocalPoiSearchApiResponse;
return data;
}
catch (error) {
this.handleError(error);
throw error;
}
}
private async localDescriptionsSearch(ids: string[]) {
try {
const qs = ids.map(id => `ids=${encodeURIComponent(id)}`).join('&');
const url = `${this.baseUrl}/local/descriptions?${qs}`;
const res = await fetch(url, {
method: 'GET',
headers: this.getHeaders(),
redirect: 'follow',
});
if (!res.ok) {
const responseText = await res.text();
this.braveMcpServer.log(`Error response body: ${responseText}`, 'error');
this.braveMcpServer.log(`Response headers: ${JSON.stringify(Object.fromEntries(res.headers.entries()))}`, 'error');
this.braveMcpServer.log(`Request URL: ${url}`, 'error');
this.braveMcpServer.log(`Request headers: ${JSON.stringify(this.getHeaders())}`, 'error');
if (res.status === 429) {
this.braveMcpServer.log('429 Rate limit exceeded, consider adding delay between requests', 'error');
}
else if (res.status === 403) {
this.braveMcpServer.log('403 Authentication error - check your API key', 'error');
}
else if (res.status === 500) {
this.braveMcpServer.log('500 Internal server error - might be an issue with request format or API temporary issues', 'error');
}
// return an empty response instead of error so we can at least return the pois results
return {
type: 'local_descriptions',
results: [],
} as LocalDescriptionsSearchApiResponse;
}
const data = (await res.json()) as LocalDescriptionsSearchApiResponse;
return data;
}
catch (error) {
this.handleError(error);
throw error;
}
}
private handleError(error: any) {
this.braveMcpServer.log(`${error}`, 'error');
}
private getHeaders() {
return {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'X-Subscription-Token': this.apiKey,
'User-Agent': 'BraveSearchMCP/1.0',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
};
}
// end workaround
}
```