# 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 |
```