A TypeScript Model Context Protocol (MCP) server that exposes Google Maps Platform APIs as tools for LLMs. Gives AI assistants real, structured map data — directions, transit routes, place search, address validation, photos, elevation, and more — instead of guessing from training data. Works with Claude Desktop and any other MCP-compatible client. 15 tools across three categories: Transport: HTTP
Add this skill
npx mdskills install apurvaumredkar/google-maps-mcpComprehensive MCP server exposing 15 Google Maps APIs with excellent documentation and multiple deployment options
1# google-maps-mcp23A TypeScript [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes Google Maps Platform APIs as tools for LLMs. Gives AI assistants real, structured map data — directions, transit routes, place search, address validation, photos, elevation, and more — instead of guessing from training data.45Works with Claude Desktop and any other MCP-compatible client.67---89## Features1011**15 tools across three categories:**1213| Category | Tools |14|----------|-------|15| **Maps** | Static map image URL, embed URL (iframe), elevation data, Street View image URL |16| **Routes** | Turn-by-turn directions (drive/walk/cycle/transit), distance matrix, multi-stop route optimization |17| **Places** | Geocoding / reverse geocoding, place details, text search, nearby search, autocomplete, photos, address validation, timezone |1819**Transport**: HTTP Streamable (stateful sessions, SSE keep-alive) — the modern MCP transport, compatible with `mcp-remote` and all HTTP-capable clients.2021**Minimal footprint**: only two runtime dependencies (`@modelcontextprotocol/sdk`, `zod`). All Google Maps calls use Node.js built-in `fetch` against REST APIs — no Google SDK required.2223---2425## Prerequisites2627- **Node.js 22+** (or Docker)28- A **Google Maps Platform API key** with the relevant APIs enabled (see below)29- A Google Cloud project with billing enabled3031### APIs to enable in Google Cloud Console3233Go to [APIs & Services → Library](https://console.cloud.google.com/apis/library) and enable:3435| API | Used by |36|-----|---------|37| Maps Static API | `maps_static_map` |38| Street View Static API | `maps_street_view` |39| Maps Embed API | `maps_embed_url` |40| Elevation API | `maps_elevation` |41| Geocoding API | `places_geocode` |42| Time Zone API | `places_timezone` |43| Places API (New) | `places_details`, `places_text_search`, `places_nearby_search`, `places_autocomplete`, `places_photos` |44| Address Validation API | `places_address_validation` |45| Routes API | `routes_compute`, `routes_matrix` |46| Route Optimization API | `routes_optimize` *(optional)* |4748You can restrict the key to these APIs and to your server's IP for production use.4950---5152## Quick Start5354### Option A — Run with Docker (recommended)5556```bash57docker run -d \58 --name google-maps-mcp \59 -p 127.0.0.1:3003:3003 \60 -e GOOGLE_MAPS_API_KEY=your_key_here \61 -e MCP_AUTH_TOKEN=your_secret_token \62 ghcr.io/apurvaumredkar/google-maps-mcp:latest63```6465Verify:66```bash67curl http://localhost:3003/health68# {"status":"ok","service":"google-maps-mcp"}69```7071### Option B — npm / npx7273No install required — run directly with `npx`:7475```bash76GOOGLE_MAPS_API_KEY=your_key_here \77MCP_AUTH_TOKEN=your_secret_token \78npx mcp-server-google-maps79# google-maps-mcp listening on port 300380```8182Or install globally:8384```bash85npm install -g mcp-server-google-maps86GOOGLE_MAPS_API_KEY=your_key_here MCP_AUTH_TOKEN=your_secret_token mcp-server-google-maps87```8889Set `PORT=` to change the default port (`3003`).9091---9293### Option C — Build from source9495```bash96git clone https://github.com/apurvaumredkar/google-maps-mcp.git97cd google-maps-mcp98npm install99npm run build100```101102Create a `.env` file (or export the vars):103```104GOOGLE_MAPS_API_KEY=your_key_here105MCP_AUTH_TOKEN=your_secret_token106# Optional — only needed for routes_optimize:107GOOGLE_CLOUD_PROJECT_ID=your_project_id108```109110Start the server:111```bash112GOOGLE_MAPS_API_KEY=... MCP_AUTH_TOKEN=... npm start113# google-maps-mcp listening on port 3003114```115116### Option D — Docker Compose (self-hosted stack)117118Add to your `docker-compose.yml`:119120```yaml121services:122 google-maps-mcp:123 build: .124 container_name: google-maps-mcp125 restart: unless-stopped126 ports:127 - "127.0.0.1:3003:3003"128 environment:129 - GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}130 - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}131 - GOOGLE_CLOUD_PROJECT_ID=${GOOGLE_CLOUD_PROJECT_ID:-}132```133134---135136## Environment Variables137138| Variable | Required | Description |139|----------|----------|-------------|140| `GOOGLE_MAPS_API_KEY` | Yes | Your Google Maps Platform API key |141| `MCP_AUTH_TOKEN` | No | Secret token clients must send in the `X-Api-Key` header. Omit for local-only use; set when exposing the server over a network or proxy. Generate with `openssl rand -hex 32` |142| `PORT` | No | HTTP port (default: `3003`) |143| `GOOGLE_CLOUD_PROJECT_ID` | No | Required only for `routes_optimize` (Route Optimization API) |144145---146147## Connecting a Client148149The server exposes a single endpoint: `POST/GET http://localhost:3003/mcp`150151If `MCP_AUTH_TOKEN` is set, all requests must include the header:152```153X-Api-Key: <MCP_AUTH_TOKEN>154```155156If `MCP_AUTH_TOKEN` is not set, no header is required (suitable for local-only use).157158### Claude Desktop159160Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):161162```json163{164 "mcpServers": {165 "google-maps": {166 "command": "npx",167 "args": [168 "mcp-remote",169 "http://localhost:3003/mcp",170 "--header",171 "X-Api-Key: your_secret_token"172 ]173 }174 }175}176```177178> **Windows + WSL**: if the server runs inside WSL, use the full node path:179> ```json180> {181> "mcpServers": {182> "google-maps": {183> "command": "wsl",184> "args": [185> "--",186> "/home/user/.nvm/versions/node/v25.2.1/bin/node",187> "/home/user/.nvm/versions/node/v25.2.1/bin/mcp-remote",188> "http://localhost:3003/mcp",189> "--header",190> "X-Api-Key: your_secret_token"191> ]192> }193> }194> }195> ```196197---198199## Tool Reference200201### Maps202203#### `maps_static_map` — Static Map Image204Returns a direct image URL for a static map.205206| Parameter | Type | Default | Description |207|-----------|------|---------|-------------|208| `center` | string | required | Address or `lat,lng` |209| `zoom` | integer | `13` | Zoom level 0–21 |210| `size` | string | `640x480` | Image dimensions WxH in pixels |211| `maptype` | enum | `roadmap` | `roadmap` \| `satellite` \| `terrain` \| `hybrid` |212| `markers` | string | — | Marker spec e.g. `color:red\|48.8566,2.3522` |213| `path` | string | — | Path spec for drawing routes |214| `format` | enum | `png` | `png` \| `jpg` \| `gif` |215| `scale` | enum | `1` | `1` = standard, `2` = HiDPI/retina |216| `language` | string | — | BCP 47 language code for labels |217| `region` | string | — | ISO 3166-1 alpha-2 region code |218219---220221#### `maps_embed_url` — Maps Embed URL222Returns an iframe-ready embed URL.223224| Parameter | Type | Description |225|-----------|------|-------------|226| `mode` | enum | `place` \| `directions` \| `search` \| `view` \| `streetview` |227| `q` | string | Place/search query (place, search modes) |228| `center` | string | `lat,lng` for view/streetview mode |229| `zoom` | integer | Zoom level |230| `origin` / `destination` | string | For directions mode |231| `waypoints` | string | Pipe-separated waypoints |232| `maptype` | enum | `roadmap` \| `satellite` |233234---235236#### `maps_elevation` — Elevation Data237Returns elevation in metres above sea level.238239| Parameter | Type | Description |240|-----------|------|-------------|241| `locations` | string | Pipe-separated `lat,lng` pairs |242| `path` | string | Pipe-separated `lat,lng` path |243| `samples` | integer | Number of samples along path (2–512) |244245---246247#### `maps_street_view` — Street View Image248Returns a direct Street View panorama image URL.249250| Parameter | Type | Default | Description |251|-----------|------|---------|-------------|252| `location` | string | — | Address or `lat,lng` |253| `pano` | string | — | Specific panorama ID (overrides location) |254| `size` | string | `640x480` | Image size WxH |255| `heading` | number | — | Camera heading 0–360° |256| `pitch` | number | — | Camera pitch -90° to 90° |257| `fov` | number | `90` | Field of view 10–120° |258| `source` | enum | — | `outdoor` to exclude indoor panoramas |259260---261262### Routes263264#### `routes_compute` — Compute Route265Turn-by-turn directions with real-time traffic.266267| Parameter | Type | Default | Description |268|-----------|------|---------|-------------|269| `origin` | string | required | Address or `lat,lng` |270| `destination` | string | required | Address or `lat,lng` |271| `travel_mode` | enum | `DRIVE` | `DRIVE` \| `WALK` \| `BICYCLE` \| `TRANSIT` \| `TWO_WHEELER` |272| `intermediates` | string[] | — | Waypoints between origin and destination |273| `departure_time` | string | — | ISO 8601 datetime for traffic-aware routing |274| `avoid_tolls` | boolean | `false` | Avoid toll roads |275| `avoid_highways` | boolean | `false` | Avoid highways |276| `avoid_ferries` | boolean | `false` | Avoid ferries |277| `units` | enum | `METRIC` | `METRIC` \| `IMPERIAL` |278| `compute_alternative_routes` | boolean | `false` | Return up to 3 alternatives |279280---281282#### `routes_matrix` — Route Distance Matrix283Compute travel time/distance between multiple origins and destinations simultaneously.284285| Parameter | Type | Default | Description |286|-----------|------|---------|-------------|287| `origins` | string[] | required | Up to 25 addresses or `lat,lng` strings |288| `destinations` | string[] | required | Up to 25 addresses or `lat,lng` strings |289| `travel_mode` | enum | `DRIVE` | `DRIVE` \| `WALK` \| `BICYCLE` \| `TRANSIT` |290| `departure_time` | string | — | ISO 8601 datetime |291| `units` | enum | `METRIC` | `METRIC` \| `IMPERIAL` |292293---294295#### `routes_optimize` — Optimize Multi-Stop Route296Optimizes stop order to minimize total travel. Requires `GOOGLE_CLOUD_PROJECT_ID`.297298| Parameter | Type | Description |299|-----------|------|-------------|300| `vehicle_start` | string | Start location — **must be `lat,lng`** (geocode first if needed) |301| `vehicle_end` | string | End location (defaults to start) |302| `visits` | object[] | Array of `{ address, label?, duration_minutes? }` — addresses must be `lat,lng` |303| `travel_mode` | enum | `DRIVING` \| `WALKING` |304305---306307### Places308309#### `places_geocode` — Geocode / Reverse Geocode310Convert addresses ↔ coordinates.311312| Parameter | Type | Description |313|-----------|------|-------------|314| `address` | string | Address to geocode |315| `latlng` | string | `lat,lng` for reverse geocoding |316| `region` | string | ISO 3166-1 alpha-2 region bias |317| `components` | string | Component filter e.g. `country:FR\|postal_code:75001` |318319---320321#### `places_details` — Place Details322Full details for a place by its Google Place ID.323324| Parameter | Type | Description |325|-----------|------|-------------|326| `place_id` | string | Google Place ID |327| `fields` | string | Comma-separated field mask (has sensible default) |328| `language_code` | string | Response language |329330---331332#### `places_text_search` — Search Places by Text333Find places matching a natural language query.334335| Parameter | Type | Description |336|-----------|------|-------------|337| `query` | string | e.g. `"best ramen in Tokyo"` |338| `location_bias_lat/lng` | number | Bias results toward this location |339| `location_bias_radius_m` | number | Bias circle radius |340| `max_results` | integer | 1–20, default 10 |341| `min_rating` | number | Minimum average star rating (0–5) |342| `open_now` | boolean | Only currently open places |343| `included_type` | string | Filter by place type e.g. `restaurant` |344| `price_levels` | enum[] | `PRICE_LEVEL_FREE` … `PRICE_LEVEL_VERY_EXPENSIVE` |345346---347348#### `places_nearby_search` — Search Nearby Places349Find places near a coordinate within a radius.350351| Parameter | Type | Description |352|-----------|------|-------------|353| `latitude` / `longitude` | number | Center of search |354| `radius_m` | number | Search radius in metres (max 50,000) |355| `included_types` | string[] | Place type filters |356| `excluded_types` | string[] | Place types to exclude |357| `max_results` | integer | 1–20, default 10 |358| `rank_preference` | enum | `DISTANCE` \| `POPULARITY` |359360---361362#### `places_autocomplete` — Place Autocomplete363Predict place names from partial input.364365| Parameter | Type | Description |366|-----------|------|-------------|367| `input` | string | Partial text to complete |368| `location_bias_lat/lng` | number | Bias toward this location |369| `included_primary_types` | string[] | Type filter |370| `country_codes` | string[] | ISO 3166-1 alpha-2 country filter |371| `include_query_predictions` | boolean | Also return query predictions |372373---374375#### `places_photos` — Place Photos376Get photo URLs for a place.377378| Parameter | Type | Default | Description |379|-----------|------|---------|-------------|380| `place_id` | string | required | Google Place ID |381| `max_photos` | integer | `3` | Max photos to return (1–10) |382| `max_width_px` | integer | `1200` | Max photo width in pixels |383| `max_height_px` | integer | `900` | Max photo height in pixels |384385---386387#### `places_address_validation` — Validate Address388Validate and standardize a postal address.389390| Parameter | Type | Description |391|-----------|------|-------------|392| `address_lines` | string[] | Address lines |393| `region_code` | string | ISO 3166-1 alpha-2 country code |394| `locality` | string | City/town |395| `administrative_area` | string | State/province |396| `postal_code` | string | Postal code |397| `enable_usps_cass` | boolean | USPS CASS validation (US only) |398399---400401#### `places_timezone` — Get Timezone402Get IANA timezone and UTC/DST offset for any coordinates.403404| Parameter | Type | Description |405|-----------|------|-------------|406| `latitude` / `longitude` | number | Location |407| `timestamp` | integer | Unix timestamp for DST calculation (defaults to now) |408| `language` | string | Response language |409410---411412## Architecture413414```415src/416├── index.ts # Raw Node.js HTTP server, auth, stateful session management417├── server.ts # McpServer instantiation + tool registration418├── maps-client.ts # Typed fetch wrappers for all Google Maps REST APIs419└── tools/420 ├── maps.ts # 4 tools: static map, embed, elevation, street view421 ├── routes.ts # 3 tools: compute route, matrix, optimize422 └── places.ts # 8 tools: geocode, details, text search, nearby, autocomplete,423 # photos, address validation, timezone424```425426**Key design decisions:**427428- **Raw `node:http`** instead of Express — required for correct interop with the MCP SDK's internal Hono-based request handling. Express pre-consumes the request body stream in a way that breaks `StreamableHTTPServerTransport`.429- **Stateful session map** — `mcp-remote` and SSE keep-alive require sessions to persist across requests. Sessions are keyed by `Mcp-Session-Id` header and cleaned up on transport close.430- **Auth before body read** — the `X-Api-Key` check happens on the header before any body stream is touched, so rejected requests drain cleanly.431- **Auth split for Google APIs** — legacy REST APIs (Static Maps, Geocoding, Elevation, Timezone, Street View) use `?key=` query param; new APIs (Places v1, Routes v2, Address Validation) use `X-Goog-Api-Key` header.432433---434435## Development436437```bash438npm run dev # TypeScript watch mode (tsc --watch)439npm run build # Compile to dist/440npm start # Run compiled server441```442443### Rebuild Docker image after changes444445```bash446docker compose build google-maps-mcp447docker compose up -d google-maps-mcp448```449450### Testing the MCP endpoint451452```bash453# Health check (no auth required)454curl http://localhost:3003/health455456# MCP initialize (auth required)457TOKEN=your_secret_token458curl -s -X POST http://localhost:3003/mcp \459 -H "Content-Type: application/json" \460 -H "Accept: application/json, text/event-stream" \461 -H "X-Api-Key: $TOKEN" \462 -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}},"id":1}'463464# List tools (use session ID from Mcp-Session-Id response header)465SESSION=<Mcp-Session-Id from above>466curl -s -X POST http://localhost:3003/mcp \467 -H "Content-Type: application/json" \468 -H "Accept: application/json, text/event-stream" \469 -H "X-Api-Key: $TOKEN" \470 -H "Mcp-Session-Id: $SESSION" \471 -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'472```473474> **Windows/WSL gotcha**: if your `.env` file has Windows CRLF line endings, extract values with `tr -d '\r'`:475> ```bash476> TOKEN=$(grep MCP_AUTH_TOKEN .env | cut -d= -f2 | tr -d '\r')477> ```478479---480
Full transparency — inspect the skill content before installing.