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.
- google_maps_keyless_mcp-0.5.3/.dockerignore +14 -0
- google_maps_keyless_mcp-0.5.3/.github/workflows/ci.yml +23 -0
- google_maps_keyless_mcp-0.5.3/.github/workflows/pypi.yml +38 -0
- google_maps_keyless_mcp-0.5.3/.github/workflows/release.yml +105 -0
- google_maps_keyless_mcp-0.5.3/.gitignore +17 -0
- google_maps_keyless_mcp-0.5.3/AGENTS.md +22 -0
- google_maps_keyless_mcp-0.5.3/CONTRIBUTING.md +27 -0
- google_maps_keyless_mcp-0.5.3/Dockerfile +34 -0
- google_maps_keyless_mcp-0.5.3/Dockerfile.deps +22 -0
- google_maps_keyless_mcp-0.5.3/LICENSE +21 -0
- google_maps_keyless_mcp-0.5.3/Makefile +9 -0
- google_maps_keyless_mcp-0.5.3/NOTICE +22 -0
- google_maps_keyless_mcp-0.5.3/PKG-INFO +199 -0
- google_maps_keyless_mcp-0.5.3/README.md +181 -0
- google_maps_keyless_mcp-0.5.3/SECURITY.md +13 -0
- google_maps_keyless_mcp-0.5.3/THIRD_PARTY_NOTICES +30 -0
- google_maps_keyless_mcp-0.5.3/pyproject.toml +69 -0
- google_maps_keyless_mcp-0.5.3/requirements-runtime.lock +1000 -0
- google_maps_keyless_mcp-0.5.3/scripts/bootstrap.sh +5 -0
- google_maps_keyless_mcp-0.5.3/scripts/capture-fixtures +267 -0
- google_maps_keyless_mcp-0.5.3/scripts/source-identity.py +39 -0
- google_maps_keyless_mcp-0.5.3/scripts/test.sh +40 -0
- google_maps_keyless_mcp-0.5.3/scripts/vendor-gmaps.sh +29 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/__init__.py +8 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/__main__.py +3 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/auth.py +36 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/config.py +125 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/errors.py +67 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/google.py +229 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/logs.py +136 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/planner.py +57 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/server.py +509 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/service.py +703 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/shapes.py +385 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/state.py +158 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/staticmap.py +182 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/__init__.py +0 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/LICENSE +21 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/VENDORED_COMMIT +1 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/__init__.py +35 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_auth/__init__.py +12 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_auth/session.py +294 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_directions.py +74 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_hosts.py +21 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_places.py +71 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_reviews.py +123 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/_search.py +1409 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/client.py +327 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/control.py +276 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/exceptions.py +93 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/grid.py +188 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/__init__.py +56 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/decoder.py +386 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/directions.py +529 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/encoder.py +173 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/ids.py +50 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/parser.py +1132 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/pb_encoder.py +294 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/rpc/types.py +103 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/transport.py +461 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/vendor/gmaps/validation.py +214 -0
- google_maps_keyless_mcp-0.5.3/src/maps_mcp/view.html +613 -0
- google_maps_keyless_mcp-0.5.3/tests/conftest.py +161 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/README.md +25 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/details_albertina.json +719 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/details_cafe.json +529 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_3_stop.json +3274 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_arrive.json +9272 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/drive_depart.json +9267 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/hours_samples.json +1127 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/korea_walk.json +187 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/noroute_drive.json +189 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/search_museums.json +3423 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/search_single.json +766 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/unknown.json +140 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/walk_coords.json +3170 -0
- google_maps_keyless_mcp-0.5.3/tests/fixtures/walk_text.json +3735 -0
- google_maps_keyless_mcp-0.5.3/tests/test_cards.py +73 -0
- google_maps_keyless_mcp-0.5.3/tests/test_google.py +104 -0
- google_maps_keyless_mcp-0.5.3/tests/test_hours.py +112 -0
- google_maps_keyless_mcp-0.5.3/tests/test_places.py +86 -0
- google_maps_keyless_mcp-0.5.3/tests/test_plan.py +188 -0
- google_maps_keyless_mcp-0.5.3/tests/test_routes.py +191 -0
- google_maps_keyless_mcp-0.5.3/tests/test_safety.py +124 -0
- google_maps_keyless_mcp-0.5.3/tests/test_server.py +159 -0
- google_maps_keyless_mcp-0.5.3/tests/test_static_map.py +176 -0
- google_maps_keyless_mcp-0.5.3/uv.lock +1667 -0
|
@@ -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,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).
|