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.
- google_maps_keyless_mcp-0.5.3.dist-info/METADATA +199 -0
- google_maps_keyless_mcp-0.5.3.dist-info/RECORD +46 -0
- google_maps_keyless_mcp-0.5.3.dist-info/WHEEL +4 -0
- google_maps_keyless_mcp-0.5.3.dist-info/entry_points.txt +3 -0
- google_maps_keyless_mcp-0.5.3.dist-info/licenses/LICENSE +21 -0
- google_maps_keyless_mcp-0.5.3.dist-info/licenses/NOTICE +22 -0
- google_maps_keyless_mcp-0.5.3.dist-info/licenses/THIRD_PARTY_NOTICES +30 -0
- maps_mcp/__init__.py +8 -0
- maps_mcp/__main__.py +3 -0
- maps_mcp/auth.py +36 -0
- maps_mcp/config.py +125 -0
- maps_mcp/errors.py +67 -0
- maps_mcp/google.py +229 -0
- maps_mcp/logs.py +136 -0
- maps_mcp/planner.py +57 -0
- maps_mcp/server.py +509 -0
- maps_mcp/service.py +703 -0
- maps_mcp/shapes.py +385 -0
- maps_mcp/state.py +158 -0
- maps_mcp/staticmap.py +182 -0
- maps_mcp/vendor/__init__.py +0 -0
- maps_mcp/vendor/gmaps/LICENSE +21 -0
- maps_mcp/vendor/gmaps/VENDORED_COMMIT +1 -0
- maps_mcp/vendor/gmaps/__init__.py +35 -0
- maps_mcp/vendor/gmaps/_auth/__init__.py +12 -0
- maps_mcp/vendor/gmaps/_auth/session.py +294 -0
- maps_mcp/vendor/gmaps/_directions.py +74 -0
- maps_mcp/vendor/gmaps/_hosts.py +21 -0
- maps_mcp/vendor/gmaps/_places.py +71 -0
- maps_mcp/vendor/gmaps/_reviews.py +123 -0
- maps_mcp/vendor/gmaps/_search.py +1409 -0
- maps_mcp/vendor/gmaps/client.py +327 -0
- maps_mcp/vendor/gmaps/control.py +276 -0
- maps_mcp/vendor/gmaps/exceptions.py +93 -0
- maps_mcp/vendor/gmaps/grid.py +188 -0
- maps_mcp/vendor/gmaps/rpc/__init__.py +56 -0
- maps_mcp/vendor/gmaps/rpc/decoder.py +386 -0
- maps_mcp/vendor/gmaps/rpc/directions.py +529 -0
- maps_mcp/vendor/gmaps/rpc/encoder.py +173 -0
- maps_mcp/vendor/gmaps/rpc/ids.py +50 -0
- maps_mcp/vendor/gmaps/rpc/parser.py +1132 -0
- maps_mcp/vendor/gmaps/rpc/pb_encoder.py +294 -0
- maps_mcp/vendor/gmaps/rpc/types.py +103 -0
- maps_mcp/vendor/gmaps/transport.py +461 -0
- maps_mcp/vendor/gmaps/validation.py +214 -0
- 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,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
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)
|