#
tokens: 11461/50000 20/20 files
lines: off (toggle) GitHub
raw markdown copy
# 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
}

```