#
tokens: 4285/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── server.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | # Version control
 2 | .git
 3 | .gitignore
 4 | 
 5 | # Dependencies
 6 | node_modules
 7 | npm-debug.log*
 8 | yarn-debug.log*
 9 | yarn-error.log*
10 | 
11 | # Build output
12 | dist
13 | build
14 | 
15 | # Environment files
16 | .env
17 | .env.*
18 | 
19 | # IDE files
20 | .vscode
21 | .idea
22 | *.swp
23 | *.swo
24 | 
25 | # OS files
26 | .DS_Store
27 | Thumbs.db
28 | 
29 | # Docker
30 | .docker
31 | docker-compose.yml
32 | .dockerignore
33 | 
34 | # Logs
35 | logs
36 | *.log
37 | 
38 | # Test coverage
39 | coverage
40 | 
41 | # Documentation
42 | README.md
43 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # TypeScript build output
 8 | dist/
 9 | build/
10 | 
11 | # Environment variables
12 | .env
13 | .env.local
14 | .env.*.local
15 | 
16 | # IDE
17 | .vscode/
18 | .idea/
19 | *.swp
20 | *.swo
21 | 
22 | # OS
23 | .DS_Store
24 | Thumbs.db
25 | 
26 | # Logs
27 | logs/
28 | *.log
29 | 
30 | # Optional npm cache directory
31 | .npm
32 | 
33 | # Optional eslint cache
34 | .eslintcache
35 | 
36 | # Optional REPL history
37 | .node_repl_history
38 | 
39 | # Docker
40 | .docker/
41 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Brave Search MCP with SSE Support
  2 | 
  3 | This is a Model Context Protocol (MCP) server that provides Brave Search capabilities with Server-Sent Events (SSE) integration. It can be deployed to Coolify and used as a real-time search service.
  4 | 
  5 | ## Features
  6 | 
  7 | - Brave Search API integration through MCP
  8 | - Real-time search results using SSE
  9 | - Docker and Coolify ready
 10 | - TypeScript implementation
 11 | - Express.js SSE endpoint
 12 | 
 13 | ## Prerequisites
 14 | 
 15 | - Brave Search API key
 16 | - Node.js 18+
 17 | - Docker (for containerized deployment)
 18 | - Coolify instance
 19 | 
 20 | ## Local Development
 21 | 
 22 | 1. Clone the repository
 23 | 2. Create a `.env` file with your Brave API key:
 24 |    ```
 25 |    BRAVE_API_KEY=your_api_key_here
 26 |    PORT=3001
 27 |    ```
 28 | 3. Install dependencies:
 29 |    ```bash
 30 |    npm install
 31 |    ```
 32 | 4. Start development server:
 33 |    ```bash
 34 |    npm run dev
 35 |    ```
 36 | 
 37 | ## Docker Deployment
 38 | 
 39 | 1. Build and run using docker-compose:
 40 |    ```bash
 41 |    docker-compose up --build
 42 |    ```
 43 | 
 44 | ## Coolify Deployment
 45 | 
 46 | 1. In your Coolify dashboard, create a new service
 47 | 2. Choose "Deploy from Source"
 48 | 3. Configure the following:
 49 |    - Repository URL: Your repository URL
 50 |    - Branch: main
 51 |    - Build Command: `npm run build`
 52 |    - Start Command: `npm start`
 53 |    - Port: 3001
 54 |    - Environment Variables:
 55 |      - BRAVE_API_KEY=your_api_key_here
 56 |      - PORT=3001
 57 | 
 58 | ## Using the SSE Integration
 59 | 
 60 | ### SSE Endpoint
 61 | ```
 62 | GET http://your-server:3001/sse
 63 | ```
 64 | 
 65 | The SSE endpoint provides real-time search results. Connect to it using the EventSource API:
 66 | 
 67 | ```javascript
 68 | const eventSource = new EventSource('http://your-server:3001/sse');
 69 | 
 70 | eventSource.onmessage = (event) => {
 71 |   const data = JSON.parse(event.data);
 72 |   // Handle the search results
 73 |   console.log(data);
 74 | };
 75 | 
 76 | eventSource.onerror = (error) => {
 77 |   console.error('SSE Error:', error);
 78 |   eventSource.close();
 79 | };
 80 | ```
 81 | 
 82 | ### Messages Endpoint
 83 | ```
 84 | POST http://your-server:3001/messages
 85 | Content-Type: application/json
 86 | 
 87 | {
 88 |   "query": "your search query",
 89 |   "count": 10  // optional, default: 10, max: 20
 90 | }
 91 | ```
 92 | 
 93 | Use this endpoint to trigger searches that will be broadcast to all connected SSE clients.
 94 | 
 95 | ## MCP Usage
 96 | 
 97 | The server provides the following MCP tool:
 98 | 
 99 | - `brave_web_search`: Performs a web search using the Brave Search API
100 |   ```typescript
101 |   {
102 |     query: string;    // Search query
103 |     count?: number;   // Number of results (1-20, default: 10)
104 |   }
105 |   ```
106 | 
107 | ## Error Handling
108 | 
109 | - The server broadcasts errors to all connected SSE clients
110 | - Errors are formatted as:
111 |   ```json
112 |   {
113 |     "type": "error",
114 |     "error": "error message"
115 |   }
116 |   ```
117 | 
118 | ## Notes
119 | 
120 | - The SSE connection will stay open until the client closes it
121 | - Each search result is broadcast to all connected clients
122 | - The server automatically handles disconnections and cleanup
123 | - For production deployment, consider implementing authentication for the messages endpoint
124 | 
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3.8'
 2 | 
 3 | services:
 4 |   brave-search-mcp:
 5 |     build: .
 6 |     ports:
 7 |       - "3001:3001"
 8 |     environment:
 9 |       - BRAVE_API_KEY=${BRAVE_API_KEY}
10 |       - PORT=3001
11 |     restart: unless-stopped
12 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:18-alpine
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # Copy package files
 6 | COPY package*.json ./
 7 | 
 8 | # Install dependencies
 9 | RUN npm install
10 | 
11 | # Copy source code
12 | COPY . .
13 | 
14 | # Build TypeScript
15 | RUN npm run build
16 | 
17 | # Expose port for SSE
18 | EXPOSE 3001
19 | 
20 | # Start the server
21 | CMD ["npm", "start"]
22 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "commonjs",
 5 |     "outDir": "./dist",
 6 |     "rootDir": "./src",
 7 |     "strict": true,
 8 |     "esModuleInterop": true,
 9 |     "skipLibCheck": true,
10 |     "forceConsistentCasingInFileNames": true
11 |   },
12 |   "include": ["src/**/*"],
13 |   "exclude": ["node_modules", "dist"]
14 | }
15 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "brave-search-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "Brave Search MCP Server with SSE Support",
 5 |   "main": "dist/server.js",
 6 |   "type": "commonjs",
 7 |   "scripts": {
 8 |     "build": "tsc",
 9 |     "start": "node dist/server.js",
10 |     "dev": "ts-node src/server.ts",
11 |     "test": "echo \"Error: no test specified\" && exit 1"
12 |   },
13 |   "keywords": [
14 |     "mcp",
15 |     "brave-search",
16 |     "sse"
17 |   ],
18 |   "author": "",
19 |   "license": "ISC",
20 |   "dependencies": {
21 |     "@modelcontextprotocol/sdk": "^1.7.0",
22 |     "@types/node": "^22.13.10",
23 |     "cors": "^2.8.5",
24 |     "dotenv": "^16.4.7",
25 |     "express": "^5.0.1",
26 |     "typescript": "^5.8.2"
27 |   },
28 |   "devDependencies": {
29 |     "@types/cors": "^2.8.17",
30 |     "@types/express": "^5.0.1",
31 |     "ts-node": "^10.9.2"
32 |   }
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  3 | import {
  4 |   CallToolRequestSchema,
  5 |   ErrorCode,
  6 |   ListToolsRequestSchema,
  7 |   McpError,
  8 | } from '@modelcontextprotocol/sdk/types.js';
  9 | import express, { Request, Response } from 'express';
 10 | import cors from 'cors';
 11 | import dotenv from 'dotenv';
 12 | 
 13 | dotenv.config();
 14 | 
 15 | const API_KEY = process.env.BRAVE_API_KEY;
 16 | if (!API_KEY) {
 17 |   throw new Error('BRAVE_API_KEY environment variable is required');
 18 | }
 19 | 
 20 | const PORT = process.env.PORT || 3001;
 21 | 
 22 | // Store active SSE clients
 23 | const clients = new Set<Response>();
 24 | 
 25 | class BraveSearchServer {
 26 |   private server: Server;
 27 |   private expressApp: express.Express;
 28 | 
 29 |   constructor() {
 30 |     this.server = new Server(
 31 |       {
 32 |         name: 'brave-search-mcp',
 33 |         version: '0.1.0',
 34 |       },
 35 |       {
 36 |         capabilities: {
 37 |           tools: {},
 38 |         },
 39 |       }
 40 |     );
 41 | 
 42 |     this.expressApp = express();
 43 |     this.expressApp.use(cors());
 44 |     this.expressApp.use(express.json());
 45 | 
 46 |     this.setupToolHandlers();
 47 |     this.setupSSEEndpoints();
 48 |     
 49 |     // Error handling
 50 |     this.server.onerror = (error) => this.broadcastError(error);
 51 |     process.on('SIGINT', async () => {
 52 |       await this.server.close();
 53 |       process.exit(0);
 54 |     });
 55 |   }
 56 | 
 57 |   private setupToolHandlers() {
 58 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 59 |       tools: [
 60 |         {
 61 |           name: 'brave_web_search',
 62 |           description: 'Performs a web search using the Brave Search API with SSE support',
 63 |           inputSchema: {
 64 |             type: 'object',
 65 |             properties: {
 66 |               query: {
 67 |                 type: 'string',
 68 |                 description: 'Search query (max 400 chars, 50 words)',
 69 |               },
 70 |               count: {
 71 |                 type: 'number',
 72 |                 description: 'Number of results (1-20, default 10)',
 73 |                 default: 10,
 74 |               },
 75 |             },
 76 |             required: ['query'],
 77 |           },
 78 |         },
 79 |       ],
 80 |     }));
 81 | 
 82 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 83 |       if (request.params.name !== 'brave_web_search') {
 84 |         throw new McpError(
 85 |           ErrorCode.MethodNotFound,
 86 |           `Unknown tool: ${request.params.name}`
 87 |         );
 88 |       }
 89 | 
 90 |       const { query, count = 10 } = request.params.arguments as {
 91 |         query: string;
 92 |         count?: number;
 93 |       };
 94 | 
 95 |       try {
 96 |         const searchParams = new URLSearchParams({
 97 |           q: query,
 98 |           count: Math.min(Math.max(1, count), 20).toString()
 99 |         });
100 | 
101 |         const response = await fetch(
102 |           `https://api.search.brave.com/res/v1/web/search?${searchParams}`,
103 |           {
104 |             method: 'GET',
105 |             headers: {
106 |               'Accept': 'application/json',
107 |               'Accept-Encoding': 'gzip',
108 |               'X-Subscription-Token': API_KEY || ''
109 |             }
110 |           }
111 |         );
112 | 
113 |         if (!response.ok) {
114 |           throw new Error(`HTTP error! status: ${response.status}`);
115 |         }
116 | 
117 |         const results = await response.json();
118 |         
119 |         // Broadcast results to all connected SSE clients
120 |         this.broadcast(results);
121 | 
122 |         return {
123 |           content: [
124 |             {
125 |               type: 'text',
126 |               text: JSON.stringify(results, null, 2),
127 |             },
128 |           ],
129 |         };
130 |       } catch (error) {
131 |         const errorMessage = error instanceof Error ? error.message : 'Unknown error';
132 |         this.broadcastError(errorMessage);
133 |         throw new McpError(ErrorCode.InternalError, errorMessage);
134 |       }
135 |     });
136 |   }
137 | 
138 |   private setupSSEEndpoints() {
139 |     // SSE endpoint
140 |     this.expressApp.get('/sse', (req: Request, res: Response) => {
141 |       res.writeHead(200, {
142 |         'Content-Type': 'text/event-stream',
143 |         'Cache-Control': 'no-cache',
144 |         'Connection': 'keep-alive'
145 |       });
146 | 
147 |       // Send initial connection established message
148 |       res.write('data: {"type":"connected"}\n\n');
149 | 
150 |       // Add client to active connections
151 |       clients.add(res);
152 | 
153 |       // Remove client on connection close
154 |       req.on('close', () => {
155 |         clients.delete(res);
156 |       });
157 |     });
158 | 
159 |     // Messages endpoint for manual search requests
160 |     this.expressApp.post('/messages', async (req: Request, res: Response) => {
161 |       try {
162 |         const { query, count } = req.body;
163 |         // Handle the search request directly
164 |         const response = await fetch(
165 |           `https://api.search.brave.com/res/v1/web/search?${new URLSearchParams({
166 |             q: query,
167 |             count: Math.min(Math.max(1, count || 10), 20).toString()
168 |           })}`,
169 |           {
170 |             method: 'GET',
171 |             headers: {
172 |               'Accept': 'application/json',
173 |               'Accept-Encoding': 'gzip',
174 |               'X-Subscription-Token': API_KEY || ''
175 |             }
176 |           }
177 |         );
178 | 
179 |         if (!response.ok) {
180 |           throw new Error(`HTTP error! status: ${response.status}`);
181 |         }
182 | 
183 |         const results = await response.json();
184 |         res.json(results);
185 |       } catch (error) {
186 |         res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
187 |       }
188 |     });
189 |   }
190 | 
191 |   private broadcast(data: unknown) {
192 |     const message = `data: ${JSON.stringify(data)}\n\n`;
193 |     clients.forEach(client => {
194 |       client.write(message);
195 |     });
196 |   }
197 | 
198 |   private broadcastError(error: unknown) {
199 |     const errorMessage = error instanceof Error ? error.message : String(error);
200 |     const message = `data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n`;
201 |     clients.forEach(client => {
202 |       client.write(message);
203 |     });
204 |   }
205 | 
206 |   async run() {
207 |     // Start Express server
208 |     this.expressApp.listen(PORT, () => {
209 |       console.error(`SSE server running on port ${PORT}`);
210 |     });
211 | 
212 |     // Start MCP server
213 |     const transport = new StdioServerTransport();
214 |     await this.server.connect(transport);
215 |     console.error('Brave Search MCP server running on stdio');
216 |   }
217 | }
218 | 
219 | const server = new BraveSearchServer();
220 | server.run().catch(console.error);
221 | 
```