@cablate/mcp-google-map 0.0.64 → 0.0.65
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.
- package/.codex-plugin/plugin.json +6 -6
- package/README.md +25 -14
- package/README.zh-TW.md +25 -14
- package/dist/cli.d.ts +14 -1
- package/dist/cli.js +9 -8
- package/examples/agent-skill-demo.md +9 -9
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/skills/_shared/setup-and-diagnostics.md +59 -0
- package/skills/google-maps/SKILL.md +39 -90
- package/skills/google-maps/references/tools-api.md +2 -2
- package/skills/google-maps-local-seo/SKILL.md +47 -0
- package/skills/google-maps-travel-planning/SKILL.md +48 -0
- package/skills/google-maps/SKILL.skill +0 -0
- /package/skills/{google-maps/references → _shared}/content-attribution.md +0 -0
- /package/skills/{google-maps → google-maps-local-seo}/references/local-seo.md +0 -0
- /package/skills/{google-maps → google-maps-travel-planning}/references/travel-planning.md +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-map",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Use Google Maps
|
|
3
|
+
"version": "0.0.65",
|
|
4
|
+
"description": "Use focused Skills for Google Maps place research, travel planning, and local SEO through a standalone CLI.",
|
|
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": "
|
|
24
|
-
"longDescription": "
|
|
23
|
+
"shortDescription": "Focused maps, travel planning, and local SEO workflows through a standalone CLI.",
|
|
24
|
+
"longDescription": "Three focused Skills guide an AI agent to use the @cablate/mcp-google-map CLI for general geospatial research, evidence-backed travel planning, and local SEO analysis. No MCP connection is required.",
|
|
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
|
|
34
|
-
"
|
|
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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
- **18 tools** — 14 atomic + 4 composite (explore-area, plan-route, compare-places, local-rank-tracker)
|
|
23
23
|
- **3 modes** — stdio, StreamableHTTP, standalone exec CLI
|
|
24
|
-
- **Agent
|
|
24
|
+
- **3 Agent Skills** — focused workflows for general maps, travel planning, and local SEO ([`skills/`](./skills/))
|
|
25
25
|
|
|
26
26
|
### vs Google Grounding Lite
|
|
27
27
|
|
|
@@ -89,7 +89,7 @@ All tools are annotated with `readOnlyHint: true` and `destructiveHint: false`
|
|
|
89
89
|
|
|
90
90
|
## Installation
|
|
91
91
|
|
|
92
|
-
### Codex Plugin (Agent
|
|
92
|
+
### Codex Plugin (3 Agent Skills, no MCP required)
|
|
93
93
|
|
|
94
94
|
Install the CabLate marketplace, then install the Skill-only plugin:
|
|
95
95
|
|
|
@@ -98,7 +98,13 @@ codex plugin marketplace add cablate/mcp-google-map --ref main
|
|
|
98
98
|
codex plugin add mcp-google-map@cablate
|
|
99
99
|
```
|
|
100
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.
|
|
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 discovers three focused Skills and 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.
|
|
102
|
+
|
|
103
|
+
At runtime, Codex sees the name and description of each Skill and loads the full instructions only when the request matches:
|
|
104
|
+
|
|
105
|
+
- `google-maps` — place search, geocoding, routes, neighborhood and environmental facts
|
|
106
|
+
- `google-maps-travel-planning` — day trips and multi-day itineraries
|
|
107
|
+
- `google-maps-local-seo` — Google Business Profile visibility and geographic rank analysis
|
|
102
108
|
|
|
103
109
|
### Method 1: stdio (Recommended for most clients)
|
|
104
110
|
|
|
@@ -169,17 +175,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
|
|
|
169
175
|
npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
|
|
170
176
|
```
|
|
171
177
|
|
|
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
|
|
178
|
+
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
179
|
|
|
174
180
|
To use the **Agent Skill without MCP**:
|
|
175
181
|
|
|
176
|
-
1. Install the entire [`skills
|
|
182
|
+
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
183
|
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
184
|
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
185
|
|
|
180
|
-
|
|
186
|
+
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
187
|
|
|
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/
|
|
188
|
+
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
189
|
|
|
184
190
|
### Batch Geocode
|
|
185
191
|
|
|
@@ -301,13 +307,18 @@ src/
|
|
|
301
307
|
tests/
|
|
302
308
|
└── smoke.test.ts # Smoke + E2E test suite
|
|
303
309
|
skills/
|
|
304
|
-
|
|
305
|
-
│ ├── SKILL.md
|
|
306
|
-
│
|
|
307
|
-
|
|
308
|
-
│
|
|
309
|
-
│
|
|
310
|
-
|
|
310
|
+
├── google-maps/ # General place, route, and environment workflow
|
|
311
|
+
│ ├── SKILL.md
|
|
312
|
+
│ └── references/tools-api.md # Tool parameters + generic recipes
|
|
313
|
+
├── google-maps-travel-planning/ # Day-trip and itinerary workflow
|
|
314
|
+
│ ├── SKILL.md
|
|
315
|
+
│ └── references/travel-planning.md
|
|
316
|
+
├── google-maps-local-seo/ # Business visibility and grid-rank workflow
|
|
317
|
+
│ ├── SKILL.md
|
|
318
|
+
│ └── references/local-seo.md
|
|
319
|
+
└── _shared/ # On-demand resources used by all three Skills
|
|
320
|
+
├── setup-and-diagnostics.md
|
|
321
|
+
└── content-attribution.md
|
|
311
322
|
.agents/
|
|
312
323
|
├── plugins/marketplace.json # CabLate marketplace catalog
|
|
313
324
|
└── skills/project-docs/ # Maintainer-only development Skill
|
package/README.zh-TW.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
- **18 個工具** — 14 個原子工具 + 4 個組合工具(explore-area、plan-route、compare-places、local-rank-tracker)
|
|
23
23
|
- **3 種模式** — stdio、StreamableHTTP、獨立 exec CLI
|
|
24
|
-
- **Agent
|
|
24
|
+
- **3 個 Agent Skills** — 分別處理一般地圖、旅行規劃與 Local SEO([`skills/`](./skills/))
|
|
25
25
|
|
|
26
26
|
### vs Google Grounding Lite
|
|
27
27
|
|
|
@@ -89,7 +89,7 @@ npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
|
|
|
89
89
|
|
|
90
90
|
## 安裝
|
|
91
91
|
|
|
92
|
-
### Codex Plugin(Agent
|
|
92
|
+
### Codex Plugin(3 個 Agent Skills,不需要 MCP)
|
|
93
93
|
|
|
94
94
|
先加入 CabLate marketplace,再安裝只包含 Skill 的 plugin:
|
|
95
95
|
|
|
@@ -98,7 +98,13 @@ codex plugin marketplace add cablate/mcp-google-map --ref main
|
|
|
98
98
|
codex plugin add mcp-google-map@cablate
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin
|
|
101
|
+
請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin 會探索三個用途明確的 Skills,並教代理直接執行 `npx -y @cablate/mcp-google-map exec ...`;它不會註冊或啟動 MCP server。下方的 MCP 設定仍是另一種獨立使用方式。
|
|
102
|
+
|
|
103
|
+
執行階段會先看到每個 Skill 的名稱與描述,只有請求符合時才載入完整指令:
|
|
104
|
+
|
|
105
|
+
- `google-maps` — 地點搜尋、地址解析、路線、區域與環境資訊
|
|
106
|
+
- `google-maps-travel-planning` — 單日與多日旅行行程
|
|
107
|
+
- `google-maps-local-seo` — Google 商家檔案能見度與地理排名分析
|
|
102
108
|
|
|
103
109
|
### 方法一:stdio(大多數客戶端推薦)
|
|
104
110
|
|
|
@@ -169,17 +175,17 @@ npx @cablate/mcp-google-map exec geocode '{"address":"台北101"}'
|
|
|
169
175
|
npx @cablate/mcp-google-map exec search-places '{"query":"東京拉麵"}'
|
|
170
176
|
```
|
|
171
177
|
|
|
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
|
|
178
|
+
全部 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
179
|
|
|
174
180
|
若要**不透過 MCP、只用 Agent Skill**:
|
|
175
181
|
|
|
176
|
-
1. 依你的代理工具說明,將整個 [`skills
|
|
182
|
+
1. 依你的代理工具說明,將整個 [`skills/`](./skills/) 目錄樹安裝到它的 Skills 目錄;三個 Skill 資料夾與 `_shared/` 必須放在一起,跨 Skill 參考才會正常解析。除非透過 Plugin Marketplace 安裝,否則只安裝 npm 套件不會自動把 Skill 註冊到代理工具。
|
|
177
183
|
2. 讓代理工具可使用 Node.js 18+、`npx`,並在其環境設定 `GOOGLE_MAPS_API_KEY`。建議使用環境變數;`--apikey` 可能讓金鑰出現在 shell 歷史或程序清單。
|
|
178
184
|
3. 直接向代理工具提問地點問題。Skill 會引導它呼叫獨立 CLI,例如 `npx -y @cablate/mcp-google-map exec geocode '{"address":"台北101"}'`;不必啟動 MCP server 或設定 MCP client。
|
|
179
185
|
|
|
180
|
-
|
|
186
|
+
先執行 `npx -y @cablate/mcp-google-map doctor`,即可在不呼叫 Google API、不產生 API 費用的情況下檢查本機準備狀態。告知使用者測試請求可能產生 Google Maps Platform 費用後,可用 `doctor --live` 分別驗證 Geocoding、Places API (New) 與 Routes API。
|
|
181
187
|
|
|
182
|
-
完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/
|
|
188
|
+
完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/_shared/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。
|
|
183
189
|
|
|
184
190
|
### 批次地理編碼
|
|
185
191
|
|
|
@@ -298,13 +304,18 @@ src/
|
|
|
298
304
|
tests/
|
|
299
305
|
└── smoke.test.ts # Smoke + E2E 測試套件
|
|
300
306
|
skills/
|
|
301
|
-
|
|
302
|
-
│ ├── SKILL.md
|
|
303
|
-
│
|
|
304
|
-
|
|
305
|
-
│
|
|
306
|
-
│
|
|
307
|
-
|
|
307
|
+
├── google-maps/ # 一般地點、路線與環境資訊工作流程
|
|
308
|
+
│ ├── SKILL.md
|
|
309
|
+
│ └── references/tools-api.md # 工具參數 + 一般場景食譜
|
|
310
|
+
├── google-maps-travel-planning/ # 單日與多日行程工作流程
|
|
311
|
+
│ ├── SKILL.md
|
|
312
|
+
│ └── references/travel-planning.md
|
|
313
|
+
├── google-maps-local-seo/ # 商家能見度與地理排名工作流程
|
|
314
|
+
│ ├── SKILL.md
|
|
315
|
+
│ └── references/local-seo.md
|
|
316
|
+
└── _shared/ # 三個 Skills 按需載入的共用資源
|
|
317
|
+
├── setup-and-diagnostics.md
|
|
318
|
+
└── content-attribution.md
|
|
308
319
|
.agents/
|
|
309
320
|
├── plugins/marketplace.json # CabLate marketplace 目錄
|
|
310
321
|
└── 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
|
|
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(
|
|
5
|
-
`),0)}catch(
|
|
6
|
-
`),1}}
|
|
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=
|
|
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
|
|
10
|
-
`);let m=
|
|
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,24 @@
|
|
|
1
1
|
# Agent Skill demo (no MCP server)
|
|
2
2
|
|
|
3
|
-
This walkthrough demonstrates the two pieces separately: the Agent
|
|
3
|
+
This walkthrough demonstrates the two pieces separately: the plugin's Agent Skills tell an agent which geographic workflow and 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 Skills so the instructions and executable stay aligned.
|
|
4
4
|
|
|
5
|
-
## 1. Install the
|
|
5
|
+
## 1. Install the Skills
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Prefer installing the Codex plugin from the CabLate marketplace. For a manual installation, clone a chosen release tag and copy the whole `skills/` tree according to the client's instructions. Keep the three Skill folders and `_shared/` together. Installing npm alone does not register a Skill unless it is installed through the plugin 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
|
+
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.
|
|
10
10
|
|
|
11
11
|
## 2. Validate without an API call
|
|
12
12
|
|
|
13
|
-
From the cloned repository, install and build the package, then run
|
|
13
|
+
From the cloned repository, install and build the package, then run the local doctor:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm ci
|
|
17
17
|
npm run build
|
|
18
|
-
node dist/cli.js
|
|
18
|
+
node dist/cli.js doctor
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The
|
|
21
|
+
The report should pass the Node.js, package, and API-key checks and skip `live-api`. It makes no Google API requests. To test Geocoding, Places (New), and Routes after disclosing that the calls may be billable, run `node dist/cli.js doctor --live`.
|
|
22
22
|
|
|
23
23
|
## 3. Try one live request
|
|
24
24
|
|
|
@@ -34,6 +34,6 @@ node dist/cli.js exec geocode '{"address":"Tokyo Tower"}'
|
|
|
34
34
|
|
|
35
35
|
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
36
|
|
|
37
|
-
For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map
|
|
37
|
+
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
38
|
|
|
39
|
-
If you ask for place reviews, photos, or an AI summary in a later step, read the
|
|
39
|
+
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.
|
|
3
|
+
"version": "0.0.65",
|
|
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.
|
|
5
|
-
"description": "Use Google Maps
|
|
4
|
+
"version": "0.0.65",
|
|
5
|
+
"description": "Use focused Skills for Google Maps place research, travel planning, and local SEO through a standalone CLI.",
|
|
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:
|
|
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
|
|
7
|
+
# Google Maps
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
## Before calling the CLI
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
##
|
|
58
|
+
## References
|
|
107
59
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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`](
|
|
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
|
-
>
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|