# Directory Structure ``` ├── .dockerignore ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── package-lock.json ├── package.json ├── README.md ├── src │ └── server.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` # Version control .git .gitignore # Dependencies node_modules npm-debug.log* yarn-debug.log* yarn-error.log* # Build output dist build # Environment files .env .env.* # IDE files .vscode .idea *.swp *.swo # OS files .DS_Store Thumbs.db # Docker .docker docker-compose.yml .dockerignore # Logs logs *.log # Test coverage coverage # Documentation README.md ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # TypeScript build output dist/ build/ # Environment variables .env .env.local .env.*.local # IDE .vscode/ .idea/ *.swp *.swo # OS .DS_Store Thumbs.db # Logs logs/ *.log # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Docker .docker/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Brave Search MCP with SSE Support 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. ## Features - Brave Search API integration through MCP - Real-time search results using SSE - Docker and Coolify ready - TypeScript implementation - Express.js SSE endpoint ## Prerequisites - Brave Search API key - Node.js 18+ - Docker (for containerized deployment) - Coolify instance ## Local Development 1. Clone the repository 2. Create a `.env` file with your Brave API key: ``` BRAVE_API_KEY=your_api_key_here PORT=3001 ``` 3. Install dependencies: ```bash npm install ``` 4. Start development server: ```bash npm run dev ``` ## Docker Deployment 1. Build and run using docker-compose: ```bash docker-compose up --build ``` ## Coolify Deployment 1. In your Coolify dashboard, create a new service 2. Choose "Deploy from Source" 3. Configure the following: - Repository URL: Your repository URL - Branch: main - Build Command: `npm run build` - Start Command: `npm start` - Port: 3001 - Environment Variables: - BRAVE_API_KEY=your_api_key_here - PORT=3001 ## Using the SSE Integration ### SSE Endpoint ``` GET http://your-server:3001/sse ``` The SSE endpoint provides real-time search results. Connect to it using the EventSource API: ```javascript const eventSource = new EventSource('http://your-server:3001/sse'); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); // Handle the search results console.log(data); }; eventSource.onerror = (error) => { console.error('SSE Error:', error); eventSource.close(); }; ``` ### Messages Endpoint ``` POST http://your-server:3001/messages Content-Type: application/json { "query": "your search query", "count": 10 // optional, default: 10, max: 20 } ``` Use this endpoint to trigger searches that will be broadcast to all connected SSE clients. ## MCP Usage The server provides the following MCP tool: - `brave_web_search`: Performs a web search using the Brave Search API ```typescript { query: string; // Search query count?: number; // Number of results (1-20, default: 10) } ``` ## Error Handling - The server broadcasts errors to all connected SSE clients - Errors are formatted as: ```json { "type": "error", "error": "error message" } ``` ## Notes - The SSE connection will stay open until the client closes it - Each search result is broadcast to all connected clients - The server automatically handles disconnections and cleanup - For production deployment, consider implementing authentication for the messages endpoint ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml version: '3.8' services: brave-search-mcp: build: . ports: - "3001:3001" environment: - BRAVE_API_KEY=${BRAVE_API_KEY} - PORT=3001 restart: unless-stopped ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:18-alpine WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm install # Copy source code COPY . . # Build TypeScript RUN npm run build # Expose port for SSE EXPOSE 3001 # Start the server CMD ["npm", "start"] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "brave-search-mcp", "version": "1.0.0", "description": "Brave Search MCP Server with SSE Support", "main": "dist/server.js", "type": "commonjs", "scripts": { "build": "tsc", "start": "node dist/server.js", "dev": "ts-node src/server.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "mcp", "brave-search", "sse" ], "author": "", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "@types/node": "^22.13.10", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^5.0.1", "typescript": "^5.8.2" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^5.0.1", "ts-node": "^10.9.2" } } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import express, { Request, Response } from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config(); const API_KEY = process.env.BRAVE_API_KEY; if (!API_KEY) { throw new Error('BRAVE_API_KEY environment variable is required'); } const PORT = process.env.PORT || 3001; // Store active SSE clients const clients = new Set<Response>(); class BraveSearchServer { private server: Server; private expressApp: express.Express; constructor() { this.server = new Server( { name: 'brave-search-mcp', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.expressApp = express(); this.expressApp.use(cors()); this.expressApp.use(express.json()); this.setupToolHandlers(); this.setupSSEEndpoints(); // Error handling this.server.onerror = (error) => this.broadcastError(error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'brave_web_search', description: 'Performs a web search using the Brave Search API with SSE support', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (max 400 chars, 50 words)', }, count: { type: 'number', description: 'Number of results (1-20, default 10)', default: 10, }, }, required: ['query'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'brave_web_search') { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } const { query, count = 10 } = request.params.arguments as { query: string; count?: number; }; try { const searchParams = new URLSearchParams({ q: query, count: Math.min(Math.max(1, count), 20).toString() }); const response = await fetch( `https://api.search.brave.com/res/v1/web/search?${searchParams}`, { method: 'GET', headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': API_KEY || '' } } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const results = await response.json(); // Broadcast results to all connected SSE clients this.broadcast(results); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.broadcastError(errorMessage); throw new McpError(ErrorCode.InternalError, errorMessage); } }); } private setupSSEEndpoints() { // SSE endpoint this.expressApp.get('/sse', (req: Request, res: Response) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // Send initial connection established message res.write('data: {"type":"connected"}\n\n'); // Add client to active connections clients.add(res); // Remove client on connection close req.on('close', () => { clients.delete(res); }); }); // Messages endpoint for manual search requests this.expressApp.post('/messages', async (req: Request, res: Response) => { try { const { query, count } = req.body; // Handle the search request directly const response = await fetch( `https://api.search.brave.com/res/v1/web/search?${new URLSearchParams({ q: query, count: Math.min(Math.max(1, count || 10), 20).toString() })}`, { method: 'GET', headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': API_KEY || '' } } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const results = await response.json(); res.json(results); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }); } private broadcast(data: unknown) { const message = `data: ${JSON.stringify(data)}\n\n`; clients.forEach(client => { client.write(message); }); } private broadcastError(error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); const message = `data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n`; clients.forEach(client => { client.write(message); }); } async run() { // Start Express server this.expressApp.listen(PORT, () => { console.error(`SSE server running on port ${PORT}`); }); // Start MCP server const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Brave Search MCP server running on stdio'); } } const server = new BraveSearchServer(); server.run().catch(console.error); ```