Skip to main content
Glama

Brave Search MCP Server

Official
index.ts6.19 kB
import type { LocationResult, LocationDescription, OpeningHours, DayOpeningHours, } from './types.js'; import webParams, { type QueryParams as WebQueryParams } from '../web/params.js'; import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; import API from '../../BraveAPI/index.js'; import { formatWebResults } from '../web/index.js'; import { stringify } from '../../utils.js'; import { type WebSearchApiResponse } from '../web/types.js'; import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; export const name = 'brave_local_search'; export const annotations: ToolAnnotations = { title: 'Brave Local Search', openWorldHint: true, }; export const description = ` Brave Local Search API provides enrichments for location search results. Access to this API is available only through the Brave Search API Pro plans; confirm the user's plan before using this tool (if the user does not have a Pro plan, use the brave_web_search tool). Searches for local businesses and places using Brave's Local Search API. Best for queries related to physical locations, businesses, restaurants, services, etc. Returns detailed information including: - Business names and addresses - Ratings and review counts - Phone numbers and opening hours Use this when the query implies 'near me', 'in my area', or mentions specific locations (e.g., 'in San Francisco'). This tool automatically falls back to brave_web_search if no local results are found. `; // Access to Local API is available through the Pro plans. export const execute = async (params: WebQueryParams) => { // Make sure both 'web' and 'locations' are in the result_filter params = { ...params, result_filter: [...(params.result_filter || []), 'web', 'locations'] }; // Starts with a web search to retrieve potential location IDs const { locations, web: web_fallback } = await API.issueRequest<'web'>('web', params); // We can send up to 20 location IDs at a time to the Local API // TODO (Sampson): Add support for multiple requests const locationIDs = (locations?.results || []).map((poi) => poi.id as string).slice(0, 20); // No locations were found - user's plan may not include access to the Local API if (!locations || locationIDs.length === 0) { // If we have web results, but no locations, we'll fall back to the web results if (web_fallback && web_fallback.results.length > 0) { return buildFallbackWebResponse(web_fallback); } // If we have no web results, we'll send a message to the user return { content: [ { type: 'text' as const, text: "No location data was returned. User's plan does not support local search, or the query may be unclear.", }, ], }; } // Fetch AI-generated descriptions const descriptions = await API.issueRequest<'localDescriptions'>('localDescriptions', { ids: locationIDs, }); return { content: formatLocalResults(locations.results, descriptions.results).map((formattedPOI) => ({ type: 'text' as const, text: formattedPOI, })), }; }; export const register = (mcpServer: McpServer) => { mcpServer.registerTool( name, { title: name, description: description, inputSchema: webParams.shape, annotations: annotations, }, execute ); }; const buildFallbackWebResponse = (web_fallback: WebSearchApiResponse['web']): CallToolResult => { if (!web_fallback || web_fallback.results.length === 0) throw new Error('No web results found'); const fallback = { content: [ { type: 'text' as const, text: "No location data was returned. Either the user's plan does not support local search, or the API was unable to find locations for the provided query. Falling back to general web search.", }, ], }; for (const web_result of formatWebResults(web_fallback)) { fallback.content.push({ type: 'text' as const, text: stringify(web_result), }); } return fallback; }; const formatLocalResults = ( poisData: LocationResult[], descData: LocationDescription[] = [] ): string[] => { return poisData.map((poi) => { return stringify({ name: poi.title, price_range: poi.price_range, phone: poi.contact?.telephone, rating: poi.rating?.ratingValue, hours: formatOpeningHours(poi.opening_hours), rating_count: poi.rating?.reviewCount, description: descData.find(({ id }) => id === poi.id)?.description, address: poi.postal_address?.displayAddress, }); }); }; const formatOpeningHours = (openingHours?: OpeningHours): Record<string, string> | undefined => { if (!openingHours) return undefined; /** * Response will be something like { * 'sunday': '10:00-18:00', * 'monday': '10:00-18:00', * 'tuesday': '10:00-18:00', * 'wednesday': '10:00-18:00, 19:00-22:00', * 'thursday': '10:00-18:00', * 'friday': '10:00-18:00', * 'saturday': '12:00-18:00', * } */ const today: DayOpeningHours[] = openingHours.current_day || []; const response = {} as Record<string, string>; const dayHours: [string, string[]][] = [ [ `today (${today[0].full_name.toLowerCase()})`, today.map(({ opens, closes }) => `${opens}-${closes}`), ], ]; // Add the rest of the days to the response for (let parts of openingHours.days || []) { // Not all days have arrays of hours, so normalize to an array if (!Array.isArray(parts)) parts = [parts]; // Add the hours for each day to the response for (const { full_name, opens, closes } of parts) { const dayName = full_name.toLowerCase(); const existingEntry = dayHours.find(([name]) => name === dayName); existingEntry ? existingEntry[1].push(`${opens}-${closes}`) : dayHours.push([dayName, [`${opens}-${closes}`]]); } } for (const [name, hours] of dayHours) { response[name] = hours.join(', '); } return response; }; export default { name, description, annotations, inputSchema: webParams.shape, execute, register, };

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/brave/brave-search-mcp-server'

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