google-maps-keyless-mcp 0.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. google_maps_keyless_mcp-0.5.3.dist-info/METADATA +199 -0
  2. google_maps_keyless_mcp-0.5.3.dist-info/RECORD +46 -0
  3. google_maps_keyless_mcp-0.5.3.dist-info/WHEEL +4 -0
  4. google_maps_keyless_mcp-0.5.3.dist-info/entry_points.txt +3 -0
  5. google_maps_keyless_mcp-0.5.3.dist-info/licenses/LICENSE +21 -0
  6. google_maps_keyless_mcp-0.5.3.dist-info/licenses/NOTICE +22 -0
  7. google_maps_keyless_mcp-0.5.3.dist-info/licenses/THIRD_PARTY_NOTICES +30 -0
  8. maps_mcp/__init__.py +8 -0
  9. maps_mcp/__main__.py +3 -0
  10. maps_mcp/auth.py +36 -0
  11. maps_mcp/config.py +125 -0
  12. maps_mcp/errors.py +67 -0
  13. maps_mcp/google.py +229 -0
  14. maps_mcp/logs.py +136 -0
  15. maps_mcp/planner.py +57 -0
  16. maps_mcp/server.py +509 -0
  17. maps_mcp/service.py +703 -0
  18. maps_mcp/shapes.py +385 -0
  19. maps_mcp/state.py +158 -0
  20. maps_mcp/staticmap.py +182 -0
  21. maps_mcp/vendor/__init__.py +0 -0
  22. maps_mcp/vendor/gmaps/LICENSE +21 -0
  23. maps_mcp/vendor/gmaps/VENDORED_COMMIT +1 -0
  24. maps_mcp/vendor/gmaps/__init__.py +35 -0
  25. maps_mcp/vendor/gmaps/_auth/__init__.py +12 -0
  26. maps_mcp/vendor/gmaps/_auth/session.py +294 -0
  27. maps_mcp/vendor/gmaps/_directions.py +74 -0
  28. maps_mcp/vendor/gmaps/_hosts.py +21 -0
  29. maps_mcp/vendor/gmaps/_places.py +71 -0
  30. maps_mcp/vendor/gmaps/_reviews.py +123 -0
  31. maps_mcp/vendor/gmaps/_search.py +1409 -0
  32. maps_mcp/vendor/gmaps/client.py +327 -0
  33. maps_mcp/vendor/gmaps/control.py +276 -0
  34. maps_mcp/vendor/gmaps/exceptions.py +93 -0
  35. maps_mcp/vendor/gmaps/grid.py +188 -0
  36. maps_mcp/vendor/gmaps/rpc/__init__.py +56 -0
  37. maps_mcp/vendor/gmaps/rpc/decoder.py +386 -0
  38. maps_mcp/vendor/gmaps/rpc/directions.py +529 -0
  39. maps_mcp/vendor/gmaps/rpc/encoder.py +173 -0
  40. maps_mcp/vendor/gmaps/rpc/ids.py +50 -0
  41. maps_mcp/vendor/gmaps/rpc/parser.py +1132 -0
  42. maps_mcp/vendor/gmaps/rpc/pb_encoder.py +294 -0
  43. maps_mcp/vendor/gmaps/rpc/types.py +103 -0
  44. maps_mcp/vendor/gmaps/transport.py +461 -0
  45. maps_mcp/vendor/gmaps/validation.py +214 -0
  46. maps_mcp/view.html +613 -0
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.5
2
+ Name: google-maps-keyless-mcp
3
+ Version: 0.5.3
4
+ Summary: Read-only MCP server for Google Maps travel times, routes, places and map images, without an API key
5
+ Project-URL: Repository, https://github.com/benjamin-di/google-maps-mcp
6
+ Project-URL: Issues, https://github.com/benjamin-di/google-maps-mcp/issues
7
+ Author: benjamin-di
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: NOTICE
11
+ License-File: THIRD_PARTY_NOTICES
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: fastmcp==4.0.10
14
+ Requires-Dist: httpx<0.29,>=0.27
15
+ Requires-Dist: pillow>=12.0
16
+ Requires-Dist: tzdata>=2025.2
17
+ Description-Content-Type: text/markdown
18
+
19
+ # google-maps-mcp
20
+
21
+ A read-only [MCP](https://modelcontextprotocol.io) server that gives AI assistants Google Maps:
22
+ **how long it takes to get from A to B** (driving with traffic, walking, cycling), **turn-by-turn
23
+ directions**, **multi-stop route planning**, **places with their opening hours**, and **map
24
+ images**, without an API key, billing or a Google account. It reads the same data the
25
+ google.com/maps web page loads when you are signed out.
26
+
27
+ Results use the field names of Google's official
28
+ [Places API (New)](https://developers.google.com/maps/documentation/places/web-service/reference/rest/v1/places)
29
+ and [Routes API](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes),
30
+ so they read like those APIs' JSON. In clients that support MCP Apps (claude.ai, ChatGPT),
31
+ routes and places are drawn as interactive cards.
32
+
33
+ > Unofficial. Not affiliated with, endorsed or sponsored by Google. Automated access to
34
+ > google.com/maps is not covered by Google's terms of service: use it for personal,
35
+ > low-volume purposes, at your own risk (see [Terms and limits](#terms-and-limits)).
36
+
37
+ ## Tools
38
+
39
+ | Tool | What it returns | Google requests |
40
+ |---|---|---|
41
+ | `maps_search_places(textQuery, latitude, longitude, radiusMeters, pageSize)` | Places like `places:searchText`: id, name, address, location, rating, review count, category, phone, website, price, time zone, `currentOpeningHours` | 1 |
42
+ | `maps_get_place(id)` | One place by its id (`ChIJ…`), the same fields | 1 |
43
+ | `maps_compute_route(origin, destination, intermediates≤8, travelMode, departureTime \| arrivalTime, computeAlternativeRoutes, includeSteps, includePolyline)` | Like `computeRoutes`: distance and duration per route and leg; for driving, duration in traffic (now or at the given time), typical duration, Google's range ("26–50 min") and the arrival or departure time; optional turn-by-turn steps and route line; a Google Maps link | 1 |
44
+ | `maps_compute_route_matrix(origins, destinations, travelMode, departureTime)` | Like `computeRouteMatrix`, up to 25 pairs | 1 per pair |
45
+ | `maps_plan_route(origin, stops≤8, destination \| roundTrip, travelMode, departureTime, dwellMinutes, optimizeOrder)` | A multi-stop plan: the fastest order, arrival and leave time per stop, whether each stop is open on arrival, warnings | 1 + up to 5 candidate orders + 1 per stop for hours |
46
+ | `maps_show_route(...)` | The same plan as an interactive card | as `maps_plan_route` |
47
+ | `maps_show_places(items[{id, note}]≤10, title)` | Place cards with photo, rating, open now, today's hours, holidays, links, and a pin map | 1 per place |
48
+ | `maps_static_map(center, zoom, size, maptype, markers, route)` | A JPEG like the Static Maps API: an area, lettered pins and/or a route line; roadmap, satellite, hybrid or terrain | the lookups, plus about 10-25 map tiles |
49
+ | `maps_status(check)` | Daily budgets, breaker, last error; `check=true` runs one real check (cached 55 min) | 0 (or 3) |
50
+
51
+ Durations are Google's `"<seconds>s"` strings and distances are meters. Every key is present;
52
+ `null` means Google did not say. Stops are a name or address, a place id from
53
+ `maps_search_places`, or `"lat,lng"`; times are the local wall-clock time at the stop
54
+ (`2026-10-29T08:30`).
55
+
56
+ **Opening hours.** `currentOpeningHours` covers the next seven days with dates, so holiday
57
+ hours land on the right day: `periods`, `weekdayDescriptions`, `openNow`,
58
+ `nextOpenTime`/`nextCloseTime` (in the place's time zone), and `holidays`: the holiday Google
59
+ attaches to a date, with `confirmed: false` when Maps only says it "might affect these hours".
60
+
61
+ **Turn-by-turn.** `includeSteps` adds Routes API `steps` per leg (`navigationInstruction`
62
+ {`maneuver`, `instructions`} such as "Turn left onto Gertrude-Fröhlich-Sandner-Straße",
63
+ distance, time, start point, plus `notices` like "Toll road") and the grouped `segments` Maps
64
+ lists ("Get on A4"). `maneuver` is Google's own code where the step carries one (ON_RAMP,
65
+ MERGE, ROUNDABOUT_…, TURN_SLIGHT_RIGHT); it is never guessed from the text.
66
+
67
+ **Multi-stop planning.** Google's web directions cannot reorder waypoints and a full
68
+ travel-time matrix costs a request per pair, so `maps_plan_route` ranks every order of the
69
+ stops by straight-line length, routes the four shortest plus the order given with real Google
70
+ directions, and keeps the fastest (`savedVsGivenOrder` says what that saved). Stays
71
+ (`dwellMinutes`) and traffic shift the arrival times; each stop's `openingStatus` is `open`,
72
+ `closes_during_visit`, `closed` or `unknown`.
73
+
74
+ **Cards.** `maps_show_route` and `maps_show_places` return their facts to the model, and MCP
75
+ Apps hosts draw them with `ui://maps/view.html` (`src/maps_mcp/view.html`): a map built from
76
+ Google Maps tiles in your browser (drag, zoom, map/satellite, fit) with the route and lettered
77
+ pins, a stop timeline with arrival times and open/closed status, steps per leg and "Open in
78
+ Google Maps"; or place cards with photo, rating, today's hours and links. The route line,
79
+ steps and photos travel in the result's `_meta`, so they do not fill the model's context. The
80
+ view loads only from www.google.com and lh3-6.googleusercontent.com, connects nowhere, sets
81
+ every text as text and opens links through the host.
82
+
83
+ ## Quick start
84
+
85
+ ### Docker (HTTP)
86
+
87
+ ```bash
88
+ docker run -d --name maps-mcp -p 127.0.0.1:8000:8000 \
89
+ -e MCP_BEARER_TOKEN="$(openssl rand -hex 32)" \
90
+ --read-only --tmpfs /state:uid=1000 \
91
+ ghcr.io/benjamin-di/google-maps-mcp:latest
92
+ ```
93
+
94
+ The MCP endpoint is `http://127.0.0.1:8000/mcp` (stateless streamable HTTP with JSON replies)
95
+ with `Authorization: Bearer <token>`; `/health` is open. `MCP_BEARER_TOKEN` may hold several
96
+ tokens separated by commas. For claude.ai or ChatGPT, put it behind HTTPS with a reverse proxy
97
+ they can reach. Mount a volume on `/state` to keep the daily budgets and breaker across
98
+ restarts.
99
+
100
+ ### stdio (Claude Desktop, Claude Code, Codex)
101
+
102
+ ```bash
103
+ # from a checkout, with uv (https://docs.astral.sh/uv/)
104
+ claude mcp add maps -- uv --directory /path/to/google-maps-mcp run maps-mcp --transport stdio
105
+ # or with Docker
106
+ claude mcp add maps -- docker run -i --rm --tmpfs /state:uid=1000 ghcr.io/benjamin-di/google-maps-mcp:latest --transport stdio
107
+ ```
108
+
109
+ Outside Docker, state goes to `$XDG_STATE_HOME/google-maps-mcp` (else
110
+ `~/.local/state/google-maps-mcp`); set `MAPS_STATE_DIR` to put it elsewhere.
111
+
112
+ ### Build the image yourself
113
+
114
+ The image is two stages: the pinned dependencies (`Dockerfile.deps`) and the source on top.
115
+
116
+ ```bash
117
+ docker build -f Dockerfile.deps -t maps-mcp-deps .
118
+ docker build -t google-maps-mcp .
119
+ ```
120
+
121
+ ### Command line
122
+
123
+ `maps-mcp [--transport http|stdio] [--host 127.0.0.1] [--port 8000]`: HTTP (the default)
124
+ listens on 127.0.0.1:8000 (or `$MAPS_HOST`) and requires `MCP_BEARER_TOKEN`; the image sets
125
+ `MAPS_HOST=0.0.0.0`.
126
+
127
+ ## Configuration
128
+
129
+ | Variable | Default | Meaning |
130
+ |---|---|---|
131
+ | `MCP_BEARER_TOKEN` | (required for HTTP) | Accepted bearer token(s), comma-separated, each at least 32 characters |
132
+ | `MAPS_HOST` | `127.0.0.1` (image: `0.0.0.0`) | HTTP listen address when `--host` is not given |
133
+ | `MAPS_STATE_DIR` | per-user state dir (image: `/state`) | Budgets, breaker, Google's consent cookies |
134
+ | `MAPS_DAILY_REQUEST_BUDGET` | `1000` | Requests to google.com per day, all tools together |
135
+ | `MAPS_DAILY_TILE_BUDGET` | `3000` | Map tiles per day for `maps_static_map` |
136
+ | `MAPS_BUDGET_TZ` | `UTC` | The time zone whose midnight resets the budgets |
137
+ | `MAPS_MIN_INTERVAL_S` | `1.0` | Minimum seconds between requests (jittered ±30%) |
138
+ | `MAPS_REQUEST_TIMEOUT_S` | `15` | Per-request timeout |
139
+ | `MAPS_LANGUAGE` | `en` | Language of names and instructions |
140
+ | `MAPS_CANARY_FROM` / `MAPS_CANARY_TO` | Wien Hauptbahnhof / Albertina | The status check's walk; `MAPS_CANARY_TO` must be a place id with opening hours |
141
+ | `LOG_LEVEL` | `INFO` | JSON log lines on stderr; tool arguments are never logged, only short hashes |
142
+
143
+ ## How it works
144
+
145
+ Four endpoints on www.google.com, the ones google.com/maps itself calls: `/search?tbm=map`
146
+ (search), `/maps/preview/place` (place details), `/maps/preview/directions` (routes, steps
147
+ and geometry) and `/maps/vt` (map tiles). Requests carry no account and no key; Google's
148
+ consent cookies are fetched once and kept in the state directory. Parsing comes from
149
+ [gmaps-scraper](https://github.com/nick-choudhary/gmaps-scraper), vendored in
150
+ `src/maps_mcp/vendor/gmaps` from a fork that adds this project's contributions, each offered
151
+ upstream as a pull request (see NOTICE). Refresh it with `scripts/vendor-gmaps.sh`; never edit
152
+ the vendored copy by hand.
153
+
154
+ It is a deliberately small, polite client: one request at a time, about one a second, daily
155
+ budgets, results cached in memory (places 6 h, walking and cycling routes 12 h, driving
156
+ 10 min, or 1 h for a given time), and a breaker that stops all requests for 30 min, then 2 h,
157
+ then 8 h when Google answers 429, `/sorry` or a sign-in wall.
158
+
159
+ ## Terms and limits
160
+
161
+ - Google's terms of service do not allow automated access to google.com/maps. This project is
162
+ for personal, low-volume use; you are responsible for how you use it. If you need
163
+ guarantees, use Google's paid [Maps Platform APIs](https://developers.google.com/maps).
164
+ - Do not store or redistribute Google's place data, and keep Google's attribution ("Map data
165
+ © Google") with maps, as the cards and images do.
166
+ - Google can change its page format at any time. Parsing failures surface as
167
+ `schema_changed`, never as wrong numbers; please open an issue.
168
+ - No walking or driving routes in South Korea (Google's own limit: `condition:
169
+ ROUTE_NOT_FOUND` with a note). Data in mainland China is thin. Opening hours are Google's: a
170
+ good secondary source, not proof.
171
+
172
+ | Error code | Meaning |
173
+ |---|---|
174
+ | `blocked` | Google pushed back; the breaker is open (`maps_status` says until when) |
175
+ | `schema_changed` | Google's response format changed; the tool cannot read it |
176
+ | `upstream_error` | Network or Google failed after retries |
177
+ | `waypoint_not_found` / `place_not_found` / `route_not_found` | The stop, the place id or a route could not be found; the message says which |
178
+ | `budget_exhausted`, `busy`, `too_many_elements`, `invalid_argument`, `internal_error` | as named |
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ make bootstrap # uv sync
184
+ make check # ruff, format, lock consistency, tests
185
+ ```
186
+
187
+ Tests never reach Google: the vendored library runs against responses recorded from
188
+ google.com/maps (`tests/fixtures`, recorded and scrubbed by `scripts/capture-fixtures`; see
189
+ `tests/fixtures/README.md`). `tests/test_safety.py` guards the boundary: the only host is
190
+ www.google.com, and the card view loads only from its declared hosts and connects nowhere.
191
+
192
+ Contributions: see `CONTRIBUTING.md`; security problems: `SECURITY.md`.
193
+
194
+ ## Credits
195
+
196
+ Built on [gmaps-scraper](https://github.com/nick-choudhary/gmaps-scraper) (MIT). The directions
197
+ request layout was first documented by
198
+ [ai-ecoverse/skills](https://github.com/ai-ecoverse/skills) (`skills/gmaps`). See NOTICE and
199
+ THIRD_PARTY_NOTICES. Licensed under the MIT licence (LICENSE).
@@ -0,0 +1,46 @@
1
+ maps_mcp/__init__.py,sha256=XEhkFjdG_ywUhfYkq08gqtjApiFmdEfpW3Bp7jlSc7Q,328
2
+ maps_mcp/__main__.py,sha256=3dYKHfmWsrdExFlTFlcR5a_icR9fAkn06Yh14TQkEd8,33
3
+ maps_mcp/auth.py,sha256=z1A4sX_8RdiqXHskdu-6pED9x5avJyfOYr6OgdAZ_iY,1444
4
+ maps_mcp/config.py,sha256=WmIB1Bpw-SFrCj2i-hDi08fvzVC8NIg_vK0jvy-8bwY,4945
5
+ maps_mcp/errors.py,sha256=DWCuRGLSAKvx32_uBnR9NCN4Rq0YceEbOM8kC0yoJgE,2950
6
+ maps_mcp/google.py,sha256=SiX011ZulzmqUhXwk1ACJO8Akj9d-RAVaC_9g2b1Fbk,9524
7
+ maps_mcp/logs.py,sha256=x3TFOkPHRpZtH1dGipDx7EYA-HNn7ktj2lr1mp8y2SM,5480
8
+ maps_mcp/planner.py,sha256=gZbUHwR30h21xw-l9YptJaJ-gEerbJEUILYj5gEu_Gg,1921
9
+ maps_mcp/server.py,sha256=KcCg7HXKcjMoY-Uq6Pp01SfDNVqr7Fi2nL8qYK9UW2M,25130
10
+ maps_mcp/service.py,sha256=Qpz417tbXDPY3gMGcxTeBfSkxLyJWhZiE5hwzrUQCRs,31631
11
+ maps_mcp/shapes.py,sha256=c-wAdWJGtNXBgaHFCX3xYuuijc9gcQfe-cus87hHgtY,16288
12
+ maps_mcp/state.py,sha256=KJpT8ecwTN7kW5MeXQGxuaExJ4cjr8LBFZAEnF_tGko,6212
13
+ maps_mcp/staticmap.py,sha256=9cf_NCgo6jAUvcTLdZCuWM2YKur50abNnNXK9aCwsbI,6405
14
+ maps_mcp/view.html,sha256=W9yx6QJR8P6JkHdj0ELd8cJw7PuhT_OE2KBW47178PY,30749
15
+ maps_mcp/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ maps_mcp/vendor/gmaps/LICENSE,sha256=oUiz9FNjnv2M-ecIoc0YgMMunLGtJnC4s2lvuAtItL8,1063
17
+ maps_mcp/vendor/gmaps/VENDORED_COMMIT,sha256=qzkOMu0WpyUBd2BhXsc8X-Sxfp1C8poxiYtFGQCUCBI,41
18
+ maps_mcp/vendor/gmaps/__init__.py,sha256=KhQ_CKegaaWvdroW7MAP8i8-HatKu-YTyLKSvsuccMc,839
19
+ maps_mcp/vendor/gmaps/_directions.py,sha256=Q6_oZSMj4VLE-vDDhYV0DO4ihaOni8Ybd9UgNOfylV4,2633
20
+ maps_mcp/vendor/gmaps/_hosts.py,sha256=uZcuMd0JPfKcd3PmKS1nU0A4cbeKMSCq7uV-Lq7_flE,766
21
+ maps_mcp/vendor/gmaps/_places.py,sha256=jK_-iyJbHgb3JLY-Kt0jN3L1E3S4106Wn4gSf60RHEw,2360
22
+ maps_mcp/vendor/gmaps/_reviews.py,sha256=HeUL8iMR5MqTSy7E0qr4UDaiYh8emJ2jH94M7OWqGKw,3305
23
+ maps_mcp/vendor/gmaps/_search.py,sha256=pwqWLs3BD821fbvTnivYK4vh8TvcdqA1WMj0RTERGFs,54582
24
+ maps_mcp/vendor/gmaps/client.py,sha256=54kiE9LSzhSS3hyyEK2S1V0QOVOaExCvPP-xqDMoGZw,12825
25
+ maps_mcp/vendor/gmaps/control.py,sha256=DehEBqidDbaIionVFBXRoZFrz39SIrElVwO_FdobUnQ,9600
26
+ maps_mcp/vendor/gmaps/exceptions.py,sha256=Uv7KEc4g6mPOdfKdLMMG0oJzpR3FoDsIePJm3L2SZj0,2595
27
+ maps_mcp/vendor/gmaps/grid.py,sha256=387DT_nEXDfR4cuDEkFFRmqHDmL0VWVDRhuCzK3zDAk,6212
28
+ maps_mcp/vendor/gmaps/transport.py,sha256=dnnsh-6Wr9196OBrTBHJ3_dwnVKlVzJj-726jtirJJY,16783
29
+ maps_mcp/vendor/gmaps/validation.py,sha256=StJKbxdOskwkjhZFU9i7pRF7ZMvMdbI3kA3dy7HBztk,7380
30
+ maps_mcp/vendor/gmaps/_auth/__init__.py,sha256=pewKmaJqfXt47AfgMHHKgNdw4uQLS0-qeSOpmjqvI04,345
31
+ maps_mcp/vendor/gmaps/_auth/session.py,sha256=HKJ4XPHy0haB2l_Mn08tXr6IMPfZhu6IiaktlGRXm20,10392
32
+ maps_mcp/vendor/gmaps/rpc/__init__.py,sha256=76h52dzjz4LArGKOnofWMnpOO57ep7qWw7ck-MpwOIA,1324
33
+ maps_mcp/vendor/gmaps/rpc/decoder.py,sha256=D7um_8qvCa45c3sU5etNE8CdIi88vIqjx7oRFsho7-c,12761
34
+ maps_mcp/vendor/gmaps/rpc/directions.py,sha256=zrt4irxhLXvxXjh4Kr4s1-ktmaB6EzJJKoJGQg6fYy4,20163
35
+ maps_mcp/vendor/gmaps/rpc/encoder.py,sha256=GFh6MSL5-OQ5Dnrc5kMvz-SqLajJat_cKc56Rkouc0E,5021
36
+ maps_mcp/vendor/gmaps/rpc/ids.py,sha256=Q6gEpAvHRxZba8k9DsnOZz5i0NhvsyJjHKck4l1ll3s,2066
37
+ maps_mcp/vendor/gmaps/rpc/parser.py,sha256=xl2AsoYHKsK-pC4LymLkMCTeCuBPvx4M4apy3M3uOWk,42461
38
+ maps_mcp/vendor/gmaps/rpc/pb_encoder.py,sha256=l4BELff1fhiuSuLl2iAXya9UsF_f7giZIYKHvRL-BOk,8114
39
+ maps_mcp/vendor/gmaps/rpc/types.py,sha256=WrXwJAwFifDxO3sxvBvOIUMat6ITTyOIqivXLdbXOas,3602
40
+ google_maps_keyless_mcp-0.5.3.dist-info/METADATA,sha256=8mcPuVOd7rqKGWB2sQ7CStVXAbU5_qQST0HcbUo3-Ms,11718
41
+ google_maps_keyless_mcp-0.5.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
42
+ google_maps_keyless_mcp-0.5.3.dist-info/entry_points.txt,sha256=0Qj-62uHUkVSG0mDMag48E55xEKGjxnPO_Qf4-vHJpM,97
43
+ google_maps_keyless_mcp-0.5.3.dist-info/licenses/LICENSE,sha256=R9rLPJhizyGD3Z958_ZKuwCeUq77gz732CEJq5rq-gw,1068
44
+ google_maps_keyless_mcp-0.5.3.dist-info/licenses/NOTICE,sha256=YEx6Fx2SkxOZfakjl21cT_V-8Fv2gLFur5Ema64rmNc,1277
45
+ google_maps_keyless_mcp-0.5.3.dist-info/licenses/THIRD_PARTY_NOTICES,sha256=6kGXu20Aw-Gbe3UhJ6_-vJd9SZTJxibZXfFt-VBg9QI,1462
46
+ google_maps_keyless_mcp-0.5.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ google-maps-keyless-mcp = maps_mcp.server:main
3
+ maps-mcp = maps_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 benjamin-di
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ maps-mcp
2
+ Copyright (c) 2026 benjamin-di. Licensed under the MIT licence (LICENSE).
3
+
4
+ Third-party code (full licence texts in THIRD_PARTY_NOTICES):
5
+
6
+ - gmaps-scraper, https://github.com/nick-choudhary/gmaps-scraper. MIT License,
7
+ Copyright (c) 2026 Naveen. Vendored in src/maps_mcp/vendor/gmaps/ (with its LICENSE) from
8
+ the fork https://github.com/benjamin-di/gmaps-scraper at the commit in
9
+ src/maps_mcp/vendor/gmaps/VENDORED_COMMIT: upstream plus this project's contributions,
10
+ each offered upstream as a pull request (single exact-match search results, dated opening
11
+ hours with holiday flags, place details by place id, directions with steps and route
12
+ geometry, error classification in the HTTP transport). Only the modules maps-mcp runs are
13
+ vendored (the list is in scripts/vendor-gmaps.sh); contact and website extraction, the
14
+ CLI and its MCP server are left out.
15
+
16
+ Acknowledgement: the request and response layout of Google Maps' directions endpoint was
17
+ first documented by ai-ecoverse/skills (skills/gmaps, https://github.com/ai-ecoverse/skills,
18
+ Apache-2.0, by Lars Trieloff). No code was copied; the facts were re-verified against live
19
+ responses.
20
+
21
+ Google Maps is a trademark of Google LLC. This project is not affiliated with, endorsed or
22
+ sponsored by Google.
@@ -0,0 +1,30 @@
1
+ Third-party code in maps-mcp, with its licence.
2
+
3
+ 1. gmaps-scraper
4
+ https://github.com/nick-choudhary/gmaps-scraper, vendored from
5
+ https://github.com/benjamin-di/gmaps-scraper at the commit in
6
+ src/maps_mcp/vendor/gmaps/VENDORED_COMMIT (upstream plus this project's pull requests),
7
+ in src/maps_mcp/vendor/gmaps/: only the modules maps-mcp runs (the list is in
8
+ scripts/vendor-gmaps.sh).
9
+
10
+ MIT License
11
+
12
+ Copyright (c) 2026 Naveen
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
maps_mcp/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """maps-mcp: walking/driving travel times and places from the signed-out Google Maps web page."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("google-maps-keyless-mcp")
7
+ except PackageNotFoundError: # pragma: no cover - source checkout without install
8
+ __version__ = "0.0.0"
maps_mcp/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
maps_mcp/auth.py ADDED
@@ -0,0 +1,36 @@
1
+ """Bearer auth for the HTTP transport.
2
+
3
+ ``MCP_BEARER_TOKEN`` holds the accepted token, or several separated by commas (for example
4
+ one for a reverse proxy and one for direct clients, so either can be rotated alone), each at
5
+ least 32 characters. Only
6
+ ``/mcp`` is protected; ``/health`` stays open. The stdio transport needs no token.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from collections.abc import Mapping
13
+
14
+ from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
15
+
16
+ TOKEN_VAR = "MCP_BEARER_TOKEN"
17
+ MIN_TOKEN_LENGTH = 32
18
+
19
+
20
+ class AuthError(Exception):
21
+ """No token configured for a transport that requires one."""
22
+
23
+
24
+ def bearer_auth_from_env(env: Mapping[str, str] | None = None) -> StaticTokenVerifier:
25
+ """A verifier accepting every configured token; raises AuthError when there is none."""
26
+ env = os.environ if env is None else env
27
+ tokens = [token.strip() for token in (env.get(TOKEN_VAR) or "").split(",") if token.strip()]
28
+ if not tokens:
29
+ raise AuthError(f"{TOKEN_VAR} must be set to serve over HTTP (comma-separated for several tokens)")
30
+ if any(len(token) < MIN_TOKEN_LENGTH for token in tokens):
31
+ raise AuthError(
32
+ f"each {TOKEN_VAR} token needs at least {MIN_TOKEN_LENGTH} characters (e.g. openssl rand -hex 32)"
33
+ )
34
+ return StaticTokenVerifier(
35
+ tokens={token: {"client_id": f"token-{index}", "scopes": []} for index, token in enumerate(tokens, 1)}
36
+ )
maps_mcp/config.py ADDED
@@ -0,0 +1,125 @@
1
+ """Settings from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
11
+
12
+
13
+ class ConfigError(Exception):
14
+ """Bad or missing configuration; raised at startup only."""
15
+
16
+
17
+ def default_state_dir(env: Mapping[str, str]) -> Path:
18
+ """Where state lives when MAPS_STATE_DIR is not set: the per-user state directory
19
+ ($XDG_STATE_HOME/google-maps-mcp, else ~/.local/state/google-maps-mcp). The container
20
+ image sets MAPS_STATE_DIR=/state."""
21
+ base = (env.get("XDG_STATE_HOME") or "").strip()
22
+ return (Path(base) if base else Path.home() / ".local" / "state") / "google-maps-mcp"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Settings:
27
+ state_dir: Path = Path("/state")
28
+ # Requests to google.com per day (in budget_tz), every tool together. A route or a place is
29
+ # one request; a matrix is one per pair. Keep it modest: it shares your IP's standing with
30
+ # anything else of yours that talks to Google.
31
+ daily_request_budget: int = 1000
32
+ # Map tiles (256 px images) per day for maps_static_map; a 640x480 map is about 12 tiles,
33
+ # cached in memory for a day.
34
+ daily_tile_budget: int = 3000
35
+ # The day the budgets count in (an IANA zone); they reset at its midnight.
36
+ budget_tz: str = "UTC"
37
+ # The canary's real check: a walk between two places and one place's details, given as
38
+ # place ids (or text). Defaults: Wien Hauptbahnhof -> Albertina, Vienna.
39
+ canary_from: str = "ChIJUU071tupbUcRyFdx20Nwgqg"
40
+ canary_to: str = "ChIJwVp-8psHbUcRuN43n5iK72Y"
41
+ # Minimum seconds between requests to Google (jittered +-30% by the library).
42
+ min_interval_s: float = 1.0
43
+ request_timeout_s: float = 15.0
44
+ language: str = "en"
45
+
46
+ @property
47
+ def cookie_file(self) -> Path:
48
+ return self.state_dir / "google-cookies.json"
49
+
50
+ @classmethod
51
+ def from_env(cls, env: Mapping[str, str] | None = None) -> Settings:
52
+ env = os.environ if env is None else env
53
+ return cls(
54
+ state_dir=_state_dir(env),
55
+ daily_request_budget=_int(env, "MAPS_DAILY_REQUEST_BUDGET", 1000, 0, 100_000),
56
+ daily_tile_budget=_int(env, "MAPS_DAILY_TILE_BUDGET", 3000, 0, 100_000),
57
+ min_interval_s=_float(env, "MAPS_MIN_INTERVAL_S", 1.0, 0.2, 30.0),
58
+ request_timeout_s=_float(env, "MAPS_REQUEST_TIMEOUT_S", 15.0, 2.0, 60.0),
59
+ language=_language(env),
60
+ budget_tz=_zone(env, "MAPS_BUDGET_TZ", "UTC"),
61
+ canary_from=(env.get("MAPS_CANARY_FROM") or "").strip() or cls.canary_from,
62
+ canary_to=_place_id(env, "MAPS_CANARY_TO", cls.canary_to),
63
+ )
64
+
65
+
66
+ def _int(env: Mapping[str, str], name: str, default: int, minimum: int, maximum: int) -> int:
67
+ raw = (env.get(name) or "").strip()
68
+ if not raw:
69
+ return default
70
+ try:
71
+ value = int(raw)
72
+ except ValueError:
73
+ raise ConfigError(f"{name} must be an integer") from None
74
+ if not minimum <= value <= maximum:
75
+ raise ConfigError(f"{name} must be between {minimum} and {maximum}")
76
+ return value
77
+
78
+
79
+ def _float(env: Mapping[str, str], name: str, default: float, minimum: float, maximum: float) -> float:
80
+ raw = (env.get(name) or "").strip()
81
+ if not raw:
82
+ return default
83
+ try:
84
+ value = float(raw)
85
+ except ValueError:
86
+ raise ConfigError(f"{name} must be a number") from None
87
+ if not minimum <= value <= maximum:
88
+ raise ConfigError(f"{name} must be between {minimum} and {maximum}")
89
+ return value
90
+
91
+
92
+ def _zone(env: Mapping[str, str], name: str, default: str) -> str:
93
+ raw = (env.get(name) or "").strip() or default
94
+ try:
95
+ ZoneInfo(raw)
96
+ except (ZoneInfoNotFoundError, ValueError):
97
+ raise ConfigError(f"{name} must be an IANA time zone such as Europe/Vienna") from None
98
+ return raw
99
+
100
+
101
+ def _place_id(env: Mapping[str, str], name: str, default: str) -> str:
102
+ raw = (env.get(name) or "").strip() or default
103
+ if not raw.startswith("ChIJ"):
104
+ raise ConfigError(f"{name} must be a place id (ChIJ…) of a place with opening hours")
105
+ return raw
106
+
107
+
108
+ def _state_dir(env: Mapping[str, str]) -> Path:
109
+ raw = (env.get("MAPS_STATE_DIR") or "").strip()
110
+ path = Path(raw).expanduser() if raw else default_state_dir(env)
111
+ try:
112
+ path.mkdir(parents=True, exist_ok=True, mode=0o700)
113
+ probe = path / ".write-test"
114
+ probe.write_text("")
115
+ probe.unlink()
116
+ except OSError:
117
+ raise ConfigError(f"the state directory {path} is not writable (set MAPS_STATE_DIR)") from None
118
+ return path
119
+
120
+
121
+ def _language(env: Mapping[str, str]) -> str:
122
+ raw = (env.get("MAPS_LANGUAGE") or "").strip() or "en"
123
+ if not re.fullmatch(r"[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?", raw):
124
+ raise ConfigError("MAPS_LANGUAGE must be a language tag such as en, de or pt-BR")
125
+ return raw
maps_mcp/errors.py ADDED
@@ -0,0 +1,67 @@
1
+ """The error taxonomy. Tools report ``{code, next_step, retryable}`` and nothing else.
2
+
3
+ Exception text never reaches a tool result: every message a model sees comes from the fixed
4
+ ``NEXT_STEP`` table below plus, for a few codes, our own detail text (which stop, which argument).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ NEXT_STEP: dict[str, str] = {
12
+ "invalid_argument": "Fix the arguments and call again.",
13
+ "place_not_found": (
14
+ "Google Maps has no place for this ID. Find the place again with maps_search_places and use the id it returns."
15
+ ),
16
+ "waypoint_not_found": (
17
+ "Google Maps could not find this stop. Make it more specific (add the city or the street), "
18
+ "or pass the place's id from maps_search_places or 'lat,lng'."
19
+ ),
20
+ "route_not_found": (
21
+ "Google Maps found no route connecting these stops in this mode (an island, a border, or a "
22
+ "country Google does not route in). Try another travelMode or other stops."
23
+ ),
24
+ "too_many_elements": "Ask for fewer origins x destinations (at most 25 pairs) or split the matrix.",
25
+ "blocked": (
26
+ "Google pushed back (rate limit, /sorry or captcha). No Google calls until the breaker closes "
27
+ "(maps_status shows when). Do not retry now."
28
+ ),
29
+ "budget_exhausted": (
30
+ "The daily Google request budget is spent. It resets at midnight (maps_status shows the day and zone)."
31
+ ),
32
+ "busy": "Other requests to Google are still running. Retry in a few seconds.",
33
+ "upstream_error": "Google Maps or the network failed after retries. Retry in a minute.",
34
+ "schema_changed": (
35
+ "Google Maps' response format changed and this tool cannot read it; other tools may still work. "
36
+ "Tell the person running this server: it needs an update."
37
+ ),
38
+ "internal_error": "maps-mcp failed unexpectedly. Retry once; if it repeats, tell the operator.",
39
+ }
40
+
41
+ RETRYABLE = frozenset({"busy", "upstream_error", "internal_error"})
42
+ # The caller's own mistakes: routine, logged at INFO.
43
+ AGENT_INPUT_CODES = frozenset(
44
+ {"invalid_argument", "place_not_found", "waypoint_not_found", "route_not_found", "too_many_elements"}
45
+ )
46
+
47
+
48
+ class MapsError(Exception):
49
+ """A classified failure. ``log_detail`` is for logs only."""
50
+
51
+ def __init__(self, code: str, detail: str | None = None, *, log_detail: str | None = None) -> None:
52
+ if code not in NEXT_STEP:
53
+ raise ValueError(f"unknown error code {code}")
54
+ super().__init__(code)
55
+ self.code = code
56
+ self.detail = detail
57
+ self.log_detail = log_detail
58
+
59
+ def payload(self) -> dict[str, Any]:
60
+ next_step = NEXT_STEP[self.code]
61
+ if self.detail:
62
+ next_step = f"{self.detail}. {next_step}"
63
+ return {"code": self.code, "next_step": next_step, "retryable": self.code in RETRYABLE}
64
+
65
+
66
+ def invalid(detail: str) -> MapsError:
67
+ return MapsError("invalid_argument", detail)