Brave Search MCP Server
by isaacgounton
Verified
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);