@cablate/mcp-google-map 0.0.64 → 0.0.67

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-google-map",
3
- "version": "0.0.64",
4
- "description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.",
3
+ "version": "0.0.67",
4
+ "description": "Use Google Maps for place research, travel planning, and local SEO through focused Skills and a standalone CLI—no MCP setup required.",
5
5
  "author": {
6
6
  "name": "CabLate",
7
7
  "url": "https://github.com/cablate"
@@ -20,8 +20,8 @@
20
20
  "skills": "./skills/",
21
21
  "interface": {
22
22
  "displayName": "Google Maps CLI",
23
- "shortDescription": "Geospatial research and planning through a standalone CLI.",
24
- "longDescription": "Guide an AI agent to use the @cablate/mcp-google-map exec CLI for place discovery, routing, geocoding, travel planning, local SEO, weather, air quality, and other geographic workflows. No MCP connection is required.",
23
+ "shortDescription": "Google Maps, travel planning, and local SEO without MCP setup.",
24
+ "longDescription": "Three focused Skills guide an AI agent to use the @cablate/mcp-google-map standalone CLI for place research, evidence-backed travel planning, and local SEO analysis. The plugin does not start or register an MCP server.",
25
25
  "developerName": "CabLate",
26
26
  "category": "Productivity",
27
27
  "capabilities": [
@@ -30,8 +30,8 @@
30
30
  "websiteURL": "https://github.com/cablate/mcp-google-map",
31
31
  "defaultPrompt": [
32
32
  "Find and compare places for my trip.",
33
- "Plan an efficient route between these stops.",
34
- "Research what is near this location."
33
+ "Plan a practical multi-day itinerary with checked routes.",
34
+ "Analyze this business's local Google Maps visibility."
35
35
  ]
36
36
  }
37
37
  }
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
+ # MCP Google Maps
2
+
3
+ Give AI agents reliable Google Maps search, geocoding, routing, weather, air quality, and local SEO data. Use it as a Codex Plugin with standalone CLI calls, or connect the same 18 tools through MCP.
4
+
5
+ <p align="center"><b>English</b> | <a href="./README.zh-TW.md">繁體中文</a></p>
6
+
7
+ <p align="center">
8
+ <img src="./assets/banner.webp" alt="MCP Google Maps — AI-Powered Geospatial Tools" width="800">
9
+ </p>
10
+
1
11
  <p align="center">
2
12
  <a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/v/@cablate/mcp-google-map" alt="npm version"></a>
3
13
  <a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/dm/@cablate/mcp-google-map" alt="npm downloads"></a>
@@ -6,22 +16,45 @@
6
16
  </p>
7
17
 
8
18
  <p align="center">
9
- <img src="./assets/banner.webp" alt="MCP Google Maps — AI-Powered Geospatial Tools" width="800">
19
+ <img src="./assets/demo-grid-en.png" alt="Travel planning demo — Kyoto 2-day, Tokyo outdoor, Japan 5-day, Bangkok budget" width="800">
10
20
  </p>
11
21
 
12
- <h3 align="center"><b>Give your AI agent the ability to understand the physical world —<br>geocode, route, search, and reason about locations.</b></h3>
22
+ - **18 tools** — 14 atomic tools and 4 higher-level workflows
23
+ - **Three ways to connect** — Codex Plugin, MCP over stdio, or Streamable HTTP
24
+ - **Three focused Skills** — general maps, travel planning, and local SEO
13
25
 
14
- <p align="center">
15
- <b>English</b> | <a href="./README.zh-TW.md">繁體中文</a>
16
- </p>
26
+ ## Choose how to use it
17
27
 
18
- <p align="center">
19
- <img src="./assets/demo-grid-en.png" alt="Travel planning demo — Kyoto 2-day, Tokyo outdoor, Japan 5-day, Bangkok budget" width="800">
20
- </p>
28
+ | If you want to… | Use | What runs |
29
+ |---|---|---|
30
+ | Let Codex answer map, travel, or local SEO questions without configuring MCP | **Codex Plugin** | The matching Skill loads on demand and calls the standalone CLI |
31
+ | Add Google Maps tools to Claude Desktop, Cursor, VS Code, or another local MCP client | **MCP stdio** | The client starts one local MCP process |
32
+ | Serve multiple or remote MCP sessions | **Streamable HTTP** | A self-hosted HTTP server at `/mcp` |
33
+
34
+ All options require Node.js 18+ and a Google Maps Platform API key. Enable **Places API (New)** and **Routes API** in [Google Cloud Console](https://console.cloud.google.com) for place and route workflows. Google may charge for live API requests.
35
+
36
+ ## Fastest start: Codex Plugin
37
+
38
+ ```bash
39
+ codex plugin marketplace add cablate/mcp-google-map --ref main
40
+ codex plugin add mcp-google-map@cablate
41
+ ```
42
+
43
+ Set `GOOGLE_MAPS_API_KEY` in the environment where Codex runs, then start a new conversation and ask a location question. Confirm local readiness without calling Google APIs:
44
+
45
+ ```bash
46
+ npx -y @cablate/mcp-google-map doctor
47
+ ```
48
+
49
+ A ready installation reports successful Node.js, package, and API-key checks; `live-api` is skipped. Use `doctor --live` only when you intend to make potentially billable test requests.
21
50
 
22
- - **18 tools** — 14 atomic + 4 composite (explore-area, plan-route, compare-places, local-rank-tracker)
23
- - **3 modes** — stdio, StreamableHTTP, standalone exec CLI
24
- - **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/))
51
+ The plugin does **not** start or register an MCP server. At runtime, Codex first sees the names and descriptions of three Skills and loads full instructions only when the request matches:
52
+
53
+ - `google-maps` — place search, geocoding, routes, neighborhood, and environmental facts
54
+ - `google-maps-travel-planning` — day trips and multi-day itineraries
55
+ - `google-maps-local-seo` — business visibility and geographic rank analysis
56
+
57
+ For a complete no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md).
25
58
 
26
59
  ### vs Google Grounding Lite
27
60
 
@@ -42,7 +75,7 @@
42
75
  | Self-hosted | Yes | Google-managed only |
43
76
  | Agent Skill | Yes | No |
44
77
 
45
- ### Quick Start
78
+ ### CLI and server quick checks
46
79
 
47
80
  ```bash
48
81
  # stdio (Claude Desktop, Cursor, etc.)
@@ -89,18 +122,9 @@ All tools are annotated with `readOnlyHint: true` and `destructiveHint: false`
89
122
 
90
123
  ## Installation
91
124
 
92
- ### Codex Plugin (Agent Skill, no MCP required)
93
-
94
- Install the CabLate marketplace, then install the Skill-only plugin:
95
-
96
- ```bash
97
- codex plugin marketplace add cablate/mcp-google-map --ref main
98
- codex plugin add mcp-google-map@cablate
99
- ```
100
-
101
- Set `GOOGLE_MAPS_API_KEY` in the environment available to Codex, make sure Node.js 18+ and `npx` are installed, then start a new conversation. The plugin teaches the agent to run `npx -y @cablate/mcp-google-map exec ...` directly. It does not register or start an MCP server; the MCP setup below remains an independent option.
125
+ The [fastest-start section](#fastest-start-codex-plugin) covers the Codex Plugin. Use one of the following configurations when you specifically need MCP.
102
126
 
103
- ### Method 1: stdio (Recommended for most clients)
127
+ ### MCP stdio (recommended for local clients)
104
128
 
105
129
  Works with Claude Desktop, Cursor, VS Code, and any MCP client that supports stdio:
106
130
 
@@ -131,7 +155,7 @@ Works with Claude Desktop, Cursor, VS Code, and any MCP client that supports std
131
155
 
132
156
  Omit or set to `*` for all 18 tools (default).
133
157
 
134
- ### Method 2: HTTP Server
158
+ ### Streamable HTTP
135
159
 
136
160
  For multi-session deployments, per-request API key isolation, or remote access:
137
161
 
@@ -169,17 +193,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
169
193
  npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
170
194
  ```
171
195
 
172
- All 18 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `search-along-route`, `explore-area`, `plan-route`, `compare-places`, `local-rank-tracker`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
196
+ All 18 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `search-along-route`, `explore-area`, `plan-route`, `compare-places`, `local-rank-tracker`. See [`skills/`](./skills/) for the Skill definitions and full parameter docs.
173
197
 
174
198
  To use the **Agent Skill without MCP**:
175
199
 
176
- 1. Install the entire [`skills/google-maps/`](./skills/google-maps/) folder in your agent's Skills directory, following that client's instructions. Keep `SKILL.md` and `references/` together. Clients that support `.skill` imports can use the bundled [`SKILL.skill`](./skills/google-maps/SKILL.skill) archive instead. Installing the npm package alone does not register a Skill with an agent.
200
+ 1. Install the entire [`skills/`](./skills/) tree in your agent's Skills directory, following that client's instructions. Keep all three Skill folders and `_shared/` together so cross-Skill references continue to resolve. Installing the npm package alone does not register a Skill unless the package is installed through its plugin marketplace.
177
201
  2. Make Node.js 18+ and `npx` available to the agent, and set `GOOGLE_MAPS_API_KEY` in its environment. Prefer the environment variable to `--apikey`, which can expose a key in shell history or process listings.
178
202
  3. Ask the agent a location question. The Skill guides it to call the standalone CLI, for example `npx -y @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'`. No MCP server or MCP client configuration is needed.
179
203
 
180
- You can verify the CLI is available with `npx -y @cablate/mcp-google-map exec --help` before making a billable API call.
204
+ Run `npx -y @cablate/mcp-google-map doctor` for a non-billable local readiness check. After informing the user that test requests may incur Google Maps Platform charges, `doctor --live` can verify Geocoding, Places API (New), and Routes API independently.
181
205
 
182
- For a reproducible no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md). If your agent or app displays Places reviews, photos, or AI summaries, follow the [content attribution and storage guidance](./skills/google-maps/references/content-attribution.md); the tool preserves source/disclosure metadata but does not render a compliant UI for you.
206
+ For a reproducible no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md). If your agent or app displays Places reviews, photos, or AI summaries, follow the [content attribution and storage guidance](./skills/_shared/content-attribution.md); the tool preserves source/disclosure metadata but does not render a compliant UI for you.
183
207
 
184
208
  ### Batch Geocode
185
209
 
@@ -301,13 +325,18 @@ src/
301
325
  tests/
302
326
  └── smoke.test.ts # Smoke + E2E test suite
303
327
  skills/
304
- └── google-maps/ # Public Agent Skill — standalone CLI workflows
305
- │ ├── SKILL.md # Tool map, recipes, invocation
306
- │ ├── SKILL.skill # Importable archive of this Skill
307
- │ └── references/
308
- │ ├── tools-api.md # Tool parameters + scenario recipes
309
- │ ├── travel-planning.md # Travel planning methodology
310
- │ └── local-seo.md # Local SEO / Google Business Profile ranking analysis
328
+ ├── google-maps/ # General place, route, and environment workflow
329
+ │ ├── SKILL.md
330
+ │ └── references/tools-api.md # Tool parameters + generic recipes
331
+ ├── google-maps-travel-planning/ # Day-trip and itinerary workflow
332
+ │ ├── SKILL.md
333
+ │ └── references/travel-planning.md
334
+ ├── google-maps-local-seo/ # Business visibility and grid-rank workflow
335
+ │ ├── SKILL.md
336
+ │ └── references/local-seo.md
337
+ └── _shared/ # On-demand resources used by all three Skills
338
+ ├── setup-and-diagnostics.md
339
+ └── content-attribution.md
311
340
  .agents/
312
341
  ├── plugins/marketplace.json # CabLate marketplace catalog
313
342
  └── skills/project-docs/ # Maintainer-only development Skill
package/README.zh-TW.md CHANGED
@@ -1,3 +1,13 @@
1
+ # MCP Google Maps
2
+
3
+ 讓 AI 代理可靠地取得 Google Maps 地點搜尋、地址解析、路線、天氣、空氣品質與 Local SEO 資料。你可以安裝 Codex Plugin,讓代理直接呼叫 CLI;也可以透過 MCP 使用同一組 18 個工具。
4
+
5
+ <p align="center"><a href="./README.md">English</a> | <b>繁體中文</b></p>
6
+
7
+ <p align="center">
8
+ <img src="./assets/banner.webp" alt="MCP Google Maps — AI 驅動的地理空間工具" width="800">
9
+ </p>
10
+
1
11
  <p align="center">
2
12
  <a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/v/@cablate/mcp-google-map" alt="npm version"></a>
3
13
  <a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/dm/@cablate/mcp-google-map" alt="npm downloads"></a>
@@ -6,22 +16,45 @@
6
16
  </p>
7
17
 
8
18
  <p align="center">
9
- <img src="./assets/banner.webp" alt="MCP Google Maps — AI 驅動的地理空間工具" width="800">
19
+ <img src="./assets/demo-grid-zh.png" alt="旅行規劃展示 — 京都二日遊、東京戶外一日、日本五日、曼谷背包客" width="800">
10
20
  </p>
11
21
 
12
- <h3 align="center"><b>讓你的 AI 代理理解真實世界 —<br>地理編碼、路線規劃、地點搜尋、空間推理。</b></h3>
22
+ - **18 個工具** — 14 個原子工具與 4 個高階工作流程
23
+ - **三種連接方式** — Codex Plugin、MCP stdio、Streamable HTTP
24
+ - **三個用途明確的 Skills** — 一般地圖、旅行規劃、Local SEO
13
25
 
14
- <p align="center">
15
- <a href="./README.md">English</a> | <b>繁體中文</b>
16
- </p>
26
+ ## 選擇使用方式
17
27
 
18
- <p align="center">
19
- <img src="./assets/demo-grid-zh.png" alt="旅行規劃展示 — 京都二日遊、東京戶外一日、日本五日、曼谷背包客" width="800">
20
- </p>
28
+ | 你的需求 | 建議方式 | 實際執行方式 |
29
+ |---|---|---|
30
+ | 讓 Codex 回答地圖、旅行或 Local SEO 問題,不想設定 MCP | **Codex Plugin** | 符合需求的 Skill 按需載入,並直接呼叫 CLI |
31
+ | 在 Claude Desktop、Cursor、VS Code 或其他本機 MCP client 加入 Google Maps 工具 | **MCP stdio** | Client 啟動一個本機 MCP 程序 |
32
+ | 提供多 session 或遠端 MCP 存取 | **Streamable HTTP** | 自架 HTTP server,端點為 `/mcp` |
33
+
34
+ 三種方式都需要 Node.js 18+ 與 Google Maps Platform API key。地點與路線工作流程還要先在 [Google Cloud Console](https://console.cloud.google.com) 啟用 **Places API (New)** 與 **Routes API**。實際 API 請求可能產生 Google 費用。
35
+
36
+ ## 最快開始:Codex Plugin
37
+
38
+ ```bash
39
+ codex plugin marketplace add cablate/mcp-google-map --ref main
40
+ codex plugin add mcp-google-map@cablate
41
+ ```
42
+
43
+ 在 Codex 的執行環境設定 `GOOGLE_MAPS_API_KEY`,重新開啟對話後直接提出地點問題。先用下列命令檢查本機環境,不會呼叫 Google API:
44
+
45
+ ```bash
46
+ npx -y @cablate/mcp-google-map doctor
47
+ ```
48
+
49
+ 環境正確時,Node.js、套件與 API key 檢查會通過,`live-api` 會顯示略過。只有在你確定要送出可能計費的測試請求時,才使用 `doctor --live`。
21
50
 
22
- - **18 個工具** — 14 個原子工具 + 4 個組合工具(explore-area、plan-route、compare-places、local-rank-tracker)
23
- - **3 種模式** — stdio、StreamableHTTP、獨立 exec CLI
24
- - **Agent Skill** — 內建技能定義,教 AI 如何串接地理工具([`skills/google-maps/`](./skills/google-maps/))
51
+ Plugin **不會**啟動或註冊 MCP server。Codex 執行時先看到三個 Skill 的名稱與描述,只有請求符合時才載入完整指令:
52
+
53
+ - `google-maps` — 地點搜尋、地址解析、路線、區域與環境資訊
54
+ - `google-maps-travel-planning` — 單日與多日旅行行程
55
+ - `google-maps-local-seo` — 商家能見度與地理排名分析
56
+
57
+ 完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。
25
58
 
26
59
  ### vs Google Grounding Lite
27
60
 
@@ -42,7 +75,7 @@
42
75
  | 自架部署 | 有 | 僅 Google 託管 |
43
76
  | Agent Skill | 有 | 無 |
44
77
 
45
- ### 快速開始
78
+ ### CLI 與 server 快速檢查
46
79
 
47
80
  ```bash
48
81
  # stdio(Claude Desktop、Cursor 等)
@@ -85,22 +118,13 @@ npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
85
118
 
86
119
  所有工具標註 `readOnlyHint: true` 和 `destructiveHint: false` — MCP 客戶端可自動核准,無需使用者確認。
87
120
 
88
- > **前置條件**:使用地點相關工具前,請在 [Google Cloud Console](https://console.cloud.google.com) 啟用 **Places API (New)**。
121
+ > **前置條件**:使用地點與路線相關工具前,請在 [Google Cloud Console](https://console.cloud.google.com) 啟用 **Places API (New)** 與 **Routes API**。
89
122
 
90
123
  ## 安裝
91
124
 
92
- ### Codex Plugin(Agent Skill,不需要 MCP)
93
-
94
- 先加入 CabLate marketplace,再安裝只包含 Skill 的 plugin:
95
-
96
- ```bash
97
- codex plugin marketplace add cablate/mcp-google-map --ref main
98
- codex plugin add mcp-google-map@cablate
99
- ```
100
-
101
- 請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin 會教代理直接執行 `npx -y @cablate/mcp-google-map exec ...`;它不會註冊或啟動 MCP server。下方的 MCP 設定仍是另一種獨立使用方式。
125
+ [最快開始](#最快開始codex-plugin)已說明 Codex Plugin。只有在你確定需要 MCP 時,才選擇下列其中一種設定。
102
126
 
103
- ### 方法一:stdio(大多數客戶端推薦)
127
+ ### MCP stdio(本機 client 推薦)
104
128
 
105
129
  適用於 Claude Desktop、Cursor、VS Code 及任何支援 stdio 的 MCP 客戶端:
106
130
 
@@ -131,7 +155,7 @@ codex plugin add mcp-google-map@cablate
131
155
 
132
156
  不設定或設為 `*` 即啟用全部 18 個工具(預設)。
133
157
 
134
- ### 方法二:HTTP Server
158
+ ### Streamable HTTP
135
159
 
136
160
  適用於多 session 部署、per-request API key 隔離或遠端存取:
137
161
 
@@ -169,17 +193,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"台北101"}'
169
193
  npx @cablate/mcp-google-map exec search-places '{"query":"東京拉麵"}'
170
194
  ```
171
195
 
172
- 全部 18 個工具可用:`geocode`、`reverse-geocode`、`search-nearby`、`search-places`、`place-details`、`directions`、`distance-matrix`、`elevation`、`timezone`、`weather`、`air-quality`、`static-map`、`batch-geocode-tool`、`search-along-route`、`explore-area`、`plan-route`、`compare-places`、`local-rank-tracker`。完整參數文件見 [`skills/google-maps/`](./skills/google-maps/)。
196
+ 全部 18 個工具可用:`geocode`、`reverse-geocode`、`search-nearby`、`search-places`、`place-details`、`directions`、`distance-matrix`、`elevation`、`timezone`、`weather`、`air-quality`、`static-map`、`batch-geocode-tool`、`search-along-route`、`explore-area`、`plan-route`、`compare-places`、`local-rank-tracker`。Skill 定義與完整參數文件見 [`skills/`](./skills/)。
173
197
 
174
198
  若要**不透過 MCP、只用 Agent Skill**:
175
199
 
176
- 1. 依你的代理工具說明,將整個 [`skills/google-maps/`](./skills/google-maps/) 資料夾安裝到它的 Skills 目錄;`SKILL.md` 與 `references/` 必須放在一起。支援匯入 `.skill` 的工具也可使用隨附的 [`SKILL.skill`](./skills/google-maps/SKILL.skill)。只安裝 npm 套件不會自動把 Skill 註冊到代理工具。
200
+ 1. 依你的代理工具說明,將整個 [`skills/`](./skills/) 目錄樹安裝到它的 Skills 目錄;三個 Skill 資料夾與 `_shared/` 必須放在一起,跨 Skill 參考才會正常解析。除非透過 Plugin Marketplace 安裝,否則只安裝 npm 套件不會自動把 Skill 註冊到代理工具。
177
201
  2. 讓代理工具可使用 Node.js 18+、`npx`,並在其環境設定 `GOOGLE_MAPS_API_KEY`。建議使用環境變數;`--apikey` 可能讓金鑰出現在 shell 歷史或程序清單。
178
202
  3. 直接向代理工具提問地點問題。Skill 會引導它呼叫獨立 CLI,例如 `npx -y @cablate/mcp-google-map exec geocode '{"address":"台北101"}'`;不必啟動 MCP server 或設定 MCP client。
179
203
 
180
- 呼叫可能計費的 API 前,可先用 `npx -y @cablate/mcp-google-map exec --help` 確認 CLI 可用。
204
+ 先執行 `npx -y @cablate/mcp-google-map doctor`,即可在不呼叫 Google API、不產生 API 費用的情況下檢查本機準備狀態。告知使用者測試請求可能產生 Google Maps Platform 費用後,可用 `doctor --live` 分別驗證 Geocoding、Places API (New) 與 Routes API。
181
205
 
182
- 完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/google-maps/references/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。
206
+ 完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/_shared/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。
183
207
 
184
208
  ### 批次地理編碼
185
209
 
@@ -298,13 +322,18 @@ src/
298
322
  tests/
299
323
  └── smoke.test.ts # Smoke + E2E 測試套件
300
324
  skills/
301
- └── google-maps/ # 公開 Agent Skill — 獨立 CLI 工作流程
302
- │ ├── SKILL.md # 工具對照表、場景食譜、呼叫方式
303
- │ ├── SKILL.skill # 可匯入的 Skill 封裝檔
304
- │ └── references/
305
- │ ├── tools-api.md # 工具參數 + 場景食譜
306
- │ ├── travel-planning.md # 旅行規劃方法論
307
- │ └── local-seo.md # Local SEO / Google 商家排名分析
325
+ ├── google-maps/ # 一般地點、路線與環境資訊工作流程
326
+ │ ├── SKILL.md
327
+ │ └── references/tools-api.md # 工具參數 + 一般場景食譜
328
+ ├── google-maps-travel-planning/ # 單日與多日行程工作流程
329
+ │ ├── SKILL.md
330
+ │ └── references/travel-planning.md
331
+ ├── google-maps-local-seo/ # 商家能見度與地理排名工作流程
332
+ │ ├── SKILL.md
333
+ │ └── references/local-seo.md
334
+ └── _shared/ # 三個 Skills 按需載入的共用資源
335
+ ├── setup-and-diagnostics.md
336
+ └── content-attribution.md
308
337
  .agents/
309
338
  ├── plugins/marketplace.json # CabLate marketplace 目錄
310
339
  └── skills/project-docs/ # 僅供維護者使用的開發 Skill
package/dist/cli.d.ts CHANGED
@@ -9,7 +9,20 @@ interface ExecStreams {
9
9
  stderr: ExecOutput;
10
10
  }
11
11
  type ExecToolRunner = (toolName: string, params: any, apiKey: string) => Promise<unknown>;
12
+ type DoctorCheck = {
13
+ name: string;
14
+ status: "pass" | "fail" | "skip";
15
+ detail: string;
16
+ };
17
+ type DoctorReport = {
18
+ success: boolean;
19
+ version: string;
20
+ live: boolean;
21
+ checks: DoctorCheck[];
22
+ };
23
+ type DoctorToolRunner = (toolName: string, params: any, apiKey: string) => Promise<unknown>;
12
24
  declare function execTool(toolName: string, params: any, apiKey: string): Promise<any>;
13
25
  declare function runExecCommand(toolName: string, params: any, apiKey: string, runner?: ExecToolRunner, streams?: ExecStreams): Promise<number>;
26
+ declare function runDoctor(packageVersion: string, apiKey: string | undefined, live?: boolean, runner?: DoctorToolRunner): Promise<DoctorReport>;
14
27
 
15
- export { EXEC_TOOLS, execTool, runExecCommand, startServer };
28
+ export { type DoctorCheck, type DoctorReport, EXEC_TOOLS, execTool, runDoctor, runExecCommand, startServer };
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import{b as a,c as n}from"./chunk-BKUJWPKE.js";import{config as de}from"dotenv";import{resolve as oe}from"path";import qt from"yargs";import{hideBin as jt}from"yargs/helpers";import{z as A}from"zod";import{AsyncLocalStorage as ye}from"async_hooks";var ae=new ye;function i(){return ae.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function ee(t,e){return ae.run(t,e)}var fe="maps_search_nearby",he="Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.",Ee={center:A.object({value:A.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:A.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:A.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:A.number().default(1e3).describe("Search radius in meters"),openNow:A.boolean().default(!1).describe("Only show places that are currently open"),minRating:A.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function Se(t){try{let e=i(),r=await new a(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
3
- `+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:fe,DESCRIPTION:he,SCHEMA:Ee,ACTION:Se};import{z as ne}from"zod";var be="maps_place_details",Pe="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get reviews, phone, website, and hours. Reviews, photos, and AI summaries include source/disclosure metadata that must accompany end-user display. Set maxPhotos (1-10) to include photo URLs; omit or set 0 for none.",xe={placeId:ne.string().describe("Google Maps place ID"),maxPhotos:ne.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Ae(t){try{let e=i(),r=await new a(e).getPlaceDetails(t.placeId,t.maxPhotos||0);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var C={NAME:be,DESCRIPTION:Pe,SCHEMA:xe,ACTION:Ae};import{z as ve}from"zod";var we="maps_geocode",Oe="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Ce={address:ve.string().describe("Address or place name to convert to coordinates")};async function Me(t){try{let e=i(),r=await new a(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:we,DESCRIPTION:Oe,SCHEMA:Ce,ACTION:Me};import{z as ie}from"zod";var Ne="maps_reverse_geocode",Te="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",_e={latitude:ie.number().describe("Latitude coordinate"),longitude:ie.number().describe("Longitude coordinate")};async function Ie(t){try{let e=i(),r=await new a(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var N={NAME:Ne,DESCRIPTION:Te,SCHEMA:_e,ACTION:Ie};import{z as b}from"zod";var ke="maps_distance_matrix",Re="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",ze={origins:b.array(b.string()).describe("List of origin addresses or coordinates"),destinations:b.array(b.string()).describe("List of destination addresses or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation"),departure_time:b.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."),avoid_tolls:b.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:b.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function He(t){try{let e=i(),r=await new a(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode,t.departure_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:ke,DESCRIPTION:Re,SCHEMA:ze,ACTION:He};import{z as v}from"zod";var De="maps_directions",Ke="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",$e={origin:v.string().describe("Starting point address or coordinates"),destination:v.string().describe("Destination address or coordinates"),mode:v.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:v.string().optional().describe("Departure time (ISO string format)"),arrival_time:v.string().optional().describe("Arrival time (ISO string format)"),avoid_tolls:v.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:v.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function Ge(t){try{let e=i(),r=await new a(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:De,DESCRIPTION:Ke,SCHEMA:$e,ACTION:Ge};import{z as Y}from"zod";var Le="maps_elevation",Je="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",qe={locations:Y.array(Y.object({latitude:Y.number().describe("Latitude coordinate"),longitude:Y.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function je(t){try{let e=i(),r=await new a(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:Le,DESCRIPTION:Je,SCHEMA:qe,ACTION:je};import{z as P}from"zod";var Ue="maps_search_places",Fe="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ze={query:P.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:P.object({latitude:P.number().describe("Latitude to bias results toward"),longitude:P.number().describe("Longitude to bias results toward"),radius:P.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:P.boolean().optional().describe("Only return places that are currently open"),minRating:P.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:P.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Be(t){try{let e=i(),r=await new a(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var k={NAME:Ue,DESCRIPTION:Fe,SCHEMA:Ze,ACTION:Be};import{z as te}from"zod";var We="maps_timezone",Ve="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",Ye={latitude:te.number().describe("Latitude coordinate"),longitude:te.number().describe("Longitude coordinate"),timestamp:te.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Qe(t){try{let e=i(),r=await new a(e).getTimezone(t.latitude,t.longitude,t.timestamp);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var R={NAME:We,DESCRIPTION:Ve,SCHEMA:Ye,ACTION:Qe};import{z}from"zod";var Xe="maps_weather",et="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",tt={latitude:z.number().describe("Latitude coordinate"),longitude:z.number().describe("Longitude coordinate"),type:z.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:z.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:z.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function rt(t){try{let e=i(),r=await new a(e).getWeather(t.latitude,t.longitude,t.type||"current",t.forecastDays,t.forecastHours);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var H={NAME:Xe,DESCRIPTION:et,SCHEMA:tt,ACTION:rt};import{z as D}from"zod";var ot="maps_explore_area",st="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",at={location:D.string().describe("Address or landmark to explore around"),types:D.array(D.string()).optional().describe("Place types to search (default: restaurant, cafe, tourist_attraction). Must be Places API (New) type names. Examples: hotel, bar, park, museum"),radius:D.number().optional().describe("Search radius in meters (default: 1000)"),topN:D.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function nt(t){try{let e=i(),r=await new a(e).exploreArea(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var K={NAME:ot,DESCRIPTION:st,SCHEMA:at,ACTION:nt};import{z as w}from"zod";var it="maps_plan_route",ct="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (2 to 25 intermediate stops) to find the most efficient visit order, and returns directions for each leg. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. Waypoint optimization requires at least 4 stops (2 intermediates); with 2 or 3 stops the route is returned in the original order. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",lt={stops:w.array(w.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:w.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:w.boolean().optional().describe("Auto-optimize visit order via Routes API waypoint optimization (default: true). Requires at least 4 stops (2 intermediates) \u2014 ignored for 2-3 stops. Set false to keep original order. Not available for transit mode."),departure_time:w.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),avoid_tolls:w.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:w.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function pt(t){try{let e=i(),r=await new a(e).planRoute(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var $={NAME:it,DESCRIPTION:ct,SCHEMA:lt,ACTION:pt};import{z as G}from"zod";var dt="maps_compare_places",ut="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",mt={query:G.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:G.object({latitude:G.number().describe("Your latitude"),longitude:G.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:G.number().optional().describe("Max places to compare (default: 5)")};async function gt(t){try{let e=i(),r=await new a(e).comparePlaces(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var L={NAME:dt,DESCRIPTION:ut,SCHEMA:mt,ACTION:gt};import{z as Q}from"zod";var yt="maps_air_quality",ft="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",ht={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),includeHealthRecommendations:Q.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:Q.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function Et(t){try{let e=i(),r=await new a(e).getAirQuality(t.latitude,t.longitude,t.includeHealthRecommendations,t.includePollutants);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:yt,DESCRIPTION:ft,SCHEMA:ht,ACTION:Et};import{z as x}from"zod";var St="maps_static_map",bt="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Pt={center:x.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:x.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:x.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:x.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:x.array(x.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:x.array(x.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function xt(t){try{let e=i(),r=await new a(e).getStaticMap(t);return r.success?{content:[{type:"image",data:r.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${r.data.size} bytes, ${r.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var q={NAME:St,DESCRIPTION:bt,SCHEMA:Pt,ACTION:xt};import{z as ce}from"zod";var At="maps_batch_geocode",vt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",wt={addresses:ce.array(ce.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function Ot(t){try{let e=i(),o=new a(e),r=t.addresses,s=await Promise.all(r.map(async p=>{try{let d=await o.geocode(p);return{address:p,...d}}catch(d){return{address:p,success:!1,error:d.message}}})),c=s.filter(p=>p.success).length,l=s.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:r.length,succeeded:c,failed:l,results:s},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:At,DESCRIPTION:vt,SCHEMA:wt,ACTION:Ot};import{z as U}from"zod";var Ct="maps_search_along_route",Mt="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Nt={textQuery:U.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:U.string().describe("Route start point \u2014 address or landmark name"),destination:U.string().describe("Route end point \u2014 address or landmark name"),mode:U.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:U.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Tt(t){try{let e=i(),r=await new a(e).searchAlongRoute(t);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var F={NAME:Ct,DESCRIPTION:Mt,SCHEMA:Nt,ACTION:Tt};import{z as E}from"zod";var _t="maps_local_rank_tracker",It="Track a business's local search ranking across a geographic grid (like LocalFalcon). Searches the same keyword(s) from multiple coordinates around a center point to see how rank varies by location. Supports up to 3 keywords for batch scanning. Returns rank at each grid point, top-3 competitors per point, and summary metrics (ARP, ATRP, SoLV). Useful for local SEO analysis.",kt={keyword:E.string().optional().describe("Single search keyword (e.g., 'dentist'). Use 'keywords' for multi-keyword scanning."),keywords:E.array(E.string()).min(1).max(3).optional().describe("Array of 1-3 keywords to scan (e.g., ['dentist', 'dental clinic', 'teeth cleaning']). Overrides 'keyword'."),placeId:E.string().describe("Google Maps place_id of the target business to track"),center:E.object({latitude:E.number().describe("Center latitude of the grid"),longitude:E.number().describe("Center longitude of the grid")}).describe("Center coordinate for the grid (typically the business location)"),gridSize:E.number().int().min(3).max(7).optional().describe("Grid dimension (3 = 3\xD73 = 9 points, 5 = 5\xD75 = 25 points, 7 = 7\xD77 = 49 points). Default: 3"),gridSpacing:E.number().min(100).max(1e4).optional().describe("Distance between grid points in meters (100-10000). Default: 1000")};async function Rt(t){try{let e=t.keywords||(t.keyword?[t.keyword]:[]);if(e.length===0)return{content:[{type:"text",text:"Either 'keyword' or 'keywords' must be provided."}],isError:!0};let o=i(),s=await new a(o).localRankTracker({...t,keywords:e});return s.success?{content:[{type:"text",text:JSON.stringify(s.data,null,2)}],isError:!1}:{content:[{type:"text",text:s.error||"Failed to track local rank"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error tracking local rank: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var Z={NAME:_t,DESCRIPTION:It,SCHEMA:kt,ACTION:Rt};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},zt=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:u,action:t=>O.ACTION(t)},{name:C.NAME,description:C.DESCRIPTION,schema:C.SCHEMA,annotations:u,action:t=>C.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:t=>M.ACTION(t)},{name:N.NAME,description:N.DESCRIPTION,schema:N.SCHEMA,annotations:u,action:t=>N.ACTION(t)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:u,action:t=>T.ACTION(t)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:u,action:t=>_.ACTION(t)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:u,action:t=>I.ACTION(t)},{name:k.NAME,description:k.DESCRIPTION,schema:k.SCHEMA,annotations:u,action:t=>k.ACTION(t)},{name:R.NAME,description:R.DESCRIPTION,schema:R.SCHEMA,annotations:u,action:t=>R.ACTION(t)},{name:H.NAME,description:H.DESCRIPTION,schema:H.SCHEMA,annotations:u,action:t=>H.ACTION(t)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:u,action:t=>K.ACTION(t)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:u,action:t=>$.ACTION(t)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:u,action:t=>L.ACTION(t)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:u,action:t=>J.ACTION(t)},{name:q.NAME,description:q.DESCRIPTION,schema:q.SCHEMA,annotations:u,action:t=>q.ACTION(t)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:u,action:t=>j.ACTION(t)},{name:F.NAME,description:F.DESCRIPTION,schema:F.SCHEMA,annotations:u,action:t=>F.ACTION(t)},{name:Z.NAME,description:Z.DESCRIPTION,schema:Z.SCHEMA,annotations:u,action:t=>Z.ACTION(t)}]}];function re(t){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return t;let o=new Set(e.split(",").map(s=>s.trim()).filter(Boolean)),r=t.filter(s=>o.has(s.name));return r.length===0?(n.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${t.map(s=>s.name).join(", ")}`),t):(n.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${r.length}/${t.length} tools active`),r)}var X=zt;import{McpServer as Ht}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Dt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Kt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as $t}from"@modelcontextprotocol/sdk/types.js";import le from"express";import{randomUUID as Gt}from"crypto";import{z as Lt}from"zod";var B=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let s=e.headers.authorization;if(s&&s.startsWith("Bearer "))return s.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Jt="0.0.1",W=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Ht({name:this.serverName,version:Jt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Lt.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,s,c)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,s,c),n.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e,o="0.0.0.0"){let r=le();r.use(le.json()),r.post("/mcp",async(l,p)=>{let d=l.headers["mcp-session-id"],m,S=B.getInstance().getApiKey(l,d?this.sessions[d]?.apiKey:void 0);if(n.log(`${this.serverName} API key received from request context`),d&&this.sessions[d])m=this.sessions[d],S&&(m.apiKey=S);else if(!d&&$t(l.body)){let f=new Kt({sessionIdGenerator:()=>Gt(),onsessioninitialized:h=>{this.sessions[h]=m,n.log(`[${this.serverName}] New session initialized: ${h}`)}});m={transport:f,apiKey:S},f.onclose=()=>{f.sessionId&&(delete this.sessions[f.sessionId],n.log(`[${this.serverName}] Session closed: ${f.sessionId}`))},await this.createMcpServer().connect(f)}else{p.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p,l.body)})});let s=async(l,p)=>{let d=l.headers["mcp-session-id"];if(!d||!this.sessions[d]){p.status(400).send("Invalid or missing session ID");return}let m=this.sessions[d],S=B.getInstance().getApiKey(l,m.apiKey);S&&(m.apiKey=S),await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p)})};r.get("/mcp",s),r.delete("/mcp",s);let c=o==="0.0.0.0"?"localhost":o;this.httpServer=r.listen(e,o,()=>{n.log(`[${this.serverName}] HTTP server listening on ${o}:${e}`),n.log(`[${this.serverName}] MCP endpoint available at http://${c}:${e}/mcp`)})}async startStdio(){let e=new Dt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){n.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){n.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}n.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let s=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(s).then(()=>{n.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{n.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Ut}from"url";import{dirname as Ft}from"path";import{readFileSync as pe,writeFileSync as Zt,existsSync as Bt}from"fs";import{createInterface as Wt}from"readline";var Vt=Ut(import.meta.url),ue=Ft(Vt);de({path:oe(process.cwd(),".env")});de({path:oe(ue,"../.env")});async function Yt(t,e,o){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),o&&(process.env.MCP_SERVER_HOST=o),n.log("\u{1F680} Starting Google Maps MCP Server..."),n.log("\u{1F4CD} 18 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),n.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),n.log("");let r=X.map(async s=>{let c=process.env[s.portEnvVar];if(!c){n.error(`\u26A0\uFE0F [${s.name}] Port environment variable ${s.portEnvVar} not set.`),n.log(`\u{1F4A1} Please set ${s.portEnvVar} in your .env file or use --port parameter.`),n.log(` Example: ${s.portEnvVar}=3000 or --port 3000`);return}let l=Number(c);if(isNaN(l)||l<=0){n.error(`\u274C [${s.name}] Invalid port number "${c}" defined in ${s.portEnvVar}.`);return}try{let p=new W(s.name,re(s.tools)),d=process.env.MCP_SERVER_HOST||"0.0.0.0";n.log(`\u{1F527} [${s.name}] Initializing MCP Server in HTTP mode on ${d}:${l}...`),await p.startHttpServer(l,d);let m=d==="0.0.0.0"?"localhost":d;n.log(`\u2705 [${s.name}] MCP Server started successfully!`),n.log(` \u{1F310} Endpoint: http://${m}:${l}/mcp`),n.log(` \u{1F4DA} Tools: ${s.tools.length} available`)}catch(p){n.error(`\u274C [${s.name}] Failed to start MCP Server on port ${l}:`,p)}});await Promise.allSettled(r),n.log(""),n.log("\u{1F389} Server initialization completed!"),n.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var me=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route","local-rank-tracker"];function Qt(t){return typeof t=="object"&&t!==null&&"success"in t&&t.success===!1}function Xt(t,e){return typeof e.error=="string"&&e.error.trim().length>0?e.error:e.error!==void 0&&e.error!==null?String(e.error):`Tool "${t}" returned a failure response`}async function er(t,e,o){let r=new a(o);switch(t){case"geocode":case"maps_geocode":return r.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return r.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return r.searchNearby(e);case"search-places":case"maps_search_places":return r.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return r.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return r.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time,e.avoid_tolls,e.avoid_highways);case"distance-matrix":case"maps_distance_matrix":return r.calculateDistanceMatrix(e.origins,e.destinations,e.mode,e.departure_time,e.avoid_tolls,e.avoid_highways);case"elevation":case"maps_elevation":return r.getElevation(e.locations);case"timezone":case"maps_timezone":return r.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return r.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return r.exploreArea(e);case"plan-route":case"maps_plan_route":return r.planRoute(e);case"compare-places":case"maps_compare_places":return r.comparePlaces(e);case"air-quality":case"maps_air_quality":return r.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return r.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let s=await Promise.all(e.addresses.map(async l=>{try{let p=await r.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=s.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:s}}}case"search-along-route":case"maps_search_along_route":return r.searchAlongRoute(e);case"local-rank-tracker":case"maps_local_rank_tracker":return r.localRankTracker(e);default:throw new Error(`Unknown tool: ${t}. Available: ${me.join(", ")}`)}}async function tr(t,e,o,r=er,s={stdout:process.stdout,stderr:process.stderr}){try{let c=await r(t,e,o);return Qt(c)?(s.stderr.write(JSON.stringify({error:Xt(t,c)},null,2)+`
4
- `),1):(s.stdout.write(JSON.stringify(c,null,2)+`
5
- `),0)}catch(c){return s.stderr.write(JSON.stringify({error:c instanceof Error?c.message:String(c)},null,2)+`
6
- `),1}}var rr=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),or=import.meta.url===`file://${process.argv[1]}`;if(rr||or){let t="0.0.0";try{let e=oe(ue,"../package.json");t=JSON.parse(pe(e,"utf-8")).version}catch{t="0.0.0"}qt(jt(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${me.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{if(!e.apikey){process.stderr.write(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)+`
2
+ import{b as n,c as i}from"./chunk-BKUJWPKE.js";import{config as de}from"dotenv";import{resolve as oe}from"path";import Ft from"yargs";import{hideBin as Zt}from"yargs/helpers";import{z as A}from"zod";import{AsyncLocalStorage as Ee}from"async_hooks";var ae=new Ee;function c(){return ae.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function ee(t,e){return ae.run(t,e)}var Se="maps_search_nearby",be="Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.",Pe={center:A.object({value:A.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:A.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:A.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:A.number().default(1e3).describe("Search radius in meters"),openNow:A.boolean().default(!1).describe("Only show places that are currently open"),minRating:A.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function xe(t){try{let e=c(),r=await new n(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
3
+ `+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:Se,DESCRIPTION:be,SCHEMA:Pe,ACTION:xe};import{z as ne}from"zod";var Ae="maps_place_details",ve="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get reviews, phone, website, and hours. Reviews, photos, and AI summaries include source/disclosure metadata that must accompany end-user display. Set maxPhotos (1-10) to include photo URLs; omit or set 0 for none.",we={placeId:ne.string().describe("Google Maps place ID"),maxPhotos:ne.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Oe(t){try{let e=c(),r=await new n(e).getPlaceDetails(t.placeId,t.maxPhotos||0);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var C={NAME:Ae,DESCRIPTION:ve,SCHEMA:we,ACTION:Oe};import{z as Ce}from"zod";var Me="maps_geocode",Ne="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",_e={address:Ce.string().describe("Address or place name to convert to coordinates")};async function Ie(t){try{let e=c(),r=await new n(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:Me,DESCRIPTION:Ne,SCHEMA:_e,ACTION:Ie};import{z as ie}from"zod";var Te="maps_reverse_geocode",ke="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",Re={latitude:ie.number().describe("Latitude coordinate"),longitude:ie.number().describe("Longitude coordinate")};async function ze(t){try{let e=c(),r=await new n(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var N={NAME:Te,DESCRIPTION:ke,SCHEMA:Re,ACTION:ze};import{z as b}from"zod";var De="maps_distance_matrix",Ge="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",He={origins:b.array(b.string()).describe("List of origin addresses or coordinates"),destinations:b.array(b.string()).describe("List of destination addresses or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation"),departure_time:b.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."),avoid_tolls:b.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:b.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function Ke(t){try{let e=c(),r=await new n(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode,t.departure_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:De,DESCRIPTION:Ge,SCHEMA:He,ACTION:Ke};import{z as v}from"zod";var $e="maps_directions",Le="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",Je={origin:v.string().describe("Starting point address or coordinates"),destination:v.string().describe("Destination address or coordinates"),mode:v.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:v.string().optional().describe("Departure time (ISO string format)"),arrival_time:v.string().optional().describe("Arrival time (ISO string format)"),avoid_tolls:v.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:v.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function qe(t){try{let e=c(),r=await new n(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:$e,DESCRIPTION:Le,SCHEMA:Je,ACTION:qe};import{z as V}from"zod";var je="maps_elevation",Ue="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",Fe={locations:V.array(V.object({latitude:V.number().describe("Latitude coordinate"),longitude:V.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Ze(t){try{let e=c(),r=await new n(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:je,DESCRIPTION:Ue,SCHEMA:Fe,ACTION:Ze};import{z as P}from"zod";var Be="maps_search_places",Ye="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",We={query:P.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:P.object({latitude:P.number().describe("Latitude to bias results toward"),longitude:P.number().describe("Longitude to bias results toward"),radius:P.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:P.boolean().optional().describe("Only return places that are currently open"),minRating:P.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:P.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Ve(t){try{let e=c(),r=await new n(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var k={NAME:Be,DESCRIPTION:Ye,SCHEMA:We,ACTION:Ve};import{z as te}from"zod";var Qe="maps_timezone",Xe="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",et={latitude:te.number().describe("Latitude coordinate"),longitude:te.number().describe("Longitude coordinate"),timestamp:te.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function tt(t){try{let e=c(),r=await new n(e).getTimezone(t.latitude,t.longitude,t.timestamp);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var R={NAME:Qe,DESCRIPTION:Xe,SCHEMA:et,ACTION:tt};import{z}from"zod";var rt="maps_weather",ot="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",st={latitude:z.number().describe("Latitude coordinate"),longitude:z.number().describe("Longitude coordinate"),type:z.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:z.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:z.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function at(t){try{let e=c(),r=await new n(e).getWeather(t.latitude,t.longitude,t.type||"current",t.forecastDays,t.forecastHours);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var D={NAME:rt,DESCRIPTION:ot,SCHEMA:st,ACTION:at};import{z as G}from"zod";var nt="maps_explore_area",it="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",ct={location:G.string().describe("Address or landmark to explore around"),types:G.array(G.string()).optional().describe("Place types to search (default: restaurant, cafe, tourist_attraction). Must be Places API (New) type names. Examples: hotel, bar, park, museum"),radius:G.number().optional().describe("Search radius in meters (default: 1000)"),topN:G.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function lt(t){try{let e=c(),r=await new n(e).exploreArea(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var H={NAME:nt,DESCRIPTION:it,SCHEMA:ct,ACTION:lt};import{z as w}from"zod";var pt="maps_plan_route",dt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (2 to 25 intermediate stops) to find the most efficient visit order, and returns directions for each leg. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. Waypoint optimization requires at least 4 stops (2 intermediates); with 2 or 3 stops the route is returned in the original order. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",ut={stops:w.array(w.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:w.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:w.boolean().optional().describe("Auto-optimize visit order via Routes API waypoint optimization (default: true). Requires at least 4 stops (2 intermediates) \u2014 ignored for 2-3 stops. Set false to keep original order. Not available for transit mode."),departure_time:w.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),avoid_tolls:w.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:w.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function mt(t){try{let e=c(),r=await new n(e).planRoute(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:pt,DESCRIPTION:dt,SCHEMA:ut,ACTION:mt};import{z as $}from"zod";var gt="maps_compare_places",yt="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",ft={query:$.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:$.object({latitude:$.number().describe("Your latitude"),longitude:$.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:$.number().optional().describe("Max places to compare (default: 5)")};async function ht(t){try{let e=c(),r=await new n(e).comparePlaces(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var L={NAME:gt,DESCRIPTION:yt,SCHEMA:ft,ACTION:ht};import{z as Q}from"zod";var Et="maps_air_quality",St="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",bt={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),includeHealthRecommendations:Q.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:Q.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function Pt(t){try{let e=c(),r=await new n(e).getAirQuality(t.latitude,t.longitude,t.includeHealthRecommendations,t.includePollutants);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:Et,DESCRIPTION:St,SCHEMA:bt,ACTION:Pt};import{z as x}from"zod";var xt="maps_static_map",At="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",vt={center:x.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:x.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:x.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:x.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:x.array(x.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:x.array(x.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function wt(t){try{let e=c(),r=await new n(e).getStaticMap(t);return r.success?{content:[{type:"image",data:r.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${r.data.size} bytes, ${r.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var q={NAME:xt,DESCRIPTION:At,SCHEMA:vt,ACTION:wt};import{z as ce}from"zod";var Ot="maps_batch_geocode",Ct="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",Mt={addresses:ce.array(ce.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function Nt(t){try{let e=c(),o=new n(e),r=t.addresses,s=await Promise.all(r.map(async p=>{try{let d=await o.geocode(p);return{address:p,...d}}catch(d){return{address:p,success:!1,error:d.message}}})),a=s.filter(p=>p.success).length,l=s.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:r.length,succeeded:a,failed:l,results:s},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:Ot,DESCRIPTION:Ct,SCHEMA:Mt,ACTION:Nt};import{z as U}from"zod";var _t="maps_search_along_route",It="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Tt={textQuery:U.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:U.string().describe("Route start point \u2014 address or landmark name"),destination:U.string().describe("Route end point \u2014 address or landmark name"),mode:U.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:U.number().optional().describe("Max results to return (default: 5, max: 20)")};async function kt(t){try{let e=c(),r=await new n(e).searchAlongRoute(t);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var F={NAME:_t,DESCRIPTION:It,SCHEMA:Tt,ACTION:kt};import{z as E}from"zod";var Rt="maps_local_rank_tracker",zt="Track a business's local search ranking across a geographic grid (like LocalFalcon). Searches the same keyword(s) from multiple coordinates around a center point to see how rank varies by location. Supports up to 3 keywords for batch scanning. Returns rank at each grid point, top-3 competitors per point, and summary metrics (ARP, ATRP, SoLV). Useful for local SEO analysis.",Dt={keyword:E.string().optional().describe("Single search keyword (e.g., 'dentist'). Use 'keywords' for multi-keyword scanning."),keywords:E.array(E.string()).min(1).max(3).optional().describe("Array of 1-3 keywords to scan (e.g., ['dentist', 'dental clinic', 'teeth cleaning']). Overrides 'keyword'."),placeId:E.string().describe("Google Maps place_id of the target business to track"),center:E.object({latitude:E.number().describe("Center latitude of the grid"),longitude:E.number().describe("Center longitude of the grid")}).describe("Center coordinate for the grid (typically the business location)"),gridSize:E.number().int().min(3).max(7).optional().describe("Grid dimension (3 = 3\xD73 = 9 points, 5 = 5\xD75 = 25 points, 7 = 7\xD77 = 49 points). Default: 3"),gridSpacing:E.number().min(100).max(1e4).optional().describe("Distance between grid points in meters (100-10000). Default: 1000")};async function Gt(t){try{let e=t.keywords||(t.keyword?[t.keyword]:[]);if(e.length===0)return{content:[{type:"text",text:"Either 'keyword' or 'keywords' must be provided."}],isError:!0};let o=c(),s=await new n(o).localRankTracker({...t,keywords:e});return s.success?{content:[{type:"text",text:JSON.stringify(s.data,null,2)}],isError:!1}:{content:[{type:"text",text:s.error||"Failed to track local rank"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error tracking local rank: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var Z={NAME:Rt,DESCRIPTION:zt,SCHEMA:Dt,ACTION:Gt};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Ht=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:u,action:t=>O.ACTION(t)},{name:C.NAME,description:C.DESCRIPTION,schema:C.SCHEMA,annotations:u,action:t=>C.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:t=>M.ACTION(t)},{name:N.NAME,description:N.DESCRIPTION,schema:N.SCHEMA,annotations:u,action:t=>N.ACTION(t)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:u,action:t=>_.ACTION(t)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:u,action:t=>I.ACTION(t)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:u,action:t=>T.ACTION(t)},{name:k.NAME,description:k.DESCRIPTION,schema:k.SCHEMA,annotations:u,action:t=>k.ACTION(t)},{name:R.NAME,description:R.DESCRIPTION,schema:R.SCHEMA,annotations:u,action:t=>R.ACTION(t)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:u,action:t=>D.ACTION(t)},{name:H.NAME,description:H.DESCRIPTION,schema:H.SCHEMA,annotations:u,action:t=>H.ACTION(t)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:u,action:t=>K.ACTION(t)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:u,action:t=>L.ACTION(t)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:u,action:t=>J.ACTION(t)},{name:q.NAME,description:q.DESCRIPTION,schema:q.SCHEMA,annotations:u,action:t=>q.ACTION(t)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:u,action:t=>j.ACTION(t)},{name:F.NAME,description:F.DESCRIPTION,schema:F.SCHEMA,annotations:u,action:t=>F.ACTION(t)},{name:Z.NAME,description:Z.DESCRIPTION,schema:Z.SCHEMA,annotations:u,action:t=>Z.ACTION(t)}]}];function re(t){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return t;let o=new Set(e.split(",").map(s=>s.trim()).filter(Boolean)),r=t.filter(s=>o.has(s.name));return r.length===0?(i.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${t.map(s=>s.name).join(", ")}`),t):(i.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${r.length}/${t.length} tools active`),r)}var X=Ht;import{McpServer as Kt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as $t}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Lt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Jt}from"@modelcontextprotocol/sdk/types.js";import le from"express";import{randomUUID as qt}from"crypto";import{z as jt}from"zod";var B=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let s=e.headers.authorization;if(s&&s.startsWith("Bearer "))return s.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Ut="0.0.1",Y=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Kt({name:this.serverName,version:Ut},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:jt.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,s,a)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,s,a),i.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e,o="0.0.0.0"){let r=le();r.use(le.json()),r.post("/mcp",async(l,p)=>{let d=l.headers["mcp-session-id"],m,S=B.getInstance().getApiKey(l,d?this.sessions[d]?.apiKey:void 0);if(i.log(`${this.serverName} API key received from request context`),d&&this.sessions[d])m=this.sessions[d],S&&(m.apiKey=S);else if(!d&&Jt(l.body)){let f=new Lt({sessionIdGenerator:()=>qt(),onsessioninitialized:h=>{this.sessions[h]=m,i.log(`[${this.serverName}] New session initialized: ${h}`)}});m={transport:f,apiKey:S},f.onclose=()=>{f.sessionId&&(delete this.sessions[f.sessionId],i.log(`[${this.serverName}] Session closed: ${f.sessionId}`))},await this.createMcpServer().connect(f)}else{p.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p,l.body)})});let s=async(l,p)=>{let d=l.headers["mcp-session-id"];if(!d||!this.sessions[d]){p.status(400).send("Invalid or missing session ID");return}let m=this.sessions[d],S=B.getInstance().getApiKey(l,m.apiKey);S&&(m.apiKey=S),await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p)})};r.get("/mcp",s),r.delete("/mcp",s);let a=o==="0.0.0.0"?"localhost":o;this.httpServer=r.listen(e,o,()=>{i.log(`[${this.serverName}] HTTP server listening on ${o}:${e}`),i.log(`[${this.serverName}] MCP endpoint available at http://${a}:${e}/mcp`)})}async startStdio(){let e=new $t;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){i.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){i.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}i.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let s=Object.values(this.sessions).map(a=>(a.transport.sessionId&&delete this.sessions[a.transport.sessionId],Promise.resolve()));Promise.all(s).then(()=>{i.log(`[${this.serverName}] All transports closed.`),e()}).catch(a=>{i.error(`[${this.serverName}] Error during bulk transport closing:`,a),o(a)})})})}};import{fileURLToPath as Bt}from"url";import{dirname as Yt}from"path";import{readFileSync as pe,writeFileSync as Wt,existsSync as Vt}from"fs";import{createInterface as Qt}from"readline";var Xt=Bt(import.meta.url),ue=Yt(Xt);de({path:oe(process.cwd(),".env")});de({path:oe(ue,"../.env")});async function er(t,e,o){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),o&&(process.env.MCP_SERVER_HOST=o),i.log("\u{1F680} Starting Google Maps MCP Server..."),i.log("\u{1F4CD} 18 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),i.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),i.log("");let r=X.map(async s=>{let a=process.env[s.portEnvVar];if(!a){i.error(`\u26A0\uFE0F [${s.name}] Port environment variable ${s.portEnvVar} not set.`),i.log(`\u{1F4A1} Please set ${s.portEnvVar} in your .env file or use --port parameter.`),i.log(` Example: ${s.portEnvVar}=3000 or --port 3000`);return}let l=Number(a);if(isNaN(l)||l<=0){i.error(`\u274C [${s.name}] Invalid port number "${a}" defined in ${s.portEnvVar}.`);return}try{let p=new Y(s.name,re(s.tools)),d=process.env.MCP_SERVER_HOST||"0.0.0.0";i.log(`\u{1F527} [${s.name}] Initializing MCP Server in HTTP mode on ${d}:${l}...`),await p.startHttpServer(l,d);let m=d==="0.0.0.0"?"localhost":d;i.log(`\u2705 [${s.name}] MCP Server started successfully!`),i.log(` \u{1F310} Endpoint: http://${m}:${l}/mcp`),i.log(` \u{1F4DA} Tools: ${s.tools.length} available`)}catch(p){i.error(`\u274C [${s.name}] Failed to start MCP Server on port ${l}:`,p)}});await Promise.allSettled(r),i.log(""),i.log("\u{1F389} Server initialization completed!"),i.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var me=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route","local-rank-tracker"];function ge(t){return typeof t=="object"&&t!==null&&"success"in t&&t.success===!1}function ye(t,e){return typeof e.error=="string"&&e.error.trim().length>0?e.error:e.error!==void 0&&e.error!==null?String(e.error):`Tool "${t}" returned a failure response`}async function fe(t,e,o){let r=new n(o);switch(t){case"geocode":case"maps_geocode":return r.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return r.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return r.searchNearby(e);case"search-places":case"maps_search_places":return r.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return r.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return r.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time,e.avoid_tolls,e.avoid_highways);case"distance-matrix":case"maps_distance_matrix":return r.calculateDistanceMatrix(e.origins,e.destinations,e.mode,e.departure_time,e.avoid_tolls,e.avoid_highways);case"elevation":case"maps_elevation":return r.getElevation(e.locations);case"timezone":case"maps_timezone":return r.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return r.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return r.exploreArea(e);case"plan-route":case"maps_plan_route":return r.planRoute(e);case"compare-places":case"maps_compare_places":return r.comparePlaces(e);case"air-quality":case"maps_air_quality":return r.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return r.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let s=await Promise.all(e.addresses.map(async l=>{try{let p=await r.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),a=s.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:a,failed:e.addresses.length-a,results:s}}}case"search-along-route":case"maps_search_along_route":return r.searchAlongRoute(e);case"local-rank-tracker":case"maps_local_rank_tracker":return r.localRankTracker(e);default:throw new Error(`Unknown tool: ${t}. Available: ${me.join(", ")}`)}}async function tr(t,e,o,r=fe,s={stdout:process.stdout,stderr:process.stderr}){try{let a=await r(t,e,o);return ge(a)?(s.stderr.write(JSON.stringify({error:ye(t,a)},null,2)+`
4
+ `),1):(s.stdout.write(JSON.stringify(a,null,2)+`
5
+ `),0)}catch(a){return s.stderr.write(JSON.stringify({error:a instanceof Error?a.message:String(a)},null,2)+`
6
+ `),1}}async function rr(t,e,o=!1,r=fe){let a=[{name:"node",status:Number.parseInt(process.versions.node.split(".")[0]??"0",10)>=18?"pass":"fail",detail:`Node.js ${process.versions.node} detected; version 18 or newer is required.`},{name:"package",status:t!=="0.0.0"?"pass":"fail",detail:t!=="0.0.0"?`@cablate/mcp-google-map ${t} is available.`:"Could not read the installed package version."},{name:"api-key",status:e?"pass":"fail",detail:e?"GOOGLE_MAPS_API_KEY is configured (value hidden).":"GOOGLE_MAPS_API_KEY is not configured. Set it in the environment before making API calls."}];if(!o)a.push({name:"live-api",status:"skip",detail:"Live API checks were not requested; no Google Maps API calls were made."});else if(!e)a.push({name:"live-api",status:"skip",detail:"Live API checks require GOOGLE_MAPS_API_KEY."});else{let l=[{name:"geocoding-api",tool:"geocode",params:{address:"Tokyo Tower"}},{name:"places-api-new",tool:"search-places",params:{query:"Tokyo Tower"}},{name:"routes-api",tool:"directions",params:{origin:"Tokyo Tower",destination:"Tokyo Station",mode:"walking"}}];for(let p of l)try{let d=await r(p.tool,p.params,e);ge(d)?a.push({name:p.name,status:"fail",detail:ye(p.tool,d)}):a.push({name:p.name,status:"pass",detail:`${p.name} request succeeded.`})}catch(d){a.push({name:p.name,status:"fail",detail:d instanceof Error?d.message:String(d)})}}return{success:a.every(l=>l.status!=="fail"),version:t,live:o,checks:a}}var or=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),sr=import.meta.url===`file://${process.argv[1]}`;if(or||sr){let t="0.0.0";try{let e=oe(ue,"../package.json");t=JSON.parse(pe(e,"utf-8")).version}catch{t="0.0.0"}Ft(Zt(process.argv)).command("doctor","Check standalone CLI readiness without starting an MCP server",e=>e.option("live",{type:"boolean",default:!1,description:"Make billable test requests to Geocoding, Places (New), and Routes APIs"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key (prefer GOOGLE_MAPS_API_KEY to avoid shell history exposure)",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 doctor","Check local runtime and API key configuration without network calls"],["$0 doctor --live","Also verify three Google Maps APIs with billable requests"]]),async e=>{let o=await rr(t,e.apikey,e.live);process.stdout.write(JSON.stringify(o,null,2)+`
7
+ `),process.exitCode=o.success?0:1}).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${me.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{if(!e.apikey){process.stderr.write(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)+`
7
8
  `),process.exitCode=1;return}try{let o=e.params?JSON.parse(e.params):{};process.exitCode=await tr(e.tool,o,e.apikey)}catch(o){process.stderr.write(JSON.stringify({error:o.message},null,2)+`
8
- `),process.exitCode=1}}).command("batch-geocode","Geocode multiple addresses from a file (one address per line)",e=>e.option("input",{alias:"i",type:"string",describe:"Input file path (one address per line). Use - for stdin.",demandOption:!0}).option("output",{alias:"o",type:"string",describe:"Output file path (JSON). Defaults to stdout."}).option("concurrency",{alias:"c",type:"number",describe:"Max parallel requests",default:20}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 batch-geocode -i addresses.txt","Geocode to stdout"],["$0 batch-geocode -i addresses.txt -o results.json","Geocode to file"],["cat addresses.txt | $0 batch-geocode -i -","Geocode from stdin"]]),async e=>{e.apikey||(console.error("Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var."),process.exit(1));let o;if(e.input==="-"){let g=Wt({input:process.stdin});o=[];for await(let h of g){let y=h.trim();y&&o.push(y)}}else Bt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=pe(e.input,"utf-8").split(`
9
- `).map(g=>g.trim()).filter(g=>g.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let r=new a(e.apikey),s=Math.min(Math.max(e.concurrency,1),50),c=[],l=0,p=async(g,h)=>{let y=[];for(let ge of g){let se=ge().then(()=>{y.splice(y.indexOf(se),1)});y.push(se),y.length>=h&&await Promise.race(y)}await Promise.all(y)},d=o.map((g,h)=>async()=>{try{let y=await r.geocode(g);c[h]={address:g,...y}}catch(y){c[h]={address:g,success:!1,error:y.message}}l++,e.output&&process.stderr.write(`\r ${l}/${o.length} geocoded`)});await p(d,s),e.output&&process.stderr.write(`
10
- `);let m=c.filter(g=>g.success).length,V=c.filter(g=>!g.success).length,S={total:o.length,succeeded:m,failed:V,results:c},f=JSON.stringify(S,null,2);e.output?(Zt(e.output,f,"utf-8"),console.error(`Done: ${m}/${o.length} succeeded. Output: ${e.output}`)):console.log(f),process.exit(V>0?1:0)}).command("$0","Start the MCP server (HTTP by default, --stdio for stdio mode)",e=>e.option("port",{alias:"p",type:"number",description:"Port to run the MCP server on",default:process.env.MCP_SERVER_PORT?parseInt(process.env.MCP_SERVER_PORT):3e3}).option("host",{type:"string",description:"Hostname to bind the server to (e.g. 0.0.0.0 for all interfaces)",default:process.env.MCP_SERVER_HOST||"0.0.0.0"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).option("stdio",{type:"boolean",description:"Use stdio transport instead of HTTP",default:!1}).example([["$0","Start HTTP server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start HTTP with custom port and API key"],["$0 --host 0.0.0.0 --port 3000","Start HTTP accessible from all interfaces"],["$0 --stdio","Start in stdio mode (for Claude Desktop, Cursor, etc.)"]]),async e=>{e.apikey&&(process.env.GOOGLE_MAPS_API_KEY=e.apikey);let o=re(X[0].tools);e.stdio?await new W(X[0].name,o).startStdio():(n.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),n.log(" A Model Context Protocol server for Google Maps services"),n.log(""),e.apikey||(n.log("\u26A0\uFE0F Google Maps API Key not found!"),n.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),n.log("")),Yt(e.port,e.apikey,e.host).catch(r=>{n.error("\u274C Failed to start server:",r),process.exit(1)}))}).version(t).alias("version","v").help().parse()}export{me as EXEC_TOOLS,er as execTool,tr as runExecCommand,Yt as startServer};
9
+ `),process.exitCode=1}}).command("batch-geocode","Geocode multiple addresses from a file (one address per line)",e=>e.option("input",{alias:"i",type:"string",describe:"Input file path (one address per line). Use - for stdin.",demandOption:!0}).option("output",{alias:"o",type:"string",describe:"Output file path (JSON). Defaults to stdout."}).option("concurrency",{alias:"c",type:"number",describe:"Max parallel requests",default:20}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 batch-geocode -i addresses.txt","Geocode to stdout"],["$0 batch-geocode -i addresses.txt -o results.json","Geocode to file"],["cat addresses.txt | $0 batch-geocode -i -","Geocode from stdin"]]),async e=>{e.apikey||(console.error("Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var."),process.exit(1));let o;if(e.input==="-"){let g=Qt({input:process.stdin});o=[];for await(let h of g){let y=h.trim();y&&o.push(y)}}else Vt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=pe(e.input,"utf-8").split(`
10
+ `).map(g=>g.trim()).filter(g=>g.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let r=new n(e.apikey),s=Math.min(Math.max(e.concurrency,1),50),a=[],l=0,p=async(g,h)=>{let y=[];for(let he of g){let se=he().then(()=>{y.splice(y.indexOf(se),1)});y.push(se),y.length>=h&&await Promise.race(y)}await Promise.all(y)},d=o.map((g,h)=>async()=>{try{let y=await r.geocode(g);a[h]={address:g,...y}}catch(y){a[h]={address:g,success:!1,error:y.message}}l++,e.output&&process.stderr.write(`\r ${l}/${o.length} geocoded`)});await p(d,s),e.output&&process.stderr.write(`
11
+ `);let m=a.filter(g=>g.success).length,W=a.filter(g=>!g.success).length,S={total:o.length,succeeded:m,failed:W,results:a},f=JSON.stringify(S,null,2);e.output?(Wt(e.output,f,"utf-8"),console.error(`Done: ${m}/${o.length} succeeded. Output: ${e.output}`)):console.log(f),process.exit(W>0?1:0)}).command("$0","Start the MCP server (HTTP by default, --stdio for stdio mode)",e=>e.option("port",{alias:"p",type:"number",description:"Port to run the MCP server on",default:process.env.MCP_SERVER_PORT?parseInt(process.env.MCP_SERVER_PORT):3e3}).option("host",{type:"string",description:"Hostname to bind the server to (e.g. 0.0.0.0 for all interfaces)",default:process.env.MCP_SERVER_HOST||"0.0.0.0"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).option("stdio",{type:"boolean",description:"Use stdio transport instead of HTTP",default:!1}).example([["$0","Start HTTP server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start HTTP with custom port and API key"],["$0 --host 0.0.0.0 --port 3000","Start HTTP accessible from all interfaces"],["$0 --stdio","Start in stdio mode (for Claude Desktop, Cursor, etc.)"]]),async e=>{e.apikey&&(process.env.GOOGLE_MAPS_API_KEY=e.apikey);let o=re(X[0].tools);e.stdio?await new Y(X[0].name,o).startStdio():(i.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),i.log(" A Model Context Protocol server for Google Maps services"),i.log(""),e.apikey||(i.log("\u26A0\uFE0F Google Maps API Key not found!"),i.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),i.log("")),er(e.port,e.apikey,e.host).catch(r=>{i.error("\u274C Failed to start server:",r),process.exit(1)}))}).version(t).alias("version","v").help().parse()}export{me as EXEC_TOOLS,fe as execTool,rr as runDoctor,tr as runExecCommand,er as startServer};
@@ -1,24 +1,37 @@
1
1
  # Agent Skill demo (no MCP server)
2
2
 
3
- This walkthrough demonstrates the two pieces separately: the Agent Skill tells an agent which geographic tool to use, and the package CLI performs the API call. It does not require an MCP client or server. Build and run the CLI from the same repository revision as the Skill so the instructions and executable stay aligned.
3
+ This walkthrough verifies the no-MCP path end to end: the plugin's Agent Skills choose a geographic workflow, and the npm package CLI performs the API call. You do not need an MCP client or server.
4
4
 
5
- ## 1. Install the Skill
5
+ ## 1. Install the Skills
6
6
 
7
- Clone this repository at a chosen release tag, then copy the whole `skills/google-maps/` directory into your agent's Skills directory according to that client's instructions. Keep `SKILL.md` and `references/` together. If the client accepts `.skill` archives, import `skills/google-maps/SKILL.skill` instead. Installing npm alone does not register the Skill.
7
+ Install the Codex plugin from the CabLate marketplace:
8
8
 
9
- Check that your agent can discover `google-maps` and run shell commands. Provide a Google Maps Platform API key to the agent's environment as `GOOGLE_MAPS_API_KEY` through your normal secret-management method; never paste it into the prompt or commit it. Node.js 18+ and `npx` must be available.
9
+ ```bash
10
+ codex plugin marketplace add cablate/mcp-google-map --ref main
11
+ codex plugin add mcp-google-map@cablate
12
+ ```
13
+
14
+ Start a new conversation after installation. For a manual installation in another Skill-compatible agent, clone a chosen release tag and copy the whole `skills/` tree according to that client's instructions. Keep the three Skill folders and `_shared/` together. Installing the npm package alone does not register a Skill.
15
+
16
+ Check that your agent can discover `google-maps`, `google-maps-travel-planning`, and `google-maps-local-seo`, and can run shell commands. Provide a Google Maps Platform API key to the agent's environment as `GOOGLE_MAPS_API_KEY` through your normal secret-management method; never paste it into the prompt or commit it. Node.js 18+ and `npx` must be available. Enable Places API (New) and Routes API for workflows that use them.
10
17
 
11
18
  ## 2. Validate without an API call
12
19
 
13
- From the cloned repository, install and build the package, then run its CLI help:
20
+ For a marketplace installation, run:
21
+
22
+ ```bash
23
+ npx -y @cablate/mcp-google-map doctor
24
+ ```
25
+
26
+ For a cloned repository, install and build the package, then run the same check against the checkout:
14
27
 
15
28
  ```bash
16
29
  npm ci
17
30
  npm run build
18
- node dist/cli.js exec --help
31
+ node dist/cli.js doctor
19
32
  ```
20
33
 
21
- The help should list `geocode` and `search-places`. This only checks CLI availability; it does not prove that your key or Google APIs are enabled.
34
+ Success means the Node.js, package, and API-key checks pass and `live-api` is skipped. This check makes no Google API requests. To test Geocoding, Places (New), and Routes, run the same command with `--live`; those calls may be billable.
22
35
 
23
36
  ## 3. Try one live request
24
37
 
@@ -34,6 +47,6 @@ node dist/cli.js exec geocode '{"address":"Tokyo Tower"}'
34
47
 
35
48
  Success means the CLI exits 0 and returns JSON with `success: true`, a `data.location` latitude/longitude, and a `data.place_id`. The exact coordinates and address may change; do not compare them to a hard-coded snapshot. A failed call exits nonzero and writes JSON to stderr. This step calls a billable Google API using your key.
36
49
 
37
- For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map exec --help`. Running `npx` inside a checkout of the same package can resolve the local package instead of the published executable.
50
+ For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map doctor`. Running `npx` inside a checkout of the same package can resolve the local package instead of the published executable.
38
51
 
39
- If you ask for place reviews, photos, or an AI summary in a later step, read the Skill's [content-attribution guidance](../skills/google-maps/references/content-attribution.md) before displaying them. This geocoding example does not validate every Google Maps Platform policy requirement for a downstream app.
52
+ If you ask for place reviews, photos, or an AI summary in a later step, read the shared [content-attribution guidance](../skills/_shared/content-attribution.md) before displaying them. This geocoding example does not validate every Google Maps Platform policy requirement for a downstream app.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/mcp-google-map",
3
- "version": "0.0.64",
3
+ "version": "0.0.67",
4
4
  "mcpName": "io.github.cablate/google-map",
5
5
  "description": "18 Google Maps tools for AI agents — geocode, search, directions, weather, air quality, local rank tracking, map images via MCP server or standalone CLI",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "mcp-google-map",
4
- "version": "0.0.64",
5
- "description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.",
4
+ "version": "0.0.67",
5
+ "description": "Use Google Maps for place research, travel planning, and local SEO through focused Skills and a standalone CLI—no MCP setup required.",
6
6
  "author": {
7
7
  "name": "CabLate",
8
8
  "url": "https://github.com/cablate"
@@ -0,0 +1,59 @@
1
+ # Setup and diagnostics
2
+
3
+ Use this procedure before the first Google Maps CLI call in a session and when a call fails. Do not start an MCP server: these Skills use the standalone CLI.
4
+
5
+ ## Local preflight
6
+
7
+ Run the local doctor first. It makes no Google API requests and does not print the key:
8
+
9
+ ```bash
10
+ npx -y @cablate/mcp-google-map doctor
11
+ ```
12
+
13
+ Interpret its JSON checks:
14
+
15
+ - `node`: Node.js 18 or newer is required.
16
+ - `package`: the npm package and CLI entrypoint were resolved.
17
+ - `api-key`: `GOOGLE_MAPS_API_KEY` is visible to the agent process; the value stays hidden.
18
+ - `live-api: skip`: expected during local-only diagnosis and not a failure.
19
+
20
+ If the key is missing, ask the user to set `GOOGLE_MAPS_API_KEY` in the environment used to launch the agent and start a new session. Prefer the environment variable to `--apikey`; command-line keys may appear in shell history or process listings.
21
+
22
+ ## Google Cloud prerequisites
23
+
24
+ The key must belong to a Google Cloud project with billing enabled and restrictions compatible with the environment. Enable only the APIs needed by the requested tools:
25
+
26
+ | Capability | Google Maps Platform API |
27
+ |---|---|
28
+ | Address geocoding and reverse geocoding | Geocoding API |
29
+ | Text search, nearby search, place details, along-route place search, comparisons, local rank tracking | Places API (New) |
30
+ | Directions, distance matrix, waypoint optimization, along-route routing | Routes API |
31
+ | Elevation | Elevation API |
32
+ | Timezone | Time Zone API |
33
+ | Weather | Weather API |
34
+ | Air quality | Air Quality API |
35
+ | Static maps | Maps Static API |
36
+
37
+ ## Optional live diagnosis
38
+
39
+ Only run this after telling the user that it makes billable test requests:
40
+
41
+ ```bash
42
+ npx -y @cablate/mcp-google-map doctor --live
43
+ ```
44
+
45
+ It independently tests Geocoding API, Places API (New), and Routes API so one disabled capability does not hide the others. It does not validate every optional API in the table. Test an optional capability with the smallest relevant tool call only when the user's task needs it.
46
+
47
+ ## Failure classification
48
+
49
+ | Symptom | Likely cause | Next action |
50
+ |---|---|---|
51
+ | Package or command cannot be found | Node.js/npm unavailable, registry blocked, or package resolution failed | Repair Node/npm or registry access, then rerun local doctor |
52
+ | API key missing | Environment variable was not passed to the agent process | Set the variable and start a new session |
53
+ | `REQUEST_DENIED`, permission, or API-disabled error | API disabled, billing unavailable, or key restriction mismatch | Use the failed check name to review that API and the key restrictions in Google Cloud Console |
54
+ | `OVER_QUERY_LIMIT`, quota, or rate-limit error | Project quota or billing limit | Stop retries and ask the user to review quota/billing |
55
+ | Invalid request | Wrong parameters or unsupported mode | Read the selected tool's parameter reference and correct the request |
56
+ | Network or timeout error | Connectivity, proxy, DNS, or transient service issue | Report the observed failure; retry only when reasonable and requested |
57
+ | One live check fails while others pass | Capability-specific API configuration | Fix only the named API; do not claim the entire key is invalid |
58
+
59
+ Never include the API key in diagnostic output, copied commands, bug reports, or final answers.
@@ -1,84 +1,43 @@
1
1
  ---
2
2
  name: google-maps
3
- description: Use Google Maps-backed geocoding, place search, routing, and geographic analysis through the standalone @cablate/mcp-google-map CLI when a user asks about real-world locations. No MCP connection is required.
3
+ description: Search places, resolve addresses, compare routes, inspect neighborhoods, and retrieve geographic or environmental facts through the standalone @cablate/mcp-google-map CLI. Use for concrete location questions; use the travel-planning or local-seo Skill for those specialized outcomes.
4
4
  license: MIT
5
5
  ---
6
6
 
7
- # Google Maps - Geospatial Query Capabilities
7
+ # Google Maps
8
8
 
9
- ## Overview
9
+ Answer real-world location questions with the package's standalone `exec` CLI. Do not start or configure an MCP server for this workflow.
10
10
 
11
- Use the package's `exec` CLI to answer location questions without starting an MCP server. The Skill supplies tool-selection and chaining guidance; the npm package makes the API calls. The user must provide their own Google Maps Platform API key.
11
+ ## Before calling the CLI
12
12
 
13
- ---
14
-
15
- ## Core Principles
16
-
17
- | Principle | Explanation |
18
- |-----------|-------------|
19
- | Chain over single-shot | Most geo questions require 2-5 tool calls chained together. See Scenario Recipes in references/tools-api.md for the full patterns. |
20
- | Match recipe to intent | Map the user's question to a recipe (Trip Planning, Local Discovery, Route Comparison, Neighborhood Analysis, Multi-Stop, Place Comparison, Along the Route) before calling any tool. |
21
- | Precise input saves trouble | Use coordinates over address strings when available. Use place_id over name search. More precise input = more reliable output. |
22
- | Output is structured | Every tool returns JSON. Use it directly for downstream computation or comparison — no extra parsing needed. |
23
- | Present results clearly | Summarize comparisons in a table when it helps; do not pass raw JSON through as the final answer. |
24
- | Preserve source context | Google Maps content has display, attribution, and retention conditions. Read `references/content-attribution.md` before presenting place reviews, photos, or AI summaries. |
25
-
26
- ---
27
-
28
- ## Tool Map
29
-
30
- 18 tools in five categories — pick by scenario:
31
-
32
- ### Place Discovery
33
- | Tool | When to use | Example |
34
- |------|-------------|---------|
35
- | `maps_geocode` | Have an address/landmark, need coordinates | "What are the coordinates of Tokyo Tower?" |
36
- | `maps_reverse_geocode` | Have coordinates, need an address | "What's at 35.65, 139.74?" |
37
- | `maps_search_nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" |
38
- | `maps_search_places` | Natural language place search | "Best ramen in Tokyo" |
39
- | `maps_place_details` | Have a place_id, need full info (+ optional photo URLs via `maxPhotos`) | "Opening hours and reviews for this restaurant?" |
40
- | `maps_batch_geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" |
41
-
42
- ### Routing & Distance
43
- | Tool | When to use | Example |
44
- |------|-------------|---------|
45
- | `maps_directions` | How to get from A to B, including drive-only avoid-tolls/highways controls | "Route from Taipei Main Station to airport avoiding tolls" |
46
- | `maps_distance_matrix` | Compare distances across multiple points, including drive-only avoid-tolls/highways controls | "Which of these 3 hotels is closest to airport without highways?" |
47
- | `maps_search_along_route` | Find places along a route (meals, stops) ranked by detour time | "Restaurants between Fushimi Inari and Kiyomizu-dera" |
13
+ - On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight.
14
+ - The user must supply `GOOGLE_MAPS_API_KEY`. Never print it or place it in a command unless the user explicitly accepts shell-history and process-list exposure.
15
+ - Before presenting Places reviews, photos, or AI summaries, read `../_shared/content-attribution.md`.
48
16
 
49
- ### Environment
50
- | Tool | When to use | Example |
51
- |------|-------------|---------|
52
- | `maps_elevation` | Query altitude | "Elevation profile along this hiking trail" |
53
- | `maps_timezone` | Need local time at a destination | "What time is it in Tokyo?" |
54
- | `maps_weather` | Weather at a location (current or forecast) | "What's the weather in Paris?" |
55
- | `maps_air_quality` | AQI, pollutants, health recommendations | "Is the air safe for jogging?" |
17
+ ## Workflow
56
18
 
57
- ### Visualization
58
- | Tool | When to use | Example |
59
- |------|-------------|---------|
60
- | `maps_static_map` | Show locations/routes on a map image | "Show me these places on a map" |
19
+ 1. Identify the concrete location outcome and missing inputs. Do not request details that can be resolved by geocoding or place search.
20
+ 2. Read `references/tools-api.md` when exact parameters, response fields, or a multi-tool recipe are needed.
21
+ 3. Prefer coordinates over ambiguous addresses and `place_id` over repeated name searches.
22
+ 4. Chain only the calls needed for the outcome. Reuse returned identifiers and coordinates.
23
+ 5. Treat nonzero exit status or `{ "success": false }` as a failure, not as geographic data.
24
+ 6. Summarize the useful result rather than returning raw JSON. Preserve source and attribution metadata where required.
61
25
 
62
- ### Composite (one-call shortcuts)
63
- | Tool | When to use | Example |
64
- |------|-------------|---------|
65
- | `maps_explore_area` | Overview of a neighborhood | "What's around Tokyo Tower?" |
66
- | `maps_plan_route` | Multi-stop optimized itinerary (Routes API waypoint optimization, up to 25 stops) with drive-only avoid-tolls/highways controls | "Visit these 5 places efficiently without tolls" |
67
- | `maps_compare_places` | Side-by-side comparison | "Which ramen shop near Shibuya?" |
68
- | `maps_local_rank_tracker` | Local SEO grid rank tracking | "How does this dentist rank across the area?" |
26
+ ## Tool selection
69
27
 
70
- ---
71
-
72
- ## Known API Limitations
73
-
74
- | Tool | Limitation | Workaround |
75
- |------|-----------|------------|
76
- | `maps_weather` | Unsupported regions: Japan, China, South Korea, Cuba, Iran, North Korea, Syria | Use web search for weather in these regions |
77
- | `maps_distance_matrix` | Transit mode may return null in some regions | Fall back to `driving` or `walking` mode, or use `maps_directions` for transit |
78
- | `maps_plan_route` | Transit mode does not support waypoint optimization | Set `optimize: false` for transit mode |
79
- | `maps_air_quality` | Works globally including Japan (unlike weather) | — |
80
-
81
- ---
28
+ | Outcome | Preferred tools |
29
+ |---|---|
30
+ | Address or landmark to coordinates | `maps_geocode` |
31
+ | Coordinates to address | `maps_reverse_geocode` |
32
+ | Nearby discovery | `maps_search_nearby`, then `maps_place_details` |
33
+ | Natural-language place search | `maps_search_places`, then `maps_place_details` |
34
+ | Compare candidate places | `maps_compare_places` or search → details → distance matrix |
35
+ | Route or travel-mode comparison | `maps_directions`, `maps_distance_matrix` |
36
+ | Multi-stop route without a broader itinerary | `maps_plan_route` |
37
+ | Things along a route | `maps_search_along_route` |
38
+ | Neighborhood facts | `maps_explore_area` plus targeted nearby searches |
39
+ | Elevation, timezone, weather, or air quality | corresponding environment tool; geocode first when needed |
40
+ | Visual map | `maps_static_map` after locations or a route are known |
82
41
 
83
42
  ## Invocation
84
43
 
@@ -86,28 +45,18 @@ Use the package's `exec` CLI to answer location questions without starting an MC
86
45
  npx -y @cablate/mcp-google-map exec <tool> '<json_params>'
87
46
  ```
88
47
 
89
- - **API Key**: Set `GOOGLE_MAPS_API_KEY` in the environment. The `-k` flag also works, but may expose the key in shell history or process listings. If no key is available, ask the user to configure one; do not invent results.
90
- - **Output**: Successful calls return JSON on stdout. A failed call exits nonzero with error details on stderr; report the failure rather than treating it as map data.
91
- - **Stateless**: each call is independent
92
- - **Tool names**: CLI accepts both `maps_geocode` and `geocode` short forms
48
+ The CLI accepts both MCP-style names such as `maps_geocode` and short names such as `geocode`. Each call is stateless and returns JSON on stdout.
93
49
 
94
- ---
95
-
96
- ## When to Update This Skill
50
+ ## Boundaries
97
51
 
98
- | Trigger | What to update |
99
- |---------|----------------|
100
- | New tool added to the package | Tool Map table + references/tools-api.md |
101
- | Tool parameters changed | references/tools-api.md |
102
- | New chaining pattern discovered in practice | references/tools-api.md chaining section |
103
-
104
- ---
52
+ - For a multi-day itinerary, trip schedule, or travel-day optimization, use `google-maps-travel-planning` instead.
53
+ - For Google Business Profile visibility, grid ranking, keyword coverage, or competitor audits, use `google-maps-local-seo` instead.
54
+ - `maps_weather` is unavailable in Japan, China, South Korea, Cuba, Iran, North Korea, and Syria; use a current web source there.
55
+ - Transit waypoint optimization is unsupported; use `optimize: false` for transit routes.
56
+ - Geographic proximity and elevation alone do not establish safety, accessibility, flood risk, or suitability. State those limitations when relevant.
105
57
 
106
- ## Reference
58
+ ## References
107
59
 
108
- | File | Content | When to read |
109
- |------|---------|--------------|
110
- | `references/tools-api.md` | Full parameter specs, response formats, 7 scenario recipes, and decision guide | When you need exact parameters, response shapes, or multi-tool workflow patterns |
111
- | `references/travel-planning.md` | Travel planning methodology — 6-layer model, Search Along Route, anti-patterns | When planning multi-day trips — **read before Recipe 1** |
112
- | `references/local-seo.md` | Local SEO / Google Business Profile ranking analysis — competitor audit, keyword landscape, gap analysis | When analyzing business rankings, comparing competitors, or scouting locations |
113
- | `references/content-attribution.md` | Google Maps content attribution, source links, AI disclosure, and storage limits | Before presenting reviews, photos, or AI summaries from Places |
60
+ - `references/tools-api.md`: read for exact parameters, response shapes, and generic chaining recipes.
61
+ - `../_shared/setup-and-diagnostics.md`: read for first-run checks and error classification.
62
+ - `../_shared/content-attribution.md`: read before displaying regulated Google Maps content.
@@ -156,7 +156,7 @@ Response includes (when available from Google):
156
156
  | `reviews[].author_uri`, `author_photo_uri`, `google_maps_uri` | string | Author profile/avatar and individual review source |
157
157
  | `photos[].author_attributions`, `google_maps_uri` | object[], string | Photo credits and individual photo source, returned with `maxPhotos > 0` |
158
158
 
159
- Before showing reviews, photos, or AI summaries to end users, read [`content-attribution.md`](./content-attribution.md). The old legacy-review merge was removed because it could not supply a per-review Google Maps source link; reviews now come from Places API (New) only.
159
+ Before showing reviews, photos, or AI summaries to end users, read [`content-attribution.md`](../../_shared/content-attribution.md). The old legacy-review merge was removed because it could not supply a per-review Google Maps source link; reviews now come from Places API (New) only.
160
160
 
161
161
  ---
162
162
 
@@ -494,7 +494,7 @@ Use these recipes when the user's question maps to a multi-step workflow. Think
494
494
 
495
495
  This is the most common complex scenario. The goal is a time-ordered itinerary with routes between stops.
496
496
 
497
- > **Read `references/travel-planning.md` first** — it contains the full methodology, anti-patterns, and time budget guidelines.
497
+ > This workflow belongs to the `google-maps-travel-planning` Skill. Read its `references/travel-planning.md` before planning an itinerary.
498
498
 
499
499
  **Steps:**
500
500
  1. `maps_search_places` — Search "top attractions in {city}" → geographically diverse **anchor points**
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: google-maps-local-seo
3
+ description: Analyze Google Business Profile visibility, geographic grid rankings, local search keywords, and nearby competitors through the standalone Google Maps CLI. Use for local SEO or location-strategy decisions, not ordinary place recommendations.
4
+ license: MIT
5
+ ---
6
+
7
+ # Google Maps Local SEO
8
+
9
+ Produce a reproducible local-search visibility analysis grounded in Google Maps results. Use the standalone CLI; no MCP connection is required.
10
+
11
+ ## Before calling the CLI
12
+
13
+ - On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight.
14
+ - The user must supply `GOOGLE_MAPS_API_KEY`. Never expose its value.
15
+ - Read `../_shared/content-attribution.md` before presenting reviews, photos, or Google-generated summaries.
16
+
17
+ ## Workflow
18
+
19
+ 1. Confirm the target business or `place_id`, service area, relevant keywords, grid size, and decision the user needs to make.
20
+ 2. Read `references/local-seo.md` before running an audit.
21
+ 3. Resolve the target with `maps_search_places` and `maps_place_details`; do not assume businesses with similar names are identical.
22
+ 4. Use `maps_local_rank_tracker` for geographic rankings. Keep the keyword, center, spacing, and grid size with every reported result.
23
+ 5. Use place search, comparison, and area exploration to identify competitors and evidence for gaps.
24
+ 6. Separate observed API results from recommendations. Explain ARP, ATRP, SoLV, unfound grid points, and sample limitations.
25
+ 7. Present prioritized actions tied to the observed evidence; do not promise ranking improvements.
26
+
27
+ ## CLI
28
+
29
+ ```bash
30
+ npx -y @cablate/mcp-google-map exec <tool> '<json_params>'
31
+ ```
32
+
33
+ For exact parameters, read the relevant sections of `../google-maps/references/tools-api.md`.
34
+
35
+ ## Boundaries
36
+
37
+ - Ordinary nearby-place discovery and place comparison belong to `google-maps`.
38
+ - Consumer travel itineraries belong to `google-maps-travel-planning`.
39
+ - Grid results are a sampled snapshot, not an exhaustive or stable representation of Google ranking.
40
+ - Do not recommend deceptive reviews, keyword stuffing, impersonation, or other policy-violating tactics.
41
+
42
+ ## References
43
+
44
+ - `references/local-seo.md`: required audit method, competitor analysis, metrics, and reporting structure.
45
+ - `../google-maps/references/tools-api.md`: selected tool parameters and response shapes.
46
+ - `../_shared/setup-and-diagnostics.md`: first-run and failure recovery.
47
+ - `../_shared/content-attribution.md`: Places presentation requirements.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: google-maps-travel-planning
3
+ description: Build evidence-backed day trips, multi-day itineraries, and efficient stop sequences with Google Maps data through the standalone CLI. Use when the desired output is a practical travel plan, not merely a route or list of places.
4
+ license: MIT
5
+ ---
6
+
7
+ # Google Maps Travel Planning
8
+
9
+ Build a time-ordered itinerary whose geography, travel times, opening constraints, and along-route stops have been checked. Use the standalone CLI; no MCP connection is required.
10
+
11
+ ## Before calling the CLI
12
+
13
+ - On the first call in a session, or after an execution failure, read `../_shared/setup-and-diagnostics.md` and run the non-billable local preflight.
14
+ - The user must supply `GOOGLE_MAPS_API_KEY`. Never expose its value.
15
+ - Read `../_shared/content-attribution.md` before presenting reviews, photos, or Google-generated summaries.
16
+
17
+ ## Workflow
18
+
19
+ 1. Establish destination, dates or duration, fixed anchors, mobility constraints, pace, and relevant preferences. Ask only for omissions that materially change the itinerary.
20
+ 2. Read `references/travel-planning.md` before designing the itinerary.
21
+ 3. Use `maps_search_places` to find candidate anchors, then group nearby anchors into one-direction daily arcs.
22
+ 4. Use `maps_search_along_route` for meals and breaks between anchors. Use `maps_place_details` for hours, ratings, and source metadata.
23
+ 5. Validate every day with `maps_plan_route` or `maps_directions`. Do not present estimated timing as validated timing.
24
+ 6. Check weather and air quality when they affect the plan. Respect regional weather limitations described in the reference.
25
+ 7. When useful, produce a numbered static map after the stop order is final.
26
+ 8. Present a day-by-day schedule with travel time, visit duration, reservations or opening-hour caveats, and clearly marked fallbacks.
27
+
28
+ ## CLI
29
+
30
+ ```bash
31
+ npx -y @cablate/mcp-google-map exec <tool> '<json_params>'
32
+ ```
33
+
34
+ For exact tool parameters, read `../google-maps/references/tools-api.md` only for the tools selected by the plan.
35
+
36
+ ## Boundaries
37
+
38
+ - A request for only A-to-B directions or a simple multi-stop ordering belongs to `google-maps`.
39
+ - A request to assess business visibility or local search rankings belongs to `google-maps-local-seo`.
40
+ - Do not infer that a venue is accessible, open, safe, or suitable when the API response does not establish it.
41
+ - Do not silently substitute web estimates for unavailable Google Maps results; identify the alternate source.
42
+
43
+ ## References
44
+
45
+ - `references/travel-planning.md`: required methodology, timing budgets, geographic arcs, and anti-patterns.
46
+ - `../google-maps/references/tools-api.md`: exact parameters and response shapes for selected tools.
47
+ - `../_shared/setup-and-diagnostics.md`: first-run and failure recovery.
48
+ - `../_shared/content-attribution.md`: Places presentation requirements.
Binary file