A constrained, experimental MCP browser controller for user-directed interaction with Google Maps through a dedicated Chrome/Chromium session.
Status: V1–V4 are implemented/closed out for the current unauthenticated scope. V5-A through V5-D authenticated workflows are implemented behind a disabled-by-default, fail-closed single-user/dedicated-profile opt-in; V5-E history was evaluated and intentionally adds no history tool. Remaining partial capabilities stay explicitly observation/design-gated rather than guessed. Google Maps UI-dependent interaction and bounded visible-state reading remain experimental because the live Maps UI can change.
When Google sign-in, re-authentication, consent, or an access challenge requires a person, maps-browser-mcp keeps Maps semantics, profile ownership, and fresh post-Human verification on the Maps side while mcp-execution-handoff owns temporary Human authority, exact-window capture/input, WebRTC/TURN, generation fencing, and revoke. Human Done ends Human authority; it is not authentication proof or approval for a later Maps mutation. See Maps ↔ Handoff responsibility boundary.
General-purpose browser MCPs are powerful, but they expose a much larger control surface than a Maps-only task needs. maps-browser-mcp takes the opposite approach:
- expose only Maps-specific MCP tools,
- use official Google Maps URLs whenever possible,
- keep Chrome DevTools Protocol (CDP) local,
- use a dedicated browser profile,
- fail closed when the page/state is ambiguous,
- make visible-state reading explicit, bounded, and disabled by default,
- do not implement scraping, CAPTCHA bypass, stealth, or internal Maps API harvesting.
If a supported Google Maps Platform API or Google-managed Maps MCP already satisfies a workflow without requiring the rendered Maps Web experience, prefer that supported structured interface. maps-browser-mcp exists for bounded workflows that genuinely require the user-visible Maps web surface. Official-interface overlap is therefore a priority signal, not an automatic scope exclusion, and the browser path is never an API-avoidance mechanism.
| Surface | Best fit | This project intentionally differs by |
|---|---|---|
| Google Maps Platform / Google-managed Maps MCP | supported structured Maps, grounding, place, route, or related data where available | prefer these when the rendered Maps Web experience is not required; this project controls a bounded user-visible Maps browser session |
| General-purpose browser MCPs | broad web navigation and arbitrary browser automation | exposes Maps-specific actions and a substantially smaller capability surface |
| Scrapers / dataset harvesters | bulk collection and persistent extraction | explicitly out of scope; visible-state reading is bounded, transient, and opt-in |
See Project positioning, V4 Maps Web Capability Inventory, and Compliance boundaries for the detailed category, coverage, and safety boundaries.
V4 is intentionally broader than V1–V3 but keeps the same constrained architecture:
V4 = broad semantic coverage of major Google Maps Web capabilities available without authentication.
Priority order is browser-native/UI-dependent behavior first; search/directions/place operations required to complete the browser workflow second; and capabilities that mostly duplicate official structured interfaces third.
The canonical unauthenticated per-capability status table is Google Maps Web Capability Inventory. The current V5 authenticated-workflows baseline implements V5-A identity-free readiness, V5-B bounded save-state reads, V5-C one exact existing-list Save mutation, and V5-D bounded selected-route Send-to-phone with explicit one-shot MCP action approval. V5-E history is intentionally blocked from adding a tool because the observed History surface crosses into My Activity and Maps-local Recent lacks a stable bounded activity-row contract. Neither V4 nor V5 exposes raw DOM, raw Accessibility Tree, raw CDP, generic browser actions, desktop actions, or shell execution through MCP.
Requirements: Node.js 20+ and Google Chrome/Chromium.
git clone https://github.com/git-ksk/maps-browser-mcp.git
cd maps-browser-mcp
npm ci --ignore-scripts
npm run build
npm startThat starts the MCP over stdio in safe mode. The first Maps action starts/reuses a dedicated Chrome profile.
For Streamable HTTP instead:
npm run start:httpDefault MCP endpoint:
http://127.0.0.1:8787/mcp
Process liveness:
curl -i http://127.0.0.1:8787/healthzBrowser/CDP readiness without visiting Google Maps:
curl -i http://127.0.0.1:8787/readyzFor a complete first-run walkthrough, browser behavior, generic MCP client configuration, V3/V4 Interactive Assist opt-in, and cleanup, see Getting Started.
Navigation does not require Interactive Assist:
maps_search({ query: "Tokyo Station" })
maps_directions({
origin: "Tokyo Station",
destination: "Yokohama Station",
mode: "transit"
})
When Interactive Assist is enabled, the safe selection pattern is:
maps_search(...)
-> maps_read_place_summary()
-> choose items[{ index, label }]
-> maps_select_result({ index, expectedLabel: label })
A V4-C search filter keeps the same search identity chain explicit:
maps_search({ query })
-> maps_set_search_rating({ expectedQuery: query, rating: "4.0" })
-> maps_read_place_summary()
maps_set_search_rating exposes only the live-reobserved 2.0|2.5|3.0|3.5|4.0|4.5 Rating options. It revalidates the visible search query before each bounded UI action, then verifies the exact requested selected chip (for example 4.0+) with the Rating menu closed before advancing the resource epoch. Price, Hours, and All filters remain observation/design-gated rather than sharing a generic filter API.
Autocomplete is also bounded rather than generic browser input:
maps_read_search_suggestions({ query: "Tokyo Station" })
-> choose items[{ index, label }]
-> maps_select_search_suggestion({ query: "Tokyo Station", index, expectedLabel: label })
maps_read_search_suggestions opens a fresh Maps suggestion surface and returns at most six unique composite visible identities from the exact combobox-controlled grid. Primary names may repeat, so maps_select_search_suggestion requires the same active query plus the exact returned index and label immediately before activation; stale/reordered/duplicate identities fail closed. Success is accepted only after the suggestion grid closes and Maps settles to a verified search or place view. Raw combobox/DOM access is never exposed.
For an active search-result list, maps_get_search_share_link({ expectedQuery }) revalidates the canonical and exact visible query, activates exactly one live-observed Share control, reads one allow-listed Maps-generated URL from the selected Send-link tab, and closes the dialog semantically. It never reads clipboard contents and does not change the search resource epoch on success.
maps_zoom_search({ expectedQuery, direction }) adds only the bounded stateful viewport operation that was re-observed safely beyond maps_show: one search-result zoom step with direction: "in" | "out". It requires one exact visible query and one exact visible enabled Zoom button immediately before the click, then verifies that the same search/query remains active and the public Maps viewport path changes by exactly one zoom level. Map-center coordinates are not treated as stable identity, and generic pan/recenter or root/place zoom is not exposed.
The first V4 browser-native workflow extends that identity chain to a Maps-generated place share URL:
maps_search(...)
-> maps_read_place_summary()
-> maps_select_result({ index, expectedLabel: label })
-> maps_get_place_share_link({ expectedLabel: selectedPlaceLabel })
maps_get_place_share_link revalidates the active place immediately before activating the visible Share control and fails closed if the place/share target changed or became ambiguous. The same verified-place model is used by maps_search_nearby, maps_open_place_photos, maps_select_place_tab, and maps_expand_opening_hours. Place-tab selection currently exposes only live-reobserved overview / about semantics; Reviews remains observation-gated. Opening-hours expansion verifies the state transition without returning or harvesting a weekly-hours dataset.
For routes:
maps_directions({ origin, destination, mode: "transit" })
-> maps_set_transit_time({
expectedOrigin: origin,
expectedDestination: destination,
mode: "depart_at",
time: "13:30"
})
-> maps_read_route_summary()
-> choose items[{ index, label }]
-> maps_select_route({ index, expectedLabel: label })
maps_set_transit_time is intentionally limited to the same-day live-reobserved depart_at|arrive_by flow with a 24-hour HH:MM input. It requires a fresh simple maps_directions transit request, revalidates the documented origin/destination identity before mutation, then verifies the localized mode trigger, the exact transit-time input, unchanged visible route endpoints, and the directions view. Because the resulting UI-only time state is not represented by the original documented navigation action, the successful operation clears that replayable action while keeping the current route results readable/selectable in the same browser session. Date selection, last-available service, and transit preference options remain separate observation/design-gated slices.
maps_set_recommended_travel_mode({ expectedOrigin, expectedDestination }) covers the live-observed Best / おすすめ radio only for a fresh simple transit request. It rejects omitted origins, waypoints, avoid constraints, and non-transit starts; verifies the exact radio plus unchanged resolved endpoints and the directions surface; then advances the resource epoch and drops the stale replayable travelmode=transit action while preserving the current route results for bounded read/select. This avoids pretending the original documented URL still represents the UI's Recommended mode.
maps_swap_route_endpoints({ expectedOrigin, expectedDestination }) covers the observed origin/destination swap without automating the Maps swap button. Live JA/en-US observation verified the exact semantic swap control and visible endpoint A/B -> B/A transition, but also showed that the UI click leaves the canonical URL/action stale. The MCP operation therefore requires a fresh simple documented directions request, revalidates the expected canonical endpoints, rejects omitted origins and waypoint routes, preserves travel mode and bounded avoid constraints, and rebuilds the documented Maps URL with the endpoints reversed.
maps_get_route_share_link({ expectedOrigin, expectedDestination }) returns the Maps-generated short link from the selected-route transit share dialog. After a guarded maps_select_route, it requires the expected simple canonical transit identity, activates exactly one live-observed Share directions control, verifies the selected Send a link tab plus exactly one allow-listed visible Maps URL field, then closes the dialog semantically before returning. It never reads clipboard contents. The earlier unselected Copy link surface remains unused, and driving/other modes stay observation-gated because the visible link field was not stable in the bounded re-observation.
expectedLabel is important: if Google Maps dynamically reorders or replaces the target, the runtime refuses stale interaction with UI_STATE_CHANGED instead of acting on a different target.
maps_searchmaps_directionsmaps_showmaps_streetview
maps_select_resultmaps_read_search_suggestions— V4-F, max 6 composite suggestion identities; establishes bounded suggestion state, Interactive Assist requiredmaps_select_search_suggestion— V4-F, same active query + guardedindex/expectedLabel, Interactive Assist requiredmaps_get_search_share_link— V4-F, active search-result Share dialog, clipboard-free, Interactive Assist requiredmaps_set_search_rating— V4, fixed observed rating enum, Interactive Assist requiredmaps_zoom_search— V4, search-only one-levelin|out, Interactive Assist requiredmaps_get_place_share_link— V4, Interactive Assist requiredmaps_search_nearby— V4, Interactive Assist requiredmaps_open_place_photos— V4, Interactive Assist requiredmaps_select_place_tab— V4,overview|aboutonly, Interactive Assist requiredmaps_expand_opening_hours— V4, expansion-state verification only, Interactive Assist requiredmaps_select_routemaps_set_travel_modemaps_set_recommended_travel_mode— V4-F, fresh simple transit -> Best/Recommended only, Interactive Assist requiredmaps_set_transit_time— V4-D, same-daydepart_at|arrive_by, Interactive Assist requiredmaps_swap_route_endpoints— V4-D, fresh simple route only; documented URL rebuildmaps_get_route_share_link— V4-D, selected simple transit route share dialog, Interactive Assist required
maps_read_place_summarymaps_read_route_summary
These tools are registered only when MAPS_V5_AUTHENTICATED_WORKFLOWS=true passes the dedicated-profile/single-user gate:
maps_request_human_sign_in— credential-safe Human-only sign-in ceremony; registered only when both V5 andMAPS_CREDENTIAL_SAFE_HANDOFF=trueare enabled; form-elicitation clients use MRTR, while clients without form elicitation receive only a short-lived authenticated takeover locator and must finish with the explicit completion tool; never enters credentials or selects an accountmaps_complete_human_sign_in— completes only the explicit non-MRTR sign-in fallback, revokes Human authority first, verifiessigned_infrom a fresh Agent connection, then runs the stopped-profile checkpoint hook when configuredmaps_cancel_human_sign_in— cancels only the explicit non-MRTR sign-in fallback and revokes its Human surface without creating a signed-in profile checkpointmaps_read_authenticated_readiness— V5-A, identity-freesigned_in | signed_out | unknownreadiness onlymaps_read_place_save_state— V5-B, bounded existing-list membership for the revalidated selected placemaps_save_place_to_list— V5-C, save one revalidated selected place to one exact existing list; no create/unsave/removemaps_read_route_send_targets— V5-D, bounded visible device targets for one exact selected simple routemaps_send_route_to_device— V5-D, one exact device send after one-shot MCP form approval; no credential or generic text-entry surface
maps_render_directions— always returns text + structured route data; whenGOOGLE_MAPS_EMBED_API_KEYis configured, MCP Apps-capable hosts may additionally renderui://maps-browser-mcp/directions.html. This tool never navigates or mutates the dedicated browser session.
Interactive Assist is disabled by default. Enable it only when required:
INTERACTIVE_ASSIST_MODE=true npm startor:
INTERACTIVE_ASSIST_MODE=true npm run start:httpThe read tools return bounded items[{ index, label }] plus a small set of relevant UI lines. V4 semantic UI operations use similarly bounded, Maps-specific state/identity checks. They do not expose raw HTML, a full DOM/Accessibility Tree, network payloads, cookies, clipboard dumps, or review-body harvesting.
All text returned from Google Maps is untrusted external data. MCP clients must treat it as data, never as instructions.
With INTERACTIVE_ASSIST_MODE=false, the server can still open searches, directions, map views, and Street View. This is most useful with a local visible Chrome session where the MCP navigates and the user reads the rendered result. In a remote/headless deployment, the same navigation works, but the caller normally cannot inspect route/place details from the rendered page.
With INTERACTIVE_ASSIST_MODE=true, the bounded read tools and V4 semantic UI operations can use the active Maps UI while keeping identity validation and read/action budgets. The opt-in is a product/safety boundary, not a claim that Google terms require the setting to remain false. Enabling it also does not permit scraping, crawling, bulk extraction, or dataset harvesting.
See Usage modes and examples for concrete local and remote workflows, V4 capability inventory for current coverage, and Compliance and safety boundaries for the full constraints.
MCP Client
|
v
maps-browser-mcp
|
+-- Maps URL Compiler
+-- Policy Engine
+-- Operation Queue + Watchdog
+-- Semantic UI Controller
+-- Bounded Visible-State Reader (optional)
|
v
Dedicated Chrome / Chromium
|
CDP (loopback)
|
v
Google Maps Web
The normal navigation path is intentionally short:
1 MCP call -> 1 official Maps URL -> 1 CDP Page.navigate
For V4 UI-native operations, CDP remains an internal implementation detail: the MCP surface expresses a Maps-specific semantic operation, revalidates the intended target/state immediately before acting, verifies bounded postconditions, and fails closed on ambiguity.
One process controls one semantic browser state. Browser operations are serialized, the pending queue is bounded, and a watchdog resets the browser/CDP session if an operation exceeds the configured timeout.
See Architecture for the detailed runtime/state/security model.
- Node.js 20+
- Google Chrome or Chromium
- macOS, Linux, or Windows
Common Chrome/Chromium install locations are auto-detected. Set MAPS_CHROME_EXECUTABLE if required.
Normal CI covers Node.js 20/22/24 and real Chrome/CDP startup. Browser startup is additionally smoke-tested on GitHub-hosted macOS and Windows runners. The required Node 22 check also builds the container image, exercises sandboxed and explicit restricted-runtime browser paths, and verifies both /healthz and /readyz without visiting Google Maps.
By default the managed browser profile lives at:
~/.maps-browser-mcp/chrome-profile
Do not point this project at your everyday Chrome profile.
The managed CDP endpoint binds to 127.0.0.1. The runtime validates the managed browser identity before reusing a profile and refuses to guess between multiple open Google Maps tabs.
If Google displays consent, sign-in, CAPTCHA, or another access challenge, the MCP stops with HUMAN_INTERVENTION_REQUIRED. Resolve legitimate manual steps through the existing Human Intervention flow. Completion does not approve a different action, and stateful semantic operations require fresh reissue/revalidation rather than automatic replay.
The HTTP server binds to loopback by default:
127.0.0.1:8787
Recommended remote architecture:
Remote MCP client
-> authenticated HTTPS tunnel / reverse proxy
-> 127.0.0.1:8787/mcp
-> maps-browser-mcp
-> dedicated local Chrome
Only the MCP transport should cross the remote boundary. Never expose the Chrome DevTools port publicly.
If you deliberately bind the Node server to a non-loopback address, startup requires both:
MCP_ALLOW_NONLOOPBACK=true
MCP_BEARER_TOKEN=<at least 24 characters>
This is an advanced escape hatch, not the recommended deployment shape.
For ChatGPT-specific deployment and tool refresh notes, see ChatGPT connection notes.
The server does not automatically load .env. Use your shell, process manager, or preferred environment loader. See .env.example.
| Variable | Default | Purpose |
|---|---|---|
MCP_HTTP_HOST |
127.0.0.1 |
HTTP bind address |
MCP_HTTP_PORT |
8787 |
Project-specific HTTP port; takes precedence over PORT |
PORT |
unset | Generic HTTP-port fallback when MCP_HTTP_PORT is unset |
MCP_ALLOWED_HOSTS |
localhost,127.0.0.1,::1 |
Accepted Host names |
MCP_ALLOWED_ORIGINS |
empty | Optional exact Origin allowlist |
MCP_ALLOW_NONLOOPBACK |
false |
Explicit opt-in before non-loopback bind |
MCP_BEARER_TOKEN |
empty | Optional guard; mandatory for non-loopback bind; minimum 24 chars |
MCP_MAX_BODY_BYTES |
262144 |
Maximum MCP request body size |
MAPS_CHROME_EXECUTABLE |
auto-detect | Dedicated Chrome/Chromium executable used by the process-owned browser session |
MAPS_CHROME_PROFILE_DIR |
~/.maps-browser-mcp/chrome-profile |
Dedicated process-owned Chrome profile directory |
MAPS_ALLOW_EXTERNAL_CDP |
false |
Explicit opt-in before existing local CDP attachment; incompatible with credential-safe handoff |
MAPS_CDP_PORT |
unset | Advanced: existing local CDP endpoint |
MAPS_HEADLESS |
false |
Headless Chrome |
MAPS_ALLOW_UNSANDBOXED_CHROMIUM |
false |
Linux-only last-resort opt-in for restricted isolated runtimes; adds --no-sandbox to Agent Chromium and credential-safe normal Chromium |
INTERACTIVE_ASSIST_MODE |
false |
Enable bounded visible-state reading and V4 semantic UI operations that require it |
MAPS_V5_AUTHENTICATED_WORKFLOWS |
false |
Enable the fail-closed bounded V5 authenticated tools; also requires Interactive Assist and the dedicated single-user profile gate |
MAPS_CREDENTIAL_SAFE_HANDOFF |
false |
Enable Human-only handoff for Google sign-in/consent/challenge surfaces |
MAPS_CREDENTIAL_SAFE_TRANSPORT |
external |
external uses an OS-level local surface; cua_takeover uses local Cua; thin_takeover uses the Native Thin Takeover runtime; webrtc_takeover uses install-free iPhone Safari WebRTC takeover; legacy hosted_cdp is retained only for deployment compatibility and fails closed for Human credential/consent/challenge control |
MAPS_CREDENTIAL_SAFE_OPERATOR_URL |
unset | Optional fixed HTTPS locator for local external transport only; credentials/query/fragment are rejected |
MAPS_CUA_DRIVER_COMMAND |
cua-driver |
Cua Driver executable used only by cua_takeover; the integration invokes only a fixed seven-tool Human transport allowlist |
MAPS_WEBRTC_TAKEOVER_HOST_EXECUTABLE |
unset | Absolute path to the Handoff WebRTC host executable; macOS uses takeover-webrtc-host, Linux uses the pinned Linux normal-browser helper/wrapper |
MAPS_WEBRTC_TAKEOVER_DISPLAY_ID |
unset | Optional explicit macOS display ID for WebRTC capture |
MAPS_WEBRTC_TAKEOVER_DISPLAY_NAME |
unset | Required on Linux webrtc_takeover; local X11 display such as :99 |
GOOGLE_MAPS_EMBED_API_KEY |
unset | Optional restricted Maps Embed API key for the MCP Apps directions view; the text/structured render tool remains available without it |
MAPS_MAX_ACTIONS_PER_MINUTE |
30 |
Process-local action guard |
MAPS_MAX_VISIBLE_READS_PER_HOUR |
30 |
Independent bounded visible-state/UI read budget |
MAPS_MAX_AX_NODES |
120 |
Accessibility-node bound for bounded visible-state reading |
MAPS_MAX_READ_CHARS |
1800 |
Returned-text bound for bounded visible-state reading |
MAPS_MAX_PENDING_ACTIONS |
8 |
Maximum queued browser operations |
MAPS_OPERATION_TIMEOUT_MS |
25000 |
Per-operation watchdog |
Invalid boolean/integer configuration fails fast instead of being silently coerced.
MAPS_V5_AUTHENTICATED_WORKFLOWS=true is an additional fail-closed opt-in for bounded authenticated V5 semantics. With Interactive Assist enabled it exposes only the staged identity-free readiness, bounded selected-place save-state read, exact existing-list Save, bounded selected-route Send-to-phone target read, and approval-gated single-device send documented in the V5 baseline. When MAPS_CREDENTIAL_SAFE_HANDOFF=true is also enabled, maps_request_human_sign_in adds a Human-only ceremony for Google sign-in; naturally detected consent and CAPTCHA/access-challenge surfaces use the same Human boundary. Credentials, MFA/OTP values, passkey material, cookies, browser-session bearer material, and provider API keys never become MCP/model/log content, and passkey/WebAuthn ceremonies are not bypassed.
V5 itself does not require a remote handoff transport. A persistent dedicated Chrome profile that is already signed in is the simplest deployment: fresh readiness can immediately return signed_in and the bounded V5 tools can run without Cloudflare/WebRTC. Credential-safe handoff is only needed when a Human must perform sign-in, re-authentication, consent, or another naturally occurring challenge.
All credential-safe transports use the profile-switch lifecycle: managed CDP Chrome is stopped, the same dedicated profile is opened in normal Chrome without remote-debugging/automation flags, and automation relaunches only after the Human surface is revoked and fresh readiness is re-read. external uses an OS-level Human surface, cua_takeover is a local fallback/reference, thin_takeover uses the low-latency Native runtime, and webrtc_takeover exposes the Handoff-owned direct-touch Safari surface. For Native/WebRTC credential takeover, Maps passes only the PID of the normal Chrome process it starts; Handoff requires exactly one eligible window and scopes capture/input to that window rather than the desktop. ScreenCaptureKit, VideoToolbox, WebRTC signaling/RTP/DataChannel, reconnect fencing, and input delivery remain outside Maps. Done/revoke stops Human authority and closes normal Chrome; it is never treated as authentication proof.
Operationally, webrtc_takeover has macOS and Linux platform hosts behind the same browser/session boundary. macOS is physically accepted with iPhone Safari over direct and TURN, including real Google sign-in recovery. The Linux/Cloud Run production path is also accepted for the core transport/sign-in boundary: Xvfb/Openbox/xdotool/ffmpeg + the packaged exact-window Handoff host, Cloudflare Realtime TURN, physical iPhone Safari Live · relay, and real Human-only Google sign-in have all been exercised. The remaining Cloud Run work is deliberately narrower: issue #135 tracks explicit Done -> revoke -> stale-client fencing -> fresh signed_in -> stopped-profile checkpoint -> fresh restore, and #134 tracks the final iPhone keyboard/Backspace/CJK UX regression. The normal-browser boundary is transport- and OS-independent: Human credential/consent/challenge control must never reuse the Agent automation browser or Human-owned CDP. Legacy hosted_cdp therefore remains disabled for those Human steps. external remains the configuration default and thin_takeover remains optional/experimental.
The WebRTC path is direct-first and remains Handoff-owned. The Safari/browser peer stays host-only (iceServers: []), while the Node/werift peer uses explicit Cloudflare STUN to avoid an implicit dependency-selected third-party STUN default. When Cloudflare Realtime TURN is configured, short-lived peer-specific TURN credentials are added with iceTransportPolicy: all, so same-LAN/direct ICE remains preferred and WAN/CGNAT can fall back to relay. The long-lived TURN token stays server-side and is never forwarded to Maps, Chrome, the browser client, or the helper. Maps does not process ICE/STUN/TURN details, candidate/address data, SDP, RTP, framebuffer bytes, or raw Human input. No vendor browser API key or hosted-browser backend is required. Steel may be used only as an external comparison/UX benchmark, not as a runtime dependency.
Both lifecycle shapes reject stale automatic replay after Human handoff. Existing-CDP attachment remains incompatible with credential-safe ownership, MCP_AUTH_PROVIDER=module remains rejected for V5 until per-principal isolation exists, and the send mutation still requires a modern MCP 2026-07-28 client with form elicitation support.
For the current remote single-user design, authenticate the public MCP client at an external gateway and use the private static-bearer hop to this core server. Do not forward a caller's public OAuth access token into the browser runtime. The versioned reference OAuth gateway implements this shape as an isolated dogfood package; it is not included in the published root npm package. See V5 authenticated workflows and OAuth gateway pattern.
MAPS_CDP_PORT is intentionally guarded. It is rejected unless MAPS_ALLOW_EXTERNAL_CDP=true is also set.
Only attach to a local, dedicated Chrome/Chromium instance you control. Attaching to an everyday personal browser weakens profile isolation and is not recommended.
This project is a constrained, user-directed browser agent. It is not intended to be:
- a general-purpose browser MCP,
- a bulk Google Maps scraper/crawler,
- a place/review/route dataset harvester,
- a CAPTCHA solver,
- an anti-bot bypass tool.
Overlap with Google Maps Platform or a Google-managed Maps MCP is not itself out of scope, but browser implementation is prioritized only where it contributes to the user-visible Maps Web workflow.
It intentionally does not implement Google Maps internal API interception, XHR/fetch harvesting, stealth plugins, fingerprint spoofing, proxy rotation, persistent Maps datasets, raw DOM/CDP MCP tools, generic desktop control, or shell control.
Obvious bulk-collection requests are rejected by the policy layer. Interactive Assist has a separate rolling hourly read budget. Navigation remains restricted to the Google Maps HTTPS web surface. Visible inline access challenges are detected and stop the operation.
This project does not claim that every browser-agent usage is guaranteed permitted by Google. Users are responsible for applicable service terms and laws. Where supported structured Google Maps interfaces already satisfy the workflow without requiring Maps Web, prefer those interfaces. See Compliance boundaries.
The server does not intentionally persist Maps result datasets. The dedicated Chrome profile is persistent local browser state, so Chrome may retain ordinary browser artifacts such as cookies, cache, preferences, and history.
Use a dedicated profile, avoid signing in unless necessary, and remove the dedicated profile when you need those local browser artifacts deleted.
Tool handlers do not log search queries or Maps result contents by default. Remote clients receive generalized unexpected-error responses rather than local paths/environment details. HTTP responses use Cache-Control: no-store.
Never commit browser profiles, .env files, tunnel credentials, tokens, screenshots/traces containing personal data, or generated Maps datasets.
Local verification:
npm run typecheck
npm test
npm run build
npm run smoke:stdio
npm run smoke:http
npm run smoke:browserNormal CI intentionally does not visit Google Maps. It verifies protocol, package, browser/CDP, security, cross-platform behavior, and container/headless portability without turning GitHub Actions into unattended Maps automation.
Container validation is part of the existing required check (22) job rather than a separate optional job. It verifies that the image does not enable --no-sandbox by default, exercises a sandbox-capable container path, checks fail-closed behavior in a restricted runtime, exercises the explicit compatibility mode, and verifies /healthz, /readyz, and generic PORT fallback behavior.
The repository also provides Live Maps E2E (manual), a workflow_dispatch-only, fixed, low-volume compatibility check for the experimental live-UI paths. See Manual live E2E. Consent/sign-in/CAPTCHA/challenge behavior is never deliberately triggered or bypassed; those live cases are rechecked opportunistically when they occur naturally, while deterministic repository tests enforce the no-bypass boundary.
GitHub Actions dependencies are pinned to full commit SHAs. Dependabot monitors npm, GitHub Actions, and the container base image. CodeQL runs JavaScript/TypeScript analysis and the protected main branch requires the configured CI/CodeQL checks before merge.
- Google Maps UI changes can break experimental semantic selectors.
- V4 coverage closeout is complete for the current unauthenticated scope; the canonical inventory records implemented, partial, and explicitly observation/design-gated capabilities plus their re-open conditions.
- Bounded visible-state/UI interaction remains experimental and opt-in.
- One process is designed for one local user/browser session, not multi-tenant hosting.
- CAPTCHA, consent, and sign-in flows are not bypassed.
- Rate/read counters are process-local safety guards, not persistent accounting or a legal-compliance mechanism.
See Troubleshooting for recovery guidance and error-code explanations.
| Document | Purpose |
|---|---|
| Getting Started | Installation, first run, client shape, Interactive Assist opt-in, cleanup |
| Container / headless Linux | Standard Linux container, headless Chromium, ports, profiles, readiness, and sandbox boundaries |
| Troubleshooting | Error codes and safe recovery procedures |
| WebRTC Human Takeover | macOS + iPhone Safari setup, helper build, authenticated operator origin, direct/TURN behavior |
| ChatGPT | Remote ChatGPT/App connection boundary and tool refresh |
| Architecture | Runtime, CDP, state, queue/watchdog, semantic UI operation model |
| Project positioning | Competitive category, Maps Web priority, official-interface overlap, and product direction |
| V4 Maps Web Capability Inventory | Canonical unauthenticated capability coverage/status table and V4 slices |
| MCP Apps portability | Host-neutral UI contract, fallback, layout, security, deployment and compatibility evidence |
| V5 authenticated workflows | Authenticated boundary, bounded saved-state/send scopes, and explicit approval gates |
| Roadmap | V4 closeout baseline plus MCP Apps portability status and future direction |
| Compliance | Intended-use and non-goal boundaries |
| Manual live E2E | User-triggered Google Maps compatibility verification |
| Release checklist | Pre-release CI, live check, security and tagging procedure |
| Security Policy | Security model and private vulnerability reporting |
| Contributing | Scope, PR rules, tests and security-sensitive changes |
Contributions are welcome within the project's constrained scope. Read CONTRIBUTING.md before opening a PR.
main is protected; changes should land through pull requests with the required CI and CodeQL checks.
The current release baseline is v0.3.3 and repository metadata is 0.3.3. V5-A through V5-D remain disabled by default behind the authenticated-workflow opt-in, WebRTC Human takeover is optional, and maps-browser-mcp remains unpublished on npm. Use the GitHub source tag/Release for the stable source baseline.
See Release checklist before tagging or publishing.
Use GitHub Private Vulnerability Reporting for security issues. Do not publish exploit details, credentials, browser profiles, private locations, or tokens in public issues.
See SECURITY.md.
This is an independent open-source project and is not affiliated with or endorsed by Google. Google Maps and related marks are trademarks of their respective owner. Users are responsible for complying with applicable service terms and laws.
MIT