Skip to main content
Glama

Brave Search MCP Server

client.ts7.75 kB
import { RateLimit, RequestCount, BraveWeb, BravePoiResponse, BraveDescription, BraveLocation } from './types.js'; /** * Client for interacting with the Brave Search API * @class BraveClient */ export class BraveClient { private readonly apiKey: string; private readonly rateLimit: RateLimit = { perSecond: 1, perMonth: 15000 }; private requestCount: RequestCount = { second: 0, month: 0, lastReset: Date.now() }; /** * Creates a new BraveClient instance * @param {string} apiKey - Brave API key for authentication */ constructor(apiKey: string) { if (!apiKey) { throw new Error('API key is required'); } this.apiKey = apiKey; } /** * Checks and enforces rate limiting * @throws {Error} If rate limit is exceeded * @private */ private checkRateLimit(): void { const now = Date.now(); if (now - this.requestCount.lastReset > 1000) { this.requestCount.second = 0; this.requestCount.lastReset = now; } if (this.requestCount.second >= this.rateLimit.perSecond || this.requestCount.month >= this.rateLimit.perMonth) { throw new Error('Rate limit exceeded'); } this.requestCount.second++; this.requestCount.month++; } /** * Performs a web search using Brave's Web Search API * @param {string} query - Search query * @param {number} count - Number of results to return (default: 10, max: 20) * @param {number} offset - Pagination offset (default: 0, max: 9) * @returns {Promise<string>} Formatted search results */ async performWebSearch(query: string, count: number = 10, offset: number = 0): Promise<string> { this.checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/web/search'); url.searchParams.set('q', query); url.searchParams.set('count', Math.min(count, 20).toString()); url.searchParams.set('offset', offset.toString()); const response = await fetch(url, { headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': this.apiKey } }); if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } const data = await response.json() as BraveWeb; // Extract web results const results = (data.web?.results || []).map(result => ({ title: result.title || '', description: result.description || '', url: result.url || '' })); return results.map(r => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}` ).join('\n\n'); } /** * Performs a local search using Brave's Local Search API * @param {string} query - Local search query * @param {number} count - Number of results to return (default: 5, max: 20) * @returns {Promise<string>} Formatted local search results */ async performLocalSearch(query: string, count: number = 5): Promise<string> { this.checkRateLimit(); // Initial search to get location IDs const webUrl = new URL('https://api.search.brave.com/res/v1/web/search'); webUrl.searchParams.set('q', query); webUrl.searchParams.set('search_lang', 'en'); webUrl.searchParams.set('result_filter', 'locations'); webUrl.searchParams.set('count', Math.min(count, 20).toString()); const webResponse = await fetch(webUrl, { headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': this.apiKey } }); if (!webResponse.ok) { throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`); } const webData = await webResponse.json() as BraveWeb; const locationIds = webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map(r => r.id) || []; if (locationIds.length === 0) { return this.performWebSearch(query, count); // Fallback to web search } // Get POI details and descriptions in parallel const [poisData, descriptionsData] = await Promise.all([ this.getPoisData(locationIds), this.getDescriptionsData(locationIds) ]); return this.formatLocalResults(poisData, descriptionsData); } /** * Fetches POI (Points of Interest) data for given location IDs * @param {string[]} ids - Array of location IDs * @returns {Promise<BravePoiResponse>} POI data response * @private */ private async getPoisData(ids: string[]): Promise<BravePoiResponse> { this.checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/local/pois'); ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id)); const response = await fetch(url, { headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': this.apiKey } }); if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } return await response.json() as BravePoiResponse; } /** * Fetches description data for given location IDs * @param {string[]} ids - Array of location IDs * @returns {Promise<BraveDescription>} Descriptions data response * @private */ private async getDescriptionsData(ids: string[]): Promise<BraveDescription> { this.checkRateLimit(); const url = new URL('https://api.search.brave.com/res/v1/local/descriptions'); ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id)); const response = await fetch(url, { headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': this.apiKey } }); if (!response.ok) { throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`); } return await response.json() as BraveDescription; } /** * Formats local search results for display * @param {BravePoiResponse} poisData - POI data * @param {BraveDescription} descData - Descriptions data * @returns {string} Formatted results string * @private */ private formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string { return (poisData.results || []).map(poi => { const address = [ poi.address?.streetAddress ?? '', poi.address?.addressLocality ?? '', poi.address?.addressRegion ?? '', poi.address?.postalCode ?? '' ].filter(part => part !== '').join(', ') || 'N/A'; return `Name: ${poi.name} Address: ${address} Phone: ${poi.phone || 'N/A'} Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews) Price Range: ${poi.priceRange || 'N/A'} Hours: ${(poi.openingHours || []).join(', ') || 'N/A'} Description: ${descData.descriptions[poi.id] || 'No description available'} `; }).join('\n---\n') || 'No local results found'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/windsornguyen/brave-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server