google-maps-keyless-mcp 0.5.3__tar.gz

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 (87) hide show
  1. google_maps_keyless_mcp-0.5.3/.dockerignore +14 -0
  2. google_maps_keyless_mcp-0.5.3/.github/workflows/ci.yml +23 -0
  3. google_maps_keyless_mcp-0.5.3/.github/workflows/pypi.yml +38 -0
  4. google_maps_keyless_mcp-0.5.3/.github/workflows/release.yml +105 -0
  5. google_maps_keyless_mcp-0.5.3/.gitignore +17 -0
  6. google_maps_keyless_mcp-0.5.3/AGENTS.md +22 -0
  7. google_maps_keyless_mcp-0.5.3/CONTRIBUTING.md +27 -0
  8. google_maps_keyless_mcp-0.5.3/Dockerfile +34 -0
  9. google_maps_keyless_mcp-0.5.3/Dockerfile.deps +22 -0
  10. google_maps_keyless_mcp-0.5.3/LICENSE +21 -0
  11. google_maps_keyless_mcp-0.5.3/Makefile +9 -0
  12. google_maps_keyless_mcp-0.5.3/NOTICE +22 -0
  13. google_maps_keyless_mcp-0.5.3/PKG-INFO +199 -0
  14. google_maps_keyless_mcp-0.5.3/README.md +181 -0
  15. google_maps_keyless_mcp-0.5.3/SECURITY.md +13 -0
  16. google_maps_keyless_mcp-0.5.3/THIRD_PARTY_NOTICES +30 -0
  17. google_maps_keyless_mcp-0.5.3/pyproject.toml +69 -0
  18. google_maps_keyless_mcp-0.5.3/requirements-runtime.lock +1000 -0
  19. google_maps_keyless_mcp-0.5.3/scripts/bootstrap.sh +5 -0
  20. google_maps_keyless_mcp-0.5.3/scripts/capture-fixtures +267 -0
  21. google_maps_keyless_mcp-0.5.3/scripts/source-identity.py +39 -0
  22. google_maps_keyless_mcp-0.5.3/scripts/test.sh +40 -0
  23. google_maps_keyless_mcp-0.5.3/scripts/vendor-gmaps.sh +29 -0
  24. google_maps_keyless_mcp-0.5.3/src/maps_mcp/__init__.py +8 -0
  25. google_maps_keyless_mcp-0.5.3/src/maps_mcp/__main__.py +3 -0
  26. google_maps_keyless_mcp-0.5.3/src/maps_mcp/auth.py +36 -0
  27. google_maps_keyless_mcp-0.5.3/src/maps_mcp/config.py +125 -0
  28. google_maps_keyless_mcp-0.5.3/src/maps_mcp/errors.py +67 -0
  29. google_maps_keyless_mcp-0.5.3/src/maps_mcp/google.py +229 -0
  30. google_maps_keyless_mcp-0.5.3/src/maps_mcp/logs.py +136 -0
  31. google_maps_keyless_mcp-0.5.3/src/maps_mcp/planner.py +57 -0
  32. google_maps_keyless_mcp-0.5.3/src/maps_mcp/server.py +509 -0
  33. google_maps_keyless_mcp-0.5.3/src/maps_mcp/service.py +703 -0
  34. google_maps_keyless_mcp-0.5.3/src/maps_mcp/shapes.py +385 -0
  35. google_maps_keyless_mcp-0.5.3/src/maps_mcp/state.py +158 -0
  36. google_maps_keyless_mcp-0.5.3/src/maps_mcp/staticmap.py +182 -0
  37. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/__init__.py +0 -0
  38. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/LICENSE +21 -0
  39. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/VENDORED_COMMIT +1 -0
  40. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/__init__.py +35 -0
  41. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_auth/__init__.py +12 -0
  42. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_auth/session.py +294 -0
  43. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_directions.py +74 -0
  44. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_hosts.py +21 -0
  45. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_places.py +71 -0
  46. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_reviews.py +123 -0
  47. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_search.py +1409 -0
  48. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/client.py +327 -0
  49. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/control.py +276 -0
  50. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/exceptions.py +93 -0
  51. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/grid.py +188 -0
  52. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/__init__.py +56 -0
  53. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/decoder.py +386 -0
  54. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/directions.py +529 -0
  55. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/encoder.py +173 -0
  56. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/ids.py +50 -0
  57. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/parser.py +1132 -0
  58. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/pb_encoder.py +294 -0
  59. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/types.py +103 -0
  60. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/transport.py +461 -0
  61. google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/validation.py +214 -0
  62. google_maps_keyless_mcp-0.5.3/src/maps_mcp/view.html +613 -0
  63. google_maps_keyless_mcp-0.5.3/tests/conftest.py +161 -0
  64. google_maps_keyless_mcp-0.5.3/tests/fixtures/README.md +25 -0
  65. google_maps_keyless_mcp-0.5.3/tests/fixtures/details_albertina.json +719 -0
  66. google_maps_keyless_mcp-0.5.3/tests/fixtures/details_cafe.json +529 -0
  67. google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_3_stop.json +3274 -0
  68. google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_arrive.json +9272 -0
  69. google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_depart.json +9267 -0
  70. google_maps_keyless_mcp-0.5.3/tests/fixtures/hours_samples.json +1127 -0
  71. google_maps_keyless_mcp-0.5.3/tests/fixtures/korea_walk.json +187 -0
  72. google_maps_keyless_mcp-0.5.3/tests/fixtures/noroute_drive.json +189 -0
  73. google_maps_keyless_mcp-0.5.3/tests/fixtures/search_museums.json +3423 -0
  74. google_maps_keyless_mcp-0.5.3/tests/fixtures/search_single.json +766 -0
  75. google_maps_keyless_mcp-0.5.3/tests/fixtures/unknown.json +140 -0
  76. google_maps_keyless_mcp-0.5.3/tests/fixtures/walk_coords.json +3170 -0
  77. google_maps_keyless_mcp-0.5.3/tests/fixtures/walk_text.json +3735 -0
  78. google_maps_keyless_mcp-0.5.3/tests/test_cards.py +73 -0
  79. google_maps_keyless_mcp-0.5.3/tests/test_google.py +104 -0
  80. google_maps_keyless_mcp-0.5.3/tests/test_hours.py +112 -0
  81. google_maps_keyless_mcp-0.5.3/tests/test_places.py +86 -0
  82. google_maps_keyless_mcp-0.5.3/tests/test_plan.py +188 -0
  83. google_maps_keyless_mcp-0.5.3/tests/test_routes.py +191 -0
  84. google_maps_keyless_mcp-0.5.3/tests/test_safety.py +124 -0
  85. google_maps_keyless_mcp-0.5.3/tests/test_server.py +159 -0
  86. google_maps_keyless_mcp-0.5.3/tests/test_static_map.py +176 -0
  87. google_maps_keyless_mcp-0.5.3/uv.lock +1667 -0
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .deps
5
+ .artifacts
6
+ **/__pycache__
7
+ **/.pytest_cache
8
+ **/.ruff_cache
9
+ **/*.pyc
10
+ tests
11
+ scripts
12
+ docs
13
+ google-cookies.json
14
+ .env
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
14
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 20
20
+ steps:
21
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
22
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
23
+ - run: make check
@@ -0,0 +1,38 @@
1
+ name: PyPI
2
+ # A vX.Y.Z tag builds the wheel and sdist from the tagged tree and uploads them to PyPI as
3
+ # `google-maps-keyless-mcp` with trusted publishing (OIDC through the `pypi` environment; no token is
4
+ # stored). Tag pushes upload once the repository variable PYPI_PUBLISH is `true`; until then,
5
+ # or to retry, run it by hand on the tag: `gh workflow run pypi.yml --ref vX.Y.Z`. Files that
6
+ # are already on PyPI are skipped, so a repeated run is harmless.
7
+ on:
8
+ push:
9
+ tags: ['v*.*.*']
10
+ workflow_dispatch: {}
11
+
12
+ permissions: {}
13
+
14
+ concurrency:
15
+ group: pypi-${{ github.ref }}
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ publish:
20
+ if: github.ref_type == 'tag' && (github.event_name == 'workflow_dispatch' || vars.PYPI_PUBLISH == 'true')
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 10
23
+ environment: pypi
24
+ permissions:
25
+ contents: read
26
+ id-token: write
27
+ steps:
28
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
29
+ with:
30
+ persist-credentials: false
31
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
32
+ - name: Build
33
+ run: |
34
+ version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
35
+ [ "${GITHUB_REF_NAME#v}" = "$version" ] || { echo "tag $GITHUB_REF_NAME is not version $version" >&2; exit 1; }
36
+ uv build
37
+ - name: Upload
38
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/
@@ -0,0 +1,105 @@
1
+ name: Release
2
+
3
+ # On a vX.Y.Z tag: test the tagged tree, build the image for linux/amd64 and linux/arm64
4
+ # (dependency stage first, keyed on its inputs and reused), smoke-test it, then tag it
5
+ # X.Y.Z and latest in ghcr.io/<owner>/google-maps-mcp, and publish a GitHub release.
6
+ on:
7
+ push:
8
+ tags: ['v*.*.*']
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ # GitHub can deliver one tag push twice: the runs of a ref queue one behind the other, and a
14
+ # run whose tag already has a GitHub release (the last job) stops at `released`.
15
+ concurrency:
16
+ group: release-${{ github.ref }}
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ released:
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 5
23
+ permissions:
24
+ contents: read
25
+ outputs:
26
+ done: ${{ steps.check.outputs.done }}
27
+ steps:
28
+ - id: check
29
+ env:
30
+ GH_TOKEN: ${{ github.token }}
31
+ TAG: ${{ github.ref_name }}
32
+ run: |
33
+ done=false
34
+ if [ "$GITHUB_REF_TYPE" = tag ] && gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
35
+ done=true
36
+ echo "$TAG is already released: nothing to do"
37
+ fi
38
+ echo "done=$done" >> "$GITHUB_OUTPUT"
39
+
40
+ test:
41
+ needs: released
42
+ if: needs.released.outputs.done != 'true'
43
+ runs-on: ubuntu-latest
44
+ timeout-minutes: 20
45
+ steps:
46
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
47
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
48
+ - run: make check
49
+
50
+ image:
51
+ needs: test
52
+ runs-on: ubuntu-latest
53
+ timeout-minutes: 45
54
+ permissions:
55
+ contents: read
56
+ packages: write
57
+ env:
58
+ IMAGE: ghcr.io/${{ github.repository_owner }}/google-maps-mcp
59
+ PLATFORMS: linux/amd64,linux/arm64
60
+ steps:
61
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
62
+ - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
63
+ - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
64
+ - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
65
+ with:
66
+ registry: ghcr.io
67
+ username: ${{ github.actor }}
68
+ password: ${{ secrets.GITHUB_TOKEN }}
69
+ - name: Build, smoke-test, tag
70
+ env:
71
+ VERSION: ${{ github.ref_name }}
72
+ run: |
73
+ set -euo pipefail
74
+ version="${VERSION#v}"
75
+ deps="$IMAGE:deps-$(sha256sum Dockerfile.deps requirements-runtime.lock | sha256sum | cut -c1-16)"
76
+ if ! docker buildx imagetools inspect "$deps" >/dev/null 2>&1; then
77
+ docker buildx build --platform "$PLATFORMS" -f Dockerfile.deps -t "$deps" --push .
78
+ fi
79
+ candidate="$IMAGE:sha-$(git rev-parse --short=12 HEAD)"
80
+ docker buildx build --platform "$PLATFORMS" -f Dockerfile --push -t "$candidate" \
81
+ --build-arg MAPS_MCP_DEPS_IMAGE="$deps" \
82
+ --build-arg VCS_REF="$GITHUB_SHA" \
83
+ --build-arg BUILD_DATE="$(git show -s --format=%cI HEAD)" \
84
+ --build-arg SOURCE_URL="https://github.com/$GITHUB_REPOSITORY" .
85
+ # Smoke: HTTP refuses to start without a bearer; stdio answers initialize.
86
+ rc=0
87
+ docker run --rm --network none --tmpfs /state:uid=1000 "$candidate" || rc=$?
88
+ [ "$rc" = 1 ] || { echo "expected a refusal without MCP_BEARER_TOKEN, got exit $rc" >&2; exit 1; }
89
+ reply="$(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}' \
90
+ | timeout 60 docker run --rm -i --network none --read-only --tmpfs /state:uid=1000 "$candidate" --transport stdio)"
91
+ grep -q '"serverInfo"' <<<"$reply" || { echo "stdio initialize failed" >&2; exit 1; }
92
+ docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "$candidate"
93
+
94
+ github-release:
95
+ needs: image
96
+ if: github.event_name == 'push' && github.ref_type == 'tag'
97
+ runs-on: ubuntu-latest
98
+ timeout-minutes: 5
99
+ permissions:
100
+ contents: write
101
+ steps:
102
+ - env:
103
+ GH_TOKEN: ${{ github.token }}
104
+ TAG: ${{ github.ref_name }}
105
+ run: gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$TAG" --generate-notes
@@ -0,0 +1,17 @@
1
+ # secrets and local state
2
+ google-cookies.json
3
+ .env
4
+
5
+ # python
6
+ .venv/
7
+ __pycache__/
8
+ *.py[cod]
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+
15
+ # gate
16
+ .deps/
17
+ .artifacts/
@@ -0,0 +1,22 @@
1
+ # Working on google-maps-mcp
2
+
3
+ Run `make bootstrap`, then `make check` (ruff, the format check, the lock-consistency check and
4
+ pytest). After a dependency change run `make lock`: `requirements-runtime.lock` must equal
5
+ `uv export` of `uv.lock`, because the image installs exactly that file.
6
+
7
+ Tests never reach Google: the vendored library runs against recorded responses
8
+ (`tests/conftest.py`, `tests/fixtures/README.md`). `tests/test_safety.py` is the boundary: a new
9
+ host, a network import outside `google.py`, using more of the library (reviews, contacts,
10
+ websites, login cookies), or a card view that loads or connects beyond its CSP is a deliberate
11
+ change there, never a side effect.
12
+
13
+ `src/maps_mcp/vendor/gmaps` is generated by `scripts/vendor-gmaps.sh`; never edit it here.
14
+ Parsing fixes and new Google fields go to gmaps-scraper first (README "How it works").
15
+
16
+ Probing Google live from a development machine is fine in small amounts: a handful of
17
+ requests, paced, never in a loop. Re-record fixtures only with `scripts/capture-fixtures`,
18
+ which scrubs per-request tokens, photo URLs and non-Google links; review the diff before
19
+ committing it.
20
+
21
+ Keep the code, comments, docs and fixtures free of personal details (names, addresses,
22
+ accounts, hosts): this repository is public.
@@ -0,0 +1,27 @@
1
+ # Contributing
2
+
3
+ Issues and pull requests are welcome, especially when Google changes its responses and a tool
4
+ starts failing with `schema_changed`.
5
+
6
+ ## Before you open a pull request
7
+
8
+ ```sh
9
+ make bootstrap # uv sync --locked
10
+ make check # ruff, format, lock consistency, tests (offline)
11
+ ```
12
+
13
+ [AGENTS.md](AGENTS.md) has the rules: tests never reach Google, `tests/test_safety.py` is the
14
+ boundary, `src/maps_mcp/vendor/gmaps` is generated by `scripts/vendor-gmaps.sh` (parsing fixes
15
+ go to [gmaps-scraper](https://github.com/benjamin-di/gmaps-scraper) first), and fixtures are
16
+ recorded only with `scripts/capture-fixtures`. Keep personal details (names, addresses,
17
+ accounts) out of code, docs and fixtures.
18
+
19
+ Security problems: see [SECURITY.md](SECURITY.md) instead of opening an issue.
20
+
21
+ ## How changes land
22
+
23
+ Each release of this repository is exported from a private working repository as one
24
+ `Release X.Y.Z` commit, so pull requests are not merged here: the next release would replace
25
+ them. When a change fits, the maintainer applies it there, credits you in the release notes
26
+ and closes the pull request with a link to the release that contains it. Issues are welcome;
27
+ answers are best effort.
@@ -0,0 +1,34 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # maps-mcp: source on top of maps-mcp-deps (Dockerfile.deps). The release workflow builds
3
+ # both stages as one chain.
4
+ ARG MAPS_MCP_DEPS_IMAGE=maps-mcp-deps:latest
5
+ FROM ${MAPS_MCP_DEPS_IMAGE}
6
+
7
+ WORKDIR /app
8
+ COPY pyproject.toml README.md LICENSE NOTICE THIRD_PARTY_NOTICES ./
9
+ COPY src ./src
10
+ RUN --mount=type=cache,target=/root/.cache/uv \
11
+ uv pip install --system --compile-bytecode --no-deps . \
12
+ && rm -rf /app/src
13
+
14
+ # /state (daily budgets, breaker, Google's consent cookies) is the only writable path; run the
15
+ # container with a read-only root and a /state volume or tmpfs owned by uid 1000.
16
+ RUN mkdir -p /state && chown 1000:1000 /state
17
+
18
+ ARG VCS_REF=unknown
19
+ ARG BUILD_DATE=unknown
20
+ ARG SOURCE_URL=unknown
21
+ LABEL org.opencontainers.image.title="maps-mcp" \
22
+ org.opencontainers.image.description="Read-only MCP server for Google Maps travel times, routes and places" \
23
+ org.opencontainers.image.licenses="MIT" \
24
+ org.opencontainers.image.source="${SOURCE_URL}" \
25
+ org.opencontainers.image.revision="${VCS_REF}" \
26
+ org.opencontainers.image.created="${BUILD_DATE}"
27
+
28
+ ENV PYTHONUNBUFFERED=1 \
29
+ MAPS_HOST=0.0.0.0 \
30
+ MAPS_STATE_DIR=/state
31
+ USER 1000:1000
32
+ EXPOSE 8000
33
+ ENTRYPOINT ["maps-mcp"]
34
+
@@ -0,0 +1,22 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # Runtime dependencies for maps-mcp on a digest-pinned Python base. Kept apart from the
3
+ # source so code-only releases reuse this layer: release.yml keys the image on this file and
4
+ # requirements-runtime.lock. Rebuild it when dependencies change (make lock rewrites
5
+ # requirements-runtime.lock) or when these pins are upgraded (deliberately, never a moving tag).
6
+ FROM ghcr.io/astral-sh/uv:0.9.26@sha256:9a23023be68b2ed09750ae636228e903a54a05ea56ed03a934d00fe9fbeded4b AS uv
7
+
8
+ FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
9
+ COPY --from=uv /uv /usr/local/bin/uv
10
+
11
+ # The committed hash lock makes rebuilds independent of the package index's state.
12
+ COPY requirements-runtime.lock /tmp/requirements-runtime.lock
13
+ RUN --mount=type=cache,target=/root/.cache/uv \
14
+ uv pip install --system --compile-bytecode --require-hashes -r /tmp/requirements-runtime.lock \
15
+ && rm -f /tmp/requirements-runtime.lock \
16
+ && python -c 'import httpx, PIL, fastmcp, tzdata; assert fastmcp.__version__ == "4.0.10"'
17
+
18
+ ARG DEPS_REF=unknown
19
+ LABEL org.opencontainers.image.title="maps-mcp-deps" \
20
+ org.opencontainers.image.revision="${DEPS_REF}"
21
+
22
+ CMD ["true"]
@@ -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,9 @@
1
+ .PHONY: bootstrap check lock
2
+ bootstrap:
3
+ ./scripts/bootstrap.sh
4
+ check:
5
+ ./scripts/test.sh
6
+ # After editing dependencies in pyproject.toml: refresh uv.lock and the image's hash lock.
7
+ lock:
8
+ uv lock
9
+ uv export --frozen --no-dev --no-emit-project --no-header --quiet -o requirements-runtime.lock
@@ -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,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).