This guide walks you through setting up and running a Model Context Protocol (MCP) server that provides Brave Search functionality via Streamable HTTP transport. The server offers two search tools: web search and local business search.
- Bun (JavaScript runtime) - Install from bun.sh
- Brave Search API Key - Obtain from Brave Search API
- Basic terminal/command line knowledge
- Node.js 18+ compatible environment
- Internet connection for API calls
- Available port (default: 8000)
# Clone or download the brave-search MCP server files
# Ensure you have these files:
# - index.ts (main server file)
# - package.json (dependencies)
# - tsconfig.json (TypeScript config)# Navigate to the brave-search directory
cd /path/to/brave-search
# Install all required packages
bun installWhat this does: Downloads the MCP SDK and other dependencies needed to run the server.
# Option 1: Environment variable (recommended for production)
export BRAVE_API_KEY="your-brave-api-key-here"
# Option 2: The server has a fallback hardcoded key for testing
# (already configured in the current implementation)Security Note: Never commit API keys to version control. Use environment variables in production.
# Start the server on port 8000 with Streamable HTTP transport
bun run index.ts --port 8000Expected Output:
Listening on http://localhost:8000
Put this in your client config:
{
"mcpServers": {
"brave-search": {
"url": "http://localhost:8000/sse"
}
}
}
If your client supports streamable HTTP, you can use the /mcp endpoint instead.
The server provides two endpoints:
/sse- Server-Sent Events transport (for backward compatibility)/mcp- Streamable HTTP transport (recommended for new implementations)
curl -v -X POST http://localhost:8000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"clientInfo": {
"name": "TestClient",
"version": "1.0.0"
}
}
}'What to look for:
- Status:
200 OK - Header:
mcp-session-id: [uuid](save this for next steps) - Response: JSON with server capabilities
curl -X POST http://localhost:8000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: YOUR_SESSION_ID_FROM_STEP_1" \
-d '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'Critical: Replace YOUR_SESSION_ID_FROM_STEP_1 with the actual session ID from step 1.
curl -X POST http://localhost:8000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}'Expected Response: List of two tools:
brave_web_search- General web searchbrave_local_search- Local business search
curl -X POST http://localhost:8000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "brave_web_search",
"arguments": {
"query": "latest AI news",
"count": 3
}
}
}'curl -X POST http://localhost:8000/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "brave_local_search",
"arguments": {
"query": "pizza near Times Square NYC",
"count": 2
}
}
}'The original brave-search MCP server only supported stdio transport (standard input/output). We modified it to support Streamable HTTP transport for AWS Lambda compatibility and better web integration.
// Added these imports
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "http";
import { randomUUID } from "crypto";// Added argument parsing for --port flag
function parseArgs() {
const args = process.argv.slice(2);
const options: { port?: number; headless?: boolean } = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' && i + 1 < args.length) {
options.port = parseInt(args[i + 1], 10);
i++;
}
}
return options;
}// Added HTTP server with dual transport support
function startHttpServer(port: number) {
const httpServer = createServer();
httpServer.on('request', async (req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
if (url.pathname === '/sse') {
await handleSSE(req, res);
} else if (url.pathname === '/mcp') {
await handleStreamable(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
}// Added session storage for concurrent connections
const streamableSessions = new Map<string, {transport: any, server: any}>();
// Each session gets its own server instance
function createServerInstance() {
const serverInstance = new Server({...});
// Set up tool handlers...
return serverInstance;
}// Streamable HTTP transport handler
async function handleStreamable(req: any, res: any) {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId) {
// Use existing session
const session = streamableSessions.get(sessionId);
return await session.transport.handleRequest(req, res);
}
// Create new session for initialization
const serverInstance = createServerInstance();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
streamableSessions.set(sessionId, { transport, server: serverInstance });
}
});
await serverInstance.connect(transport);
await transport.handleRequest(req, res);
}- Lambda Compatibility: AWS Lambda doesn't support stdio, requiring HTTP-based communication
- Web Integration: HTTP transport enables direct browser and web service integration
- Concurrent Sessions: Multiple clients can connect simultaneously with session isolation
- Modern Protocol: Streamable HTTP is the modern MCP transport standard
| Feature | Stdio Transport | Streamable HTTP Transport |
|---|---|---|
| Use Case | CLI applications | Web services, Lambda |
| Concurrency | Single client | Multiple concurrent clients |
| State Management | Process-based | Session-based |
| AWS Lambda | ❌ Not supported | ✅ Fully supported |
| Web Browser | ❌ Not possible | ✅ Direct integration |
| Debugging | Simple | Requires HTTP tools |
- Each client connection gets a unique session ID
- Sessions maintain state between requests
- Sessions are automatically cleaned up when connections close
- Important: Always include the
Mcp-Session-Idheader after initialization
- Streamable HTTP (
/mcp): Modern, efficient, supports streaming responses - SSE (
/sse): Backward compatibility, uses Server-Sent Events
- Initialize - Establish capabilities and protocol version
- Initialized - Confirm initialization complete
- Tools/List - Discover available tools
- Tools/Call - Execute tool functions
Cause: Missing or incorrect session ID
Solution: Ensure you're using the session ID from the initialization response
Cause: Session expired or invalid session ID
Solution: Re-initialize with a new session
Cause: Another process is using port 8000
Solution:
# Kill process on port 8000
lsof -ti:8000 | xargs kill -9
# Or use a different port
bun run index.ts --port 8001Cause: Exceeded Brave Search API limits
Solution: The server has built-in rate limiting (1 request/second, 15000/month)
Cause: Network issues or server overload
Solution: Check network connectivity and server resources
- Each client gets its own server instance and transport
- Sessions are isolated - no shared state between clients
- Automatic cleanup when clients disconnect
- Memory-efficient session storage using Maps
// Current session storage (in-memory)
const streamableSessions = new Map<string, {transport: any, server: any}>();For production:
- Consider Redis for session storage across multiple instances
- Implement session cleanup timeouts
- Monitor memory usage for session maps
- Use load balancers for horizontal scaling
- Default: No explicit connection limit
- Memory-bound by available system resources
- Each session uses ~1-5MB of memory
export BRAVE_API_KEY="your-production-api-key"
export PORT="8000"
export NODE_ENV="production"# Using PM2 for process management
npm install -g pm2
pm2 start "bun run index.ts --port 8000" --name brave-search-mcpFROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install
COPY . .
EXPOSE 8000
CMD ["bun", "run", "index.ts", "--port", "8000"]The server supports Streamable HTTP, making it suitable for serverless deployment:
- Configure API Gateway to proxy requests to
/mcp - Set appropriate timeout values (30s+)
- Handle cold starts gracefully
- Use environment variables, never hardcode keys
- Rotate API keys regularly
- Monitor API usage and set alerts
- Bind to localhost (
127.0.0.1) for local development - Use HTTPS in production
- Implement proper authentication for public deployments
- The server validates JSON-RPC message format
- Search queries are limited to 400 characters
- Count parameters are bounded (1-20 results)
The server logs:
- Session creation/destruction
- API errors and rate limiting
- Connection events
# Simple health check
curl -f http://localhost:8000/mcp || echo "Server down"- Active session count
- API call frequency
- Response times
- Error rates
- Memory usage
const RATE_LIMIT = {
perSecond: 1, // Requests per second
perMonth: 15000 // Monthly request limit
};// Adjust in createServerInstance() if needed
const serverInstance = new Server({
name: "brave-search",
version: "1.0.0"
}, {
capabilities: { tools: {} },
// Add timeout configs here
});This server is now ready for production use and can handle multiple concurrent connections efficiently while providing reliable Brave Search functionality through the MCP protocol.