@rog-studios/google-maps-mcp
v1.3.0
Published
The complete Google Maps Platform as an MCP server: places, Google Business listings, photos, routes with multi-stop optimization, map & Street View images, geocoding, address validation, autocomplete, weather, air quality, pollen, solar — with layered ra
Maintainers
Readme
google-maps-mcp
The complete Google Maps Platform as a production-grade MCP server: places & Google Business listings, photos, routes with multi-stop optimization, map & Street View images the model can actually see, geocoding, address validation, autocomplete, weather, air quality, pollen and rooftop solar — built for agents, hardened for operators.
- 23 read-only tools (+ a resource template, a prompt, and a shipped agent skill), all with strict Zod input validation, typed structured output and agent-oriented "use when" descriptions.
- Images as first-class results:
maps_static_map,maps_street_viewandmaps_place_photoreturn real MCP image content blocks — the agent sees the map, the route, the storefront. URLs with embedded keys are never exposed. - Composite tools that answer in one call what normally takes 5-10 (
maps_explore_area,maps_compare_places), with graceful partial-failure semantics. - Layered abuse protection: per-client inbound rate limiting, global outbound rate limiting, concurrency caps, per-host circuit breakers, retry with exponential backoff + jitter (honoring
Retry-After), hard timeouts, optional daily budget, request coalescing. - Security first: the Google key never leaks (logs, errors and URLs are scrubbed), upstream content is sanitized and treated as data — never instructions, photo CDN fetches are host-allowlisted (SSRF defense), and the HTTP transport ships authenticated-only with DNS-rebinding and brute-force defenses.
- Two transports: stdio (local, default) and Streamable HTTP (remote, bearer-authenticated). The deprecated SSE transport is intentionally not implemented.
Documentation
| Guide | What's inside | | -------------------------------------------- | --------------------------------------------------------------------------------- | | Getting started | Zero to first tool call: API key, enabling APIs, client setup, verification | | Tools reference | All 23 tools A→Z: every parameter, outputs, examples, billing notes | | Configuration | Every environment variable + tuning recipes (low-cost, high-traffic, locked-down) | | Deployment | stdio vs HTTP, TLS/reverse proxy, Docker, observability, production checklist | | Security | Threat model, key lifecycle, prompt-injection & SSRF defenses, privacy | | Architecture | Codebase layout, request lifecycle, design decisions, how to add a tool | | Troubleshooting | Symptom → cause → fix tables and FAQ | | Changelog | Release history |
Tools
Geocoding & addresses
| Tool | What it does |
| ----------------------- | ------------------------------------------------------------------------- |
| maps_geocode | Address → coordinates + canonical place_id (up to 5 candidates) |
| maps_batch_geocode | Up to 20 addresses in one call, per-address error isolation |
| maps_reverse_geocode | Coordinates → addresses + place_ids |
| maps_validate_address | Address Validation API: completeness verdict, standardized form, metadata |
| maps_autocomplete | Partial/fuzzy text → concrete place suggestions with place_ids |
Places & Google Business listings
| Tool | What it does |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| maps_search_places | Free-text search; filters (open now, min rating, bias) + search along a route with detour costs |
| maps_search_nearby | Places around a point, filterable by type, rank by popularity or distance |
| maps_place_details | Full Google Business listing: hours, phone, website + detail_level: full facets (delivery, payment, parking, accessibility…); reviews & photo refs opt-in |
| maps_place_photo | Downloads a place photo as an image the model sees |
Routes & roads
| Tool | What it does |
| -------------------- | ------------------------------------------------------------------------------------ |
| maps_compute_route | Directions with up to 23 intermediate stops + automatic visit-order optimization |
| maps_route_matrix | Distance/duration for every origin × destination pair (≤ 625 pairs) |
| maps_snap_to_roads | Snaps noisy GPS traces to the road network |
Imagery
| Tool | What it does |
| ------------------ | ------------------------------------------------------------------------------- |
| maps_static_map | Renders a map image: markers, route polylines, satellite/terrain styles |
| maps_street_view | Street View panorama (availability probed first via the free metadata endpoint) |
Environment
| Tool | What it does |
| ---------------------- | ---------------------------------------------------------------------------------- |
| maps_weather | Current conditions, ≤ 10-day daily / ≤ 240-hour hourly forecast, 24 h history |
| maps_air_quality | Universal + local AQI, pollutants, health advice; ≤ 96 h forecast, ≤ 720 h history |
| maps_pollen_forecast | ≤ 5-day grass/tree/weed pollen indexes and in-season plants |
| maps_solar_potential | Rooftop solar potential for the nearest building (Solar API) |
| maps_timezone | IANA time zone + UTC/DST offsets at a given instant |
| maps_elevation | Ground elevation for up to 100 coordinates |
Composites (one call instead of many)
| Tool | What it does |
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
| maps_explore_area | Neighborhood overview: top places per category + area name + optional weather/AQI snapshot |
| maps_compare_places | 2-5 places side by side, with travel time/distance from an origin |
| maps_local_rank_tracker | Local-SEO grid audit: where a business ranks for a keyword across an area, visibility %, top competitors |
Also exposed: the gmaps://place/{placeId} resource template, the plan_trip prompt, and an agent skill (skills/google-maps/SKILL.md) teaching effective tool-chaining patterns — drop it into your agent's skills directory.
Requirements
- Node.js ≥ 20 (or Docker).
- A Google Maps Platform API key. Enable the APIs for the tools you'll use (each tool fails cleanly with an
auth_errornaming the missing API otherwise):- Core: Geocoding API, Places API (New), Routes API, Time Zone API, Elevation API
- Imagery: Maps Static API, Street View Static API
- Environment: Weather API, Air Quality API, Pollen API, Solar API
- Extras: Address Validation API, Roads API
Restrict the key to exactly those APIs in the Google Cloud Console and add an IP restriction when running server-side. Billing must be active.
maps_place_detailswithdetail_level: "full"orinclude_reviewshits higher-priced Places SKUs — the defaults stay on the lean masks.
Quickstart (stdio)
GOOGLE_MAPS_API_KEY=AIza... npx -y @rog-studios/google-maps-mcpThe server speaks MCP on stdout and logs JSON to stderr.
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"google-maps": {
"command": "npx",
"args": ["-y", "@rog-studios/google-maps-mcp"],
"env": { "GOOGLE_MAPS_API_KEY": "AIza..." }
}
}
}Claude Code
claude mcp add google-maps --env GOOGLE_MAPS_API_KEY=AIza... -- npx -y @rog-studios/google-maps-mcpCursor (~/.cursor/mcp.json)
{
"mcpServers": {
"google-maps": {
"command": "npx",
"args": ["-y", "@rog-studios/google-maps-mcp"],
"env": { "GOOGLE_MAPS_API_KEY": "AIza..." }
}
}
}VS Code (.vscode/mcp.json)
{
"servers": {
"google-maps": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@rog-studios/google-maps-mcp"],
"env": { "GOOGLE_MAPS_API_KEY": "AIza..." }
}
}
}Running from a clone instead of npm? Build with
npm run build, then use"command": "node", "args": ["/absolute/path/to/google-maps-mcp/dist/index.js"].
Agent integration
- Vision-grade results: image tools return MCP
imagecontent blocks plus a JSON metadata mirror — multimodal agents reason about what they see (a route drawn on a map, a storefront photo) instead of imagining it. - Chainable by design:
maps_compute_routereturnsencodedPolyline, accepted bymaps_static_map(draw it) andmaps_search_places.route_polyline(search along it with detour costs).maps_place_details.photos[].namefeedsmaps_place_photo. - Token-context control:
MCP_TOOLS_ENABLED/MCP_TOOLS_DISABLED(CSV) shrink the advertised tool surface; unknown names fail at startup. - Server instructions teach clients the typical flows, and
skills/google-maps/SKILL.mdships ready-to-use chaining patterns for Claude Code and compatible agents. - Actionable errors: every failure is one line with a machine-readable code, a hint, and
retry_after_mswhen waiting helps.
Remote mode (Streamable HTTP)
GOOGLE_MAPS_API_KEY=AIza... \
MCP_TRANSPORT=http \
MCP_SERVER_API_KEY="$(openssl rand -hex 32)" \
npx -y @rog-studios/google-maps-mcp- Endpoint:
POST/GET/DELETE http://127.0.0.1:3000/mcp— bearer auth is mandatory, the server refuses to start withoutMCP_SERVER_API_KEY. GET /healthz— unauthenticated liveness.GET /metrics— JSON counters, bearer-authenticated, opt-in viaMETRICS_ENABLED=true.- Comma-separate several keys in
MCP_SERVER_API_KEYto give each client its own identity, rate-limit bucket and revocation path. - Sessions are bound to the key that created them, capped (
MCP_MAX_SESSIONS) and evicted when idle. - Binding to anything but loopback is your explicit choice (
MCP_HTTP_HOST=0.0.0.0): put TLS termination in front and setMCP_ALLOWED_HOSTSto your public hostname.
Smoke test:
curl -s http://127.0.0.1:3000/mcp \
-H "Authorization: Bearer $MCP_SERVER_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' -i
# Reuse the mcp-session-id response header on subsequent requests.Client config (clients with native Streamable HTTP support):
{
"mcpServers": {
"google-maps": {
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <MCP_SERVER_API_KEY>" }
}
}
}Configuration
All variables are validated at boot; the process exits with a clear message on any invalid value. See .env.example for the commented reference.
| Variable | Default | Purpose |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------- |
| GOOGLE_MAPS_API_KEY | — (required) | Google Maps Platform key |
| MCP_TRANSPORT | stdio | stdio or http |
| LOG_LEVEL | info | pino level, logs on stderr |
| GOOGLE_MAPS_LANGUAGE / GOOGLE_MAPS_REGION | — | Default result language / region bias |
| MCP_TOOLS_ENABLED / MCP_TOOLS_DISABLED | (all) | CSV allow/deny lists for the tool surface |
| MCP_HTTP_HOST / MCP_HTTP_PORT | 127.0.0.1 / 3000 | HTTP bind address |
| MCP_SERVER_API_KEY | — (required in http) | Bearer key(s), comma-separated, ≥ 16 chars |
| MCP_ALLOWED_ORIGINS | (none) | Allowed browser Origins, CSV |
| MCP_ALLOWED_HOSTS | 127.0.0.1:<port>,localhost:<port> | Allowed Host headers (DNS-rebinding defense) |
| MCP_MAX_SESSIONS / MCP_SESSION_IDLE_TIMEOUT_MS | 50 / 1800000 | Session cap / idle eviction |
| METRICS_ENABLED | false | Expose /metrics (http mode) |
| RATE_LIMIT_INBOUND_RPS / RATE_LIMIT_INBOUND_BURST | 5 / 10 | Per-client inbound limit (0 disables) |
| RATE_LIMIT_OUTBOUND_RPS / RATE_LIMIT_OUTBOUND_BURST | 10 / 20 | Global limit towards Google |
| OUTBOUND_MAX_WAIT_MS | 4000 | Max wait for an outbound token |
| MAX_CONCURRENT_UPSTREAM | 8 | Simultaneous upstream calls |
| DAILY_UPSTREAM_BUDGET | 0 (off) | Hard cap on upstream calls per UTC day |
| UPSTREAM_TIMEOUT_MS | 10000 | Hard per-attempt timeout |
| RETRY_MAX_ATTEMPTS / RETRY_BASE_DELAY_MS / RETRY_MAX_DELAY_MS | 3 / 250 / 4000 | Retry policy (backoff + full jitter) |
| CIRCUIT_BREAKER_FAILURE_THRESHOLD / CIRCUIT_BREAKER_COOLDOWN_MS | 5 / 30000 | Per-host circuit breaker |
| CACHE_MAX_ENTRIES / IMAGE_CACHE_MAX_ENTRIES | 500 / 32 | LRU cache sizes (0 disables) |
| CACHE_TTL_GEOCODE_S / CACHE_TTL_PLACES_S / CACHE_TTL_ROUTES_S / CACHE_TTL_STATIC_S / CACHE_TTL_ENVIRONMENT_S | 3600 / 300 / 60 / 86400 / 600 | Per-domain TTLs (0 disables) |
Rate limiting & resilience architecture
Every tool call traverses, in order:
agent call
│
▼
[1] inbound token bucket (per client identity) → immediate [rate_limited] + retry_after_ms
│
▼
[2] TTL cache + in-flight coalescing → identical concurrent reads = 1 upstream call
│
▼
[3] concurrency semaphore (global) → agent fan-out cannot open N connections
│
▼
[4] circuit breaker (per Google host) → fail fast while an upstream is down
│
▼
[5] daily budget (optional) + outbound token bucket (global)
│
▼
[6] fetch with hard timeout ──429/5xx/timeout──► backoff + jitter, Retry-After honored,
│ tokens consumed per attempt (no retry storms)
▼
Google Maps Platform (12 APIs, per-host breakers)A 1000-call flood is part of the test suite: the upstream never sees more than the configured budget, every rejected call gets a clean [rate_limited] result with retry_after_ms, and the server keeps serving. Composite tools fan out through the same pipeline — they cannot bypass any limit.
Google's default quotas are typically expressed per minute and per API. The defaults here (10 req/s sustained, 20 burst, globally) stay well under that; align
RATE_LIMIT_OUTBOUND_RPSwith the quotas of your project, and setDAILY_UPSTREAM_BUDGETfor spend control.
Security model
| Threat | Mitigation |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Google key leakage | Key read once at boot; never logged; URLs logged as host+path only; image URLs never returned to agents; AIza… patterns, key= params and bearer tokens scrubbed from every error and log line |
| Agent retry loops / runaway automation | Inbound per-client buckets reject instantly with retry_after_ms; outbound bucket + semaphore + budget bound what ever reaches Google |
| Upstream outage hammering | Per-host circuit breakers (fail-fast + single half-open probe), bounded retries, hard timeouts via AbortSignal |
| Prompt injection via Maps content (reviews, names…) | Control characters stripped, lengths capped, reviews fetched only on explicit opt-in, server instructions tell agents to treat content strictly as data |
| SSRF via tampered photo URIs | The photo CDN hop is credential-free and hard-allowlisted to *.googleusercontent.com over HTTPS |
| Oversized image payloads | 6 MiB hard cap on binary responses; image dimensions capped by schema; dedicated small image cache |
| Unauthorized remote access | HTTP refuses to start without a strong bearer key; constant-time (hashed timingSafeEqual) comparison; failed auths throttled per IP; WWW-Authenticate on 401 |
| DNS rebinding / browser abuse | Host allowlist + Origin allowlist (default: no browser origins) |
| Session hijacking / fixation | Session IDs are crypto.randomUUID(), bound to the creating key, idle-evicted, capped in number |
| Oversized / malformed payloads | 1 MiB JSON body limit, Content-Type enforcement, strict Zod validation on every tool input — nothing unvalidated reaches Google |
| Upstream response tampering | Responses parsed with tolerant-but-typed Zod schemas; unexpected shapes become clean upstream_errors, never crashes |
| Supply chain | 3 runtime dependencies only (@modelcontextprotocol/sdk, zod, pino); raw node:http instead of a web framework |
Notes: tool arguments are never logged (addresses and coordinates are user data — privacy by design). The MCP logging capability is deliberately not exposed; operator logs stay on stderr.
Error taxonomy
Tool failures return isError: true with a single actionable line, e.g. [rate_limited] Too many requests from this client. Retry after 1200ms (retry_after_ms=1200). retryable=true. Auth errors name the exact Google API to enable.
| Code | Meaning | Agent action |
| ------------------ | -------------------------------------------------------------------------- | --------------------------------------- |
| validation_error | Arguments rejected (locally or by Google) | Fix arguments; do not retry as-is |
| auth_error | Key invalid / named API not enabled / billing off / restriction mismatch | Stop and report; operator action needed |
| rate_limited | Inbound, outbound, concurrency, budget or Google 429 | Wait retry_after_ms, then retry |
| upstream_error | Google unavailable / timeout / circuit open / not found / changed contract | Retry only if retryable=true |
| config_error | Invalid environment at boot | Fix env; process refuses to start |
Caching note
The in-memory cache is intentionally short-lived (and never persisted). Google Maps Platform terms restrict caching of most content — place IDs are the notable exception, and geocoding results have a bounded allowance. Review the current terms for your use case and set any CACHE_TTL_*_S=0 to disable a domain's cache entirely.
Docker
docker build -t google-maps-mcp .
docker run --rm -p 3000:3000 \
-e GOOGLE_MAPS_API_KEY=AIza... \
-e MCP_SERVER_API_KEY="$(openssl rand -hex 32)" \
-e MCP_ALLOWED_HOSTS=mcp.example.com \
google-maps-mcpThe image is multi-stage, slim, runs as the non-root node user and has a /healthz healthcheck. It defaults to MCP_TRANSPORT=http on 0.0.0.0:3000 — which is why the bearer key is mandatory.
Development
npm install
npm run dev # stdio server via tsx
npm test # vitest (109 tests: every tool, rate limiter, retry, breaker, cache, HTTP security, SSRF, 1000-call flood)
npm run verify # typecheck + lint + test + buildLayered architecture: tools/ (thin handlers: validate → delegate → map) → services/ (per Google domain: geocoding, places, routes, environment, weather, airQuality, solar, imagery, insights) → lib/ (httpClient, rateLimiter, circuitBreaker, cache, errors, logger, redact, metrics, toolkit). Transports live in transports/; assembly in server.ts/index.ts; configuration in config.ts.
MCP SDK v2 migration note
This server pins @modelcontextprotocol/sdk to the stable 1.x branch (currently 1.29.x) with the registerTool/registerResource/registerPrompt high-level API, Zod v4 schemas and protocol version negotiation (nothing hardcoded). When SDK v2 stabilizes (package split into @modelcontextprotocol/server etc.), migration is contained: imports in server.ts, transports/* and lib/toolkit.ts, plus the package.json dependency. Tool/service/limiter code is SDK-agnostic by design.
License
MIT © Rog Studios
