Skip to main content
Glama

Google Maps MCP Server

by iceener
google-maps.ts16.4 kB
/** * Google Maps Platform API client. * Supports Places API (New) and Routes API. */ import { logger } from '../utils/logger.js'; const PLACES_API_BASE = 'https://places.googleapis.com/v1'; const ROUTES_COMPUTE_URL = 'https://routes.googleapis.com/directions/v2:computeRoutes'; const ROUTES_MATRIX_URL = 'https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix'; // ============================================================================ // Types - Location // ============================================================================ export interface LatLng { latitude: number; longitude: number; } export interface Circle { center: LatLng; radius: number; // meters } // ============================================================================ // Types - Places // ============================================================================ export interface Place { id: string; displayName?: { text: string; languageCode: string }; formattedAddress?: string; shortFormattedAddress?: string; location?: LatLng; rating?: number; userRatingCount?: number; priceLevel?: | 'PRICE_LEVEL_FREE' | 'PRICE_LEVEL_INEXPENSIVE' | 'PRICE_LEVEL_MODERATE' | 'PRICE_LEVEL_EXPENSIVE' | 'PRICE_LEVEL_VERY_EXPENSIVE'; types?: string[]; primaryType?: string; primaryTypeDisplayName?: { text: string; languageCode: string }; nationalPhoneNumber?: string; internationalPhoneNumber?: string; websiteUri?: string; googleMapsUri?: string; businessStatus?: 'OPERATIONAL' | 'CLOSED_TEMPORARILY' | 'CLOSED_PERMANENTLY'; regularOpeningHours?: OpeningHours; currentOpeningHours?: OpeningHours; editorialSummary?: { text: string; languageCode: string }; reviews?: Review[]; photos?: Photo[]; addressComponents?: AddressComponent[]; plusCode?: { globalCode: string; compoundCode?: string }; } export interface OpeningHours { openNow?: boolean; periods?: OpeningPeriod[]; weekdayDescriptions?: string[]; } export interface OpeningPeriod { open: { day: number; hour: number; minute: number }; close?: { day: number; hour: number; minute: number }; } export interface Review { name: string; rating: number; text?: { text: string; languageCode: string }; originalText?: { text: string; languageCode: string }; authorAttribution: { displayName: string; uri?: string; photoUri?: string }; publishTime: string; relativePublishTimeDescription?: string; } export interface Photo { name: string; widthPx: number; heightPx: number; authorAttributions: Array<{ displayName: string; uri?: string; photoUri?: string }>; googleMapsUri?: string; } export interface AddressComponent { longText: string; shortText: string; types: string[]; languageCode: string; } // ============================================================================ // Types - Routes // ============================================================================ export interface Route { distanceMeters: number; duration: string; // e.g., "1234s" staticDuration?: string; polyline?: { encodedPolyline: string }; description?: string; warnings?: string[]; legs: RouteLeg[]; localizedValues?: { distance?: { text: string }; duration?: { text: string }; staticDuration?: { text: string }; }; } export interface RouteLeg { distanceMeters: number; duration: string; staticDuration?: string; polyline?: { encodedPolyline: string }; startLocation?: { latLng: LatLng }; endLocation?: { latLng: LatLng }; steps?: RouteStep[]; localizedValues?: { distance?: { text: string }; duration?: { text: string }; }; } export interface RouteStep { distanceMeters: number; staticDuration: string; polyline?: { encodedPolyline: string }; startLocation?: { latLng: LatLng }; endLocation?: { latLng: LatLng }; navigationInstruction?: { maneuver?: string; instructions?: string; }; localizedValues?: { distance?: { text: string }; staticDuration?: { text: string }; }; travelMode?: string; transitDetails?: TransitDetails; } export interface TransitDetails { stopDetails?: { arrivalStop?: { name: string; location?: LatLng }; departureStop?: { name: string; location?: LatLng }; arrivalTime?: string; departureTime?: string; }; transitLine?: { name?: string; shortName?: string; color?: string; textColor?: string; vehicle?: { type: string; name?: { text: string } }; }; headsign?: string; stopCount?: number; } export interface RouteMatrixElement { originIndex: number; destinationIndex: number; status?: { code: number; message: string }; condition?: 'ROUTE_EXISTS' | 'ROUTE_NOT_FOUND'; distanceMeters?: number; duration?: string; staticDuration?: string; localizedValues?: { distance?: { text: string }; duration?: { text: string }; }; } // ============================================================================ // Request Parameters // ============================================================================ export interface SearchNearbyParams { location: LatLng; radius: number; includedTypes?: string[]; excludedTypes?: string[]; maxResultCount?: number; rankPreference?: 'DISTANCE' | 'POPULARITY'; languageCode?: string; } export interface TextSearchParams { textQuery: string; locationBias?: LatLng | Circle; includedType?: string; openNow?: boolean; minRating?: number; priceLevels?: string[]; maxResultCount?: number; rankPreference?: 'DISTANCE' | 'RELEVANCE'; languageCode?: string; } export interface PlaceDetailsParams { placeId: string; fields: string[]; languageCode?: string; } export interface ComputeRoutesParams { origin: LatLng | { placeId: string } | { address: string }; destination: LatLng | { placeId: string } | { address: string }; intermediates?: Array<LatLng | { placeId: string } | { address: string }>; travelMode: 'WALK' | 'DRIVE' | 'BICYCLE' | 'TRANSIT' | 'TWO_WHEELER'; departureTime?: string; computeAlternativeRoutes?: boolean; routeModifiers?: { avoidTolls?: boolean; avoidHighways?: boolean; avoidFerries?: boolean; }; languageCode?: string; } export interface ComputeRouteMatrixParams { origins: Array<LatLng | { placeId: string } | { address: string }>; destinations: Array<LatLng | { placeId: string } | { address: string }>; travelMode: 'WALK' | 'DRIVE' | 'BICYCLE' | 'TRANSIT' | 'TWO_WHEELER'; departureTime?: string; languageCode?: string; } // ============================================================================ // Field Masks // ============================================================================ export const PLACE_FIELDS = { basic: [ 'id', 'displayName', 'formattedAddress', 'shortFormattedAddress', 'location', 'types', 'primaryType', 'primaryTypeDisplayName', ], rating: ['rating', 'userRatingCount', 'priceLevel'], contact: [ 'nationalPhoneNumber', 'internationalPhoneNumber', 'websiteUri', 'googleMapsUri', ], hours: ['regularOpeningHours', 'currentOpeningHours', 'businessStatus'], reviews: ['reviews'], photos: ['photos'], editorial: ['editorialSummary'], address: ['addressComponents', 'plusCode'], } as const; export function buildFieldMask(categories: string[]): string { const fields = new Set<string>(); for (const cat of categories) { const categoryFields = PLACE_FIELDS[cat as keyof typeof PLACE_FIELDS]; if (categoryFields) { for (const field of categoryFields) { fields.add(`places.${field}`); } } } return Array.from(fields).join(','); } // ============================================================================ // Client // ============================================================================ export class GoogleMapsClient { private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } private async request<T>( baseUrl: string, path: string, options: RequestInit & { fieldMask?: string } = {}, ): Promise<T> { const url = `${baseUrl}${path}`; const headers: Record<string, string> = { 'Content-Type': 'application/json', 'X-Goog-Api-Key': this.apiKey, ...(options.headers as Record<string, string>), }; if (options.fieldMask) { headers['X-Goog-FieldMask'] = options.fieldMask; } try { const response = await fetch(url, { ...options, headers, }); if (!response.ok) { let errorMessage = `Google Maps API error: ${response.status} ${response.statusText}`; try { const errorData = (await response.json()) as { error?: { message?: string; status?: string }; }; if (errorData.error?.message) { errorMessage += ` - ${errorData.error.message}`; } } catch { // Ignore JSON parse error } throw new Error(errorMessage); } return (await response.json()) as T; } catch (error) { logger.error('google-maps-client', { message: 'Request failed', url, error: (error as Error).message, }); throw error; } } // -------------------------------------------------------------------------- // Places - Search Nearby // -------------------------------------------------------------------------- async searchNearby(params: SearchNearbyParams): Promise<{ places: Place[] }> { const body: Record<string, unknown> = { locationRestriction: { circle: { center: params.location, radius: params.radius, }, }, maxResultCount: params.maxResultCount ?? 10, rankPreference: params.rankPreference ?? 'DISTANCE', languageCode: params.languageCode ?? 'en', }; if (params.includedTypes?.length) { body.includedTypes = params.includedTypes; } if (params.excludedTypes?.length) { body.excludedTypes = params.excludedTypes; } const fieldMask = buildFieldMask(['basic', 'rating', 'hours']); return this.request(PLACES_API_BASE, '/places:searchNearby', { method: 'POST', body: JSON.stringify(body), fieldMask, }); } // -------------------------------------------------------------------------- // Places - Text Search // -------------------------------------------------------------------------- async textSearch(params: TextSearchParams): Promise<{ places: Place[] }> { const body: Record<string, unknown> = { textQuery: params.textQuery, maxResultCount: params.maxResultCount ?? 10, languageCode: params.languageCode ?? 'en', }; if (params.locationBias) { if ('radius' in params.locationBias) { body.locationBias = { circle: params.locationBias }; } else { body.locationBias = { circle: { center: params.locationBias, radius: 5000, // Default 5km bias }, }; } } if (params.includedType) { body.includedType = params.includedType; } if (params.openNow !== undefined) { body.openNow = params.openNow; } if (params.minRating !== undefined) { body.minRating = params.minRating; } if (params.priceLevels?.length) { body.priceLevels = params.priceLevels; } if (params.rankPreference) { body.rankPreference = params.rankPreference; } const fieldMask = buildFieldMask(['basic', 'rating', 'hours']); return this.request(PLACES_API_BASE, '/places:searchText', { method: 'POST', body: JSON.stringify(body), fieldMask, }); } // -------------------------------------------------------------------------- // Places - Get Details // -------------------------------------------------------------------------- async getPlaceDetails(params: PlaceDetailsParams): Promise<Place> { const fieldMask = params.fields.map((f) => `${f}`).join(','); return this.request(PLACES_API_BASE, `/places/${params.placeId}`, { method: 'GET', fieldMask, }); } // -------------------------------------------------------------------------- // Places - Get Photo URI // -------------------------------------------------------------------------- getPhotoUri(photoName: string, maxWidth: number = 400, maxHeight?: number): string { const params = new URLSearchParams({ key: this.apiKey, maxWidthPx: String(maxWidth), }); if (maxHeight) { params.set('maxHeightPx', String(maxHeight)); } return `${PLACES_API_BASE}/${photoName}/media?${params.toString()}`; } // -------------------------------------------------------------------------- // Routes - Compute Routes // -------------------------------------------------------------------------- async computeRoutes(params: ComputeRoutesParams): Promise<{ routes: Route[] }> { const formatWaypoint = (wp: LatLng | { placeId: string } | { address: string }) => { if ('latitude' in wp) { return { location: { latLng: wp } }; } if ('placeId' in wp) { return { placeId: wp.placeId }; } return { address: wp.address }; }; const body: Record<string, unknown> = { origin: formatWaypoint(params.origin), destination: formatWaypoint(params.destination), travelMode: params.travelMode, languageCode: params.languageCode ?? 'en', }; if (params.intermediates?.length) { body.intermediates = params.intermediates.map(formatWaypoint); } // When departureTime is set, use TRAFFIC_AWARE routing (required by API) // Transit doesn't use routingPreference if (params.departureTime) { body.departureTime = params.departureTime; if (params.travelMode !== 'TRANSIT') { body.routingPreference = 'TRAFFIC_AWARE_OPTIMAL'; } } if (params.computeAlternativeRoutes) { body.computeAlternativeRoutes = params.computeAlternativeRoutes; } if (params.routeModifiers) { body.routeModifiers = params.routeModifiers; } const fieldMask = [ 'routes.distanceMeters', 'routes.duration', 'routes.polyline.encodedPolyline', 'routes.legs.distanceMeters', 'routes.legs.duration', 'routes.legs.startLocation', 'routes.legs.endLocation', 'routes.legs.steps.distanceMeters', 'routes.legs.steps.staticDuration', 'routes.legs.steps.navigationInstruction', 'routes.legs.steps.travelMode', 'routes.legs.steps.transitDetails', 'routes.localizedValues', 'routes.legs.localizedValues', 'routes.legs.steps.localizedValues', ].join(','); return this.request(ROUTES_COMPUTE_URL, '', { method: 'POST', body: JSON.stringify(body), fieldMask, }); } // -------------------------------------------------------------------------- // Routes - Compute Route Matrix // -------------------------------------------------------------------------- async computeRouteMatrix( params: ComputeRouteMatrixParams, ): Promise<RouteMatrixElement[]> { const formatWaypoint = (wp: LatLng | { placeId: string } | { address: string }) => { if ('latitude' in wp) { return { waypoint: { location: { latLng: wp } } }; } if ('placeId' in wp) { return { waypoint: { placeId: wp.placeId } }; } return { waypoint: { address: wp.address } }; }; const body: Record<string, unknown> = { origins: params.origins.map(formatWaypoint), destinations: params.destinations.map(formatWaypoint), travelMode: params.travelMode, languageCode: params.languageCode ?? 'en', }; // When departureTime is set, use TRAFFIC_AWARE routing (required by API) if (params.departureTime) { body.departureTime = params.departureTime; if (params.travelMode !== 'TRANSIT') { body.routingPreference = 'TRAFFIC_AWARE_OPTIMAL'; } } // Route Matrix API returns array directly (streaming format) // For simplicity, we use the non-streaming approach return this.request(ROUTES_MATRIX_URL, '', { method: 'POST', body: JSON.stringify(body), fieldMask: 'originIndex,destinationIndex,status,condition,distanceMeters,duration,staticDuration,localizedValues', }); } }

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/iceener/maps-streamable-mcp-server'

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