@cablate/mcp-google-map 0.0.40 → 0.0.43
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/README.md +18 -5
- package/README.zh-TW.md +370 -0
- package/dist/chunk-G6LPVEOX.js +1 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/google-maps/SKILL.md +1 -1
- package/skills/google-maps/references/tools-api.md +3 -1
- package/dist/chunk-72XPXAUJ.js +0 -1
package/README.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/v/@cablate/mcp-google-map" alt="npm version"></a>
|
|
3
|
+
<a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/dm/@cablate/mcp-google-map" alt="npm downloads"></a>
|
|
4
|
+
<a href="https://github.com/cablate/mcp-google-map/stargazers"><img src="https://img.shields.io/github/stars/cablate/mcp-google-map?style=social" alt="GitHub stars"></a>
|
|
5
|
+
<a href="./LICENSE"><img src="https://img.shields.io/github/license/cablate/mcp-google-map" alt="license"></a>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="./assets/banner.webp" alt="MCP Google Maps — AI-Powered Geospatial Tools" width="800">
|
|
10
|
+
</p>
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
<h3 align="center"><b>Give your AI agent the ability to understand the physical world —<br>geocode, route, search, and reason about locations.</b></h3>
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<b>English</b> | <a href="./README.zh-TW.md">繁體中文</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="./assets/demo-grid-en.png" alt="Travel planning demo — Kyoto 2-day, Tokyo outdoor, Japan 5-day, Bangkok budget" width="800">
|
|
20
|
+
</p>
|
|
8
21
|
|
|
9
22
|
- **17 tools** — 14 atomic + 3 composite (explore-area, plan-route, compare-places)
|
|
10
23
|
- **3 modes** — stdio, StreamableHTTP, standalone exec CLI
|
|
@@ -52,7 +65,7 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
|
|
|
52
65
|
|------|-------------|
|
|
53
66
|
| `maps_search_nearby` | Find places near a location by type (restaurant, cafe, hotel, etc.). Supports filtering by radius, rating, and open status. |
|
|
54
67
|
| `maps_search_places` | Free-text place search (e.g., "sushi restaurants in Tokyo"). Supports location bias, rating, open-now filters. |
|
|
55
|
-
| `maps_place_details` | Get full details for a place by its place_id — reviews, phone, website, hours
|
|
68
|
+
| `maps_place_details` | Get full details for a place by its place_id — reviews, phone, website, hours. Optional `maxPhotos` param returns photo URLs. |
|
|
56
69
|
| `maps_geocode` | Convert an address or landmark name into GPS coordinates. |
|
|
57
70
|
| `maps_reverse_geocode` | Convert GPS coordinates into a street address. |
|
|
58
71
|
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. |
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/v/@cablate/mcp-google-map" alt="npm version"></a>
|
|
3
|
+
<a href="https://www.npmjs.com/package/@cablate/mcp-google-map"><img src="https://img.shields.io/npm/dm/@cablate/mcp-google-map" alt="npm downloads"></a>
|
|
4
|
+
<a href="https://github.com/cablate/mcp-google-map/stargazers"><img src="https://img.shields.io/github/stars/cablate/mcp-google-map?style=social" alt="GitHub stars"></a>
|
|
5
|
+
<a href="./LICENSE"><img src="https://img.shields.io/github/license/cablate/mcp-google-map" alt="license"></a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="./assets/banner.webp" alt="MCP Google Maps — AI 驅動的地理空間工具" width="800">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<h3 align="center"><b>讓你的 AI 代理理解真實世界 —<br>地理編碼、路線規劃、地點搜尋、空間推理。</b></h3>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="./README.md">English</a> | <b>繁體中文</b>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="./assets/demo-grid-zh.png" alt="旅行規劃展示 — 京都二日遊、東京戶外一日、日本五日、曼谷背包客" width="800">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
- **17 個工具** — 14 個原子工具 + 3 個組合工具(explore-area、plan-route、compare-places)
|
|
23
|
+
- **3 種模式** — stdio、StreamableHTTP、獨立 exec CLI
|
|
24
|
+
- **Agent Skill** — 內建技能定義,教 AI 如何串接地理工具([`skills/google-maps/`](./skills/google-maps/))
|
|
25
|
+
|
|
26
|
+
### vs Google Grounding Lite
|
|
27
|
+
|
|
28
|
+
| | 本專案 | [Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services) |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 工具數 | **17** | 3 |
|
|
31
|
+
| 地理編碼 | 有 | 無 |
|
|
32
|
+
| 逐步導航 | 有 | 無 |
|
|
33
|
+
| 海拔查詢 | 有 | 無 |
|
|
34
|
+
| 距離矩陣 | 有 | 無 |
|
|
35
|
+
| 地點詳情 | 有 | 無 |
|
|
36
|
+
| 時區查詢 | 有 | 無 |
|
|
37
|
+
| 天氣查詢 | 有 | 有 |
|
|
38
|
+
| 空氣品質 | 有 | 無 |
|
|
39
|
+
| 地圖圖片 | 有 | 無 |
|
|
40
|
+
| 組合工具(探索、規劃、比較) | 有 | 無 |
|
|
41
|
+
| 開源 | MIT | 否 |
|
|
42
|
+
| 自架部署 | 有 | 僅 Google 託管 |
|
|
43
|
+
| Agent Skill | 有 | 無 |
|
|
44
|
+
|
|
45
|
+
### 快速開始
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# stdio(Claude Desktop、Cursor 等)
|
|
49
|
+
npx @cablate/mcp-google-map --stdio
|
|
50
|
+
|
|
51
|
+
# exec CLI — 不需啟動 server
|
|
52
|
+
npx @cablate/mcp-google-map exec geocode '{"address":"台北101"}'
|
|
53
|
+
|
|
54
|
+
# HTTP server
|
|
55
|
+
npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 特別感謝
|
|
59
|
+
|
|
60
|
+
感謝 [@junyinnnn](https://github.com/junyinnnn) 協助實作 `streamablehttp` 支援。
|
|
61
|
+
|
|
62
|
+
## 可用工具
|
|
63
|
+
|
|
64
|
+
| 工具 | 說明 |
|
|
65
|
+
|------|------|
|
|
66
|
+
| `maps_search_nearby` | 依類型搜尋附近地點(餐廳、咖啡廳、飯店等),支援半徑、評分、營業中篩選 |
|
|
67
|
+
| `maps_search_places` | 自然語言地點搜尋(如「東京拉麵」),支援位置偏好、評分、營業中篩選 |
|
|
68
|
+
| `maps_place_details` | 以 place_id 取得地點完整資訊 — 評論、電話、網站、營業時間。可選 `maxPhotos` 參數取得照片 URL。 |
|
|
69
|
+
| `maps_geocode` | 將地址或地標名稱轉換為 GPS 座標 |
|
|
70
|
+
| `maps_reverse_geocode` | 將 GPS 座標轉換為街道地址 |
|
|
71
|
+
| `maps_distance_matrix` | 計算多個起點與終點間的旅行距離和時間 |
|
|
72
|
+
| `maps_directions` | 取得兩點間的逐步導航路線 |
|
|
73
|
+
| `maps_elevation` | 查詢地理座標的海拔高度(公尺) |
|
|
74
|
+
| `maps_timezone` | 查詢座標的時區 ID、名稱、UTC/DST 偏移量和當地時間 |
|
|
75
|
+
| `maps_weather` | 查詢當前天氣或預報 — 溫度、濕度、風速、紫外線、降水 |
|
|
76
|
+
| `maps_air_quality` | 查詢空氣品質指數、污染物濃度,以及各族群健康建議 |
|
|
77
|
+
| `maps_static_map` | 產生帶標記、路徑或路線的地圖圖片 — 直接內嵌在對話中 |
|
|
78
|
+
| `maps_batch_geocode` | 一次地理編碼最多 50 個地址 |
|
|
79
|
+
| `maps_search_along_route` | 搜尋兩點間路線沿途的地點 — 依最小繞路時間排序 |
|
|
80
|
+
| **組合工具** | |
|
|
81
|
+
| `maps_explore_area` | 一次呼叫探索某地周邊 — 搜尋多種地點類型並取得詳情 |
|
|
82
|
+
| `maps_plan_route` | 規劃最佳化多站路線 — 地理編碼、最佳順序、回傳導航 |
|
|
83
|
+
| `maps_compare_places` | 並排比較地點 — 搜尋、取得詳情,可選計算距離 |
|
|
84
|
+
|
|
85
|
+
所有工具標註 `readOnlyHint: true` 和 `destructiveHint: false` — MCP 客戶端可自動核准,無需使用者確認。
|
|
86
|
+
|
|
87
|
+
> **前置條件**:使用地點相關工具前,請在 [Google Cloud Console](https://console.cloud.google.com) 啟用 **Places API (New)**。
|
|
88
|
+
|
|
89
|
+
## 安裝
|
|
90
|
+
|
|
91
|
+
### 方法一:stdio(大多數客戶端推薦)
|
|
92
|
+
|
|
93
|
+
適用於 Claude Desktop、Cursor、VS Code 及任何支援 stdio 的 MCP 客戶端:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"google-maps": {
|
|
99
|
+
"command": "npx",
|
|
100
|
+
"args": ["-y", "@cablate/mcp-google-map", "--stdio"],
|
|
101
|
+
"env": {
|
|
102
|
+
"GOOGLE_MAPS_API_KEY": "YOUR_API_KEY"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**減少上下文用量** — 如果只需要部分工具,設定 `GOOGLE_MAPS_ENABLED_TOOLS` 限制註冊的工具:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"env": {
|
|
114
|
+
"GOOGLE_MAPS_API_KEY": "YOUR_API_KEY",
|
|
115
|
+
"GOOGLE_MAPS_ENABLED_TOOLS": "maps_geocode,maps_directions,maps_search_places"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
不設定或設為 `*` 即啟用全部 17 個工具(預設)。
|
|
121
|
+
|
|
122
|
+
### 方法二:HTTP Server
|
|
123
|
+
|
|
124
|
+
適用於多 session 部署、per-request API key 隔離或遠端存取:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
然後設定你的 MCP 客戶端:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"google-maps": {
|
|
136
|
+
"type": "http",
|
|
137
|
+
"url": "http://localhost:3000/mcp"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Server 資訊
|
|
144
|
+
|
|
145
|
+
- **傳輸方式**:stdio(`--stdio`)或 Streamable HTTP(預設)
|
|
146
|
+
- **工具數**:17 個 Google Maps 工具(14 原子 + 3 組合)— 可透過 `GOOGLE_MAPS_ENABLED_TOOLS` 篩選
|
|
147
|
+
|
|
148
|
+
### CLI Exec 模式(Agent Skill)
|
|
149
|
+
|
|
150
|
+
不啟動 MCP server,直接使用工具:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx @cablate/mcp-google-map exec geocode '{"address":"台北101"}'
|
|
154
|
+
npx @cablate/mcp-google-map exec search-places '{"query":"東京拉麵"}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
全部 17 個工具可用:`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`。完整參數文件見 [`skills/google-maps/`](./skills/google-maps/)。
|
|
158
|
+
|
|
159
|
+
### 批次地理編碼
|
|
160
|
+
|
|
161
|
+
從檔案批次地理編碼:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx @cablate/mcp-google-map batch-geocode -i addresses.txt -o results.json
|
|
165
|
+
cat addresses.txt | npx @cablate/mcp-google-map batch-geocode -i -
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
輸入:每行一個地址。輸出:JSON `{ total, succeeded, failed, results[] }`。預設並行度:20。
|
|
169
|
+
|
|
170
|
+
### API Key 設定
|
|
171
|
+
|
|
172
|
+
API key 可透過三種方式提供(優先順序):
|
|
173
|
+
|
|
174
|
+
1. **HTTP Headers**(最高優先)
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"mcp-google-map": {
|
|
179
|
+
"transport": "streamableHttp",
|
|
180
|
+
"url": "http://localhost:3000/mcp",
|
|
181
|
+
"headers": {
|
|
182
|
+
"X-Google-Maps-API-Key": "YOUR_API_KEY"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
2. **命令列參數**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
mcp-google-map --apikey YOUR_API_KEY
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
3. **環境變數**(.env 檔案或命令列)
|
|
195
|
+
```env
|
|
196
|
+
GOOGLE_MAPS_API_KEY=your_api_key_here
|
|
197
|
+
MCP_SERVER_PORT=3000
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 開發
|
|
201
|
+
|
|
202
|
+
### 本地開發
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# 複製專案
|
|
206
|
+
git clone https://github.com/cablate/mcp-google-map.git
|
|
207
|
+
cd mcp-google-map
|
|
208
|
+
|
|
209
|
+
# 安裝依賴
|
|
210
|
+
npm install
|
|
211
|
+
|
|
212
|
+
# 設定環境變數
|
|
213
|
+
cp .env.example .env
|
|
214
|
+
# 編輯 .env 填入你的 API key
|
|
215
|
+
|
|
216
|
+
# 建置專案
|
|
217
|
+
npm run build
|
|
218
|
+
|
|
219
|
+
# 啟動 server
|
|
220
|
+
npm start
|
|
221
|
+
|
|
222
|
+
# 或以開發模式執行
|
|
223
|
+
npm run dev
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 測試
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# 執行 smoke tests(基本測試不需要 API key)
|
|
230
|
+
npm test
|
|
231
|
+
|
|
232
|
+
# 執行完整 E2E 測試(需要 GOOGLE_MAPS_API_KEY)
|
|
233
|
+
npm run test:e2e
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 專案結構
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
src/
|
|
240
|
+
├── cli.ts # CLI 進入點
|
|
241
|
+
├── config.ts # 工具註冊與 server 設定
|
|
242
|
+
├── index.ts # 套件匯出
|
|
243
|
+
├── core/
|
|
244
|
+
│ └── BaseMcpServer.ts # MCP server(streamable HTTP 傳輸)
|
|
245
|
+
├── services/
|
|
246
|
+
│ ├── NewPlacesService.ts # Google Places API (New) 客戶端
|
|
247
|
+
│ ├── PlacesSearcher.ts # Service facade 層
|
|
248
|
+
│ └── toolclass.ts # Legacy Google Maps API 客戶端
|
|
249
|
+
├── tools/
|
|
250
|
+
│ └── maps/
|
|
251
|
+
│ ├── searchNearby.ts # maps_search_nearby 工具
|
|
252
|
+
│ ├── searchPlaces.ts # maps_search_places 工具
|
|
253
|
+
│ ├── placeDetails.ts # maps_place_details 工具
|
|
254
|
+
│ ├── geocode.ts # maps_geocode 工具
|
|
255
|
+
│ ├── reverseGeocode.ts # maps_reverse_geocode 工具
|
|
256
|
+
│ ├── distanceMatrix.ts # maps_distance_matrix 工具
|
|
257
|
+
│ ├── directions.ts # maps_directions 工具
|
|
258
|
+
│ ├── elevation.ts # maps_elevation 工具
|
|
259
|
+
│ ├── timezone.ts # maps_timezone 工具
|
|
260
|
+
│ ├── weather.ts # maps_weather 工具
|
|
261
|
+
│ ├── airQuality.ts # maps_air_quality 工具
|
|
262
|
+
│ ├── staticMap.ts # maps_static_map 工具
|
|
263
|
+
│ ├── batchGeocode.ts # maps_batch_geocode 工具
|
|
264
|
+
│ ├── searchAlongRoute.ts # maps_search_along_route 工具
|
|
265
|
+
│ ├── exploreArea.ts # maps_explore_area(組合)
|
|
266
|
+
│ ├── planRoute.ts # maps_plan_route(組合)
|
|
267
|
+
│ └── comparePlaces.ts # maps_compare_places(組合)
|
|
268
|
+
└── utils/
|
|
269
|
+
├── apiKeyManager.ts # API key 管理
|
|
270
|
+
└── requestContext.ts # Per-request context(API key 隔離)
|
|
271
|
+
tests/
|
|
272
|
+
└── smoke.test.ts # Smoke + E2E 測試套件
|
|
273
|
+
skills/
|
|
274
|
+
├── google-maps/ # Agent Skill — 如何使用工具
|
|
275
|
+
│ ├── SKILL.md # 工具對照表、場景食譜、呼叫方式
|
|
276
|
+
│ └── references/
|
|
277
|
+
│ ├── tools-api.md # 工具參數 + 場景食譜
|
|
278
|
+
│ └── travel-planning.md # 旅行規劃方法論
|
|
279
|
+
└── project-docs/ # Project Skill — 如何開發/維護
|
|
280
|
+
├── SKILL.md # 架構概覽 + 入門指南
|
|
281
|
+
└── references/
|
|
282
|
+
├── architecture.md # 系統設計、code map、9 檔案 checklist
|
|
283
|
+
├── google-maps-api-guide.md # API 端點、定價、注意事項
|
|
284
|
+
├── geo-domain-knowledge.md # GIS 基礎、日本場景
|
|
285
|
+
└── decisions.md # 10 個 ADR(設計決策 + 理由)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 技術棧
|
|
289
|
+
|
|
290
|
+
- **TypeScript** - 型別安全開發
|
|
291
|
+
- **Node.js** - 執行環境
|
|
292
|
+
- **@googlemaps/places** - Google Places API (New) 地點搜尋與詳情
|
|
293
|
+
- **@googlemaps/google-maps-services-js** - Legacy API 地理編碼、導航、距離矩陣、海拔
|
|
294
|
+
- **@modelcontextprotocol/sdk** - MCP 協議實作(v1.27+)
|
|
295
|
+
- **Express.js** - HTTP server 框架
|
|
296
|
+
- **Zod** - Schema 驗證
|
|
297
|
+
|
|
298
|
+
## 安全性
|
|
299
|
+
|
|
300
|
+
- API key 在 server 端處理
|
|
301
|
+
- 多租戶部署的 per-session API key 隔離
|
|
302
|
+
- 正式環境可啟用 DNS rebinding 防護
|
|
303
|
+
- 使用 Zod schemas 進行輸入驗證
|
|
304
|
+
|
|
305
|
+
企業安全審查請參考 [Security Assessment Clarifications](./SECURITY_ASSESSMENT.md) — 涵蓋授權、資料保護、憑證管理、工具污染、AI 代理執行環境驗證的 23 項檢查清單。
|
|
306
|
+
|
|
307
|
+
## 路線圖
|
|
308
|
+
|
|
309
|
+
### 近期新增
|
|
310
|
+
|
|
311
|
+
| 工具 / 功能 | 解鎖場景 | 狀態 |
|
|
312
|
+
|------|----------------|--------|
|
|
313
|
+
| `maps_static_map` | 帶標記/路線的地圖圖片 — 多模態 AI 可「看見」地圖 | **完成** |
|
|
314
|
+
| `maps_air_quality` | AQI、污染物 — 健康出行、戶外規劃 | **完成** |
|
|
315
|
+
| `maps_batch_geocode` | 一次地理編碼最多 50 個地址 — 資料增強 | **完成** |
|
|
316
|
+
| `maps_search_along_route` | 沿路線搜尋地點,依繞路時間排序 — 旅行規劃 | **完成** |
|
|
317
|
+
| `maps_explore_area` | 一次呼叫的社區概覽(組合工具) | **完成** |
|
|
318
|
+
| `maps_plan_route` | 最佳化多站行程(組合工具) | **完成** |
|
|
319
|
+
| `maps_compare_places` | 並排地點比較(組合工具) | **完成** |
|
|
320
|
+
| `GOOGLE_MAPS_ENABLED_TOOLS` | 篩選工具以減少上下文用量 | **完成** |
|
|
321
|
+
|
|
322
|
+
### 計畫中
|
|
323
|
+
|
|
324
|
+
| 功能 | 解鎖場景 | 狀態 |
|
|
325
|
+
|---------|----------------|--------|
|
|
326
|
+
| `maps_place_photo` | 地點照片供多模態 AI 使用 — 「看見」餐廳氛圍 | 計畫中 |
|
|
327
|
+
| 語言參數 | 所有工具支援多語言回應(ISO 639-1) | 計畫中 |
|
|
328
|
+
| MCP Prompt Templates | Claude Desktop 中的 `/travel-planner`、`/neighborhood-scout` 斜線指令 | 計畫中 |
|
|
329
|
+
| Geo-Reasoning Benchmark | 10 場景測試套件,衡量 LLM 地理空間推理準確度 | 研究中 |
|
|
330
|
+
|
|
331
|
+
### 我們在建構的應用場景
|
|
332
|
+
|
|
333
|
+
以下是驅動工具開發方向的真實場景:
|
|
334
|
+
|
|
335
|
+
- **旅行規劃** — 「規劃東京一日遊」(geocode → search → directions → weather)
|
|
336
|
+
- **房地產分析** — 「分析這個社區:學校、通勤、洪水風險」(search-nearby × N + elevation + distance-matrix)
|
|
337
|
+
- **物流優化** — 「從倉庫出發,最佳化這 12 個配送地址的路線」(plan-route)
|
|
338
|
+
- **外勤銷售** — 「拜訪芝加哥 6 個客戶,最小化車程,找午餐地點」(plan-route + search-nearby)
|
|
339
|
+
- **災害應變** — 「最近有開的醫院?我在洪水區嗎?」(search-nearby + elevation)
|
|
340
|
+
- **內容創作** — 「Austin 前 5 社區的餐廳密度和機場距離」(explore-area + distance-matrix)
|
|
341
|
+
- **無障礙** — 「輪椅可達的餐廳,避開陡坡路線」(search-nearby + place-details + elevation)
|
|
342
|
+
|
|
343
|
+
## 更新日誌
|
|
344
|
+
|
|
345
|
+
見 [CHANGELOG.md](./CHANGELOG.md)。
|
|
346
|
+
|
|
347
|
+
## 授權
|
|
348
|
+
|
|
349
|
+
MIT
|
|
350
|
+
|
|
351
|
+
## 貢獻
|
|
352
|
+
|
|
353
|
+
歡迎社群參與和貢獻!
|
|
354
|
+
|
|
355
|
+
- 提交 Issue:回報 bug 或提供建議
|
|
356
|
+
- 建立 Pull Request:提交程式碼改進
|
|
357
|
+
- 文件:協助改善文件
|
|
358
|
+
|
|
359
|
+
## 聯絡
|
|
360
|
+
|
|
361
|
+
- Email: [reahtuoo310109@gmail.com](mailto:reahtuoo310109@gmail.com)
|
|
362
|
+
- GitHub: [CabLate](https://github.com/cablate/)
|
|
363
|
+
|
|
364
|
+
## Star History
|
|
365
|
+
|
|
366
|
+
<a href="https://glama.ai/mcp/servers/@cablate/mcp-google-map">
|
|
367
|
+
<img width="380" height="200" src="https://glama.ai/mcp/servers/@cablate/mcp-google-map/badge" alt="Google Map Server MCP server" />
|
|
368
|
+
</a>
|
|
369
|
+
|
|
370
|
+
[](https://www.star-history.com/#cablate/mcp-google-map&Date)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw m.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw m.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let o=r.data.results[0];return{formatted_address:o.formatted_address,place_id:o.place_id,address_components:o.address_components}}catch(r){throw m.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let n=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${n.status}`);let s=[],l=[];return n.rows.forEach(u=>{let a=[],c=[];u.elements.forEach(d=>{d.status==="OK"?(a.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(a.push(null),c.push(null))}),s.push(a),l.push(c)}),{distances:s,durations:l,origin_addresses:n.origin_addresses,destination_addresses:n.destination_addresses}}catch(o){throw m.error("Error in calculateDistanceMatrix:",o),new Error(`Failed to calculate distance matrix: ${f(o)}`)}}async getDirections(e,t,r="driving",o,n){try{let s;n&&(s=Math.floor(n.getTime()/1e3));let l;s||(o instanceof Date?l=Math.floor(o.getTime()/1e3):o?l=o:l="now");let a=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:l}})).data;if(a.status!=="OK")throw new Error(`Failed to get directions with status: ${a.status} (arrival_time: ${s}, departure_time: ${l}`);if(a.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=a.routes[0],d=c.legs[0],h=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),p={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(p.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),p)};return{routes:a.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:h(d.arrival_time),departure_time:h(d.departure_time)}}catch(s){throw m.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async searchAlongRoute(e){try{let t=await this.getDirections(e.origin,e.destination,e.mode||"walking"),r=t.routes[0]?.overview_polyline?.points;if(!r)throw new Error("Could not get route polyline");let o=Math.min(e.maxResults||5,20),s=await fetch("https://places.googleapis.com/v1/places:searchText",{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":"places.displayName,places.id,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.currentOpeningHours.openNow"},body:JSON.stringify({textQuery:e.textQuery,searchAlongRouteParameters:{polyline:{encodedPolyline:r}},maxResultCount:o})});if(!s.ok){let a=await s.json().catch(()=>({}));throw new Error(a?.error?.message||`HTTP ${s.status}`)}return{places:((await s.json()).places||[]).map(a=>({name:a.displayName?.text||"",place_id:a.id||"",formatted_address:a.formattedAddress||"",location:{lat:a.location?.latitude||0,lng:a.location?.longitude||0},rating:a.rating||0,user_ratings_total:a.userRatingCount||0,open_now:a.currentOpeningHours?.openNow??null})),route:{distance:t.total_distance.text,duration:t.total_duration.text,polyline:r}}}catch(t){throw m.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",o,n){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,l;switch(r){case"forecast_daily":{let c=Math.min(Math.max(o||5,1),10);l=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(n||24,1),240);l=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:l=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(l);if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let a=await u.json();return r==="current"?{temperature:a.temperature,feelsLike:a.feelsLikeTemperature,humidity:a.relativeHumidity,wind:a.wind,conditions:a.weatherCondition?.description?.text||a.weatherCondition?.type,uvIndex:a.uvIndex,precipitation:a.precipitation,visibility:a.visibility,pressure:a.airPressure,cloudCover:a.cloudCover,isDayTime:a.isDaytime}:a}catch(s){throw m.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,o=!1){try{let n=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),o&&s.push("POLLUTANT_CONCENTRATION");let l={location:{latitude:e,longitude:t}};s.length>0&&(l.extraComputations=s);let u=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!u.ok){let g=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(g)}let a=await u.json(),c=a.indexes||[],d=c[0],h={dateTime:a.dateTime,regionCode:a.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(h.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),a.healthRecommendations&&(h.healthRecommendations=a.healthRecommendations),a.pollutants&&(h.pollutants=a.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),h}catch(n){throw m.error("Error in getAirQuality:",n),new Error(n.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let a of e.markers)r.push(`markers=${encodeURIComponent(a)}`);if(e.path)for(let a of e.path)r.push(`path=${encodeURIComponent(a)}`);let o=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(o.length>16384)throw new Error(`URL exceeds 16,384 character limit (${o.length}). Reduce markers or path points.`);let n=await fetch(o);if(!n.ok){let a=n.headers.get("content-type")||"";if(a.includes("application/json")||a.includes("text/")){let c=await n.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${n.status}`)}let s=await n.arrayBuffer(),l=Buffer.from(s);return{base64:l.toString("base64"),size:l.length,dimensions:t}}catch(t){throw m.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let o=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:o,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let l=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+l).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw m.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${f(o)}`)}}async getElevation(e){try{let t=e.map(n=>({lat:n.latitude,lng:n.longitude})),o=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(o.status!=="OK")throw new Error(`Failed to get elevation data with status: ${o.status}`);return o.results.map((n,s)=>({elevation:n.elevation,location:t[s]}))}catch(t){throw m.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw m.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw m.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPhotoUri(e,t=800){try{let[r]=await this.client.getPhotoMedia({name:`${e}/media`,maxWidthPx:t,skipHttpRedirect:!0});return r.photoUri||""}catch(r){throw m.error("Error in getPhotoUri:",r),new Error(`Failed to get photo URI: ${this.extractErrorMessage(r)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw m.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let o=e?.periods;if(!Array.isArray(o)||o.length===0)return!1;let n=1440,s=n*7,{day:l,minutes:u}=this.getLocalTimeComponents(t),a=l*n+u,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},h=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),p=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(p)))return g*60+p};for(let i of o){let g=d(i?.openDay),p=d(i?.closeDay??i?.openDay),w=h(i?.openTime),N=h(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*n+w,b;p===void 0||N===void 0?b=v+n:b=p*n+N,b<=v&&(b+=s);let P=a;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var x=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),o=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(o=o.filter(n=>n.opening_hours?.open_now===!0)),e.minRating&&(o=o.filter(n=>(n.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:o.map(n=>({name:n.name,place_id:n.place_id,address:n.formatted_address,location:n.geometry.location,rating:n.rating,total_ratings:n.user_ratings_total,open_now:n.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e,t=0){try{let r=await this.newPlacesService.getPlaceDetails(e),o;if(t>0&&r.photos?.length>0){let n=r.photos.slice(0,t);o=[];for(let s of n)try{let l=await this.newPlacesService.getPhotoUri(s.photo_reference);o.push({url:l,width:s.width,height:s.height})}catch{}}return{success:!0,data:{name:r.name,address:r.formatted_address,location:r.geometry?.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now,phone:r.formatted_phone_number,website:r.website,price_level:r.price_level,photo_count:r.photos?.length||0,...o&&o.length>0?{photos:o}:{},reviews:r.reviews?.map(n=>({rating:n.rating,text:n.text,time:n.time,author_name:n.author_name}))}}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,n){try{let s=o?new Date(o):new Date,l=n?new Date(n):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,l)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",o,n){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,o,n)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,o){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,o)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,o=e.topN||3,n=await this.geocode(e.location);if(!n.success||!n.data)throw new Error(n.error||"Geocode failed");let{lat:s,lng:l}=n.data.location,u=[];for(let a of t){let c=await this.searchNearby({center:{value:`${s},${l}`,isCoordinates:!0},keyword:a,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,o),h=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);h.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}u.push({type:a,count:c.data.length,top:h})}return{success:!0,data:{location:{address:n.data.formatted_address,lat:s,lng:l},radius:r,categories:u}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let o=[];for(let a of r){let c=await this.geocode(a);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${a}`);o.push({originalName:a,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let n=o;if(e.optimize!==!1&&o.length>2){let a=await this.calculateDistanceMatrix(r,r,"driving");if(a.success&&a.data){let c=new Set([0]),d=[0],h=0;for(;c.size<o.length;){let i=-1,g=1/0;for(let p=0;p<o.length;p++){if(c.has(p))continue;let w=a.data.durations[h]?.[p]?.value??1/0;w<g&&(g=w,i=p)}if(i===-1)break;c.add(i),d.push(i),h=i}n=d.map(i=>o[i])}}let s=[],l=0,u=0;for(let a=0;a<n.length-1;a++){let c=await this.getDirections(n[a].originalName,n[a+1].originalName,t);c.success&&c.data?(l+=c.data.total_distance.value,u+=c.data.total_duration.value,s.push({from:n[a].originalName,to:n[a+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:n[a].originalName,to:n[a+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&o.length>2,stops:n.map(a=>`${a.originalName} (${a.address})`),legs:s,total_distance:`${(l/1e3).toFixed(1)} km`,total_duration:`${Math.round(u/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let o=r.data.slice(0,t),n=[];for(let s of o){let l=await this.getPlaceDetails(s.place_id);n.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:l.data?.phone,website:l.data?.website,price_level:l.data?.price_level})}if(e.userLocation&&n.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,l=o.map(a=>`${a.location.lat},${a.location.lng}`),u=await this.calculateDistanceMatrix([s],l,"driving");if(u.success&&u.data)for(let a=0;a<n.length;a++)n[a].distance=u.data.distances[0]?.[a]?.text,n[a].drive_time=u.data.durations[0]?.[a]?.text}return{success:!0,data:n}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var m={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,x as b,m as c};
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{b as a,c as s}from"./chunk-
|
|
3
|
-
`+JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 x={NAME:me,DESCRIPTION:ge,SCHEMA:ye,ACTION:fe};import{z as he}from"zod";var Se="maps_place_details",Ee="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, opening hours, and photos. Returns everything needed to evaluate or contact a business.",be={placeId:he.string().describe("Google Maps place ID")};async function Pe(r){try{let e=i(),t=await new a(e).getPlaceDetails(r.placeId);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 A={NAME:Se,DESCRIPTION:Ee,SCHEMA:be,ACTION:Pe};import{z as xe}from"zod";var Ae="maps_geocode",ve="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.",Me={address:xe.string().describe("Address or place name to convert to coordinates")};async function Ce(r){try{let e=i(),t=await new a(e).geocode(r.address);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 v={NAME:Ae,DESCRIPTION:ve,SCHEMA:Me,ACTION:Ce};import{z as se}from"zod";var Oe="maps_reverse_geocode",Ne="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.",Ie={latitude:se.number().describe("Latitude coordinate"),longitude:se.number().describe("Longitude coordinate")};async function we(r){try{let e=i(),t=await new a(e).reverseGeocode(r.latitude,r.longitude);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 M={NAME:Oe,DESCRIPTION:Ne,SCHEMA:Ie,ACTION:we};import{z as C}from"zod";var Te="maps_distance_matrix",_e="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.",Re={origins:C.array(C.string()).describe("List of origin addresses or coordinates"),destinations:C.array(C.string()).describe("List of destination addresses or coordinates"),mode:C.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ze(r){try{let e=i(),t=await new a(e).calculateDistanceMatrix(r.origins,r.destinations,r.mode);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 O={NAME:Te,DESCRIPTION:_e,SCHEMA:Re,ACTION:ze};import{z as N}from"zod";var ke="maps_directions",De="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.",Ke={origin:N.string().describe("Starting point address or coordinates"),destination:N.string().describe("Destination address or coordinates"),mode:N.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:N.string().optional().describe("Departure time (ISO string format)"),arrival_time:N.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=i(),t=await new a(e).getDirections(r.origin,r.destination,r.mode,r.departure_time,r.arrival_time);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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:ke,DESCRIPTION:De,SCHEMA:Ke,ACTION:He};import{z as B}from"zod";var Ge="maps_elevation",$e="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.",Le={locations:B.array(B.object({latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=i(),t=await new a(e).getElevation(r.locations);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 w={NAME:Ge,DESCRIPTION:$e,SCHEMA:Le,ACTION:Je};import{z as f}from"zod";var qe="maps_search_places",je="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.",Ue={query:f.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:f.object({latitude:f.number().describe("Latitude to bias results toward"),longitude:f.number().describe("Longitude to bias results toward"),radius:f.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:f.boolean().optional().describe("Only return places that are currently open"),minRating:f.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:f.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Fe(r){try{let e=i(),t=await new a(e).searchText({query:r.query,locationBias:r.locationBias,openNow:r.openNow,minRating:r.minRating,includedType:r.includedType});return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 T={NAME:qe,DESCRIPTION:je,SCHEMA:Ue,ACTION:Fe};import{z as Q}from"zod";var Be="maps_timezone",Ze="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.",We={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),timestamp:Q.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ye(r){try{let e=i(),t=await new a(e).getTimezone(r.latitude,r.longitude,r.timestamp);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 _={NAME:Be,DESCRIPTION:Ze,SCHEMA:We,ACTION:Ye};import{z as R}from"zod";var Ve="maps_weather",Qe="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.",Xe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate"),type:R.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:R.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:R.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function et(r){try{let e=i(),t=await new a(e).getWeather(r.latitude,r.longitude,r.type||"current",r.forecastDays,r.forecastHours);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 z={NAME:Ve,DESCRIPTION:Qe,SCHEMA:Xe,ACTION:et};import{z as k}from"zod";var tt="maps_explore_area",rt="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.",ot={location:k.string().describe("Address or landmark to explore around"),types:k.array(k.string()).optional().describe("Place types to search (default: restaurant, cafe, attraction). Examples: hotel, bar, park, museum"),radius:k.number().optional().describe("Search radius in meters (default: 1000)"),topN:k.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function st(r){try{let e=i(),t=await new a(e).exploreArea(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var D={NAME:tt,DESCRIPTION:rt,SCHEMA:ot,ACTION:st};import{z as Z}from"zod";var at="maps_plan_route",nt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, finds the most efficient visit order using distance-matrix, and returns step-by-step directions between each stop. 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. 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.",it={stops:Z.array(Z.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:Z.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:Z.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function ct(r){try{let e=i(),t=await new a(e).planRoute(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:at,DESCRIPTION:nt,SCHEMA:it,ACTION:ct};import{z as H}from"zod";var lt="maps_compare_places",pt="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.",dt={query:H.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:H.object({latitude:H.number().describe("Your latitude"),longitude:H.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:H.number().optional().describe("Max places to compare (default: 5)")};async function ut(r){try{let e=i(),t=await new a(e).comparePlaces(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var G={NAME:lt,DESCRIPTION:pt,SCHEMA:dt,ACTION:ut};import{z as W}from"zod";var mt="maps_air_quality",gt="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.).",yt={latitude:W.number().describe("Latitude coordinate"),longitude:W.number().describe("Longitude coordinate"),includeHealthRecommendations:W.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:W.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function ft(r){try{let e=i(),t=await new a(e).getAirQuality(r.latitude,r.longitude,r.includeHealthRecommendations,r.includePollutants);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 $={NAME:mt,DESCRIPTION:gt,SCHEMA:yt,ACTION:ft};import{z as h}from"zod";var ht="maps_static_map",St="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.",Et={center:h.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:h.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:h.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:h.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:h.array(h.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:h.array(h.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function bt(r){try{let e=i(),t=await new a(e).getStaticMap(r);return t.success?{content:[{type:"image",data:t.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${t.data.size} bytes, ${t.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:t.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 L={NAME:ht,DESCRIPTION:St,SCHEMA:Et,ACTION:bt};import{z as ae}from"zod";var Pt="maps_batch_geocode",xt="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.",At={addresses:ae.array(ae.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function vt(r){try{let e=i(),o=new a(e),t=r.addresses,n=await Promise.all(t.map(async p=>{try{let S=await o.geocode(p);return{address:p,...S}}catch(S){return{address:p,success:!1,error:S.message}}})),c=n.filter(p=>p.success).length,l=n.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:c,failed:l,results:n},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:Pt,DESCRIPTION:xt,SCHEMA:At,ACTION:vt};import{z as q}from"zod";var Mt="maps_search_along_route",Ct="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'.",Ot={textQuery:q.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:q.string().describe("Route start point \u2014 address or landmark name"),destination:q.string().describe("Route end point \u2014 address or landmark name"),mode:q.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:q.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Nt(r){try{let e=i(),t=await new a(e).searchAlongRoute(r);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 j={NAME:Mt,DESCRIPTION:Ct,SCHEMA:Ot,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},It=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:d,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:d,action:r=>A.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:d,action:r=>M.ACTION(r)},{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:d,action:r=>O.ACTION(r)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:d,action:r=>I.ACTION(r)},{name:w.NAME,description:w.DESCRIPTION,schema:w.SCHEMA,annotations:d,action:r=>w.ACTION(r)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:d,action:r=>_.ACTION(r)},{name:z.NAME,description:z.DESCRIPTION,schema:z.SCHEMA,annotations:d,action:r=>z.ACTION(r)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:d,action:r=>D.ACTION(r)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:d,action:r=>L.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.ACTION(r)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:d,action:r=>j.ACTION(r)}]}];function X(r){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return r;let o=new Set(e.split(",").map(n=>n.trim()).filter(Boolean)),t=r.filter(n=>o.has(n.name));return t.length===0?(s.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${r.map(n=>n.name).join(", ")}`),r):(s.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${t.length}/${r.length} tools active`),t)}var Y=It;import{McpServer as wt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as _t}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Rt}from"@modelcontextprotocol/sdk/types.js";import ne from"express";import{randomUUID as zt}from"crypto";import{z as kt}from"zod";var U=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.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 Dt="0.0.1",F=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new wt({name:this.serverName,version:Dt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:kt.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,c)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,c),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ne();o.use(ne.json()),o.post("/mcp",async(n,c)=>{let l=n.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Rt(n.body)){let y=new _t({sessionIdGenerator:()=>zt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{c.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c,n.body)})});let t=async(n,c)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){c.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(n);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new Tt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Gt}from"url";import{dirname as $t}from"path";import{readFileSync as ie,writeFileSync as Lt,existsSync as Jt}from"fs";import{createInterface as qt}from"readline";var jt=Gt(import.meta.url),le=$t(jt);ce({path:ee(process.cwd(),".env")});ce({path:ee(le,"../.env")});async function Ut(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} 17 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=Y.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let c=Number(n);if(isNaN(c)||c<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,X(t.tools));s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${c}...`),await l.startHttpServer(c),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${c}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${c}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var pe=["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"];async function Ft(r,e,o){let t=new a(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.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 t.getPlaceDetails(e.placeId);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let n=await Promise.all(e.addresses.map(async l=>{try{let p=await t.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=n.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:n}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${pe.join(", ")}`)}}var Bt=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")),Zt=import.meta.url===`file://${process.argv[1]}`;if(Bt||Zt){let r="0.0.0";try{let e=ee(le,"../package.json");r=JSON.parse(ie(e,"utf-8")).version}catch{r="0.0.0"}Kt(Ht(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${pe.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=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},t=await Ft(e.tool,o,e.apikey);console.log(JSON.stringify(t,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(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 u=qt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Jt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=ie(e.input,"utf-8").split(`
|
|
4
|
-
`).map(u=>u.trim()).filter(u=>u.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let t=new a(e.apikey),n=Math.min(Math.max(e.concurrency,1),50),c=[],l=0,p=async(u,b)=>{let m=[];for(let
|
|
2
|
+
import{b as a,c as s}from"./chunk-G6LPVEOX.js";import{config as le}from"dotenv";import{resolve as ee}from"path";import Kt from"yargs";import{hideBin as Ht}from"yargs/helpers";import{z as E}from"zod";import{AsyncLocalStorage as me}from"async_hooks";var oe=new me;function i(){return oe.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function V(r,e){return oe.run(r,e)}var ge="maps_search_nearby",ye="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.",fe={center:E.object({value:E.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:E.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:E.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:E.number().default(1e3).describe("Search radius in meters"),openNow:E.boolean().default(!1).describe("Only show places that are currently open"),minRating:E.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function he(r){try{let e=i(),t=await new a(e).searchNearby(r);return t.success?{content:[{type:"text",text:`location: ${JSON.stringify(t.location,null,2)}
|
|
3
|
+
`+JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 x={NAME:ge,DESCRIPTION:ye,SCHEMA:fe,ACTION:he};import{z as se}from"zod";var Se="maps_place_details",Ee="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, and opening hours. Set maxPhotos (1-10) to include photo URLs \u2014 omit or set to 0 for no photos (saves tokens).",be={placeId:se.string().describe("Google Maps place ID"),maxPhotos:se.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 Pe(r){try{let e=i(),t=await new a(e).getPlaceDetails(r.placeId,r.maxPhotos||0);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 A={NAME:Se,DESCRIPTION:Ee,SCHEMA:be,ACTION:Pe};import{z as xe}from"zod";var Ae="maps_geocode",ve="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.",Me={address:xe.string().describe("Address or place name to convert to coordinates")};async function Ce(r){try{let e=i(),t=await new a(e).geocode(r.address);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 v={NAME:Ae,DESCRIPTION:ve,SCHEMA:Me,ACTION:Ce};import{z as ae}from"zod";var Oe="maps_reverse_geocode",Ne="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.",Ie={latitude:ae.number().describe("Latitude coordinate"),longitude:ae.number().describe("Longitude coordinate")};async function we(r){try{let e=i(),t=await new a(e).reverseGeocode(r.latitude,r.longitude);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 M={NAME:Oe,DESCRIPTION:Ne,SCHEMA:Ie,ACTION:we};import{z as C}from"zod";var Te="maps_distance_matrix",_e="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.",Re={origins:C.array(C.string()).describe("List of origin addresses or coordinates"),destinations:C.array(C.string()).describe("List of destination addresses or coordinates"),mode:C.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ze(r){try{let e=i(),t=await new a(e).calculateDistanceMatrix(r.origins,r.destinations,r.mode);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 O={NAME:Te,DESCRIPTION:_e,SCHEMA:Re,ACTION:ze};import{z as N}from"zod";var ke="maps_directions",De="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.",Ke={origin:N.string().describe("Starting point address or coordinates"),destination:N.string().describe("Destination address or coordinates"),mode:N.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:N.string().optional().describe("Departure time (ISO string format)"),arrival_time:N.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=i(),t=await new a(e).getDirections(r.origin,r.destination,r.mode,r.departure_time,r.arrival_time);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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:ke,DESCRIPTION:De,SCHEMA:Ke,ACTION:He};import{z as B}from"zod";var Ge="maps_elevation",$e="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.",Le={locations:B.array(B.object({latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=i(),t=await new a(e).getElevation(r.locations);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 w={NAME:Ge,DESCRIPTION:$e,SCHEMA:Le,ACTION:Je};import{z as f}from"zod";var qe="maps_search_places",je="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.",Ue={query:f.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:f.object({latitude:f.number().describe("Latitude to bias results toward"),longitude:f.number().describe("Longitude to bias results toward"),radius:f.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:f.boolean().optional().describe("Only return places that are currently open"),minRating:f.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:f.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Fe(r){try{let e=i(),t=await new a(e).searchText({query:r.query,locationBias:r.locationBias,openNow:r.openNow,minRating:r.minRating,includedType:r.includedType});return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 T={NAME:qe,DESCRIPTION:je,SCHEMA:Ue,ACTION:Fe};import{z as Q}from"zod";var Be="maps_timezone",Ze="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.",We={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),timestamp:Q.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ye(r){try{let e=i(),t=await new a(e).getTimezone(r.latitude,r.longitude,r.timestamp);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 _={NAME:Be,DESCRIPTION:Ze,SCHEMA:We,ACTION:Ye};import{z as R}from"zod";var Ve="maps_weather",Qe="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.",Xe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate"),type:R.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:R.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:R.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function et(r){try{let e=i(),t=await new a(e).getWeather(r.latitude,r.longitude,r.type||"current",r.forecastDays,r.forecastHours);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 z={NAME:Ve,DESCRIPTION:Qe,SCHEMA:Xe,ACTION:et};import{z as k}from"zod";var tt="maps_explore_area",rt="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.",ot={location:k.string().describe("Address or landmark to explore around"),types:k.array(k.string()).optional().describe("Place types to search (default: restaurant, cafe, attraction). Examples: hotel, bar, park, museum"),radius:k.number().optional().describe("Search radius in meters (default: 1000)"),topN:k.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function st(r){try{let e=i(),t=await new a(e).exploreArea(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var D={NAME:tt,DESCRIPTION:rt,SCHEMA:ot,ACTION:st};import{z as Z}from"zod";var at="maps_plan_route",nt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, finds the most efficient visit order using distance-matrix, and returns step-by-step directions between each stop. 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. 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.",it={stops:Z.array(Z.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:Z.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:Z.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function ct(r){try{let e=i(),t=await new a(e).planRoute(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:at,DESCRIPTION:nt,SCHEMA:it,ACTION:ct};import{z as H}from"zod";var lt="maps_compare_places",pt="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.",dt={query:H.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:H.object({latitude:H.number().describe("Your latitude"),longitude:H.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:H.number().optional().describe("Max places to compare (default: 5)")};async function ut(r){try{let e=i(),t=await new a(e).comparePlaces(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var G={NAME:lt,DESCRIPTION:pt,SCHEMA:dt,ACTION:ut};import{z as W}from"zod";var mt="maps_air_quality",gt="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.).",yt={latitude:W.number().describe("Latitude coordinate"),longitude:W.number().describe("Longitude coordinate"),includeHealthRecommendations:W.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:W.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function ft(r){try{let e=i(),t=await new a(e).getAirQuality(r.latitude,r.longitude,r.includeHealthRecommendations,r.includePollutants);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 $={NAME:mt,DESCRIPTION:gt,SCHEMA:yt,ACTION:ft};import{z as h}from"zod";var ht="maps_static_map",St="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.",Et={center:h.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:h.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:h.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:h.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:h.array(h.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:h.array(h.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function bt(r){try{let e=i(),t=await new a(e).getStaticMap(r);return t.success?{content:[{type:"image",data:t.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${t.data.size} bytes, ${t.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:t.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 L={NAME:ht,DESCRIPTION:St,SCHEMA:Et,ACTION:bt};import{z as ne}from"zod";var Pt="maps_batch_geocode",xt="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.",At={addresses:ne.array(ne.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function vt(r){try{let e=i(),o=new a(e),t=r.addresses,n=await Promise.all(t.map(async p=>{try{let S=await o.geocode(p);return{address:p,...S}}catch(S){return{address:p,success:!1,error:S.message}}})),c=n.filter(p=>p.success).length,l=n.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:c,failed:l,results:n},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:Pt,DESCRIPTION:xt,SCHEMA:At,ACTION:vt};import{z as q}from"zod";var Mt="maps_search_along_route",Ct="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'.",Ot={textQuery:q.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:q.string().describe("Route start point \u2014 address or landmark name"),destination:q.string().describe("Route end point \u2014 address or landmark name"),mode:q.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:q.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Nt(r){try{let e=i(),t=await new a(e).searchAlongRoute(r);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.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 j={NAME:Mt,DESCRIPTION:Ct,SCHEMA:Ot,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},It=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:d,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:d,action:r=>A.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:d,action:r=>M.ACTION(r)},{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:d,action:r=>O.ACTION(r)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:d,action:r=>I.ACTION(r)},{name:w.NAME,description:w.DESCRIPTION,schema:w.SCHEMA,annotations:d,action:r=>w.ACTION(r)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:d,action:r=>_.ACTION(r)},{name:z.NAME,description:z.DESCRIPTION,schema:z.SCHEMA,annotations:d,action:r=>z.ACTION(r)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:d,action:r=>D.ACTION(r)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:d,action:r=>L.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.ACTION(r)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:d,action:r=>j.ACTION(r)}]}];function X(r){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return r;let o=new Set(e.split(",").map(n=>n.trim()).filter(Boolean)),t=r.filter(n=>o.has(n.name));return t.length===0?(s.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${r.map(n=>n.name).join(", ")}`),r):(s.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${t.length}/${r.length} tools active`),t)}var Y=It;import{McpServer as wt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as _t}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Rt}from"@modelcontextprotocol/sdk/types.js";import ie from"express";import{randomUUID as zt}from"crypto";import{z as kt}from"zod";var U=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.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 Dt="0.0.1",F=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new wt({name:this.serverName,version:Dt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:kt.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,c)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,c),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ie();o.use(ie.json()),o.post("/mcp",async(n,c)=>{let l=n.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Rt(n.body)){let y=new _t({sessionIdGenerator:()=>zt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{c.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c,n.body)})});let t=async(n,c)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){c.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(n);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new Tt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Gt}from"url";import{dirname as $t}from"path";import{readFileSync as ce,writeFileSync as Lt,existsSync as Jt}from"fs";import{createInterface as qt}from"readline";var jt=Gt(import.meta.url),pe=$t(jt);le({path:ee(process.cwd(),".env")});le({path:ee(pe,"../.env")});async function Ut(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} 17 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=Y.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let c=Number(n);if(isNaN(c)||c<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,X(t.tools));s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${c}...`),await l.startHttpServer(c),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${c}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${c}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var de=["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"];async function Ft(r,e,o){let t=new a(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.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 t.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let n=await Promise.all(e.addresses.map(async l=>{try{let p=await t.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=n.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:n}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${de.join(", ")}`)}}var Bt=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")),Zt=import.meta.url===`file://${process.argv[1]}`;if(Bt||Zt){let r="0.0.0";try{let e=ee(pe,"../package.json");r=JSON.parse(ce(e,"utf-8")).version}catch{r="0.0.0"}Kt(Ht(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${de.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=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},t=await Ft(e.tool,o,e.apikey);console.log(JSON.stringify(t,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(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 u=qt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Jt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=ce(e.input,"utf-8").split(`
|
|
4
|
+
`).map(u=>u.trim()).filter(u=>u.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let t=new a(e.apikey),n=Math.min(Math.max(e.concurrency,1),50),c=[],l=0,p=async(u,b)=>{let m=[];for(let ue of u){let re=ue().then(()=>{m.splice(m.indexOf(re),1)});m.push(re),m.length>=b&&await Promise.race(m)}await Promise.all(m)},S=o.map((u,b)=>async()=>{try{let m=await t.geocode(u);c[b]={address:u,...m}}catch(m){c[b]={address:u,success:!1,error:m.message}}l++,e.output&&process.stderr.write(`\r ${l}/${o.length} geocoded`)});await p(S,n),e.output&&process.stderr.write(`
|
|
5
5
|
`);let g=c.filter(u=>u.success).length,y=c.filter(u=>!u.success).length,te={total:o.length,succeeded:g,failed:y,results:c},P=JSON.stringify(te,null,2);e.output?(Lt(e.output,P,"utf-8"),console.error(`Done: ${g}/${o.length} succeeded. Output: ${e.output}`)):console.log(P),process.exit(y>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("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 --stdio","Start in stdio mode (for Claude Desktop, Cursor, etc.)"]]),async e=>{e.apikey&&(process.env.GOOGLE_MAPS_API_KEY=e.apikey);let o=X(Y[0].tools);e.stdio?await new F(Y[0].name,o).startStdio():(s.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),s.log(" A Model Context Protocol server for Google Maps services"),s.log(""),e.apikey||(s.log("\u26A0\uFE0F Google Maps API Key not found!"),s.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),s.log("")),Ut(e.port,e.apikey).catch(t=>{s.error("\u274C Failed to start server:",t),process.exit(1)}))}).version(r).alias("version","v").help().parse()}export{Ut as startServer};
|
package/dist/index.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ declare class PlacesSearcher {
|
|
|
122
122
|
minRating?: number;
|
|
123
123
|
includedType?: string;
|
|
124
124
|
}): Promise<SearchResponse>;
|
|
125
|
-
getPlaceDetails(placeId: string): Promise<PlaceDetailsResponse>;
|
|
125
|
+
getPlaceDetails(placeId: string, maxPhotos?: number): Promise<PlaceDetailsResponse>;
|
|
126
126
|
geocode(address: string): Promise<GeocodeResponse>;
|
|
127
127
|
reverseGeocode(latitude: number, longitude: number): Promise<ReverseGeocodeResponse>;
|
|
128
128
|
calculateDistanceMatrix(origins: string[], destinations: string[], mode?: "driving" | "walking" | "bicycling" | "transit"): Promise<DistanceMatrixResponse>;
|
|
@@ -201,6 +201,7 @@ declare class NewPlacesService {
|
|
|
201
201
|
includedType?: string;
|
|
202
202
|
maxResultCount?: number;
|
|
203
203
|
}): Promise<any[]>;
|
|
204
|
+
getPhotoUri(photoName: string, maxWidthPx?: number): Promise<string>;
|
|
204
205
|
getPlaceDetails(placeId: string): Promise<{
|
|
205
206
|
name: any;
|
|
206
207
|
place_id: string;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b,c}from"./chunk-
|
|
1
|
+
import{a,b,c}from"./chunk-G6LPVEOX.js";export{c as Logger,a as NewPlacesService,b as PlacesSearcher};
|
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.43",
|
|
4
4
|
"mcpName": "io.github.cablate/google-map",
|
|
5
5
|
"description": "17 Google Maps tools for AI agents — geocode, search, directions, weather, air quality, map images via MCP server or standalone CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
|
|
|
44
44
|
| `maps_reverse_geocode` | Have coordinates, need an address | "What's at 35.65, 139.74?" |
|
|
45
45
|
| `maps_search_nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" |
|
|
46
46
|
| `maps_search_places` | Natural language place search | "Best ramen in Tokyo" |
|
|
47
|
-
| `maps_place_details` | Have a place_id, need full info | "Opening hours and reviews for this restaurant?" |
|
|
47
|
+
| `maps_place_details` | Have a place_id, need full info (+ optional photo URLs via `maxPhotos`) | "Opening hours and reviews for this restaurant?" |
|
|
48
48
|
| `maps_batch_geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" |
|
|
49
49
|
|
|
50
50
|
### Routing & Distance
|
|
@@ -122,15 +122,17 @@ Response: `{ success, data: [{ name, place_id, address, location, rating, total_
|
|
|
122
122
|
|
|
123
123
|
## maps_place_details
|
|
124
124
|
|
|
125
|
-
Get full details for a place by its place_id (from search results). Returns reviews, phone, website, hours,
|
|
125
|
+
Get full details for a place by its place_id (from search results). Returns reviews, phone, website, hours. Set `maxPhotos` to include photo URLs (default: 0 = no photos, saves tokens).
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
exec maps_place_details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg"}'
|
|
129
|
+
exec maps_place_details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg", "maxPhotos": 3}'
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
| Param | Type | Required | Description |
|
|
132
133
|
|-------|------|----------|-------------|
|
|
133
134
|
| placeId | string | yes | Google Maps place ID (from search results) |
|
|
135
|
+
| maxPhotos | number | no | Number of photo URLs to include (0-10, default 0). Always returns `photo_count`. |
|
|
134
136
|
|
|
135
137
|
---
|
|
136
138
|
|
package/dist/chunk-72XPXAUJ.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw p.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw p.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let o=r.data.results[0];return{formatted_address:o.formatted_address,place_id:o.place_id,address_components:o.address_components}}catch(r){throw p.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let a=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(a.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${a.status}`);let s=[],l=[];return a.rows.forEach(u=>{let n=[],c=[];u.elements.forEach(d=>{d.status==="OK"?(n.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(n.push(null),c.push(null))}),s.push(n),l.push(c)}),{distances:s,durations:l,origin_addresses:a.origin_addresses,destination_addresses:a.destination_addresses}}catch(o){throw p.error("Error in calculateDistanceMatrix:",o),new Error(`Failed to calculate distance matrix: ${f(o)}`)}}async getDirections(e,t,r="driving",o,a){try{let s;a&&(s=Math.floor(a.getTime()/1e3));let l;s||(o instanceof Date?l=Math.floor(o.getTime()/1e3):o?l=o:l="now");let n=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:l}})).data;if(n.status!=="OK")throw new Error(`Failed to get directions with status: ${n.status} (arrival_time: ${s}, departure_time: ${l}`);if(n.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=n.routes[0],d=c.legs[0],m=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),h={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(h.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),h)};return{routes:n.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:m(d.arrival_time),departure_time:m(d.departure_time)}}catch(s){throw p.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async searchAlongRoute(e){try{let t=await this.getDirections(e.origin,e.destination,e.mode||"walking"),r=t.routes[0]?.overview_polyline?.points;if(!r)throw new Error("Could not get route polyline");let o=Math.min(e.maxResults||5,20),s=await fetch("https://places.googleapis.com/v1/places:searchText",{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":"places.displayName,places.id,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.currentOpeningHours.openNow"},body:JSON.stringify({textQuery:e.textQuery,searchAlongRouteParameters:{polyline:{encodedPolyline:r}},maxResultCount:o})});if(!s.ok){let n=await s.json().catch(()=>({}));throw new Error(n?.error?.message||`HTTP ${s.status}`)}return{places:((await s.json()).places||[]).map(n=>({name:n.displayName?.text||"",place_id:n.id||"",formatted_address:n.formattedAddress||"",location:{lat:n.location?.latitude||0,lng:n.location?.longitude||0},rating:n.rating||0,user_ratings_total:n.userRatingCount||0,open_now:n.currentOpeningHours?.openNow??null})),route:{distance:t.total_distance.text,duration:t.total_duration.text,polyline:r}}}catch(t){throw p.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",o,a){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,l;switch(r){case"forecast_daily":{let c=Math.min(Math.max(o||5,1),10);l=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(a||24,1),240);l=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:l=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(l);if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let n=await u.json();return r==="current"?{temperature:n.temperature,feelsLike:n.feelsLikeTemperature,humidity:n.relativeHumidity,wind:n.wind,conditions:n.weatherCondition?.description?.text||n.weatherCondition?.type,uvIndex:n.uvIndex,precipitation:n.precipitation,visibility:n.visibility,pressure:n.airPressure,cloudCover:n.cloudCover,isDayTime:n.isDaytime}:n}catch(s){throw p.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,o=!1){try{let a=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),o&&s.push("POLLUTANT_CONCENTRATION");let l={location:{latitude:e,longitude:t}};s.length>0&&(l.extraComputations=s);let u=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!u.ok){let g=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(g)}let n=await u.json(),c=n.indexes||[],d=c[0],m={dateTime:n.dateTime,regionCode:n.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(m.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),n.healthRecommendations&&(m.healthRecommendations=n.healthRecommendations),n.pollutants&&(m.pollutants=n.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),m}catch(a){throw p.error("Error in getAirQuality:",a),new Error(a.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let n of e.markers)r.push(`markers=${encodeURIComponent(n)}`);if(e.path)for(let n of e.path)r.push(`path=${encodeURIComponent(n)}`);let o=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(o.length>16384)throw new Error(`URL exceeds 16,384 character limit (${o.length}). Reduce markers or path points.`);let a=await fetch(o);if(!a.ok){let n=a.headers.get("content-type")||"";if(n.includes("application/json")||n.includes("text/")){let c=await a.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${a.status}`)}let s=await a.arrayBuffer(),l=Buffer.from(s);return{base64:l.toString("base64"),size:l.length,dimensions:t}}catch(t){throw p.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let o=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:o,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let l=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+l).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw p.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${f(o)}`)}}async getElevation(e){try{let t=e.map(a=>({lat:a.latitude,lng:a.longitude})),o=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(o.status!=="OK")throw new Error(`Failed to get elevation data with status: ${o.status}`);return o.results.map((a,s)=>({elevation:a.elevation,location:t[s]}))}catch(t){throw p.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw p.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let o=e?.periods;if(!Array.isArray(o)||o.length===0)return!1;let a=1440,s=a*7,{day:l,minutes:u}=this.getLocalTimeComponents(t),n=l*a+u,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},m=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),h=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(h)))return g*60+h};for(let i of o){let g=d(i?.openDay),h=d(i?.closeDay??i?.openDay),w=m(i?.openTime),N=m(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*a+w,b;h===void 0||N===void 0?b=v+a:b=h*a+N,b<=v&&(b+=s);let P=n;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var x=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),o=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(o=o.filter(a=>a.opening_hours?.open_now===!0)),e.minRating&&(o=o.filter(a=>(a.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:o.map(a=>({name:a.name,place_id:a.place_id,address:a.formatted_address,location:a.geometry.location,rating:a.rating,total_ratings:a.user_ratings_total,open_now:a.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e){try{let t=await this.newPlacesService.getPlaceDetails(e);return{success:!0,data:{name:t.name,address:t.formatted_address,location:t.geometry?.location,rating:t.rating,total_ratings:t.user_ratings_total,open_now:t.opening_hours?.open_now,phone:t.formatted_phone_number,website:t.website,price_level:t.price_level,reviews:t.reviews?.map(r=>({rating:r.rating,text:r.text,time:r.time,author_name:r.author_name}))}}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,a){try{let s=o?new Date(o):new Date,l=a?new Date(a):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,l)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",o,a){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,o,a)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,o){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,o)}}catch(a){return{success:!1,error:a instanceof Error?a.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,o=e.topN||3,a=await this.geocode(e.location);if(!a.success||!a.data)throw new Error(a.error||"Geocode failed");let{lat:s,lng:l}=a.data.location,u=[];for(let n of t){let c=await this.searchNearby({center:{value:`${s},${l}`,isCoordinates:!0},keyword:n,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,o),m=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);m.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}u.push({type:n,count:c.data.length,top:m})}return{success:!0,data:{location:{address:a.data.formatted_address,lat:s,lng:l},radius:r,categories:u}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let o=[];for(let n of r){let c=await this.geocode(n);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${n}`);o.push({originalName:n,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let a=o;if(e.optimize!==!1&&o.length>2){let n=await this.calculateDistanceMatrix(r,r,"driving");if(n.success&&n.data){let c=new Set([0]),d=[0],m=0;for(;c.size<o.length;){let i=-1,g=1/0;for(let h=0;h<o.length;h++){if(c.has(h))continue;let w=n.data.durations[m]?.[h]?.value??1/0;w<g&&(g=w,i=h)}if(i===-1)break;c.add(i),d.push(i),m=i}a=d.map(i=>o[i])}}let s=[],l=0,u=0;for(let n=0;n<a.length-1;n++){let c=await this.getDirections(a[n].originalName,a[n+1].originalName,t);c.success&&c.data?(l+=c.data.total_distance.value,u+=c.data.total_duration.value,s.push({from:a[n].originalName,to:a[n+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:a[n].originalName,to:a[n+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&o.length>2,stops:a.map(n=>`${n.originalName} (${n.address})`),legs:s,total_distance:`${(l/1e3).toFixed(1)} km`,total_duration:`${Math.round(u/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let o=r.data.slice(0,t),a=[];for(let s of o){let l=await this.getPlaceDetails(s.place_id);a.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:l.data?.phone,website:l.data?.website,price_level:l.data?.price_level})}if(e.userLocation&&a.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,l=o.map(n=>`${n.location.lat},${n.location.lng}`),u=await this.calculateDistanceMatrix([s],l,"driving");if(u.success&&u.data)for(let n=0;n<a.length;n++)a[n].distance=u.data.distances[0]?.[n]?.text,a[n].drive_time=u.data.durations[0]?.[n]?.text}return{success:!0,data:a}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var p={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,x as b,p as c};
|