@cablate/mcp-google-map 0.0.62 → 0.0.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +37 -0
- package/README.md +19 -8
- package/README.zh-TW.md +19 -8
- package/dist/chunk-BKUJWPKE.js +1 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +1 -1
- package/examples/agent-skill-demo.md +39 -0
- package/package.json +5 -2
- package/plugin.json +21 -0
- package/skills/google-maps/SKILL.md +2 -2
- package/skills/google-maps/SKILL.skill +0 -0
- package/skills/google-maps/references/content-attribution.md +11 -0
- package/skills/google-maps/references/tools-api.md +7 -0
- package/dist/chunk-V2MW4XB5.js +0 -1
- package/skills/project-docs/SKILL.md +0 -66
- package/skills/project-docs/references/architecture.md +0 -137
- package/skills/project-docs/references/decisions.md +0 -149
- package/skills/project-docs/references/geo-domain-knowledge.md +0 -286
- package/skills/project-docs/references/google-maps-api-guide.md +0 -139
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-google-map",
|
|
3
|
+
"version": "0.0.64",
|
|
4
|
+
"description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "CabLate",
|
|
7
|
+
"url": "https://github.com/cablate"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/cablate/mcp-google-map",
|
|
10
|
+
"repository": "https://github.com/cablate/mcp-google-map",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"google-maps",
|
|
14
|
+
"places",
|
|
15
|
+
"routing",
|
|
16
|
+
"geocoding",
|
|
17
|
+
"travel",
|
|
18
|
+
"agent-skill"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"interface": {
|
|
22
|
+
"displayName": "Google Maps CLI",
|
|
23
|
+
"shortDescription": "Geospatial research and planning through a standalone CLI.",
|
|
24
|
+
"longDescription": "Guide an AI agent to use the @cablate/mcp-google-map exec CLI for place discovery, routing, geocoding, travel planning, local SEO, weather, air quality, and other geographic workflows. No MCP connection is required.",
|
|
25
|
+
"developerName": "CabLate",
|
|
26
|
+
"category": "Productivity",
|
|
27
|
+
"capabilities": [
|
|
28
|
+
"Read"
|
|
29
|
+
],
|
|
30
|
+
"websiteURL": "https://github.com/cablate/mcp-google-map",
|
|
31
|
+
"defaultPrompt": [
|
|
32
|
+
"Find and compare places for my trip.",
|
|
33
|
+
"Plan an efficient route between these stops.",
|
|
34
|
+
"Research what is near this location."
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
package/README.md
CHANGED
|
@@ -89,6 +89,17 @@ All tools are annotated with `readOnlyHint: true` and `destructiveHint: false`
|
|
|
89
89
|
|
|
90
90
|
## Installation
|
|
91
91
|
|
|
92
|
+
### Codex Plugin (Agent Skill, no MCP required)
|
|
93
|
+
|
|
94
|
+
Install the CabLate marketplace, then install the Skill-only plugin:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
codex plugin marketplace add cablate/mcp-google-map --ref main
|
|
98
|
+
codex plugin add mcp-google-map@cablate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Set `GOOGLE_MAPS_API_KEY` in the environment available to Codex, make sure Node.js 18+ and `npx` are installed, then start a new conversation. The plugin teaches the agent to run `npx -y @cablate/mcp-google-map exec ...` directly. It does not register or start an MCP server; the MCP setup below remains an independent option.
|
|
102
|
+
|
|
92
103
|
### Method 1: stdio (Recommended for most clients)
|
|
93
104
|
|
|
94
105
|
Works with Claude Desktop, Cursor, VS Code, and any MCP client that supports stdio:
|
|
@@ -168,6 +179,8 @@ To use the **Agent Skill without MCP**:
|
|
|
168
179
|
|
|
169
180
|
You can verify the CLI is available with `npx -y @cablate/mcp-google-map exec --help` before making a billable API call.
|
|
170
181
|
|
|
182
|
+
For a reproducible no-MCP walkthrough, see the [Agent Skill demo](./examples/agent-skill-demo.md). If your agent or app displays Places reviews, photos, or AI summaries, follow the [content attribution and storage guidance](./skills/google-maps/references/content-attribution.md); the tool preserves source/disclosure metadata but does not render a compliant UI for you.
|
|
183
|
+
|
|
171
184
|
### Batch Geocode
|
|
172
185
|
|
|
173
186
|
Geocode hundreds of addresses from a file:
|
|
@@ -288,20 +301,18 @@ src/
|
|
|
288
301
|
tests/
|
|
289
302
|
└── smoke.test.ts # Smoke + E2E test suite
|
|
290
303
|
skills/
|
|
291
|
-
|
|
304
|
+
└── google-maps/ # Public Agent Skill — standalone CLI workflows
|
|
292
305
|
│ ├── SKILL.md # Tool map, recipes, invocation
|
|
293
306
|
│ ├── SKILL.skill # Importable archive of this Skill
|
|
294
307
|
│ └── references/
|
|
295
308
|
│ ├── tools-api.md # Tool parameters + scenario recipes
|
|
296
309
|
│ ├── travel-planning.md # Travel planning methodology
|
|
297
310
|
│ └── local-seo.md # Local SEO / Google Business Profile ranking analysis
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
├── geo-domain-knowledge.md # GIS fundamentals, Japan context
|
|
304
|
-
└── decisions.md # 10 ADRs (design decisions + rationale)
|
|
311
|
+
.agents/
|
|
312
|
+
├── plugins/marketplace.json # CabLate marketplace catalog
|
|
313
|
+
└── skills/project-docs/ # Maintainer-only development Skill
|
|
314
|
+
.codex-plugin/plugin.json # Codex compatibility manifest
|
|
315
|
+
plugin.json # Portable Agent Plugin manifest
|
|
305
316
|
```
|
|
306
317
|
|
|
307
318
|
## Tech Stack
|
package/README.zh-TW.md
CHANGED
|
@@ -89,6 +89,17 @@ npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
|
|
|
89
89
|
|
|
90
90
|
## 安裝
|
|
91
91
|
|
|
92
|
+
### Codex Plugin(Agent Skill,不需要 MCP)
|
|
93
|
+
|
|
94
|
+
先加入 CabLate marketplace,再安裝只包含 Skill 的 plugin:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
codex plugin marketplace add cablate/mcp-google-map --ref main
|
|
98
|
+
codex plugin add mcp-google-map@cablate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
請在 Codex 可讀取的環境中設定 `GOOGLE_MAPS_API_KEY`,並確認已安裝 Node.js 18+ 與 `npx`,然後開啟新對話。Plugin 會教代理直接執行 `npx -y @cablate/mcp-google-map exec ...`;它不會註冊或啟動 MCP server。下方的 MCP 設定仍是另一種獨立使用方式。
|
|
102
|
+
|
|
92
103
|
### 方法一:stdio(大多數客戶端推薦)
|
|
93
104
|
|
|
94
105
|
適用於 Claude Desktop、Cursor、VS Code 及任何支援 stdio 的 MCP 客戶端:
|
|
@@ -168,6 +179,8 @@ npx @cablate/mcp-google-map exec search-places '{"query":"東京拉麵"}'
|
|
|
168
179
|
|
|
169
180
|
呼叫可能計費的 API 前,可先用 `npx -y @cablate/mcp-google-map exec --help` 確認 CLI 可用。
|
|
170
181
|
|
|
182
|
+
完整的非 MCP 操作步驟見 [Agent Skill 示範](./examples/agent-skill-demo.md)。若代理工具或應用程式會呈現 Places 評論、照片或 AI 摘要,請遵照[內容署名與保存指引](./skills/google-maps/references/content-attribution.md);工具會保留來源及揭露欄位,但不會替你的介面完成署名呈現。
|
|
183
|
+
|
|
171
184
|
### 批次地理編碼
|
|
172
185
|
|
|
173
186
|
從檔案批次地理編碼:
|
|
@@ -285,20 +298,18 @@ src/
|
|
|
285
298
|
tests/
|
|
286
299
|
└── smoke.test.ts # Smoke + E2E 測試套件
|
|
287
300
|
skills/
|
|
288
|
-
|
|
301
|
+
└── google-maps/ # 公開 Agent Skill — 獨立 CLI 工作流程
|
|
289
302
|
│ ├── SKILL.md # 工具對照表、場景食譜、呼叫方式
|
|
290
303
|
│ ├── SKILL.skill # 可匯入的 Skill 封裝檔
|
|
291
304
|
│ └── references/
|
|
292
305
|
│ ├── tools-api.md # 工具參數 + 場景食譜
|
|
293
306
|
│ ├── travel-planning.md # 旅行規劃方法論
|
|
294
307
|
│ └── local-seo.md # Local SEO / Google 商家排名分析
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
├── geo-domain-knowledge.md # GIS 基礎、日本場景
|
|
301
|
-
└── decisions.md # 10 個 ADR(設計決策 + 理由)
|
|
308
|
+
.agents/
|
|
309
|
+
├── plugins/marketplace.json # CabLate marketplace 目錄
|
|
310
|
+
└── skills/project-docs/ # 僅供維護者使用的開發 Skill
|
|
311
|
+
.codex-plugin/plugin.json # Codex 相容 manifest
|
|
312
|
+
plugin.json # 可攜式 Agent Plugin manifest
|
|
302
313
|
```
|
|
303
314
|
|
|
304
315
|
## 技術棧
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Client as U,Language as W}from"@googlemaps/google-maps-services-js";import G from"dotenv";var O="https://routes.googleapis.com",D={driving:"DRIVE",walking:"WALK",bicycling:"BICYCLE",transit:"TRANSIT"},L=["routes.distanceMeters","routes.duration","routes.description","routes.polyline.encodedPolyline","routes.legs.distanceMeters","routes.legs.duration","routes.legs.startLocation","routes.legs.endLocation","routes.legs.steps.navigationInstruction","routes.legs.steps.distanceMeters","routes.legs.steps.staticDuration","routes.legs.steps.startLocation","routes.legs.steps.endLocation","routes.legs.steps.transitDetails","routes.legs.polyline","routes.optimizedIntermediateWaypointIndex"].join(","),F="originIndex,destinationIndex,distanceMeters,duration,status,condition";function A(l){if(!l)return 0;let e=l.match(/^(\d+)s$/);return e?parseInt(e[1],10):0}function E(l){return l>=1e3?`${(l/1e3).toFixed(1)} km`:`${l} m`}function k(l){if(l>=3600){let t=Math.floor(l/3600),r=Math.round(l%3600/60);return r>0?`${t} hour${t>1?"s":""} ${r} min${r>1?"s":""}`:`${t} hour${t>1?"s":""}`}let e=Math.round(l/60);return`${e} min${e!==1?"s":""}`}var N=/^\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*$/;function x(l){return typeof l=="string"?l:"placeId"in l?`placeId:${l.placeId}`:`${l.latLng.latitude},${l.latLng.longitude}`}function T(l){if(typeof l!="string")return"placeId"in l?{placeId:l.placeId}:{location:{latLng:l.latLng}};let e=l.match(N);return e?{location:{latLng:{latitude:parseFloat(e[1]),longitude:parseFloat(e[2])}}}:{address:l}}function C(l,e){let t={};if(l.avoidTolls&&(t.avoidTolls=!0),l.avoidHighways&&(t.avoidHighways=!0),Object.keys(t).length!==0){if(e!=="DRIVE")throw new Error('Route modifiers "avoid_tolls" and "avoid_highways" are only supported with mode "driving".');return t}}var v=class{constructor(e){if(this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async computeRoutes(e){let t=D[e.mode||"driving"]||"DRIVE",r={origin:T(e.origin),destination:T(e.destination),travelMode:t,computeAlternativeRoutes:!1};t==="DRIVE"&&(r.routingPreference="TRAFFIC_AWARE");let o=C(e,t);o&&(r.routeModifiers=o),e.arrivalTime?r.arrivalTime=e.arrivalTime.toISOString():e.departureTime&&(r.departureTime=e.departureTime.toISOString()),e.intermediates&&e.intermediates.length>0&&(r.intermediates=e.intermediates.map(T)),e.optimizeWaypointOrder&&e.intermediates&&e.intermediates.length>1&&t!=="TRANSIT"&&(r.optimizeWaypointOrder=!0);let n=await fetch(`${O}/directions/v2:computeRoutes`,{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":L},body:JSON.stringify(r)});if(!n.ok){let p=(await n.json().catch(()=>({})))?.error?.message||`HTTP ${n.status}`;throw new Error(p)}let s=await n.json();if(!s.routes||s.routes.length===0){let g=e.mode||"driving";throw g==="transit"?new Error(`No transit route found from "${e.originLabel??x(e.origin)}" to "${e.destinationLabel??x(e.destination)}". The Google Routes API does not support transit directions in some regions (notably Japan and India). Try using mode "driving" or "walking" instead, or use a regional transit service for public transportation details.`):new Error(`No route found from "${e.originLabel??x(e.origin)}" to "${e.destinationLabel??x(e.destination)}" with mode: ${g}`)}let i=s.routes[0],u=i.distanceMeters||0,a=A(i.duration);return{routes:s.routes,summary:i.description||"",total_distance:{value:u,text:E(u)},total_duration:{value:a,text:k(a)},arrival_time:"",departure_time:"",...i.optimizedIntermediateWaypointIndex?{optimizedIntermediateWaypointIndex:i.optimizedIntermediateWaypointIndex}:{}}}async computeRouteMatrix(e){let t=D[e.mode||"driving"]||"DRIVE",r={origins:e.origins.map(c=>({waypoint:T(c)})),destinations:e.destinations.map(c=>({waypoint:T(c)})),travelMode:t};t==="DRIVE"&&(r.routingPreference="TRAFFIC_AWARE");let o=C(e,t);o&&(r.routeModifiers=o),e.departureTime&&(r.departureTime=e.departureTime.toISOString());let n=await fetch(`${O}/distanceMatrix/v2:computeRouteMatrix`,{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":F},body:JSON.stringify(r)});if(!n.ok){let d=(await n.json().catch(()=>({})))?.error?.message||`HTTP ${n.status}`;throw new Error(d)}let s=await n.json(),i=e.origins.length,u=e.destinations.length,a=Array.from({length:i},()=>Array(u).fill(null)),g=Array.from({length:i},()=>Array(u).fill(null)),p=0;for(let c of s){let d=c.originIndex,h=c.destinationIndex;if(d===void 0||h===void 0)continue;if(c.condition==="ROUTE_NOT_FOUND"){p++;continue}let y=c.distanceMeters||0,_=A(c.duration);a[d][h]={value:y,text:E(y)},g[d][h]={value:_,text:k(_)}}let m=i*u;if(p===m&&t==="TRANSIT")throw new Error('No transit routes found for any origin/destination pair. The Google Routes API does not support transit directions in some regions (notably Japan and India). Try using mode "driving" or "walking" instead, or use a regional transit service for public transportation details.');return{distances:a,durations:g,origin_addresses:e.origins,destination_addresses:e.destinations,...p>0&&t==="TRANSIT"?{warning:`${p} of ${m} origin/destination pairs returned no transit route. The Google Routes API has limited transit coverage in some regions.`}:{}}}};G.config();function R(l){let e=l?.response?.status,t=l?.response?.data?.error_message,r=l?.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})`:l instanceof Error?l.message:String(l)}var M=class{constructor(e){this.defaultLanguage=W.en;if(this.client=new U({}),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 f.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${R(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 f.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${R(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 f.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${R(r)}`)}}async searchAlongRoute(e){try{let r=await new v(this.apiKey).computeRoutes({origin:e.origin,destination:e.destination,mode:e.mode||"walking"}),o=r.routes[0]?.polyline?.encodedPolyline;if(!o)throw new Error("Could not get route polyline");let n=Math.min(e.maxResults||5,20),i=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:o}},maxResultCount:n})});if(!i.ok){let g=await i.json().catch(()=>({}));throw new Error(g?.error?.message||`HTTP ${i.status}`)}return{places:((await i.json()).places||[]).map(g=>({name:g.displayName?.text||"",place_id:g.id||"",formatted_address:g.formattedAddress||"",location:{lat:g.location?.latitude||0,lng:g.location?.longitude||0},rating:g.rating||0,user_ratings_total:g.userRatingCount||0,open_now:g.currentOpeningHours?.openNow??null})),route:{distance:r.total_distance.text,duration:r.total_duration.text,polyline:o}}}catch(t){throw f.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}`,i;switch(r){case"forecast_daily":{let g=Math.min(Math.max(o||5,1),10);i=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${g}`;break}case"forecast_hourly":{let g=Math.min(Math.max(n||24,1),240);i=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${g}`;break}default:i=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(i);if(!u.ok){let p=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw p.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(p)}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 f.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 i={location:{latitude:e,longitude:t}};s.length>0&&(i.extraComputations=s);let u=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(d)}let a=await u.json(),g=a.indexes||[],p=g[0],m={dateTime:a.dateTime,regionCode:a.regionCode,aqi:p?.aqi,category:p?.category,dominantPollutant:p?.dominantPollutant,color:p?.color};return g.length>1&&(m.indexes=g.map(c=>({code:c.code,displayName:c.displayName,aqi:c.aqi,category:c.category,dominantPollutant:c.dominantPollutant}))),a.healthRecommendations&&(m.healthRecommendations=a.healthRecommendations),a.pollutants&&(m.pollutants=a.pollutants.map(c=>({code:c.code,displayName:c.displayName,concentration:c.concentration,additionalInfo:c.additionalInfo}))),m}catch(n){throw f.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 g=await n.text();throw new Error(`Static Maps API error: ${g}`)}throw new Error(`Static Maps API returned HTTP ${n.status}`)}let s=await n.arrayBuffer(),i=Buffer.from(s);return{base64:i.toString("base64"),size:i.length,dimensions:t}}catch(t){throw f.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 i=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+i).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw f.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${R(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 f.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${R(t)}`)}}};import{PlacesClient as z}from"@googlemaps/places";var P=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","primaryType","types","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","editorialSummary","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","reviews.authorAttribution.uri","reviews.authorAttribution.photoUri","reviews.googleMapsUri","reviews.flagContentUri","reviews.relativePublishTimeDescription","photos.heightPx","photos.widthPx","photos.name","photos.authorAttributions","photos.googleMapsUri","photos.flagContentUri","googleMapsUri","parkingOptions","accessibilityOptions","servesVegetarianFood","servesBeer","servesWine","servesCocktails","servesBreakfast","servesLunch","servesDinner","servesBrunch","servesCoffee","servesDessert","dineIn","delivery","takeout","curbsidePickup","reservable","goodForGroups","goodForChildren","goodForWatchingSports","liveMusic","outdoorSeating","allowsDogs","menuForChildren","restroom","paymentOptions","reviewSummary","generativeSummary"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow","places.primaryType","places.priceLevel"].join(",");if(this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",this.client=new z({apiKey:this.apiKey}),!this.apiKey)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 f.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 f.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 f.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 f.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}},primary_type:e.primaryType||null,price_level:e.priceLevel||null,rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){let t=e.parkingOptions?Object.fromEntries(Object.entries(e.parkingOptions).filter(([,i])=>i===!0)):void 0,r=e.accessibilityOptions?Object.fromEntries(Object.entries(e.accessibilityOptions).filter(([,i])=>i===!0)):void 0,o={};e.dineIn&&(o.dine_in=!0),e.delivery&&(o.delivery=!0),e.takeout&&(o.takeout=!0),e.curbsidePickup&&(o.curbside_pickup=!0),e.reservable&&(o.reservable=!0);let n={};e.servesVegetarianFood&&(n.vegetarian_food=!0),e.servesBeer&&(n.beer=!0),e.servesWine&&(n.wine=!0),e.servesCocktails&&(n.cocktails=!0),e.servesBreakfast&&(n.breakfast=!0),e.servesLunch&&(n.lunch=!0),e.servesDinner&&(n.dinner=!0),e.servesBrunch&&(n.brunch=!0),e.servesCoffee&&(n.coffee=!0),e.servesDessert&&(n.dessert=!0);let s={};return e.goodForGroups&&(s.good_for_groups=!0),e.goodForChildren&&(s.good_for_children=!0),e.goodForWatchingSports&&(s.good_for_watching_sports=!0),e.liveMusic&&(s.live_music=!0),e.outdoorSeating&&(s.outdoor_seating=!0),e.allowsDogs&&(s.allows_dogs=!0),e.menuForChildren&&(s.menu_for_children=!0),e.restroom&&(s.restroom=!0),{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",google_maps_uri:e.googleMapsUri||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},primary_type:e.primaryType||null,types:e.types||[],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,editorial_summary:e.editorialSummary?.text||null,...Object.keys(t||{}).length>0?{parking:t}:{},...Object.keys(r||{}).length>0?{accessibility:r}:{},...Object.keys(o).length>0?{dining_options:o}:{},...Object.keys(n).length>0?{serves:n}:{},...Object.keys(s).length>0?{atmosphere:s}:{},...e.paymentOptions?{payment_options:Object.fromEntries(Object.entries(e.paymentOptions).filter(([i])=>!i.startsWith("_")))}:{},...e.reviewSummary?.text?.text?{review_summary:e.reviewSummary.text.text,review_summary_attribution:{disclosure_text:e.reviewSummary.disclosureText?.text||"",flag_content_uri:e.reviewSummary.flagContentUri||"",reviews_uri:e.reviewSummary.reviewsUri||""}}:{},...e.generativeSummary?.overview?.text?{generative_summary:e.generativeSummary.overview.text,generative_summary_attribution:{disclosure_text:e.generativeSummary.disclosureText?.text||"",flag_content_uri:e.generativeSummary.flagContentUri||""}}:{},reviews:e.reviews?.map(i=>({rating:i.rating||0,text:i.text?.text||"",language:i.text?.languageCode||null,time:i.publishTime?.seconds||0,author_name:i.authorAttribution?.displayName||"",author_uri:i.authorAttribution?.uri||"",author_photo_uri:i.authorAttribution?.photoUri||"",google_maps_uri:i.googleMapsUri||"",flag_content_uri:i.flagContentUri||"",relative_publish_time_description:i.relativePublishTimeDescription||""}))||[],photos:e.photos?.map(i=>({photo_reference:i.name||"",height:i.heightPx||0,width:i.widthPx||0,google_maps_uri:i.googleMapsUri||"",flag_content_uri:i.flagContentUri||"",author_attributions:i.authorAttributions?.map(u=>({display_name:u.displayName||"",uri:u.uri||"",photo_uri:u.photoUri||""}))||[]}))||[]}}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:i,minutes:u}=this.getLocalTimeComponents(t),a=i*n+u,g={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},p=c=>{if(typeof c=="number"&&c>=0&&c<=6)return c;if(typeof c=="string"){let d=c.toUpperCase();if(d in g)return g[d]}},m=c=>{if(!c)return;let d=typeof c.hours=="number"?c.hours:Number(c.hours??NaN),h=typeof c.minutes=="number"?c.minutes:Number(c.minutes??NaN);if(!(!Number.isFinite(d)||!Number.isFinite(h)))return d*60+h};for(let c of o){let d=p(c?.openDay),h=p(c?.closeDay??c?.openDay),y=m(c?.openTime),_=m(c?.closeTime);if(d===void 0||y===void 0)continue;let w=d*n+y,b;h===void 0||_===void 0?b=w+n:b=h*n+_,b<=w&&(b+=s);let I=a;for(;I<w;)I+=s;if(I>=w&&I<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))}};function K(l){let e=l.match(N);if(!e)return null;let t=parseFloat(e[1]),r=parseFloat(e[2]);return!Number.isFinite(t)||!Number.isFinite(r)||t<-90||t>90||r<-180||r>180?null:{lat:t,lng:r}}function S(l){return l.placeId?{placeId:l.placeId}:{latLng:{latitude:l.lat,longitude:l.lng}}}var $=class{constructor(e){this.mapsTools=new M(e),this.newPlacesService=new P(e),this.routesService=new v(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,primary_type:n.primary_type||null,price_level:n.price_level||null,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,primary_type:r.primary_type||null,price_level:r.price_level||null,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 i=await this.newPlacesService.getPhotoUri(s.photo_reference);o.push({url:i,width:s.width,height:s.height,google_maps_uri:s.google_maps_uri,flag_content_uri:s.flag_content_uri,author_attributions:s.author_attributions})}catch{}}return{success:!0,data:{name:r.name,address:r.formatted_address,google_maps_uri:r.google_maps_uri,location:r.geometry?.location,primary_type:r.primary_type||null,types:r.types||[],rating:r.rating,total_ratings:r.user_ratings_total,opening_hours:r.opening_hours,phone:r.formatted_phone_number,website:r.website,price_level:r.price_level,editorial_summary:r.editorial_summary||null,...r.parking?{parking:r.parking}:{},...r.accessibility?{accessibility:r.accessibility}:{},...r.dining_options?{dining_options:r.dining_options}:{},...r.serves?{serves:r.serves}:{},...r.atmosphere?{atmosphere:r.atmosphere}:{},...r.payment_options?{payment_options:r.payment_options}:{},...r.review_summary?{review_summary:r.review_summary}:{},...r.review_summary_attribution?{review_summary_attribution:r.review_summary_attribution}:{},...r.generative_summary?{generative_summary:r.generative_summary}:{},...r.generative_summary_attribution?{generative_summary_attribution:r.generative_summary_attribution}:{},photo_count:r.photos?.length||0,...o&&o.length>0?{photos:o}:{},reviews:r.reviews?.map(n=>({rating:n.rating,text:n.text,language:n.language||null,time:n.time,author_name:n.author_name,author_uri:n.author_uri,author_photo_uri:n.author_photo_uri,google_maps_uri:n.google_maps_uri,flag_content_uri:n.flag_content_uri,relative_publish_time_description:n.relative_publish_time_description}))}}}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",o,n,s){try{return{success:!0,data:await this.routesService.computeRouteMatrix({origins:e,destinations:t,mode:r,...o?{departureTime:new Date(o)}:{},...n!==void 0?{avoidTolls:n}:{},...s!==void 0?{avoidHighways:s}:{}})}}catch(i){return{success:!1,error:i instanceof Error?i.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,n,s,i){try{let u=o?new Date(o):void 0,a=n?new Date(n):void 0;return{success:!0,data:await this.routesService.computeRoutes({origin:e,destination:t,mode:r,...u?{departureTime:u}:{},...a?{arrivalTime:a}:{},...s!==void 0?{avoidTolls:s}:{},...i!==void 0?{avoidHighways:i}:{}})}}catch(u){return{success:!1,error:u instanceof Error?u.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 resolveLocation(e){if(N.test(e)){let o=K(e);if(!o)throw new Error(`Failed to resolve location: ${e} (not a valid "latitude,longitude" pair)`);let n=await this.geocode(e);return{originalName:e,address:n.success&&n.data?n.data.formatted_address:e,lat:o.lat,lng:o.lng,placeId:""}}let t=await this.geocode(e);if(t.success&&t.data)return{originalName:e,address:t.data.formatted_address,lat:t.data.location.lat,lng:t.data.location.lng,placeId:t.data.place_id};let r=await this.searchText({query:e});if(r.success&&r.data&&r.data.length>0){let o=r.data[0];return{originalName:e,address:o.address||o.name,lat:o.location.lat,lng:o.location.lng,placeId:o.place_id||""}}throw new Error(`Failed to resolve location: ${e} (geocoding: ${t.error||"no result"}; places text search: ${r.error||"no result"})`)}async exploreArea(e){let t=e.types||["restaurant","cafe","tourist_attraction"],r=e.radius||1e3,o=e.topN||3,n=await this.resolveLocation(e.location),{lat:s,lng:i}=n,u=[];for(let a of t){let g=await this.searchNearby({center:{value:`${s},${i}`,isCoordinates:!0},keyword:a,radius:r});if(!g.success||!g.data)continue;let p=g.data.slice(0,o),m=[];for(let c of p){if(!c.place_id)continue;let d=await this.getPlaceDetails(c.place_id);m.push({name:c.name,address:c.address,rating:c.rating,total_ratings:c.total_ratings,open_now:c.open_now,phone:d.data?.phone,website:d.data?.website})}u.push({type:a,count:g.data.length,top:m})}return{success:!0,data:{location:{address:n.address,lat:s,lng:i},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 y of r)o.push(await this.resolveLocation(y));let n=S(o[0]),s=S(o[o.length-1]),i=r.length>2?o.slice(1,-1).map(y=>S(y)):void 0,u=e.optimize!==!1&&r.length>3&&t!=="transit",a=await this.routesService.computeRoutes({origin:n,destination:s,mode:t,intermediates:i,optimizeWaypointOrder:u,originLabel:r[0],destinationLabel:r[r.length-1],...e.departure_time?{departureTime:new Date(e.departure_time)}:{},...e.avoid_tolls!==void 0?{avoidTolls:e.avoid_tolls}:{},...e.avoid_highways!==void 0?{avoidHighways:e.avoid_highways}:{}}),p=a.routes[0]?.legs||[],m;if(u&&a.optimizedIntermediateWaypointIndex){let y=a.optimizedIntermediateWaypointIndex,_=o.slice(1,-1);m=[o[0],...y.map(w=>_[w]),o[o.length-1]]}else m=o;let c=[],d=0,h=0;for(let y=0;y<m.length-1;y++){let _=p[y];if(_){let w=_.distanceMeters||0,b=A(_.duration);d+=w,h+=b,c.push({from:m[y].originalName,to:m[y+1].originalName,distance:E(w),duration:k(b)})}else c.push({from:m[y].originalName,to:m[y+1].originalName,distance:"unknown",duration:"unknown",note:"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:u,stops:m.map(y=>`${y.originalName} (${y.address})`),legs:c,total_distance:`${(d/1e3).toFixed(1)} km`,total_duration:`${Math.round(h/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 i=await this.getPlaceDetails(s.place_id);n.push({name:s.name,address:s.address,primary_type:i.data?.primary_type||s.primary_type||null,rating:s.rating,total_ratings:s.total_ratings,opening_hours:i.data?.opening_hours,phone:i.data?.phone,website:i.data?.website,price_level:i.data?.price_level,...i.data?.parking?{parking:i.data.parking}:{},...i.data?.serves?{serves:i.data.serves}:{},...i.data?.atmosphere?{atmosphere:i.data.atmosphere}:{},...i.data?.dining_options?{dining_options:i.data.dining_options}:{}})}if(e.userLocation&&n.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,i=o.map(a=>`${a.location.lat},${a.location.lng}`),u=await this.calculateDistanceMatrix([s],i,"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"}}}async localRankTracker(e){try{let t=e.gridSize||3,r=e.gridSpacing||1e3,{latitude:o,longitude:n}=e.center,s=Math.floor(t/2),i=111320,u=111320*Math.cos(o*Math.PI/180),a=[];for(let m=0;m<t;m++)for(let c=0;c<t;c++){let d=(m-s)*r,h=(c-s)*r;a.push({row:m,col:c,lat:o+d/i,lng:n+h/u})}let g="";try{let m=await this.getPlaceDetails(e.placeId);m.success&&m.data&&(g=m.data.name)}catch{}let p=[];for(let m of e.keywords){let c=await this.scanKeywordGrid(m,e.placeId,a,r);p.push({keyword:m,...c})}if(p.length===1){let m=p[0];return{success:!0,data:{target:{name:g,place_id:e.placeId},grid_size:`${t}x${t}`,grid_spacing_m:r,keyword:m.keyword,metrics:m.metrics,grid:m.grid}}}return{success:!0,data:{target:{name:g,place_id:e.placeId},grid_size:`${t}x${t}`,grid_spacing_m:r,keywords:p.map(m=>({keyword:m.keyword,metrics:m.metrics,grid:m.grid}))}}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during local rank tracking"}}}async scanKeywordGrid(e,t,r,o){let s=[],i=async d=>{try{let h=await this.newPlacesService.searchText({textQuery:e,locationBias:{lat:d.lat,lng:d.lng,radius:o/2},maxResultCount:20}),y=h.findIndex(w=>w.place_id===t),_=h.slice(0,3).map(w=>w.name||"");return{row:d.row,col:d.col,lat:Math.round(d.lat*1e6)/1e6,lng:Math.round(d.lng*1e6)/1e6,rank:y>=0?y+1:null,top3:_}}catch{return{row:d.row,col:d.col,lat:Math.round(d.lat*1e6)/1e6,lng:Math.round(d.lng*1e6)/1e6,rank:null,top3:[]}}};for(let d=0;d<r.length;d+=5){let h=r.slice(d,d+5),y=await Promise.all(h.map(i));s.push(...y)}let u=s.filter(d=>d.rank!==null),a=s.length,g=u.filter(d=>d.rank<=3).length,p=u.length>0?Math.round(u.reduce((d,h)=>d+h.rank,0)/u.length*10)/10:null,m=Math.round(s.reduce((d,h)=>d+(h.rank??21),0)/a*10)/10,c=Math.round(g/a*1e3)/10;return{metrics:{arp:p,atrp:m,solv:c,found_in:`${u.length}/${a}`},grid:s}}};var f={log:(...l)=>{console.error("[INFO]",...l)},error:(...l)=>{console.error("[ERROR]",...l)}};export{P as a,$ as b,f as c};
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{b as a,c as n}from"./chunk-
|
|
3
|
-
`+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:fe,DESCRIPTION:he,SCHEMA:Ee,ACTION:Se};import{z as ne}from"zod";var be="maps_place_details",Pe="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get 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).",xe={placeId:ne.string().describe("Google Maps place ID"),maxPhotos:ne.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Ae(t){try{let e=i(),r=await new a(e).getPlaceDetails(t.placeId,t.maxPhotos||0);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var C={NAME:be,DESCRIPTION:Pe,SCHEMA:xe,ACTION:Ae};import{z as ve}from"zod";var we="maps_geocode",Oe="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Ce={address:ve.string().describe("Address or place name to convert to coordinates")};async function Me(t){try{let e=i(),r=await new a(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:we,DESCRIPTION:Oe,SCHEMA:Ce,ACTION:Me};import{z as ie}from"zod";var Ne="maps_reverse_geocode",Te="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",_e={latitude:ie.number().describe("Latitude coordinate"),longitude:ie.number().describe("Longitude coordinate")};async function Ie(t){try{let e=i(),r=await new a(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var N={NAME:Ne,DESCRIPTION:Te,SCHEMA:_e,ACTION:Ie};import{z as b}from"zod";var ke="maps_distance_matrix",Re="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",ze={origins:b.array(b.string()).describe("List of origin addresses or coordinates"),destinations:b.array(b.string()).describe("List of destination addresses or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation"),departure_time:b.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."),avoid_tolls:b.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:b.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function He(t){try{let e=i(),r=await new a(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode,t.departure_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:ke,DESCRIPTION:Re,SCHEMA:ze,ACTION:He};import{z as v}from"zod";var De="maps_directions",Ke="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",$e={origin:v.string().describe("Starting point address or coordinates"),destination:v.string().describe("Destination address or coordinates"),mode:v.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:v.string().optional().describe("Departure time (ISO string format)"),arrival_time:v.string().optional().describe("Arrival time (ISO string format)"),avoid_tolls:v.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:v.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function Ge(t){try{let e=i(),r=await new a(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:De,DESCRIPTION:Ke,SCHEMA:$e,ACTION:Ge};import{z as Y}from"zod";var Le="maps_elevation",Je="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",qe={locations:Y.array(Y.object({latitude:Y.number().describe("Latitude coordinate"),longitude:Y.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function je(t){try{let e=i(),r=await new a(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:Le,DESCRIPTION:Je,SCHEMA:qe,ACTION:je};import{z as P}from"zod";var Ue="maps_search_places",Fe="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ze={query:P.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:P.object({latitude:P.number().describe("Latitude to bias results toward"),longitude:P.number().describe("Longitude to bias results toward"),radius:P.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:P.boolean().optional().describe("Only return places that are currently open"),minRating:P.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:P.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Be(t){try{let e=i(),r=await new a(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var k={NAME:Ue,DESCRIPTION:Fe,SCHEMA:Ze,ACTION:Be};import{z as te}from"zod";var We="maps_timezone",Ve="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",Ye={latitude:te.number().describe("Latitude coordinate"),longitude:te.number().describe("Longitude coordinate"),timestamp:te.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Qe(t){try{let e=i(),r=await new a(e).getTimezone(t.latitude,t.longitude,t.timestamp);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var R={NAME:We,DESCRIPTION:Ve,SCHEMA:Ye,ACTION:Qe};import{z}from"zod";var Xe="maps_weather",et="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",tt={latitude:z.number().describe("Latitude coordinate"),longitude:z.number().describe("Longitude coordinate"),type:z.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:z.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:z.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function rt(t){try{let e=i(),r=await new a(e).getWeather(t.latitude,t.longitude,t.type||"current",t.forecastDays,t.forecastHours);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var H={NAME:Xe,DESCRIPTION:et,SCHEMA:tt,ACTION:rt};import{z as D}from"zod";var ot="maps_explore_area",st="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",at={location:D.string().describe("Address or landmark to explore around"),types:D.array(D.string()).optional().describe("Place types to search (default: restaurant, cafe, tourist_attraction). Must be Places API (New) type names. Examples: hotel, bar, park, museum"),radius:D.number().optional().describe("Search radius in meters (default: 1000)"),topN:D.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function nt(t){try{let e=i(),r=await new a(e).exploreArea(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var K={NAME:ot,DESCRIPTION:st,SCHEMA:at,ACTION:nt};import{z as w}from"zod";var it="maps_plan_route",ct="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (2 to 25 intermediate stops) to find the most efficient visit order, and returns directions for each leg. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. Waypoint optimization requires at least 4 stops (2 intermediates); with 2 or 3 stops the route is returned in the original order. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",lt={stops:w.array(w.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:w.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:w.boolean().optional().describe("Auto-optimize visit order via Routes API waypoint optimization (default: true). Requires at least 4 stops (2 intermediates) \u2014 ignored for 2-3 stops. Set false to keep original order. Not available for transit mode."),departure_time:w.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),avoid_tolls:w.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:w.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function pt(t){try{let e=i(),r=await new a(e).planRoute(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var $={NAME:it,DESCRIPTION:ct,SCHEMA:lt,ACTION:pt};import{z as G}from"zod";var dt="maps_compare_places",ut="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",mt={query:G.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:G.object({latitude:G.number().describe("Your latitude"),longitude:G.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:G.number().optional().describe("Max places to compare (default: 5)")};async function gt(t){try{let e=i(),r=await new a(e).comparePlaces(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var L={NAME:dt,DESCRIPTION:ut,SCHEMA:mt,ACTION:gt};import{z as Q}from"zod";var yt="maps_air_quality",ft="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",ht={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),includeHealthRecommendations:Q.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:Q.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function Et(t){try{let e=i(),r=await new a(e).getAirQuality(t.latitude,t.longitude,t.includeHealthRecommendations,t.includePollutants);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:yt,DESCRIPTION:ft,SCHEMA:ht,ACTION:Et};import{z as x}from"zod";var St="maps_static_map",bt="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Pt={center:x.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:x.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:x.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:x.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:x.array(x.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:x.array(x.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function xt(t){try{let e=i(),r=await new a(e).getStaticMap(t);return r.success?{content:[{type:"image",data:r.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${r.data.size} bytes, ${r.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var q={NAME:St,DESCRIPTION:bt,SCHEMA:Pt,ACTION:xt};import{z as ce}from"zod";var At="maps_batch_geocode",vt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",wt={addresses:ce.array(ce.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function Ot(t){try{let e=i(),o=new a(e),r=t.addresses,s=await Promise.all(r.map(async p=>{try{let d=await o.geocode(p);return{address:p,...d}}catch(d){return{address:p,success:!1,error:d.message}}})),c=s.filter(p=>p.success).length,l=s.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:r.length,succeeded:c,failed:l,results:s},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:At,DESCRIPTION:vt,SCHEMA:wt,ACTION:Ot};import{z as U}from"zod";var Ct="maps_search_along_route",Mt="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Nt={textQuery:U.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:U.string().describe("Route start point \u2014 address or landmark name"),destination:U.string().describe("Route end point \u2014 address or landmark name"),mode:U.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:U.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Tt(t){try{let e=i(),r=await new a(e).searchAlongRoute(t);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var F={NAME:Ct,DESCRIPTION:Mt,SCHEMA:Nt,ACTION:Tt};import{z as E}from"zod";var _t="maps_local_rank_tracker",It="Track a business's local search ranking across a geographic grid (like LocalFalcon). Searches the same keyword(s) from multiple coordinates around a center point to see how rank varies by location. Supports up to 3 keywords for batch scanning. Returns rank at each grid point, top-3 competitors per point, and summary metrics (ARP, ATRP, SoLV). Useful for local SEO analysis.",kt={keyword:E.string().optional().describe("Single search keyword (e.g., 'dentist'). Use 'keywords' for multi-keyword scanning."),keywords:E.array(E.string()).min(1).max(3).optional().describe("Array of 1-3 keywords to scan (e.g., ['dentist', 'dental clinic', 'teeth cleaning']). Overrides 'keyword'."),placeId:E.string().describe("Google Maps place_id of the target business to track"),center:E.object({latitude:E.number().describe("Center latitude of the grid"),longitude:E.number().describe("Center longitude of the grid")}).describe("Center coordinate for the grid (typically the business location)"),gridSize:E.number().int().min(3).max(7).optional().describe("Grid dimension (3 = 3\xD73 = 9 points, 5 = 5\xD75 = 25 points, 7 = 7\xD77 = 49 points). Default: 3"),gridSpacing:E.number().min(100).max(1e4).optional().describe("Distance between grid points in meters (100-10000). Default: 1000")};async function Rt(t){try{let e=t.keywords||(t.keyword?[t.keyword]:[]);if(e.length===0)return{content:[{type:"text",text:"Either 'keyword' or 'keywords' must be provided."}],isError:!0};let o=i(),s=await new a(o).localRankTracker({...t,keywords:e});return s.success?{content:[{type:"text",text:JSON.stringify(s.data,null,2)}],isError:!1}:{content:[{type:"text",text:s.error||"Failed to track local rank"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error tracking local rank: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var Z={NAME:_t,DESCRIPTION:It,SCHEMA:kt,ACTION:Rt};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},zt=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:u,action:t=>O.ACTION(t)},{name:C.NAME,description:C.DESCRIPTION,schema:C.SCHEMA,annotations:u,action:t=>C.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:t=>M.ACTION(t)},{name:N.NAME,description:N.DESCRIPTION,schema:N.SCHEMA,annotations:u,action:t=>N.ACTION(t)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:u,action:t=>T.ACTION(t)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:u,action:t=>_.ACTION(t)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:u,action:t=>I.ACTION(t)},{name:k.NAME,description:k.DESCRIPTION,schema:k.SCHEMA,annotations:u,action:t=>k.ACTION(t)},{name:R.NAME,description:R.DESCRIPTION,schema:R.SCHEMA,annotations:u,action:t=>R.ACTION(t)},{name:H.NAME,description:H.DESCRIPTION,schema:H.SCHEMA,annotations:u,action:t=>H.ACTION(t)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:u,action:t=>K.ACTION(t)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:u,action:t=>$.ACTION(t)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:u,action:t=>L.ACTION(t)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:u,action:t=>J.ACTION(t)},{name:q.NAME,description:q.DESCRIPTION,schema:q.SCHEMA,annotations:u,action:t=>q.ACTION(t)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:u,action:t=>j.ACTION(t)},{name:F.NAME,description:F.DESCRIPTION,schema:F.SCHEMA,annotations:u,action:t=>F.ACTION(t)},{name:Z.NAME,description:Z.DESCRIPTION,schema:Z.SCHEMA,annotations:u,action:t=>Z.ACTION(t)}]}];function re(t){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return t;let o=new Set(e.split(",").map(s=>s.trim()).filter(Boolean)),r=t.filter(s=>o.has(s.name));return r.length===0?(n.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${t.map(s=>s.name).join(", ")}`),t):(n.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${r.length}/${t.length} tools active`),r)}var X=zt;import{McpServer as Ht}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Dt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Kt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as $t}from"@modelcontextprotocol/sdk/types.js";import le from"express";import{randomUUID as Gt}from"crypto";import{z as Lt}from"zod";var B=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let s=e.headers.authorization;if(s&&s.startsWith("Bearer "))return s.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Jt="0.0.1",W=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Ht({name:this.serverName,version:Jt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Lt.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,s,c)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,s,c),n.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e,o="0.0.0.0"){let r=le();r.use(le.json()),r.post("/mcp",async(l,p)=>{let d=l.headers["mcp-session-id"],m,S=B.getInstance().getApiKey(l,d?this.sessions[d]?.apiKey:void 0);if(n.log(`${this.serverName} API key received from request context`),d&&this.sessions[d])m=this.sessions[d],S&&(m.apiKey=S);else if(!d&&$t(l.body)){let f=new Kt({sessionIdGenerator:()=>Gt(),onsessioninitialized:h=>{this.sessions[h]=m,n.log(`[${this.serverName}] New session initialized: ${h}`)}});m={transport:f,apiKey:S},f.onclose=()=>{f.sessionId&&(delete this.sessions[f.sessionId],n.log(`[${this.serverName}] Session closed: ${f.sessionId}`))},await this.createMcpServer().connect(f)}else{p.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p,l.body)})});let s=async(l,p)=>{let d=l.headers["mcp-session-id"];if(!d||!this.sessions[d]){p.status(400).send("Invalid or missing session ID");return}let m=this.sessions[d],S=B.getInstance().getApiKey(l,m.apiKey);S&&(m.apiKey=S),await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p)})};r.get("/mcp",s),r.delete("/mcp",s);let c=o==="0.0.0.0"?"localhost":o;this.httpServer=r.listen(e,o,()=>{n.log(`[${this.serverName}] HTTP server listening on ${o}:${e}`),n.log(`[${this.serverName}] MCP endpoint available at http://${c}:${e}/mcp`)})}async startStdio(){let e=new Dt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){n.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){n.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}n.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let s=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(s).then(()=>{n.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{n.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Ut}from"url";import{dirname as Ft}from"path";import{readFileSync as pe,writeFileSync as Zt,existsSync as Bt}from"fs";import{createInterface as Wt}from"readline";var Vt=Ut(import.meta.url),ue=Ft(Vt);de({path:oe(process.cwd(),".env")});de({path:oe(ue,"../.env")});async function Yt(t,e,o){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),o&&(process.env.MCP_SERVER_HOST=o),n.log("\u{1F680} Starting Google Maps MCP Server..."),n.log("\u{1F4CD} 18 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),n.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),n.log("");let r=X.map(async s=>{let c=process.env[s.portEnvVar];if(!c){n.error(`\u26A0\uFE0F [${s.name}] Port environment variable ${s.portEnvVar} not set.`),n.log(`\u{1F4A1} Please set ${s.portEnvVar} in your .env file or use --port parameter.`),n.log(` Example: ${s.portEnvVar}=3000 or --port 3000`);return}let l=Number(c);if(isNaN(l)||l<=0){n.error(`\u274C [${s.name}] Invalid port number "${c}" defined in ${s.portEnvVar}.`);return}try{let p=new W(s.name,re(s.tools)),d=process.env.MCP_SERVER_HOST||"0.0.0.0";n.log(`\u{1F527} [${s.name}] Initializing MCP Server in HTTP mode on ${d}:${l}...`),await p.startHttpServer(l,d);let m=d==="0.0.0.0"?"localhost":d;n.log(`\u2705 [${s.name}] MCP Server started successfully!`),n.log(` \u{1F310} Endpoint: http://${m}:${l}/mcp`),n.log(` \u{1F4DA} Tools: ${s.tools.length} available`)}catch(p){n.error(`\u274C [${s.name}] Failed to start MCP Server on port ${l}:`,p)}});await Promise.allSettled(r),n.log(""),n.log("\u{1F389} Server initialization completed!"),n.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var me=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route","local-rank-tracker"];function Qt(t){return typeof t=="object"&&t!==null&&"success"in t&&t.success===!1}function Xt(t,e){return typeof e.error=="string"&&e.error.trim().length>0?e.error:e.error!==void 0&&e.error!==null?String(e.error):`Tool "${t}" returned a failure response`}async function er(t,e,o){let r=new a(o);switch(t){case"geocode":case"maps_geocode":return r.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return r.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return r.searchNearby(e);case"search-places":case"maps_search_places":return r.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return r.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return r.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time,e.avoid_tolls,e.avoid_highways);case"distance-matrix":case"maps_distance_matrix":return r.calculateDistanceMatrix(e.origins,e.destinations,e.mode,e.departure_time,e.avoid_tolls,e.avoid_highways);case"elevation":case"maps_elevation":return r.getElevation(e.locations);case"timezone":case"maps_timezone":return r.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return r.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return r.exploreArea(e);case"plan-route":case"maps_plan_route":return r.planRoute(e);case"compare-places":case"maps_compare_places":return r.comparePlaces(e);case"air-quality":case"maps_air_quality":return r.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return r.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let s=await Promise.all(e.addresses.map(async l=>{try{let p=await r.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=s.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:s}}}case"search-along-route":case"maps_search_along_route":return r.searchAlongRoute(e);case"local-rank-tracker":case"maps_local_rank_tracker":return r.localRankTracker(e);default:throw new Error(`Unknown tool: ${t}. Available: ${me.join(", ")}`)}}async function tr(t,e,o,r=er,s={stdout:process.stdout,stderr:process.stderr}){try{let c=await r(t,e,o);return Qt(c)?(s.stderr.write(JSON.stringify({error:Xt(t,c)},null,2)+`
|
|
2
|
+
import{b as a,c as n}from"./chunk-BKUJWPKE.js";import{config as de}from"dotenv";import{resolve as oe}from"path";import qt from"yargs";import{hideBin as jt}from"yargs/helpers";import{z as A}from"zod";import{AsyncLocalStorage as ye}from"async_hooks";var ae=new ye;function i(){return ae.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function ee(t,e){return ae.run(t,e)}var fe="maps_search_nearby",he="Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.",Ee={center:A.object({value:A.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:A.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:A.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:A.number().default(1e3).describe("Search radius in meters"),openNow:A.boolean().default(!1).describe("Only show places that are currently open"),minRating:A.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function Se(t){try{let e=i(),r=await new a(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
|
|
3
|
+
`+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:fe,DESCRIPTION:he,SCHEMA:Ee,ACTION:Se};import{z as ne}from"zod";var be="maps_place_details",Pe="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get reviews, phone, website, and hours. Reviews, photos, and AI summaries include source/disclosure metadata that must accompany end-user display. Set maxPhotos (1-10) to include photo URLs; omit or set 0 for none.",xe={placeId:ne.string().describe("Google Maps place ID"),maxPhotos:ne.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Ae(t){try{let e=i(),r=await new a(e).getPlaceDetails(t.placeId,t.maxPhotos||0);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var C={NAME:be,DESCRIPTION:Pe,SCHEMA:xe,ACTION:Ae};import{z as ve}from"zod";var we="maps_geocode",Oe="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Ce={address:ve.string().describe("Address or place name to convert to coordinates")};async function Me(t){try{let e=i(),r=await new a(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:we,DESCRIPTION:Oe,SCHEMA:Ce,ACTION:Me};import{z as ie}from"zod";var Ne="maps_reverse_geocode",Te="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",_e={latitude:ie.number().describe("Latitude coordinate"),longitude:ie.number().describe("Longitude coordinate")};async function Ie(t){try{let e=i(),r=await new a(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var N={NAME:Ne,DESCRIPTION:Te,SCHEMA:_e,ACTION:Ie};import{z as b}from"zod";var ke="maps_distance_matrix",Re="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",ze={origins:b.array(b.string()).describe("List of origin addresses or coordinates"),destinations:b.array(b.string()).describe("List of destination addresses or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation"),departure_time:b.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."),avoid_tolls:b.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:b.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function He(t){try{let e=i(),r=await new a(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode,t.departure_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:ke,DESCRIPTION:Re,SCHEMA:ze,ACTION:He};import{z as v}from"zod";var De="maps_directions",Ke="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",$e={origin:v.string().describe("Starting point address or coordinates"),destination:v.string().describe("Destination address or coordinates"),mode:v.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:v.string().optional().describe("Departure time (ISO string format)"),arrival_time:v.string().optional().describe("Arrival time (ISO string format)"),avoid_tolls:v.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:v.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function Ge(t){try{let e=i(),r=await new a(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time,t.avoid_tolls,t.avoid_highways);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:De,DESCRIPTION:Ke,SCHEMA:$e,ACTION:Ge};import{z as Y}from"zod";var Le="maps_elevation",Je="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",qe={locations:Y.array(Y.object({latitude:Y.number().describe("Latitude coordinate"),longitude:Y.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function je(t){try{let e=i(),r=await new a(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:Le,DESCRIPTION:Je,SCHEMA:qe,ACTION:je};import{z as P}from"zod";var Ue="maps_search_places",Fe="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ze={query:P.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:P.object({latitude:P.number().describe("Latitude to bias results toward"),longitude:P.number().describe("Longitude to bias results toward"),radius:P.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:P.boolean().optional().describe("Only return places that are currently open"),minRating:P.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:P.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Be(t){try{let e=i(),r=await new a(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var k={NAME:Ue,DESCRIPTION:Fe,SCHEMA:Ze,ACTION:Be};import{z as te}from"zod";var We="maps_timezone",Ve="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",Ye={latitude:te.number().describe("Latitude coordinate"),longitude:te.number().describe("Longitude coordinate"),timestamp:te.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Qe(t){try{let e=i(),r=await new a(e).getTimezone(t.latitude,t.longitude,t.timestamp);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var R={NAME:We,DESCRIPTION:Ve,SCHEMA:Ye,ACTION:Qe};import{z}from"zod";var Xe="maps_weather",et="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",tt={latitude:z.number().describe("Latitude coordinate"),longitude:z.number().describe("Longitude coordinate"),type:z.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:z.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:z.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function rt(t){try{let e=i(),r=await new a(e).getWeather(t.latitude,t.longitude,t.type||"current",t.forecastDays,t.forecastHours);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var H={NAME:Xe,DESCRIPTION:et,SCHEMA:tt,ACTION:rt};import{z as D}from"zod";var ot="maps_explore_area",st="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",at={location:D.string().describe("Address or landmark to explore around"),types:D.array(D.string()).optional().describe("Place types to search (default: restaurant, cafe, tourist_attraction). Must be Places API (New) type names. Examples: hotel, bar, park, museum"),radius:D.number().optional().describe("Search radius in meters (default: 1000)"),topN:D.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function nt(t){try{let e=i(),r=await new a(e).exploreArea(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var K={NAME:ot,DESCRIPTION:st,SCHEMA:at,ACTION:nt};import{z as w}from"zod";var it="maps_plan_route",ct="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (2 to 25 intermediate stops) to find the most efficient visit order, and returns directions for each leg. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. Waypoint optimization requires at least 4 stops (2 intermediates); with 2 or 3 stops the route is returned in the original order. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",lt={stops:w.array(w.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:w.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:w.boolean().optional().describe("Auto-optimize visit order via Routes API waypoint optimization (default: true). Requires at least 4 stops (2 intermediates) \u2014 ignored for 2-3 stops. Set false to keep original order. Not available for transit mode."),departure_time:w.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),avoid_tolls:w.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),avoid_highways:w.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')};async function pt(t){try{let e=i(),r=await new a(e).planRoute(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var $={NAME:it,DESCRIPTION:ct,SCHEMA:lt,ACTION:pt};import{z as G}from"zod";var dt="maps_compare_places",ut="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",mt={query:G.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:G.object({latitude:G.number().describe("Your latitude"),longitude:G.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:G.number().optional().describe("Max places to compare (default: 5)")};async function gt(t){try{let e=i(),r=await new a(e).comparePlaces(t);return{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var L={NAME:dt,DESCRIPTION:ut,SCHEMA:mt,ACTION:gt};import{z as Q}from"zod";var yt="maps_air_quality",ft="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",ht={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),includeHealthRecommendations:Q.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:Q.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function Et(t){try{let e=i(),r=await new a(e).getAirQuality(t.latitude,t.longitude,t.includeHealthRecommendations,t.includePollutants);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:yt,DESCRIPTION:ft,SCHEMA:ht,ACTION:Et};import{z as x}from"zod";var St="maps_static_map",bt="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Pt={center:x.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:x.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:x.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:x.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:x.array(x.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:x.array(x.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function xt(t){try{let e=i(),r=await new a(e).getStaticMap(t);return r.success?{content:[{type:"image",data:r.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${r.data.size} bytes, ${r.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var q={NAME:St,DESCRIPTION:bt,SCHEMA:Pt,ACTION:xt};import{z as ce}from"zod";var At="maps_batch_geocode",vt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",wt={addresses:ce.array(ce.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function Ot(t){try{let e=i(),o=new a(e),r=t.addresses,s=await Promise.all(r.map(async p=>{try{let d=await o.geocode(p);return{address:p,...d}}catch(d){return{address:p,success:!1,error:d.message}}})),c=s.filter(p=>p.success).length,l=s.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:r.length,succeeded:c,failed:l,results:s},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:At,DESCRIPTION:vt,SCHEMA:wt,ACTION:Ot};import{z as U}from"zod";var Ct="maps_search_along_route",Mt="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Nt={textQuery:U.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:U.string().describe("Route start point \u2014 address or landmark name"),destination:U.string().describe("Route end point \u2014 address or landmark name"),mode:U.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:U.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Tt(t){try{let e=i(),r=await new a(e).searchAlongRoute(t);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var F={NAME:Ct,DESCRIPTION:Mt,SCHEMA:Nt,ACTION:Tt};import{z as E}from"zod";var _t="maps_local_rank_tracker",It="Track a business's local search ranking across a geographic grid (like LocalFalcon). Searches the same keyword(s) from multiple coordinates around a center point to see how rank varies by location. Supports up to 3 keywords for batch scanning. Returns rank at each grid point, top-3 competitors per point, and summary metrics (ARP, ATRP, SoLV). Useful for local SEO analysis.",kt={keyword:E.string().optional().describe("Single search keyword (e.g., 'dentist'). Use 'keywords' for multi-keyword scanning."),keywords:E.array(E.string()).min(1).max(3).optional().describe("Array of 1-3 keywords to scan (e.g., ['dentist', 'dental clinic', 'teeth cleaning']). Overrides 'keyword'."),placeId:E.string().describe("Google Maps place_id of the target business to track"),center:E.object({latitude:E.number().describe("Center latitude of the grid"),longitude:E.number().describe("Center longitude of the grid")}).describe("Center coordinate for the grid (typically the business location)"),gridSize:E.number().int().min(3).max(7).optional().describe("Grid dimension (3 = 3\xD73 = 9 points, 5 = 5\xD75 = 25 points, 7 = 7\xD77 = 49 points). Default: 3"),gridSpacing:E.number().min(100).max(1e4).optional().describe("Distance between grid points in meters (100-10000). Default: 1000")};async function Rt(t){try{let e=t.keywords||(t.keyword?[t.keyword]:[]);if(e.length===0)return{content:[{type:"text",text:"Either 'keyword' or 'keywords' must be provided."}],isError:!0};let o=i(),s=await new a(o).localRankTracker({...t,keywords:e});return s.success?{content:[{type:"text",text:JSON.stringify(s.data,null,2)}],isError:!1}:{content:[{type:"text",text:s.error||"Failed to track local rank"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error tracking local rank: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var Z={NAME:_t,DESCRIPTION:It,SCHEMA:kt,ACTION:Rt};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},zt=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:u,action:t=>O.ACTION(t)},{name:C.NAME,description:C.DESCRIPTION,schema:C.SCHEMA,annotations:u,action:t=>C.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:t=>M.ACTION(t)},{name:N.NAME,description:N.DESCRIPTION,schema:N.SCHEMA,annotations:u,action:t=>N.ACTION(t)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:u,action:t=>T.ACTION(t)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:u,action:t=>_.ACTION(t)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:u,action:t=>I.ACTION(t)},{name:k.NAME,description:k.DESCRIPTION,schema:k.SCHEMA,annotations:u,action:t=>k.ACTION(t)},{name:R.NAME,description:R.DESCRIPTION,schema:R.SCHEMA,annotations:u,action:t=>R.ACTION(t)},{name:H.NAME,description:H.DESCRIPTION,schema:H.SCHEMA,annotations:u,action:t=>H.ACTION(t)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:u,action:t=>K.ACTION(t)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:u,action:t=>$.ACTION(t)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:u,action:t=>L.ACTION(t)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:u,action:t=>J.ACTION(t)},{name:q.NAME,description:q.DESCRIPTION,schema:q.SCHEMA,annotations:u,action:t=>q.ACTION(t)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:u,action:t=>j.ACTION(t)},{name:F.NAME,description:F.DESCRIPTION,schema:F.SCHEMA,annotations:u,action:t=>F.ACTION(t)},{name:Z.NAME,description:Z.DESCRIPTION,schema:Z.SCHEMA,annotations:u,action:t=>Z.ACTION(t)}]}];function re(t){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return t;let o=new Set(e.split(",").map(s=>s.trim()).filter(Boolean)),r=t.filter(s=>o.has(s.name));return r.length===0?(n.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${t.map(s=>s.name).join(", ")}`),t):(n.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${r.length}/${t.length} tools active`),r)}var X=zt;import{McpServer as Ht}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Dt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Kt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as $t}from"@modelcontextprotocol/sdk/types.js";import le from"express";import{randomUUID as Gt}from"crypto";import{z as Lt}from"zod";var B=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let s=e.headers.authorization;if(s&&s.startsWith("Bearer "))return s.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Jt="0.0.1",W=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Ht({name:this.serverName,version:Jt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Lt.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,s,c)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,s,c),n.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e,o="0.0.0.0"){let r=le();r.use(le.json()),r.post("/mcp",async(l,p)=>{let d=l.headers["mcp-session-id"],m,S=B.getInstance().getApiKey(l,d?this.sessions[d]?.apiKey:void 0);if(n.log(`${this.serverName} API key received from request context`),d&&this.sessions[d])m=this.sessions[d],S&&(m.apiKey=S);else if(!d&&$t(l.body)){let f=new Kt({sessionIdGenerator:()=>Gt(),onsessioninitialized:h=>{this.sessions[h]=m,n.log(`[${this.serverName}] New session initialized: ${h}`)}});m={transport:f,apiKey:S},f.onclose=()=>{f.sessionId&&(delete this.sessions[f.sessionId],n.log(`[${this.serverName}] Session closed: ${f.sessionId}`))},await this.createMcpServer().connect(f)}else{p.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p,l.body)})});let s=async(l,p)=>{let d=l.headers["mcp-session-id"];if(!d||!this.sessions[d]){p.status(400).send("Invalid or missing session ID");return}let m=this.sessions[d],S=B.getInstance().getApiKey(l,m.apiKey);S&&(m.apiKey=S),await ee({apiKey:m.apiKey,sessionId:d},async()=>{await m.transport.handleRequest(l,p)})};r.get("/mcp",s),r.delete("/mcp",s);let c=o==="0.0.0.0"?"localhost":o;this.httpServer=r.listen(e,o,()=>{n.log(`[${this.serverName}] HTTP server listening on ${o}:${e}`),n.log(`[${this.serverName}] MCP endpoint available at http://${c}:${e}/mcp`)})}async startStdio(){let e=new Dt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){n.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){n.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}n.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let s=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(s).then(()=>{n.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{n.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Ut}from"url";import{dirname as Ft}from"path";import{readFileSync as pe,writeFileSync as Zt,existsSync as Bt}from"fs";import{createInterface as Wt}from"readline";var Vt=Ut(import.meta.url),ue=Ft(Vt);de({path:oe(process.cwd(),".env")});de({path:oe(ue,"../.env")});async function Yt(t,e,o){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),o&&(process.env.MCP_SERVER_HOST=o),n.log("\u{1F680} Starting Google Maps MCP Server..."),n.log("\u{1F4CD} 18 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),n.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),n.log("");let r=X.map(async s=>{let c=process.env[s.portEnvVar];if(!c){n.error(`\u26A0\uFE0F [${s.name}] Port environment variable ${s.portEnvVar} not set.`),n.log(`\u{1F4A1} Please set ${s.portEnvVar} in your .env file or use --port parameter.`),n.log(` Example: ${s.portEnvVar}=3000 or --port 3000`);return}let l=Number(c);if(isNaN(l)||l<=0){n.error(`\u274C [${s.name}] Invalid port number "${c}" defined in ${s.portEnvVar}.`);return}try{let p=new W(s.name,re(s.tools)),d=process.env.MCP_SERVER_HOST||"0.0.0.0";n.log(`\u{1F527} [${s.name}] Initializing MCP Server in HTTP mode on ${d}:${l}...`),await p.startHttpServer(l,d);let m=d==="0.0.0.0"?"localhost":d;n.log(`\u2705 [${s.name}] MCP Server started successfully!`),n.log(` \u{1F310} Endpoint: http://${m}:${l}/mcp`),n.log(` \u{1F4DA} Tools: ${s.tools.length} available`)}catch(p){n.error(`\u274C [${s.name}] Failed to start MCP Server on port ${l}:`,p)}});await Promise.allSettled(r),n.log(""),n.log("\u{1F389} Server initialization completed!"),n.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var me=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route","local-rank-tracker"];function Qt(t){return typeof t=="object"&&t!==null&&"success"in t&&t.success===!1}function Xt(t,e){return typeof e.error=="string"&&e.error.trim().length>0?e.error:e.error!==void 0&&e.error!==null?String(e.error):`Tool "${t}" returned a failure response`}async function er(t,e,o){let r=new a(o);switch(t){case"geocode":case"maps_geocode":return r.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return r.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return r.searchNearby(e);case"search-places":case"maps_search_places":return r.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return r.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return r.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time,e.avoid_tolls,e.avoid_highways);case"distance-matrix":case"maps_distance_matrix":return r.calculateDistanceMatrix(e.origins,e.destinations,e.mode,e.departure_time,e.avoid_tolls,e.avoid_highways);case"elevation":case"maps_elevation":return r.getElevation(e.locations);case"timezone":case"maps_timezone":return r.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return r.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return r.exploreArea(e);case"plan-route":case"maps_plan_route":return r.planRoute(e);case"compare-places":case"maps_compare_places":return r.comparePlaces(e);case"air-quality":case"maps_air_quality":return r.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return r.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let s=await Promise.all(e.addresses.map(async l=>{try{let p=await r.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=s.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:s}}}case"search-along-route":case"maps_search_along_route":return r.searchAlongRoute(e);case"local-rank-tracker":case"maps_local_rank_tracker":return r.localRankTracker(e);default:throw new Error(`Unknown tool: ${t}. Available: ${me.join(", ")}`)}}async function tr(t,e,o,r=er,s={stdout:process.stdout,stderr:process.stderr}){try{let c=await r(t,e,o);return Qt(c)?(s.stderr.write(JSON.stringify({error:Xt(t,c)},null,2)+`
|
|
4
4
|
`),1):(s.stdout.write(JSON.stringify(c,null,2)+`
|
|
5
5
|
`),0)}catch(c){return s.stderr.write(JSON.stringify({error:c instanceof Error?c.message:String(c)},null,2)+`
|
|
6
6
|
`),1}}var rr=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),or=import.meta.url===`file://${process.argv[1]}`;if(rr||or){let t="0.0.0";try{let e=oe(ue,"../package.json");t=JSON.parse(pe(e,"utf-8")).version}catch{t="0.0.0"}qt(jt(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${me.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{if(!e.apikey){process.stderr.write(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)+`
|
package/dist/index.d.ts
CHANGED
|
@@ -223,7 +223,16 @@ declare class NewPlacesService {
|
|
|
223
223
|
reviews: any;
|
|
224
224
|
photos: any;
|
|
225
225
|
generative_summary?: any;
|
|
226
|
+
generative_summary_attribution?: {
|
|
227
|
+
disclosure_text: any;
|
|
228
|
+
flag_content_uri: any;
|
|
229
|
+
} | undefined;
|
|
226
230
|
review_summary?: any;
|
|
231
|
+
review_summary_attribution?: {
|
|
232
|
+
disclosure_text: any;
|
|
233
|
+
flag_content_uri: any;
|
|
234
|
+
reviews_uri: any;
|
|
235
|
+
} | undefined;
|
|
227
236
|
payment_options?: {
|
|
228
237
|
[k: string]: unknown;
|
|
229
238
|
} | undefined;
|
|
@@ -239,6 +248,7 @@ declare class NewPlacesService {
|
|
|
239
248
|
name: any;
|
|
240
249
|
place_id: string;
|
|
241
250
|
formatted_address: any;
|
|
251
|
+
google_maps_uri: any;
|
|
242
252
|
geometry: {
|
|
243
253
|
location: {
|
|
244
254
|
lat: any;
|
|
@@ -258,8 +268,6 @@ declare class NewPlacesService {
|
|
|
258
268
|
price_level: any;
|
|
259
269
|
editorial_summary: any;
|
|
260
270
|
}>;
|
|
261
|
-
private fetchNewestReviews;
|
|
262
|
-
private mergeReviews;
|
|
263
271
|
private transformSearchResult;
|
|
264
272
|
private transformPlaceResponse;
|
|
265
273
|
private extractLegacyPlaceId;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b,c}from"./chunk-
|
|
1
|
+
import{a,b,c}from"./chunk-BKUJWPKE.js";export{c as Logger,a as NewPlacesService,b as PlacesSearcher};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Agent Skill demo (no MCP server)
|
|
2
|
+
|
|
3
|
+
This walkthrough demonstrates the two pieces separately: the Agent Skill tells an agent which geographic tool to use, and the package CLI performs the API call. It does not require an MCP client or server. Build and run the CLI from the same repository revision as the Skill so the instructions and executable stay aligned.
|
|
4
|
+
|
|
5
|
+
## 1. Install the Skill
|
|
6
|
+
|
|
7
|
+
Clone this repository at a chosen release tag, then copy the whole `skills/google-maps/` directory into your agent's Skills directory according to that client's instructions. Keep `SKILL.md` and `references/` together. If the client accepts `.skill` archives, import `skills/google-maps/SKILL.skill` instead. Installing npm alone does not register the Skill.
|
|
8
|
+
|
|
9
|
+
Check that your agent can discover `google-maps` and run shell commands. Provide a Google Maps Platform API key to the agent's environment as `GOOGLE_MAPS_API_KEY` through your normal secret-management method; never paste it into the prompt or commit it. Node.js 18+ and `npx` must be available.
|
|
10
|
+
|
|
11
|
+
## 2. Validate without an API call
|
|
12
|
+
|
|
13
|
+
From the cloned repository, install and build the package, then run its CLI help:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm ci
|
|
17
|
+
npm run build
|
|
18
|
+
node dist/cli.js exec --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The help should list `geocode` and `search-places`. This only checks CLI availability; it does not prove that your key or Google APIs are enabled.
|
|
22
|
+
|
|
23
|
+
## 3. Try one live request
|
|
24
|
+
|
|
25
|
+
Ask your agent:
|
|
26
|
+
|
|
27
|
+
> Use the google-maps Skill, without MCP, to find the coordinates of Tokyo Tower. Tell me which tool you used and cite Google Maps as the data source.
|
|
28
|
+
|
|
29
|
+
The agent should select `maps_geocode` and run an `exec geocode` call from the same checkout like:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node dist/cli.js exec geocode '{"address":"Tokyo Tower"}'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Success means the CLI exits 0 and returns JSON with `success: true`, a `data.location` latitude/longitude, and a `data.place_id`. The exact coordinates and address may change; do not compare them to a hard-coded snapshot. A failed call exits nonzero and writes JSON to stderr. This step calls a billable Google API using your key.
|
|
36
|
+
|
|
37
|
+
For a published-package check from **outside** this repository, the equivalent command is `npx -y @cablate/mcp-google-map exec --help`. Running `npx` inside a checkout of the same package can resolve the local package instead of the published executable.
|
|
38
|
+
|
|
39
|
+
If you ask for place reviews, photos, or an AI summary in a later step, read the Skill's [content-attribution guidance](../skills/google-maps/references/content-attribution.md) before displaying them. This geocoding example does not validate every Google Maps Platform policy requirement for a downstream app.
|
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.64",
|
|
4
4
|
"mcpName": "io.github.cablate/google-map",
|
|
5
5
|
"description": "18 Google Maps tools for AI agents — geocode, search, directions, weather, air quality, local rank tracking, map images via MCP server or standalone CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"dist/**/*.map",
|
|
14
14
|
"README.md",
|
|
15
|
-
"
|
|
15
|
+
"plugin.json",
|
|
16
|
+
".codex-plugin",
|
|
17
|
+
"skills",
|
|
18
|
+
"examples"
|
|
16
19
|
],
|
|
17
20
|
"scripts": {
|
|
18
21
|
"build": "tsup --dts",
|
package/plugin.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "mcp-google-map",
|
|
4
|
+
"version": "0.0.64",
|
|
5
|
+
"description": "Use Google Maps-backed place search, routing, geocoding, weather, air quality, and geographic analysis through a standalone CLI.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "CabLate",
|
|
8
|
+
"url": "https://github.com/cablate"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/cablate/mcp-google-map",
|
|
11
|
+
"repository": "https://github.com/cablate/mcp-google-map",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"google-maps",
|
|
15
|
+
"places",
|
|
16
|
+
"routing",
|
|
17
|
+
"geocoding",
|
|
18
|
+
"travel",
|
|
19
|
+
"agent-skill"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -21,6 +21,7 @@ Use the package's `exec` CLI to answer location questions without starting an MC
|
|
|
21
21
|
| Precise input saves trouble | Use coordinates over address strings when available. Use place_id over name search. More precise input = more reliable output. |
|
|
22
22
|
| Output is structured | Every tool returns JSON. Use it directly for downstream computation or comparison — no extra parsing needed. |
|
|
23
23
|
| Present results clearly | Summarize comparisons in a table when it helps; do not pass raw JSON through as the final answer. |
|
|
24
|
+
| Preserve source context | Google Maps content has display, attribution, and retention conditions. Read `references/content-attribution.md` before presenting place reviews, photos, or AI summaries. |
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
@@ -109,5 +110,4 @@ npx -y @cablate/mcp-google-map exec <tool> '<json_params>'
|
|
|
109
110
|
| `references/tools-api.md` | Full parameter specs, response formats, 7 scenario recipes, and decision guide | When you need exact parameters, response shapes, or multi-tool workflow patterns |
|
|
110
111
|
| `references/travel-planning.md` | Travel planning methodology — 6-layer model, Search Along Route, anti-patterns | When planning multi-day trips — **read before Recipe 1** |
|
|
111
112
|
| `references/local-seo.md` | Local SEO / Google Business Profile ranking analysis — competitor audit, keyword landscape, gap analysis | When analyzing business rankings, comparing competitors, or scouting locations |
|
|
112
|
-
|
|
113
|
-
> For **project development** knowledge (architecture, API guide, GIS domain, design decisions), see `skills/project-docs/SKILL.md`.
|
|
113
|
+
| `references/content-attribution.md` | Google Maps content attribution, source links, AI disclosure, and storage limits | Before presenting reviews, photos, or AI summaries from Places |
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Presenting Google Maps content
|
|
2
|
+
|
|
3
|
+
This Skill calls Google Maps Platform APIs with the user's key. Preserve the source metadata returned by `maps_place_details`; the agent or application displaying the result remains responsible for following the current [Places API policies](https://developers.google.com/maps/documentation/places/web-service/policies). Do not assume that plain tool output is a compliant end-user display.
|
|
4
|
+
|
|
5
|
+
- Attribute Google Maps content visibly. Places content shown without a Google Map generally needs the Google Maps logo; use the official attribution guidance for the actual interface. A text-only chat may have different display constraints, so do not claim that a source sentence alone satisfies every UI requirement.
|
|
6
|
+
- When showing a review, keep its author name, avatar/profile links where available, and `google_maps_uri` to the individual review. Explain any ordering or filtering of reviews. If required source metadata is absent, omit the quoted review rather than presenting it without attribution.
|
|
7
|
+
- When showing a photo, keep its `author_attributions` and `google_maps_uri` with the image. Do not detach the image URL from those fields.
|
|
8
|
+
- When showing `review_summary`, label it "Review summary," display `review_summary_attribution.disclosure_text` unmodified immediately below it, and provide its `reviews_uri` and `flag_content_uri`. When showing `generative_summary`, keep its disclosure and report link too. Provide Google's ["About this summary" explanation](https://support.google.com/local-listings/answer/9851099) with either summary. If required disclosure or links are missing, do not show the AI summary.
|
|
9
|
+
- Do not pre-fetch, cache, or store Places content beyond Google's permitted exceptions. Place IDs are exempt from the Places caching restriction; other fields are not automatically exempt.
|
|
10
|
+
|
|
11
|
+
These instructions help preserve metadata, but they are not a legal determination of compliance. Applications also need to check Google's requirements for their own terms, privacy policy, location, and presentation surface.
|
|
@@ -148,8 +148,15 @@ Response includes (when available from Google):
|
|
|
148
148
|
| `atmosphere` | object | `{ good_for_groups, good_for_children, outdoor_seating, allows_dogs, live_music, ... }` (truthy only) |
|
|
149
149
|
| `payment_options` | object | Payment methods accepted |
|
|
150
150
|
| `review_summary` | string | AI-generated review summary (region-limited: US/UK/India/Japan) |
|
|
151
|
+
| `review_summary_attribution` | object | Disclosure text, reporting URL, and source reviews URL; display with the summary |
|
|
151
152
|
| `generative_summary` | string | AI-generated place overview (region-limited: US/India) |
|
|
153
|
+
| `generative_summary_attribution` | object | Disclosure text and reporting URL; display with the summary |
|
|
154
|
+
| `google_maps_uri` | string | Source place link on Google Maps |
|
|
152
155
|
| `reviews[].language` | string | Review language code (e.g., `en`, `zh-TW`) |
|
|
156
|
+
| `reviews[].author_uri`, `author_photo_uri`, `google_maps_uri` | string | Author profile/avatar and individual review source |
|
|
157
|
+
| `photos[].author_attributions`, `google_maps_uri` | object[], string | Photo credits and individual photo source, returned with `maxPhotos > 0` |
|
|
158
|
+
|
|
159
|
+
Before showing reviews, photos, or AI summaries to end users, read [`content-attribution.md`](./content-attribution.md). The old legacy-review merge was removed because it could not supply a per-review Google Maps source link; reviews now come from Places API (New) only.
|
|
153
160
|
|
|
154
161
|
---
|
|
155
162
|
|