@cablate/mcp-google-map 0.0.43 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,12 +79,12 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
79
79
  | `maps_search_along_route` | Search for places along a route between two points — ranked by minimal detour time. |
80
80
  | **Composite Tools** | |
81
81
  | `maps_explore_area` | Explore what's around a location — searches multiple place types and gets details in one call. |
82
- | `maps_plan_route` | Plan an optimized multi-stop route — geocodes, finds best order, returns directions. |
82
+ | `maps_plan_route` | Plan an optimized multi-stop route — uses Routes API waypoint optimization (up to 25 stops) for efficient ordering. |
83
83
  | `maps_compare_places` | Compare places side-by-side — searches, gets details, and optionally calculates distances. |
84
84
 
85
85
  All tools are annotated with `readOnlyHint: true` and `destructiveHint: false` — MCP clients can auto-approve these without user confirmation.
86
86
 
87
- > **Prerequisite**: Enable **Places API (New)** in [Google Cloud Console](https://console.cloud.google.com) before using place-related tools.
87
+ > **Prerequisite**: Enable **Places API (New)** and **Routes API** in [Google Cloud Console](https://console.cloud.google.com) before using place-related and routing tools.
88
88
 
89
89
  ## Installation
90
90
 
@@ -247,7 +247,8 @@ src/
247
247
  ├── services/
248
248
  │ ├── NewPlacesService.ts # Google Places API (New) client
249
249
  │ ├── PlacesSearcher.ts # Service facade layer
250
- └── toolclass.ts # Legacy Google Maps API client
250
+ ├── RoutesService.ts # Google Routes API client (directions, distance matrix, waypoint optimization)
251
+ │ └── toolclass.ts # Google Maps API client (geocoding, timezone, elevation, static map)
251
252
  ├── tools/
252
253
  │ └── maps/
253
254
  │ ├── searchNearby.ts # maps_search_nearby tool
@@ -277,7 +278,8 @@ skills/
277
278
  │ ├── SKILL.md # Tool map, recipes, invocation
278
279
  │ └── references/
279
280
  │ ├── tools-api.md # Tool parameters + scenario recipes
280
- └── travel-planning.md # Travel planning methodology
281
+ ├── travel-planning.md # Travel planning methodology
282
+ │ └── local-seo.md # Local SEO / Google Business Profile ranking analysis
281
283
  └── project-docs/ # Project Skill — how to DEVELOP/MAINTAIN
282
284
  ├── SKILL.md # Architecture overview + onboarding
283
285
  └── references/
@@ -292,7 +294,8 @@ skills/
292
294
  - **TypeScript** - Type-safe development
293
295
  - **Node.js** - Runtime environment
294
296
  - **@googlemaps/places** - Google Places API (New) for place search and details
295
- - **@googlemaps/google-maps-services-js** - Legacy API for geocoding, directions, distance matrix, elevation
297
+ - **Google Routes API** - Directions, distance matrix, and waypoint optimization via REST
298
+ - **@googlemaps/google-maps-services-js** - Geocoding, timezone, elevation
296
299
  - **@modelcontextprotocol/sdk** - MCP protocol implementation (v1.27+)
297
300
  - **Express.js** - HTTP server framework
298
301
  - **Zod** - Schema validation
@@ -341,6 +344,7 @@ These are the real-world scenarios driving our tool decisions:
341
344
  - **Disaster response** — "Nearest open hospitals? Am I in a flood zone?" (search-nearby + elevation)
342
345
  - **Content creation** — "Top 5 neighborhoods in Austin with restaurant density and airport distance" (explore-area + distance-matrix)
343
346
  - **Accessibility** — "Wheelchair-accessible restaurants, avoid steep routes" (search-nearby + place-details + elevation)
347
+ - **Local SEO** — "Audit my restaurant's ranking vs competitors within 1km" (search-places + compare-places + explore-area)
344
348
 
345
349
  ## Changelog
346
350
 
package/README.zh-TW.md CHANGED
@@ -275,7 +275,8 @@ skills/
275
275
  │ ├── SKILL.md # 工具對照表、場景食譜、呼叫方式
276
276
  │ └── references/
277
277
  │ ├── tools-api.md # 工具參數 + 場景食譜
278
- └── travel-planning.md # 旅行規劃方法論
278
+ ├── travel-planning.md # 旅行規劃方法論
279
+ │ └── local-seo.md # Local SEO / Google 商家排名分析
279
280
  └── project-docs/ # Project Skill — 如何開發/維護
280
281
  ├── SKILL.md # 架構概覽 + 入門指南
281
282
  └── references/
@@ -339,6 +340,7 @@ skills/
339
340
  - **災害應變** — 「最近有開的醫院?我在洪水區嗎?」(search-nearby + elevation)
340
341
  - **內容創作** — 「Austin 前 5 社區的餐廳密度和機場距離」(explore-area + distance-matrix)
341
342
  - **無障礙** — 「輪椅可達的餐廳,避開陡坡路線」(search-nearby + place-details + elevation)
343
+ - **Local SEO** — 「分析我的餐廳在 1 公里內跟競爭對手的排名差距」(search-places + compare-places + explore-area)
342
344
 
343
345
  ## 更新日誌
344
346
 
@@ -0,0 +1 @@
1
+ import{Client as D,Language as C}from"@googlemaps/google-maps-services-js";import L from"dotenv";var M="https://routes.googleapis.com",O={driving:"DRIVE",walking:"WALK",bicycling:"BICYCLE",transit:"TRANSIT"},S=["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.polyline","routes.optimizedIntermediateWaypointIndex"].join(","),$="originIndex,destinationIndex,distanceMeters,duration,status,condition";function x(l){if(!l)return 0;let e=l.match(/^(\d+)s$/);return e?parseInt(e[1],10):0}function A(l){return l>=1e3?`${(l/1e3).toFixed(1)} km`:`${l} m`}function N(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":""}`}function P(l){let e=l.match(/^\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*$/);return e?{location:{latLng:{latitude:parseFloat(e[1]),longitude:parseFloat(e[2])}}}:{address:l}}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=O[e.mode||"driving"]||"DRIVE",r={origin:P(e.origin),destination:P(e.destination),travelMode:t,computeAlternativeRoutes:!1};t==="DRIVE"&&(r.routingPreference="TRAFFIC_AWARE"),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(P)),e.optimizeWaypointOrder&&e.intermediates&&e.intermediates.length>0&&t!=="TRANSIT"&&(r.optimizeWaypointOrder=!0);let n=await fetch(`${M}/directions/v2:computeRoutes`,{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":S},body:JSON.stringify(r)});if(!n.ok){let u=(await n.json().catch(()=>({})))?.error?.message||`HTTP ${n.status}`;throw new Error(u)}let o=await n.json();if(!o.routes||o.routes.length===0)throw new Error(`No route found from "${e.origin}" to "${e.destination}" with mode: ${e.mode||"driving"}`);let s=o.routes[0],i=s.distanceMeters||0,d=x(s.duration);return{routes:o.routes,summary:s.description||"",total_distance:{value:i,text:A(i)},total_duration:{value:d,text:N(d)},arrival_time:"",departure_time:"",...s.optimizedIntermediateWaypointIndex?{optimizedIntermediateWaypointIndex:s.optimizedIntermediateWaypointIndex}:{}}}async computeRouteMatrix(e){let t=O[e.mode||"driving"]||"DRIVE",r={origins:e.origins.map(u=>({waypoint:P(u)})),destinations:e.destinations.map(u=>({waypoint:P(u)})),travelMode:t};t==="DRIVE"&&(r.routingPreference="TRAFFIC_AWARE");let n=await fetch(`${M}/distanceMatrix/v2:computeRouteMatrix`,{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":$},body:JSON.stringify(r)});if(!n.ok){let m=(await n.json().catch(()=>({})))?.error?.message||`HTTP ${n.status}`;throw new Error(m)}let o=await n.json(),s=e.origins.length,i=e.destinations.length,d=Array.from({length:s},()=>Array(i).fill(null)),a=Array.from({length:s},()=>Array(i).fill(null));for(let u of o){let m=u.originIndex,g=u.destinationIndex;if(m===void 0||g===void 0||u.condition==="ROUTE_NOT_FOUND")continue;let c=u.distanceMeters||0,p=x(u.duration);d[m][g]={value:c,text:A(c)},a[m][g]={value:p,text:N(p)}}return{distances:d,durations:a,origin_addresses:e.origins,destination_addresses:e.destinations}}};L.config();function E(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 k=class{constructor(e){this.defaultLanguage=C.en;if(this.client=new D({}),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],n=r.geometry.location;return{lat:n.lat,lng:n.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw h.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${E(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 h.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${E(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 n=r.data.results[0];return{formatted_address:n.formatted_address,place_id:n.place_id,address_components:n.address_components}}catch(r){throw h.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${E(r)}`)}}async searchAlongRoute(e){try{let r=await new v(this.apiKey).computeRoutes({origin:e.origin,destination:e.destination,mode:e.mode||"walking"}),n=r.routes[0]?.polyline?.encodedPolyline;if(!n)throw new Error("Could not get route polyline");let o=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:n}},maxResultCount:o})});if(!i.ok){let u=await i.json().catch(()=>({}));throw new Error(u?.error?.message||`HTTP ${i.status}`)}return{places:((await i.json()).places||[]).map(u=>({name:u.displayName?.text||"",place_id:u.id||"",formatted_address:u.formattedAddress||"",location:{lat:u.location?.latitude||0,lng:u.location?.longitude||0},rating:u.rating||0,user_ratings_total:u.userRatingCount||0,open_now:u.currentOpeningHours?.openNow??null})),route:{distance:r.total_distance.text,duration:r.total_duration.text,polyline:n}}}catch(t){throw h.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",n,o){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,i;switch(r){case"forecast_daily":{let u=Math.min(Math.max(n||5,1),10);i=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${u}`;break}case"forecast_hourly":{let u=Math.min(Math.max(o||24,1),240);i=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${u}`;break}default:i=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let d=await fetch(i);if(!d.ok){let m=(await d.json().catch(()=>({})))?.error?.message||`HTTP ${d.status}`;throw m.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(m)}let a=await d.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 h.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,n=!1){try{let o=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),n&&s.push("POLLUTANT_CONCENTRATION");let i={location:{latitude:e,longitude:t}};s.length>0&&(i.extraComputations=s);let d=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(!d.ok){let p=(await d.json().catch(()=>({})))?.error?.message||`HTTP ${d.status}`;throw new Error(p)}let a=await d.json(),u=a.indexes||[],m=u[0],g={dateTime:a.dateTime,regionCode:a.regionCode,aqi:m?.aqi,category:m?.category,dominantPollutant:m?.dominantPollutant,color:m?.color};return u.length>1&&(g.indexes=u.map(c=>({code:c.code,displayName:c.displayName,aqi:c.aqi,category:c.category,dominantPollutant:c.dominantPollutant}))),a.healthRecommendations&&(g.healthRecommendations=a.healthRecommendations),a.pollutants&&(g.pollutants=a.pollutants.map(c=>({code:c.code,displayName:c.displayName,concentration:c.concentration,additionalInfo:c.additionalInfo}))),g}catch(o){throw h.error("Error in getAirQuality:",o),new Error(o.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 n=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(n.length>16384)throw new Error(`URL exceeds 16,384 character limit (${n.length}). Reduce markers or path points.`);let o=await fetch(n);if(!o.ok){let a=o.headers.get("content-type")||"";if(a.includes("application/json")||a.includes("text/")){let u=await o.text();throw new Error(`Static Maps API error: ${u}`)}throw new Error(`Static Maps API returned HTTP ${o.status}`)}let s=await o.arrayBuffer(),i=Buffer.from(s);return{base64:i.toString("base64"),size:i.length,dimensions:t}}catch(t){throw h.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let n=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:n,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let i=(s.rawOffset+s.dstOffset)*1e3,d=new Date(n*1e3+i).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:d}}catch(n){throw h.error("Error in getTimezone:",n),new Error(`Failed to get timezone for (${e}, ${t}): ${E(n)}`)}}async getElevation(e){try{let t=e.map(o=>({lat:o.latitude,lng:o.longitude})),n=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Failed to get elevation data with status: ${n.status}`);return n.results.map((o,s)=>({elevation:o.elevation,location:t[s]}))}catch(t){throw h.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${E(t)}`)}}};import{PlacesClient as F}from"@googlemaps/places";var R=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","photos.heightPx","photos.widthPx","photos.name","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.client=new F({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(n=>this.transformSearchResult(n))}catch(t){throw h.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(n=>this.transformSearchResult(n))}catch(t){throw h.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 h.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 h.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,n={};e.dineIn&&(n.dine_in=!0),e.delivery&&(n.delivery=!0),e.takeout&&(n.takeout=!0),e.curbsidePickup&&(n.curbside_pickup=!0),e.reservable&&(n.reservable=!0);let o={};e.servesVegetarianFood&&(o.vegetarian_food=!0),e.servesBeer&&(o.beer=!0),e.servesWine&&(o.wine=!0),e.servesCocktails&&(o.cocktails=!0),e.servesBreakfast&&(o.breakfast=!0),e.servesLunch&&(o.lunch=!0),e.servesDinner&&(o.dinner=!0),e.servesBrunch&&(o.brunch=!0),e.servesCoffee&&(o.coffee=!0),e.servesDessert&&(o.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||"",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(n).length>0?{dining_options:n}:{},...Object.keys(o).length>0?{serves:o}:{},...Object.keys(s).length>0?{atmosphere:s}:{},...e.paymentOptions?{payment_options:e.paymentOptions}:{},...e.reviewSummary?.text?.text?{review_summary:e.reviewSummary.text.text}:{},...e.generativeSummary?.overview?.text?{generative_summary:e.generativeSummary.overview.text}:{},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||""}))||[],photos:e.photos?.map(i=>({photo_reference:i.name||"",height:i.heightPx||0,width:i.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let n=e?.periods;if(!Array.isArray(n)||n.length===0)return!1;let o=1440,s=o*7,{day:i,minutes:d}=this.getLocalTimeComponents(t),a=i*o+d,u={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},m=c=>{if(typeof c=="number"&&c>=0&&c<=6)return c;if(typeof c=="string"){let p=c.toUpperCase();if(p in u)return u[p]}},g=c=>{if(!c)return;let p=typeof c.hours=="number"?c.hours:Number(c.hours??NaN),b=typeof c.minutes=="number"?c.minutes:Number(c.minutes??NaN);if(!(!Number.isFinite(p)||!Number.isFinite(b)))return p*60+b};for(let c of n){let p=m(c?.openDay),b=m(c?.closeDay??c?.openDay),y=g(c?.openTime),f=g(c?.closeTime);if(p===void 0||y===void 0)continue;let w=p*o+y,_;b===void 0||f===void 0?_=w+o:_=b*o+f,_<=w&&(_+=s);let T=a;for(;T<w;)T+=s;if(T>=w&&T<_)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var I=class{constructor(e){this.mapsTools=new k(e),this.newPlacesService=new R(e),this.routesService=new v(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),n=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(n=n.filter(o=>o.opening_hours?.open_now===!0)),e.minRating&&(n=n.filter(o=>(o.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:n.map(o=>({name:o.name,place_id:o.place_id,address:o.formatted_address,location:o.geometry.location,primary_type:o.primary_type||null,price_level:o.price_level||null,rating:o.rating,total_ratings:o.user_ratings_total,open_now:o.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),n;if(t>0&&r.photos?.length>0){let o=r.photos.slice(0,t);n=[];for(let s of o)try{let i=await this.newPlacesService.getPhotoUri(s.photo_reference);n.push({url:i,width:s.width,height:s.height})}catch{}}return{success:!0,data:{name:r.name,address:r.formatted_address,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.generative_summary?{generative_summary:r.generative_summary}:{},photo_count:r.photos?.length||0,...n&&n.length>0?{photos:n}:{},reviews:r.reviews?.map(o=>({rating:o.rating,text:o.text,language:o.language||null,time:o.time,author_name:o.author_name}))}}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.routesService.computeRouteMatrix({origins:e,destinations:t,mode:r})}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",n,o){try{let s=n?new Date(n):new Date,i=o?new Date(o):void 0;return{success:!0,data:await this.routesService.computeRoutes({origin:e,destination:t,mode:r,departureTime:s,arrivalTime:i})}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",n,o){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,n,o)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,n){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,n)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,n=e.topN||3,o=await this.geocode(e.location);if(!o.success||!o.data)throw new Error(o.error||"Geocode failed");let{lat:s,lng:i}=o.data.location,d=[];for(let a of t){let u=await this.searchNearby({center:{value:`${s},${i}`,isCoordinates:!0},keyword:a,radius:r});if(!u.success||!u.data)continue;let m=u.data.slice(0,n),g=[];for(let c of m){if(!c.place_id)continue;let p=await this.getPlaceDetails(c.place_id);g.push({name:c.name,address:c.address,rating:c.rating,total_ratings:c.total_ratings,open_now:c.open_now,phone:p.data?.phone,website:p.data?.website})}d.push({type:a,count:u.data.length,top:g})}return{success:!0,data:{location:{address:o.data.formatted_address,lat:s,lng:i},radius:r,categories:d}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let n=[];for(let y of r){let f=await this.geocode(y);if(!f.success||!f.data)throw new Error(`Failed to geocode: ${y}`);n.push({originalName:y,address:f.data.formatted_address,lat:f.data.location.lat,lng:f.data.location.lng})}let o=r[0],s=r[r.length-1],i=r.length>2?r.slice(1,-1):void 0,d=e.optimize!==!1&&r.length>2&&t!=="transit",a=await this.routesService.computeRoutes({origin:o,destination:s,mode:t,intermediates:i,optimizeWaypointOrder:d}),m=a.routes[0]?.legs||[],g;if(d&&a.optimizedIntermediateWaypointIndex){let y=a.optimizedIntermediateWaypointIndex,f=n.slice(1,-1);g=[n[0],...y.map(w=>f[w]),n[n.length-1]]}else g=n;let c=[],p=0,b=0;for(let y=0;y<g.length-1;y++){let f=m[y];if(f){let w=f.distanceMeters||0,_=x(f.duration);p+=w,b+=_,c.push({from:g[y].originalName,to:g[y+1].originalName,distance:A(w),duration:N(_)})}else c.push({from:g[y].originalName,to:g[y+1].originalName,distance:"unknown",duration:"unknown",note:"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:d,stops:g.map(y=>`${y.originalName} (${y.address})`),legs:c,total_distance:`${(p/1e3).toFixed(1)} km`,total_duration:`${Math.round(b/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 n=r.data.slice(0,t),o=[];for(let s of n){let i=await this.getPlaceDetails(s.place_id);o.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&&o.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,i=n.map(a=>`${a.location.lat},${a.location.lng}`),d=await this.calculateDistanceMatrix([s],i,"driving");if(d.success&&d.data)for(let a=0;a<o.length;a++)o[a].distance=d.data.distances[0]?.[a]?.text,o[a].drive_time=d.data.durations[0]?.[a]?.text}return{success:!0,data:o}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var h={log:(...l)=>{console.error("[INFO]",...l)},error:(...l)=>{console.error("[ERROR]",...l)}};export{R as a,I as b,h as c};
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{b as a,c as s}from"./chunk-G6LPVEOX.js";import{config as le}from"dotenv";import{resolve as ee}from"path";import Kt from"yargs";import{hideBin as Ht}from"yargs/helpers";import{z as E}from"zod";import{AsyncLocalStorage as me}from"async_hooks";var oe=new me;function i(){return oe.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function V(r,e){return oe.run(r,e)}var ge="maps_search_nearby",ye="Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.",fe={center:E.object({value:E.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:E.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:E.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:E.number().default(1e3).describe("Search radius in meters"),openNow:E.boolean().default(!1).describe("Only show places that are currently open"),minRating:E.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function he(r){try{let e=i(),t=await new a(e).searchNearby(r);return t.success?{content:[{type:"text",text:`location: ${JSON.stringify(t.location,null,2)}
3
- `+JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var x={NAME:ge,DESCRIPTION:ye,SCHEMA:fe,ACTION:he};import{z as se}from"zod";var Se="maps_place_details",Ee="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, and opening hours. Set maxPhotos (1-10) to include photo URLs \u2014 omit or set to 0 for no photos (saves tokens).",be={placeId:se.string().describe("Google Maps place ID"),maxPhotos:se.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Pe(r){try{let e=i(),t=await new a(e).getPlaceDetails(r.placeId,r.maxPhotos||0);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:Se,DESCRIPTION:Ee,SCHEMA:be,ACTION:Pe};import{z as xe}from"zod";var Ae="maps_geocode",ve="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Me={address:xe.string().describe("Address or place name to convert to coordinates")};async function Ce(r){try{let e=i(),t=await new a(e).geocode(r.address);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:Ae,DESCRIPTION:ve,SCHEMA:Me,ACTION:Ce};import{z as ae}from"zod";var Oe="maps_reverse_geocode",Ne="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",Ie={latitude:ae.number().describe("Latitude coordinate"),longitude:ae.number().describe("Longitude coordinate")};async function we(r){try{let e=i(),t=await new a(e).reverseGeocode(r.latitude,r.longitude);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:Oe,DESCRIPTION:Ne,SCHEMA:Ie,ACTION:we};import{z as C}from"zod";var Te="maps_distance_matrix",_e="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",Re={origins:C.array(C.string()).describe("List of origin addresses or coordinates"),destinations:C.array(C.string()).describe("List of destination addresses or coordinates"),mode:C.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ze(r){try{let e=i(),t=await new a(e).calculateDistanceMatrix(r.origins,r.destinations,r.mode);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:Te,DESCRIPTION:_e,SCHEMA:Re,ACTION:ze};import{z as N}from"zod";var ke="maps_directions",De="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",Ke={origin:N.string().describe("Starting point address or coordinates"),destination:N.string().describe("Destination address or coordinates"),mode:N.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:N.string().optional().describe("Departure time (ISO string format)"),arrival_time:N.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=i(),t=await new a(e).getDirections(r.origin,r.destination,r.mode,r.departure_time,r.arrival_time);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:ke,DESCRIPTION:De,SCHEMA:Ke,ACTION:He};import{z as B}from"zod";var Ge="maps_elevation",$e="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",Le={locations:B.array(B.object({latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=i(),t=await new a(e).getElevation(r.locations);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var w={NAME:Ge,DESCRIPTION:$e,SCHEMA:Le,ACTION:Je};import{z as f}from"zod";var qe="maps_search_places",je="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ue={query:f.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:f.object({latitude:f.number().describe("Latitude to bias results toward"),longitude:f.number().describe("Longitude to bias results toward"),radius:f.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:f.boolean().optional().describe("Only return places that are currently open"),minRating:f.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:f.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Fe(r){try{let e=i(),t=await new a(e).searchText({query:r.query,locationBias:r.locationBias,openNow:r.openNow,minRating:r.minRating,includedType:r.includedType});return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:qe,DESCRIPTION:je,SCHEMA:Ue,ACTION:Fe};import{z as Q}from"zod";var Be="maps_timezone",Ze="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",We={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),timestamp:Q.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ye(r){try{let e=i(),t=await new a(e).getTimezone(r.latitude,r.longitude,r.timestamp);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:Be,DESCRIPTION:Ze,SCHEMA:We,ACTION:Ye};import{z as R}from"zod";var Ve="maps_weather",Qe="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",Xe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate"),type:R.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:R.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:R.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function et(r){try{let e=i(),t=await new a(e).getWeather(r.latitude,r.longitude,r.type||"current",r.forecastDays,r.forecastHours);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var z={NAME:Ve,DESCRIPTION:Qe,SCHEMA:Xe,ACTION:et};import{z as k}from"zod";var tt="maps_explore_area",rt="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",ot={location:k.string().describe("Address or landmark to explore around"),types:k.array(k.string()).optional().describe("Place types to search (default: restaurant, cafe, attraction). Examples: hotel, bar, park, museum"),radius:k.number().optional().describe("Search radius in meters (default: 1000)"),topN:k.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function st(r){try{let e=i(),t=await new a(e).exploreArea(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var D={NAME:tt,DESCRIPTION:rt,SCHEMA:ot,ACTION:st};import{z as Z}from"zod";var at="maps_plan_route",nt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, finds the most efficient visit order using distance-matrix, and returns step-by-step directions between each stop. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",it={stops:Z.array(Z.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:Z.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:Z.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function ct(r){try{let e=i(),t=await new a(e).planRoute(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:at,DESCRIPTION:nt,SCHEMA:it,ACTION:ct};import{z as H}from"zod";var lt="maps_compare_places",pt="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",dt={query:H.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:H.object({latitude:H.number().describe("Your latitude"),longitude:H.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:H.number().optional().describe("Max places to compare (default: 5)")};async function ut(r){try{let e=i(),t=await new a(e).comparePlaces(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var G={NAME:lt,DESCRIPTION:pt,SCHEMA:dt,ACTION:ut};import{z as W}from"zod";var mt="maps_air_quality",gt="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",yt={latitude:W.number().describe("Latitude coordinate"),longitude:W.number().describe("Longitude coordinate"),includeHealthRecommendations:W.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:W.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function ft(r){try{let e=i(),t=await new a(e).getAirQuality(r.latitude,r.longitude,r.includeHealthRecommendations,r.includePollutants);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var $={NAME:mt,DESCRIPTION:gt,SCHEMA:yt,ACTION:ft};import{z as h}from"zod";var ht="maps_static_map",St="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Et={center:h.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:h.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:h.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:h.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:h.array(h.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:h.array(h.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function bt(r){try{let e=i(),t=await new a(e).getStaticMap(r);return t.success?{content:[{type:"image",data:t.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${t.data.size} bytes, ${t.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var L={NAME:ht,DESCRIPTION:St,SCHEMA:Et,ACTION:bt};import{z as ne}from"zod";var Pt="maps_batch_geocode",xt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",At={addresses:ne.array(ne.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function vt(r){try{let e=i(),o=new a(e),t=r.addresses,n=await Promise.all(t.map(async p=>{try{let S=await o.geocode(p);return{address:p,...S}}catch(S){return{address:p,success:!1,error:S.message}}})),c=n.filter(p=>p.success).length,l=n.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:c,failed:l,results:n},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:Pt,DESCRIPTION:xt,SCHEMA:At,ACTION:vt};import{z as q}from"zod";var Mt="maps_search_along_route",Ct="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Ot={textQuery:q.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:q.string().describe("Route start point \u2014 address or landmark name"),destination:q.string().describe("Route end point \u2014 address or landmark name"),mode:q.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:q.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Nt(r){try{let e=i(),t=await new a(e).searchAlongRoute(r);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:Mt,DESCRIPTION:Ct,SCHEMA:Ot,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},It=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:d,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:d,action:r=>A.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:d,action:r=>M.ACTION(r)},{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:d,action:r=>O.ACTION(r)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:d,action:r=>I.ACTION(r)},{name:w.NAME,description:w.DESCRIPTION,schema:w.SCHEMA,annotations:d,action:r=>w.ACTION(r)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:d,action:r=>_.ACTION(r)},{name:z.NAME,description:z.DESCRIPTION,schema:z.SCHEMA,annotations:d,action:r=>z.ACTION(r)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:d,action:r=>D.ACTION(r)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:d,action:r=>L.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.ACTION(r)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:d,action:r=>j.ACTION(r)}]}];function X(r){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return r;let o=new Set(e.split(",").map(n=>n.trim()).filter(Boolean)),t=r.filter(n=>o.has(n.name));return t.length===0?(s.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${r.map(n=>n.name).join(", ")}`),r):(s.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${t.length}/${r.length} tools active`),t)}var Y=It;import{McpServer as wt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as _t}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Rt}from"@modelcontextprotocol/sdk/types.js";import ie from"express";import{randomUUID as zt}from"crypto";import{z as kt}from"zod";var U=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Dt="0.0.1",F=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new wt({name:this.serverName,version:Dt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:kt.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,c)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,c),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ie();o.use(ie.json()),o.post("/mcp",async(n,c)=>{let l=n.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Rt(n.body)){let y=new _t({sessionIdGenerator:()=>zt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{c.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c,n.body)})});let t=async(n,c)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){c.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(n);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new Tt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Gt}from"url";import{dirname as $t}from"path";import{readFileSync as ce,writeFileSync as Lt,existsSync as Jt}from"fs";import{createInterface as qt}from"readline";var jt=Gt(import.meta.url),pe=$t(jt);le({path:ee(process.cwd(),".env")});le({path:ee(pe,"../.env")});async function Ut(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} 17 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=Y.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let c=Number(n);if(isNaN(c)||c<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,X(t.tools));s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${c}...`),await l.startHttpServer(c),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${c}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${c}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var de=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route"];async function Ft(r,e,o){let t=new a(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return t.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let n=await Promise.all(e.addresses.map(async l=>{try{let p=await t.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=n.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:n}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${de.join(", ")}`)}}var Bt=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),Zt=import.meta.url===`file://${process.argv[1]}`;if(Bt||Zt){let r="0.0.0";try{let e=ee(pe,"../package.json");r=JSON.parse(ce(e,"utf-8")).version}catch{r="0.0.0"}Kt(Ht(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${de.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},t=await Ft(e.tool,o,e.apikey);console.log(JSON.stringify(t,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(1)}}).command("batch-geocode","Geocode multiple addresses from a file (one address per line)",e=>e.option("input",{alias:"i",type:"string",describe:"Input file path (one address per line). Use - for stdin.",demandOption:!0}).option("output",{alias:"o",type:"string",describe:"Output file path (JSON). Defaults to stdout."}).option("concurrency",{alias:"c",type:"number",describe:"Max parallel requests",default:20}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 batch-geocode -i addresses.txt","Geocode to stdout"],["$0 batch-geocode -i addresses.txt -o results.json","Geocode to file"],["cat addresses.txt | $0 batch-geocode -i -","Geocode from stdin"]]),async e=>{e.apikey||(console.error("Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var."),process.exit(1));let o;if(e.input==="-"){let u=qt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Jt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=ce(e.input,"utf-8").split(`
2
+ import{b as a,c as s}from"./chunk-LJD6LGRL.js";import{config as le}from"dotenv";import{resolve as ee}from"path";import Kt from"yargs";import{hideBin as Ht}from"yargs/helpers";import{z as E}from"zod";import{AsyncLocalStorage as me}from"async_hooks";var oe=new me;function i(){return oe.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function V(r,e){return oe.run(r,e)}var ge="maps_search_nearby",ye="Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.",fe={center:E.object({value:E.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:E.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:E.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:E.number().default(1e3).describe("Search radius in meters"),openNow:E.boolean().default(!1).describe("Only show places that are currently open"),minRating:E.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function he(r){try{let e=i(),t=await new a(e).searchNearby(r);return t.success?{content:[{type:"text",text:`location: ${JSON.stringify(t.location,null,2)}
3
+ `+JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var x={NAME:ge,DESCRIPTION:ye,SCHEMA:fe,ACTION:he};import{z as se}from"zod";var Se="maps_place_details",Ee="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, and opening hours. Set maxPhotos (1-10) to include photo URLs \u2014 omit or set to 0 for no photos (saves tokens).",be={placeId:se.string().describe("Google Maps place ID"),maxPhotos:se.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens.")};async function Pe(r){try{let e=i(),t=await new a(e).getPlaceDetails(r.placeId,r.maxPhotos||0);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:Se,DESCRIPTION:Ee,SCHEMA:be,ACTION:Pe};import{z as xe}from"zod";var Ae="maps_geocode",ve="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Me={address:xe.string().describe("Address or place name to convert to coordinates")};async function Ce(r){try{let e=i(),t=await new a(e).geocode(r.address);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:Ae,DESCRIPTION:ve,SCHEMA:Me,ACTION:Ce};import{z as ae}from"zod";var Oe="maps_reverse_geocode",Ne="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",Ie={latitude:ae.number().describe("Latitude coordinate"),longitude:ae.number().describe("Longitude coordinate")};async function we(r){try{let e=i(),t=await new a(e).reverseGeocode(r.latitude,r.longitude);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:Oe,DESCRIPTION:Ne,SCHEMA:Ie,ACTION:we};import{z as C}from"zod";var Te="maps_distance_matrix",_e="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",Re={origins:C.array(C.string()).describe("List of origin addresses or coordinates"),destinations:C.array(C.string()).describe("List of destination addresses or coordinates"),mode:C.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ze(r){try{let e=i(),t=await new a(e).calculateDistanceMatrix(r.origins,r.destinations,r.mode);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:Te,DESCRIPTION:_e,SCHEMA:Re,ACTION:ze};import{z as N}from"zod";var ke="maps_directions",De="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",Ke={origin:N.string().describe("Starting point address or coordinates"),destination:N.string().describe("Destination address or coordinates"),mode:N.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:N.string().optional().describe("Departure time (ISO string format)"),arrival_time:N.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=i(),t=await new a(e).getDirections(r.origin,r.destination,r.mode,r.departure_time,r.arrival_time);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:ke,DESCRIPTION:De,SCHEMA:Ke,ACTION:He};import{z as B}from"zod";var Ge="maps_elevation",$e="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",Le={locations:B.array(B.object({latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=i(),t=await new a(e).getElevation(r.locations);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var w={NAME:Ge,DESCRIPTION:$e,SCHEMA:Le,ACTION:Je};import{z as f}from"zod";var qe="maps_search_places",je="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ue={query:f.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:f.object({latitude:f.number().describe("Latitude to bias results toward"),longitude:f.number().describe("Longitude to bias results toward"),radius:f.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:f.boolean().optional().describe("Only return places that are currently open"),minRating:f.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:f.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Fe(r){try{let e=i(),t=await new a(e).searchText({query:r.query,locationBias:r.locationBias,openNow:r.openNow,minRating:r.minRating,includedType:r.includedType});return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:qe,DESCRIPTION:je,SCHEMA:Ue,ACTION:Fe};import{z as Q}from"zod";var Be="maps_timezone",Ze="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",We={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),timestamp:Q.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ye(r){try{let e=i(),t=await new a(e).getTimezone(r.latitude,r.longitude,r.timestamp);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:Be,DESCRIPTION:Ze,SCHEMA:We,ACTION:Ye};import{z as R}from"zod";var Ve="maps_weather",Qe="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",Xe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate"),type:R.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:R.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:R.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function et(r){try{let e=i(),t=await new a(e).getWeather(r.latitude,r.longitude,r.type||"current",r.forecastDays,r.forecastHours);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var z={NAME:Ve,DESCRIPTION:Qe,SCHEMA:Xe,ACTION:et};import{z as k}from"zod";var tt="maps_explore_area",rt="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",ot={location:k.string().describe("Address or landmark to explore around"),types:k.array(k.string()).optional().describe("Place types to search (default: restaurant, cafe, attraction). Examples: hotel, bar, park, museum"),radius:k.number().optional().describe("Search radius in meters (default: 1000)"),topN:k.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function st(r){try{let e=i(),t=await new a(e).exploreArea(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var D={NAME:tt,DESCRIPTION:rt,SCHEMA:ot,ACTION:st};import{z as Z}from"zod";var at="maps_plan_route",nt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (up 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. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",it={stops:Z.array(Z.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:Z.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:Z.boolean().optional().describe("Auto-optimize visit order via Routes API waypoint optimization (default: true). Set false to keep original order. Not available for transit mode.")};async function ct(r){try{let e=i(),t=await new a(e).planRoute(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:at,DESCRIPTION:nt,SCHEMA:it,ACTION:ct};import{z as H}from"zod";var lt="maps_compare_places",pt="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",dt={query:H.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:H.object({latitude:H.number().describe("Your latitude"),longitude:H.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:H.number().optional().describe("Max places to compare (default: 5)")};async function ut(r){try{let e=i(),t=await new a(e).comparePlaces(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var G={NAME:lt,DESCRIPTION:pt,SCHEMA:dt,ACTION:ut};import{z as W}from"zod";var mt="maps_air_quality",gt="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",yt={latitude:W.number().describe("Latitude coordinate"),longitude:W.number().describe("Longitude coordinate"),includeHealthRecommendations:W.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:W.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function ft(r){try{let e=i(),t=await new a(e).getAirQuality(r.latitude,r.longitude,r.includeHealthRecommendations,r.includePollutants);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var $={NAME:mt,DESCRIPTION:gt,SCHEMA:yt,ACTION:ft};import{z as h}from"zod";var ht="maps_static_map",St="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Et={center:h.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:h.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:h.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:h.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:h.array(h.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:h.array(h.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function bt(r){try{let e=i(),t=await new a(e).getStaticMap(r);return t.success?{content:[{type:"image",data:t.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${t.data.size} bytes, ${t.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var L={NAME:ht,DESCRIPTION:St,SCHEMA:Et,ACTION:bt};import{z as ne}from"zod";var Pt="maps_batch_geocode",xt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",At={addresses:ne.array(ne.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function vt(r){try{let e=i(),o=new a(e),t=r.addresses,n=await Promise.all(t.map(async p=>{try{let S=await o.geocode(p);return{address:p,...S}}catch(S){return{address:p,success:!1,error:S.message}}})),c=n.filter(p=>p.success).length,l=n.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:c,failed:l,results:n},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:Pt,DESCRIPTION:xt,SCHEMA:At,ACTION:vt};import{z as q}from"zod";var Mt="maps_search_along_route",Ct="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Ot={textQuery:q.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:q.string().describe("Route start point \u2014 address or landmark name"),destination:q.string().describe("Route end point \u2014 address or landmark name"),mode:q.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:q.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Nt(r){try{let e=i(),t=await new a(e).searchAlongRoute(r);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:Mt,DESCRIPTION:Ct,SCHEMA:Ot,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},It=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:d,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:d,action:r=>A.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:d,action:r=>M.ACTION(r)},{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:d,action:r=>O.ACTION(r)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:d,action:r=>I.ACTION(r)},{name:w.NAME,description:w.DESCRIPTION,schema:w.SCHEMA,annotations:d,action:r=>w.ACTION(r)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:d,action:r=>_.ACTION(r)},{name:z.NAME,description:z.DESCRIPTION,schema:z.SCHEMA,annotations:d,action:r=>z.ACTION(r)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:d,action:r=>D.ACTION(r)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:d,action:r=>L.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.ACTION(r)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:d,action:r=>j.ACTION(r)}]}];function X(r){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return r;let o=new Set(e.split(",").map(n=>n.trim()).filter(Boolean)),t=r.filter(n=>o.has(n.name));return t.length===0?(s.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${r.map(n=>n.name).join(", ")}`),r):(s.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${t.length}/${r.length} tools active`),t)}var Y=It;import{McpServer as wt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as _t}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Rt}from"@modelcontextprotocol/sdk/types.js";import ie from"express";import{randomUUID as zt}from"crypto";import{z as kt}from"zod";var U=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Dt="0.0.1",F=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new wt({name:this.serverName,version:Dt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:kt.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,c)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,c),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ie();o.use(ie.json()),o.post("/mcp",async(n,c)=>{let l=n.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Rt(n.body)){let y=new _t({sessionIdGenerator:()=>zt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{c.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c,n.body)})});let t=async(n,c)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){c.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(n);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new Tt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Gt}from"url";import{dirname as $t}from"path";import{readFileSync as ce,writeFileSync as Lt,existsSync as Jt}from"fs";import{createInterface as qt}from"readline";var jt=Gt(import.meta.url),pe=$t(jt);le({path:ee(process.cwd(),".env")});le({path:ee(pe,"../.env")});async function Ut(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} 17 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=Y.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let c=Number(n);if(isNaN(c)||c<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,X(t.tools));s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${c}...`),await l.startHttpServer(c),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${c}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${c}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var de=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route"];async function Ft(r,e,o){let t=new a(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return t.getPlaceDetails(e.placeId,e.maxPhotos||0);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let n=await Promise.all(e.addresses.map(async l=>{try{let p=await t.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=n.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:n}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${de.join(", ")}`)}}var Bt=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),Zt=import.meta.url===`file://${process.argv[1]}`;if(Bt||Zt){let r="0.0.0";try{let e=ee(pe,"../package.json");r=JSON.parse(ce(e,"utf-8")).version}catch{r="0.0.0"}Kt(Ht(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${de.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},t=await Ft(e.tool,o,e.apikey);console.log(JSON.stringify(t,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(1)}}).command("batch-geocode","Geocode multiple addresses from a file (one address per line)",e=>e.option("input",{alias:"i",type:"string",describe:"Input file path (one address per line). Use - for stdin.",demandOption:!0}).option("output",{alias:"o",type:"string",describe:"Output file path (JSON). Defaults to stdout."}).option("concurrency",{alias:"c",type:"number",describe:"Max parallel requests",default:20}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 batch-geocode -i addresses.txt","Geocode to stdout"],["$0 batch-geocode -i addresses.txt -o results.json","Geocode to file"],["cat addresses.txt | $0 batch-geocode -i -","Geocode from stdin"]]),async e=>{e.apikey||(console.error("Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var."),process.exit(1));let o;if(e.input==="-"){let u=qt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Jt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=ce(e.input,"utf-8").split(`
4
4
  `).map(u=>u.trim()).filter(u=>u.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let t=new a(e.apikey),n=Math.min(Math.max(e.concurrency,1),50),c=[],l=0,p=async(u,b)=>{let m=[];for(let ue of u){let re=ue().then(()=>{m.splice(m.indexOf(re),1)});m.push(re),m.length>=b&&await Promise.race(m)}await Promise.all(m)},S=o.map((u,b)=>async()=>{try{let m=await t.geocode(u);c[b]={address:u,...m}}catch(m){c[b]={address:u,success:!1,error:m.message}}l++,e.output&&process.stderr.write(`\r ${l}/${o.length} geocoded`)});await p(S,n),e.output&&process.stderr.write(`
5
5
  `);let g=c.filter(u=>u.success).length,y=c.filter(u=>!u.success).length,te={total:o.length,succeeded:g,failed:y,results:c},P=JSON.stringify(te,null,2);e.output?(Lt(e.output,P,"utf-8"),console.error(`Done: ${g}/${o.length} succeeded. Output: ${e.output}`)):console.log(P),process.exit(y>0?1:0)}).command("$0","Start the MCP server (HTTP by default, --stdio for stdio mode)",e=>e.option("port",{alias:"p",type:"number",description:"Port to run the MCP server on",default:process.env.MCP_SERVER_PORT?parseInt(process.env.MCP_SERVER_PORT):3e3}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).option("stdio",{type:"boolean",description:"Use stdio transport instead of HTTP",default:!1}).example([["$0","Start HTTP server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start HTTP with custom port and API key"],["$0 --stdio","Start in stdio mode (for Claude Desktop, Cursor, etc.)"]]),async e=>{e.apikey&&(process.env.GOOGLE_MAPS_API_KEY=e.apikey);let o=X(Y[0].tools);e.stdio?await new F(Y[0].name,o).startStdio():(s.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),s.log(" A Model Context Protocol server for Google Maps services"),s.log(""),e.apikey||(s.log("\u26A0\uFE0F Google Maps API Key not found!"),s.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),s.log("")),Ut(e.port,e.apikey).catch(t=>{s.error("\u274C Failed to start server:",t),process.exit(1)}))}).version(r).alias("version","v").help().parse()}export{Ut as startServer};
package/dist/index.d.ts CHANGED
@@ -100,6 +100,7 @@ interface ElevationResponse {
100
100
  declare class PlacesSearcher {
101
101
  private mapsTools;
102
102
  private newPlacesService;
103
+ private routesService;
103
104
  constructor(apiKey?: string);
104
105
  searchNearby(params: {
105
106
  center: {
@@ -203,6 +204,20 @@ declare class NewPlacesService {
203
204
  }): Promise<any[]>;
204
205
  getPhotoUri(photoName: string, maxWidthPx?: number): Promise<string>;
205
206
  getPlaceDetails(placeId: string): Promise<{
207
+ reviews: any;
208
+ photos: any;
209
+ generative_summary?: any;
210
+ review_summary?: any;
211
+ payment_options?: any;
212
+ atmosphere?: Record<string, boolean> | undefined;
213
+ serves?: Record<string, boolean> | undefined;
214
+ dining_options?: Record<string, boolean> | undefined;
215
+ accessibility?: {
216
+ [k: string]: unknown;
217
+ } | undefined;
218
+ parking?: {
219
+ [k: string]: unknown;
220
+ } | undefined;
206
221
  name: any;
207
222
  place_id: string;
208
223
  formatted_address: any;
@@ -212,6 +227,8 @@ declare class NewPlacesService {
212
227
  lng: any;
213
228
  };
214
229
  };
230
+ primary_type: any;
231
+ types: any;
215
232
  rating: any;
216
233
  user_ratings_total: any;
217
234
  opening_hours: {
@@ -221,8 +238,7 @@ declare class NewPlacesService {
221
238
  formatted_phone_number: any;
222
239
  website: any;
223
240
  price_level: any;
224
- reviews: any;
225
- photos: any;
241
+ editorial_summary: any;
226
242
  }>;
227
243
  private transformSearchResult;
228
244
  private transformPlaceResponse;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{a,b,c}from"./chunk-G6LPVEOX.js";export{c as Logger,a as NewPlacesService,b as PlacesSearcher};
1
+ import{a,b,c}from"./chunk-LJD6LGRL.js";export{c as Logger,a as NewPlacesService,b as PlacesSearcher};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/mcp-google-map",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "mcpName": "io.github.cablate/google-map",
5
5
  "description": "17 Google Maps tools for AI agents — geocode, search, directions, weather, air quality, map images via MCP server or standalone CLI",
6
6
  "type": "module",
@@ -71,7 +71,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
71
71
  | Tool | When to use | Example |
72
72
  |------|-------------|---------|
73
73
  | `maps_explore_area` | Overview of a neighborhood | "What's around Tokyo Tower?" |
74
- | `maps_plan_route` | Multi-stop optimized itinerary | "Visit these 5 places efficiently" |
74
+ | `maps_plan_route` | Multi-stop optimized itinerary (Routes API waypoint optimization, up to 25 stops) | "Visit these 5 places efficiently" |
75
75
  | `maps_compare_places` | Side-by-side comparison | "Which ramen shop near Shibuya?" |
76
76
 
77
77
  ---
@@ -81,7 +81,8 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
81
81
  | Tool | Limitation | Workaround |
82
82
  |------|-----------|------------|
83
83
  | `maps_weather` | Unsupported regions: Japan, China, South Korea, Cuba, Iran, North Korea, Syria | Use web search for weather in these regions |
84
- | `maps_distance_matrix` | Transit mode returns null in some regions (notably Japan) | Fall back to `driving` or `walking` mode, or use `maps_directions` for transit |
84
+ | `maps_distance_matrix` | Transit mode may return null in some regions | Fall back to `driving` or `walking` mode, or use `maps_directions` for transit |
85
+ | `maps_plan_route` | Transit mode does not support waypoint optimization | Set `optimize: false` for transit mode |
85
86
  | `maps_air_quality` | Works globally including Japan (unlike weather) | — |
86
87
 
87
88
  ---
@@ -116,5 +117,6 @@ npx @cablate/mcp-google-map exec <tool> '<json_params>' [-k API_KEY]
116
117
  |------|---------|--------------|
117
118
  | `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 |
118
119
  | `references/travel-planning.md` | Travel planning methodology — 6-layer model, Search Along Route, anti-patterns | When planning multi-day trips — **read before Recipe 1** |
120
+ | `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 |
119
121
 
120
122
  > For **project development** knowledge (architecture, API guide, GIS domain, design decisions), see `skills/project-docs/SKILL.md`.
@@ -0,0 +1,206 @@
1
+ # Local SEO — Google 商家排名分析
2
+
3
+ ## Core Principle
4
+
5
+ Local SEO is **competitive intelligence through public data**. Every business on Google Maps exposes its ranking signals: category, reviews, photos, hours, website. An AI agent with geo tools can audit a business's competitive position and identify actionable gaps — work that Local SEO consultants charge $30K–50K TWD for.
6
+
7
+ The Google Maps ranking algorithm weighs three factors:
8
+ 1. **Distance** — proximity to the searcher
9
+ 2. **Relevance** — how well the business matches the search query (categories, keywords in reviews/menus/services)
10
+ 3. **Prominence** — overall reputation (review count, rating, web mentions, citations)
11
+
12
+ Distance is fixed. Relevance and Prominence are what we analyze.
13
+
14
+ ---
15
+
16
+ ## The 5-Layer Analysis Model
17
+
18
+ ### Layer 1: Keyword Landscape
19
+ **What:** Understand what searchers type and who ranks for it.
20
+ **Tool:** `maps_search_places("{keyword} in {area}")`
21
+ **Example:**
22
+ ```
23
+ maps_search_places("牛肉湯 台南") → who ranks for this keyword?
24
+ maps_search_places("餐廳 台南中西區") → broader category competition
25
+ maps_search_places("medical clinic Taipei") → English keyword landscape
26
+ ```
27
+ **Output:** Top competitors list with place_ids for deeper analysis.
28
+
29
+ ### Layer 2: Competitor Deep Dive
30
+ **What:** Compare ranking signals across top competitors.
31
+ **Tool:** `maps_compare_places` or `maps_place_details` per competitor
32
+ **Signals to extract:**
33
+ - Rating + review count (prominence)
34
+ - Business categories (primary + secondary)
35
+ - Has website / phone / hours (completeness)
36
+ - Photo count (`photo_count` field)
37
+ - Review content keywords
38
+
39
+ ```
40
+ maps_compare_places({
41
+ query: "牛肉湯 台南",
42
+ maxResults: 5,
43
+ includeDistance: true,
44
+ referenceLocation: "target business address"
45
+ })
46
+ ```
47
+
48
+ **Key insight from practitioners:** A restaurant with category "日式餐廳" ranks differently than one with "餐廳". The primary category determines which search terms surface the business. Secondary categories add breadth.
49
+
50
+ ### Layer 3: Gap Analysis
51
+ **What:** Identify where the target business falls short vs competitors.
52
+ **Tool:** `maps_place_details(target_place_id, maxPhotos: 5)` vs competitor details
53
+ **Checklist:**
54
+
55
+ | Signal | Check | Fix if missing |
56
+ |--------|-------|----------------|
57
+ | Primary category | Matches highest-volume keyword? | Change to broader or more specific |
58
+ | Review count | Within 80% of top competitor? | Review acquisition campaign |
59
+ | Rating | ≥ 4.2? | Address negative reviews |
60
+ | Photos | ≥ 10? With menu/interior/exterior? | Upload photos |
61
+ | Website | Present? | Add website link |
62
+ | Hours | Accurate? Special hours set? | Update hours |
63
+ | Menu/Services | Complete? Keywords match search terms? | Add items with searchable names |
64
+
65
+ **Practitioner insight:** Menu item names must match what people search. If you sell 半熟蛋 (soft-boiled egg) but call it "大太陽" on your menu, the algorithm cannot match the search query to your business.
66
+
67
+ ### Layer 4: Area Density & Opportunity
68
+ **What:** Find underserved areas or oversaturated markets.
69
+ **Tool:** `maps_explore_area` or `maps_search_nearby` with type filters
70
+ ```
71
+ maps_explore_area({
72
+ location: "大安區, 台北",
73
+ types: ["restaurant", "cafe", "dentist"],
74
+ radius: 1000
75
+ })
76
+ ```
77
+ **Analysis:**
78
+ - High density + low avg rating = opportunity (bad competitors)
79
+ - Low density = blue ocean (no competitors)
80
+ - High density + high avg rating = red ocean (avoid or differentiate)
81
+
82
+ ### Layer 5: Monitoring Snapshot
83
+ **What:** Capture current ranking position for future comparison.
84
+ **Tool:** `maps_search_places("{keyword}")` → record rank position of target business
85
+ ```
86
+ maps_search_places("台南牛肉湯")
87
+ → Result: target business at position #4
88
+ → Baseline recorded. Re-run monthly to track movement.
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Tool Call Sequences
94
+
95
+ ### Scenario A: Full Competitor Audit (new client)
96
+ ```
97
+ Phase 1 — Keyword Discovery (2-3 calls)
98
+ maps_search_places("{primary keyword} {area}") → top competitors
99
+ maps_search_places("{secondary keyword} {area}") → additional competitors
100
+ maps_search_places("{category} near {address}") → proximity competitors
101
+
102
+ Phase 2 — Deep Comparison (1-2 calls)
103
+ maps_compare_places(top 5 competitors + target) → side-by-side signals
104
+ maps_place_details(target, maxPhotos: 5) → full target audit
105
+
106
+ Phase 3 — Area Analysis (1-2 calls)
107
+ maps_explore_area(target location, multiple types) → neighborhood context
108
+ maps_search_nearby(target coords, same type, 2km) → direct competitors within radius
109
+
110
+ Phase 4 — Visualize (1 call)
111
+ maps_static_map(markers for all competitors + target) → competition map
112
+
113
+ Total: ~8-10 calls for a full audit
114
+ ```
115
+
116
+ ### Scenario B: Quick Rank Check (existing client)
117
+ ```
118
+ maps_search_places("{target keyword}") → find rank position
119
+ maps_place_details(target_place_id) → current signals snapshot
120
+ Total: 2 calls
121
+ ```
122
+
123
+ ### Known Edge Case
124
+ `maps_search_nearby` keyword parameter does not support Chinese category names (e.g., "日式餐廳"). Use English types (e.g., "japanese_restaurant") or use `maps_explore_area` / `maps_search_places` as alternatives — they handle Chinese queries correctly.
125
+
126
+ ### Scenario C: New Location Scouting
127
+ ```
128
+ maps_explore_area(candidate area, target business type) → competitor density
129
+ maps_search_nearby(coords, type, radius: 2000) → detailed competitor list
130
+ maps_distance_matrix(candidate locations, key landmarks) → accessibility analysis
131
+ maps_static_map(area with competitor markers) → visual density map
132
+ Total: 4 calls
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Google Business Profile Signals Reference
138
+
139
+ ### Signals the algorithm considers (from practitioner testing)
140
+
141
+ | Signal | Weight | How to check with our tools |
142
+ |--------|--------|-----------------------------|
143
+ | Primary business category | High | `place_details` → types |
144
+ | Review count | High | `place_details` → user_ratings_total |
145
+ | Average rating | High | `place_details` → rating |
146
+ | Review content keywords | High | `place_details` → reviews text |
147
+ | Photo count & quality | Medium | `place_details` → photo_count |
148
+ | Business info completeness | Medium | `place_details` → website, phone, hours |
149
+ | Menu/Service items | Medium (industry-specific) | `place_details` → check if present |
150
+ | Review response rate | Medium | Not directly visible via API |
151
+ | External citations (blogs, news) | Medium | Not available via Maps API |
152
+ | NAP consistency (Name, Address, Phone) | Medium | `place_details` → verify across sources |
153
+ | Google Posts activity | Low | Not available via API |
154
+
155
+ ### What we CAN'T see (requires Business Profile backend access)
156
+ - Search keyword impressions
157
+ - Profile view breakdown (Maps vs Search, Mobile vs Desktop)
158
+ - Direct vs Discovery search ratio
159
+ - Call/direction/website click counts
160
+ - Local 3-Pack appearance rate
161
+
162
+ ### AI Mode recommendations
163
+ Google's AI Mode pulls from:
164
+ 1. Business Profile data (hours, description, categories)
165
+ 2. Review content (specific mentions of products/services)
166
+ 3. External links (blogs, news, forums)
167
+ 4. Rating + review count (credibility signal)
168
+
169
+ High rating + high review count + keyword-rich reviews = higher chance of AI recommendation.
170
+
171
+ ---
172
+
173
+ ## Anti-Patterns
174
+
175
+ | Anti-Pattern | Symptom | Fix |
176
+ |-------------|---------|-----|
177
+ | Keyword stuffing in business name | "台北最好吃牛肉麵-王記牛肉麵" | Use real business name; keywords go in reviews/menu |
178
+ | Ignoring secondary categories | Only one category set | Add relevant secondary categories |
179
+ | Generic menu names | Creative names nobody searches for | Use searchable names that match queries |
180
+ | Photo desert | 0-3 photos | Upload 10+ (exterior, interior, menu items, team) |
181
+ | Review neglect | Many reviews with no owner reply | Reply to all reviews (affects activity score) |
182
+ | Wrong primary category | "餐廳" when should be "日式餐廳" or vice versa | Match to highest-value keyword for your positioning |
183
+ | Comparing wrong metrics | Comparing summer vs winter data | Compare year-over-year same period |
184
+
185
+ ---
186
+
187
+ ## Industry-Specific Notes
188
+
189
+ | Industry | Key ranking lever | Tool check |
190
+ |----------|------------------|------------|
191
+ | **Restaurants** | Menu completeness + review keywords about dishes | `place_details` → check menu presence |
192
+ | **Medical/Clinics** | Review keywords about treatments + rating | `search_places("推薦 {specialty} {area}")` |
193
+ | **Retail** | Product catalog + photos | `place_details` → photo_count |
194
+ | **Hotels/B&B** | Amenity details + review sentiment | `place_details` → reviews |
195
+ | **Services (plumber, lawyer, etc.)** | Service area coverage + review count | `search_nearby` to check competition density |
196
+ | **Real estate** | Area expertise signals + review testimonials | `explore_area` for neighborhood analysis |
197
+
198
+ ---
199
+
200
+ ## When to Read This
201
+
202
+ - User asks to analyze a business's Google Maps ranking
203
+ - User wants to compare competitors in an area
204
+ - User says "Local SEO", "商家排名", "地圖行銷", "Google Business Profile"
205
+ - User wants to scout a location for a new business
206
+ - User asks "why does competitor X rank higher than me?"
@@ -96,7 +96,7 @@ exec maps_search_nearby '{"center": {"value": "35.6586,139.7454", "isCoordinates
96
96
  | openNow | boolean | no | Only show currently open places |
97
97
  | minRating | number | no | Minimum rating (0-5) |
98
98
 
99
- Response: `{ success, location, data: [{ name, place_id, formatted_address, geometry, rating, user_ratings_total, opening_hours }] }`
99
+ Response: `{ success, location, data: [{ name, place_id, formatted_address, geometry, primary_type, price_level, rating, user_ratings_total, opening_hours }] }`
100
100
 
101
101
  ---
102
102
 
@@ -116,13 +116,13 @@ exec maps_search_places '{"query": "ramen in Tokyo"}'
116
116
  | minRating | number | no | Minimum rating (1.0-5.0) |
117
117
  | includedType | string | no | Place type filter |
118
118
 
119
- Response: `{ success, data: [{ name, place_id, address, location, rating, total_ratings, open_now }] }`
119
+ Response: `{ success, data: [{ name, place_id, address, location, primary_type, price_level, rating, total_ratings, open_now }] }`
120
120
 
121
121
  ---
122
122
 
123
123
  ## maps_place_details
124
124
 
125
- Get full details for a place by its place_id (from search results). Returns reviews, phone, website, hours. Set `maxPhotos` to include photo URLs (default: 0 = no photos, saves tokens).
125
+ Get full details for a place by its place_id (from search results). Returns reviews, phone, website, hours, and rich attribute data (parking, dining options, atmosphere, accessibility, AI summaries). Set `maxPhotos` to include photo URLs (default: 0 = no photos, saves tokens).
126
126
 
127
127
  ```bash
128
128
  exec maps_place_details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg"}'
@@ -134,6 +134,23 @@ exec maps_place_details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg", "maxPhotos":
134
134
  | placeId | string | yes | Google Maps place ID (from search results) |
135
135
  | maxPhotos | number | no | Number of photo URLs to include (0-10, default 0). Always returns `photo_count`. |
136
136
 
137
+ Response includes (when available from Google):
138
+
139
+ | Field | Type | Description |
140
+ |-------|------|-------------|
141
+ | `primary_type` | string | Precise place type (e.g., `wine_bar`, `sushi_restaurant`) |
142
+ | `types` | string[] | All place types |
143
+ | `editorial_summary` | string | Google's editorial description |
144
+ | `parking` | object | `{ freeParkingLot, paidParkingLot, freeStreetParking, valetParking, ... }` (truthy only) |
145
+ | `accessibility` | object | `{ wheelchairAccessibleParking, wheelchairAccessibleEntrance, ... }` (truthy only) |
146
+ | `dining_options` | object | `{ dine_in, delivery, takeout, curbside_pickup, reservable }` (truthy only) |
147
+ | `serves` | object | `{ vegetarian_food, beer, wine, cocktails, breakfast, lunch, dinner, ... }` (truthy only) |
148
+ | `atmosphere` | object | `{ good_for_groups, good_for_children, outdoor_seating, allows_dogs, live_music, ... }` (truthy only) |
149
+ | `payment_options` | object | Payment methods accepted |
150
+ | `review_summary` | string | AI-generated review summary (region-limited: US/UK/India/Japan) |
151
+ | `generative_summary` | string | AI-generated place overview (region-limited: US/India) |
152
+ | `reviews[].language` | string | Review language code (e.g., `en`, `zh-TW`) |
153
+
137
154
  ---
138
155
 
139
156
  ## maps_directions
@@ -349,7 +366,7 @@ exec maps_explore_area '{"location": "Tokyo Tower", "types": ["restaurant", "caf
349
366
 
350
367
  ## maps_plan_route (composite)
351
368
 
352
- Plan an optimized multi-stop route. Internally chains geocode distance-matrix nearest-neighbor directions.
369
+ Plan an optimized multi-stop route. Internally geocodes all stops, then uses a single Routes API call with waypoint optimization (up to 25 intermediate stops) to find the most efficient visit order and return directions for each leg.
353
370
 
354
371
  ```bash
355
372
  exec maps_plan_route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Station", "Ueno Park"], "mode": "driving"}'
@@ -357,9 +374,9 @@ exec maps_plan_route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Sta
357
374
 
358
375
  | Param | Type | Required | Description |
359
376
  |-------|------|----------|-------------|
360
- | stops | string[] | yes | Addresses or landmarks (min 2) |
377
+ | stops | string[] | yes | Addresses or landmarks (min 2, up to 27 total with origin + destination) |
361
378
  | mode | string | no | driving, walking, bicycling, transit (default: driving) |
362
- | optimize | boolean | no | Auto-optimize visit order (default: true) |
379
+ | optimize | boolean | no | Auto-optimize visit order via Routes API (default: true). Not available for transit mode. |
363
380
 
364
381
  ---
365
382
 
@@ -546,15 +563,21 @@ Elevation: 45m (not a flood risk)
546
563
 
547
564
  User has a list of places and wants the optimal visit order.
548
565
 
549
- **Steps:**
566
+ **Preferred: Use `maps_plan_route`** — handles everything in one call (geocode + Routes API waypoint optimization + directions for all legs).
567
+
568
+ ```
569
+ maps_plan_route {"stops": ["Tokyo Tower", "Shibuya", "Shinjuku", "Ueno Park", "Asakusa"], "mode": "driving"}
570
+ ```
571
+
572
+ **Manual alternative** (if you need more control):
550
573
  1. `maps_geocode` — Resolve all addresses to coordinates
551
574
  2. `maps_distance_matrix` — Calculate NxN matrix (all origins × all destinations)
552
- 3. Use the matrix to determine the nearest-neighbor route order
553
- 4. `maps_directions` — Generate route for the final order (chain waypoints)
575
+ 3. Determine optimal route order from the matrix
576
+ 4. `maps_directions` — Generate route for the final order
554
577
 
555
578
  **Key decisions:**
556
- - For 5 stops, nearest-neighbor heuristic is good enough
557
- - For the `maps_directions` call, set origin = first stop, destination = last stop, and mention intermediate stops in conversation
579
+ - `maps_plan_route` supports up to 27 stops (origin + 25 intermediates + destination)
580
+ - Transit mode does not support waypoint optimization set `optimize: false`
558
581
  - If the user says "return to start", plan a round trip
559
582
 
560
583
  ---
@@ -12,9 +12,9 @@ CLI / HTTP / stdio
12
12
  Tool ACTION() <- thin dispatch, calls PlacesSearcher
13
13
  |
14
14
  PlacesSearcher <- service facade (composition, filtering, response shaping)
15
- / \
16
- GoogleMapsTools NewPlacesService
17
- (Legacy REST SDK) (Places API New gRPC/REST client)
15
+ /|\
16
+ GoogleMapsTools RoutesService NewPlacesService
17
+ (geocode/tz/elev) (Routes API REST) (Places API New)
18
18
  ```
19
19
 
20
20
  | Layer | Files | Responsibility |
@@ -24,8 +24,9 @@ GoogleMapsTools NewPlacesService
24
24
  | Tool | `src/tools/maps/*.ts` | Declare NAME, DESCRIPTION, SCHEMA, ACTION |
25
25
  | Config | `src/config.ts` | Assemble ToolConfig[], attach MAPS_TOOL_ANNOTATIONS |
26
26
  | Facade | `src/services/PlacesSearcher.ts` | Orchestrate multi-step / composite tools |
27
- | API client (legacy) | `src/services/toolclass.ts` | Wrap `@googlemaps/google-maps-services-js` SDK |
28
- | API client (new) | `src/services/NewPlacesService.ts` | Wrap `@googlemaps/places` gRPC client |
27
+ | API client (routes) | `src/services/RoutesService.ts` | Routes API REST client (directions, distance matrix, waypoint optimization) |
28
+ | API client (legacy) | `src/services/toolclass.ts` | Wrap `@googlemaps/google-maps-services-js` SDK (geocode, timezone, elevation) |
29
+ | API client (places) | `src/services/NewPlacesService.ts` | Wrap `@googlemaps/places` gRPC client |
29
30
  | Auth | `src/utils/apiKeyManager.ts` | API key priority resolution |
30
31
  | Context | `src/utils/requestContext.ts` | Per-request AsyncLocalStorage propagation |
31
32
 
@@ -127,7 +128,8 @@ Missing any file causes doc/behavior mismatch. Verify all before opening a PR.
127
128
  | `src/core/BaseMcpServer.ts` | MCP server core — tool registration, HTTP session management, stdio transport |
128
129
  | `src/index.ts` | Package entry — exports Logger and re-exports public API |
129
130
  | `src/services/PlacesSearcher.ts` | Service facade — orchestrates multi-step composite tools (planRoute, exploreArea, comparePlaces, searchAlongRoute) |
130
- | `src/services/toolclass.ts` | Legacy Google Maps SDK wrapper geocode, directions, distanceMatrix, elevation, timezone, weather, airQuality, staticMap |
131
+ | `src/services/RoutesService.ts` | Routes API REST clientcomputeRoutes (directions), computeRouteMatrix (distance matrix), waypoint optimization |
132
+ | `src/services/toolclass.ts` | Google Maps SDK wrapper — geocode, elevation, timezone, weather, airQuality, staticMap, searchAlongRoute |
131
133
  | `src/services/NewPlacesService.ts` | Places API (New) client — searchNearby, searchText, getPlaceDetails via gRPC |
132
134
  | `src/tools/maps/*.ts` | Individual tool definitions (17 files) — each exports NAME, DESCRIPTION, SCHEMA, ACTION |
133
135
  | `src/utils/apiKeyManager.ts` | Singleton — resolves API key priority from headers / session / env |
@@ -5,8 +5,8 @@
5
5
  | API | Endpoint | Tool(s) | GCP Service to Enable |
6
6
  |---|---|---|---|
7
7
  | Geocoding API | via `@googlemaps/google-maps-services-js` SDK | `maps_geocode`, `maps_reverse_geocode`, `maps_batch_geocode` | Geocoding API |
8
- | Directions API | via SDK | `maps_directions`, `maps_plan_route` (legs), `maps_search_along_route` (step 1) | Directions API |
9
- | Distance Matrix API | via SDK | `maps_distance_matrix`, `maps_plan_route` (optimization step) | Distance Matrix API |
8
+ | Routes API (computeRoutes) | `https://routes.googleapis.com/directions/v2:computeRoutes` (REST fetch) | `maps_directions`, `maps_plan_route` (legs + waypoint optimization), `maps_search_along_route` (polyline) | Routes API |
9
+ | Routes API (computeRouteMatrix) | `https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix` (REST fetch) | `maps_distance_matrix` | Routes API |
10
10
  | Elevation API | via SDK | `maps_elevation` | Elevation API |
11
11
  | Time Zone API | via SDK | `maps_timezone` | Time Zone API |
12
12
  | Places API (New) — Nearby Search | `https://places.googleapis.com/v1/places:searchNearby` (gRPC via `@googlemaps/places`) | `maps_search_nearby` | Places API (New) |
@@ -136,4 +136,4 @@ Body:
136
136
  | Photos | Returns `photo.name` resource path | Returns `photo_reference` string |
137
137
  | Error codes | gRPC status codes (7=PERMISSION_DENIED, 8=RESOURCE_EXHAUSTED) | HTTP status codes |
138
138
 
139
- **Note**: `maps_search_along_route` uses the Legacy Directions API for step 1 (polyline extraction) and Places API (New) REST for step 2 (search). Both APIs must be enabled for this tool to function.
139
+ **Note**: `maps_search_along_route` uses the Routes API for step 1 (polyline extraction) and Places API (New) REST for step 2 (search). Both APIs must be enabled for this tool to function.
@@ -1 +0,0 @@
1
- import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw m.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw m.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let o=r.data.results[0];return{formatted_address:o.formatted_address,place_id:o.place_id,address_components:o.address_components}}catch(r){throw m.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let n=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${n.status}`);let s=[],l=[];return n.rows.forEach(u=>{let a=[],c=[];u.elements.forEach(d=>{d.status==="OK"?(a.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(a.push(null),c.push(null))}),s.push(a),l.push(c)}),{distances:s,durations:l,origin_addresses:n.origin_addresses,destination_addresses:n.destination_addresses}}catch(o){throw m.error("Error in calculateDistanceMatrix:",o),new Error(`Failed to calculate distance matrix: ${f(o)}`)}}async getDirections(e,t,r="driving",o,n){try{let s;n&&(s=Math.floor(n.getTime()/1e3));let l;s||(o instanceof Date?l=Math.floor(o.getTime()/1e3):o?l=o:l="now");let a=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:l}})).data;if(a.status!=="OK")throw new Error(`Failed to get directions with status: ${a.status} (arrival_time: ${s}, departure_time: ${l}`);if(a.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=a.routes[0],d=c.legs[0],h=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),p={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(p.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),p)};return{routes:a.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:h(d.arrival_time),departure_time:h(d.departure_time)}}catch(s){throw m.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async searchAlongRoute(e){try{let t=await this.getDirections(e.origin,e.destination,e.mode||"walking"),r=t.routes[0]?.overview_polyline?.points;if(!r)throw new Error("Could not get route polyline");let o=Math.min(e.maxResults||5,20),s=await fetch("https://places.googleapis.com/v1/places:searchText",{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":"places.displayName,places.id,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.currentOpeningHours.openNow"},body:JSON.stringify({textQuery:e.textQuery,searchAlongRouteParameters:{polyline:{encodedPolyline:r}},maxResultCount:o})});if(!s.ok){let a=await s.json().catch(()=>({}));throw new Error(a?.error?.message||`HTTP ${s.status}`)}return{places:((await s.json()).places||[]).map(a=>({name:a.displayName?.text||"",place_id:a.id||"",formatted_address:a.formattedAddress||"",location:{lat:a.location?.latitude||0,lng:a.location?.longitude||0},rating:a.rating||0,user_ratings_total:a.userRatingCount||0,open_now:a.currentOpeningHours?.openNow??null})),route:{distance:t.total_distance.text,duration:t.total_duration.text,polyline:r}}}catch(t){throw m.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",o,n){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,l;switch(r){case"forecast_daily":{let c=Math.min(Math.max(o||5,1),10);l=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(n||24,1),240);l=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:l=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(l);if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let a=await u.json();return r==="current"?{temperature:a.temperature,feelsLike:a.feelsLikeTemperature,humidity:a.relativeHumidity,wind:a.wind,conditions:a.weatherCondition?.description?.text||a.weatherCondition?.type,uvIndex:a.uvIndex,precipitation:a.precipitation,visibility:a.visibility,pressure:a.airPressure,cloudCover:a.cloudCover,isDayTime:a.isDaytime}:a}catch(s){throw m.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,o=!1){try{let n=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),o&&s.push("POLLUTANT_CONCENTRATION");let l={location:{latitude:e,longitude:t}};s.length>0&&(l.extraComputations=s);let u=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!u.ok){let g=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(g)}let a=await u.json(),c=a.indexes||[],d=c[0],h={dateTime:a.dateTime,regionCode:a.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(h.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),a.healthRecommendations&&(h.healthRecommendations=a.healthRecommendations),a.pollutants&&(h.pollutants=a.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),h}catch(n){throw m.error("Error in getAirQuality:",n),new Error(n.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let a of e.markers)r.push(`markers=${encodeURIComponent(a)}`);if(e.path)for(let a of e.path)r.push(`path=${encodeURIComponent(a)}`);let o=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(o.length>16384)throw new Error(`URL exceeds 16,384 character limit (${o.length}). Reduce markers or path points.`);let n=await fetch(o);if(!n.ok){let a=n.headers.get("content-type")||"";if(a.includes("application/json")||a.includes("text/")){let c=await n.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${n.status}`)}let s=await n.arrayBuffer(),l=Buffer.from(s);return{base64:l.toString("base64"),size:l.length,dimensions:t}}catch(t){throw m.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let o=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:o,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let l=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+l).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw m.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${f(o)}`)}}async getElevation(e){try{let t=e.map(n=>({lat:n.latitude,lng:n.longitude})),o=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(o.status!=="OK")throw new Error(`Failed to get elevation data with status: ${o.status}`);return o.results.map((n,s)=>({elevation:n.elevation,location:t[s]}))}catch(t){throw m.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw m.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw m.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPhotoUri(e,t=800){try{let[r]=await this.client.getPhotoMedia({name:`${e}/media`,maxWidthPx:t,skipHttpRedirect:!0});return r.photoUri||""}catch(r){throw m.error("Error in getPhotoUri:",r),new Error(`Failed to get photo URI: ${this.extractErrorMessage(r)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw m.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let o=e?.periods;if(!Array.isArray(o)||o.length===0)return!1;let n=1440,s=n*7,{day:l,minutes:u}=this.getLocalTimeComponents(t),a=l*n+u,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},h=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),p=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(p)))return g*60+p};for(let i of o){let g=d(i?.openDay),p=d(i?.closeDay??i?.openDay),w=h(i?.openTime),N=h(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*n+w,b;p===void 0||N===void 0?b=v+n:b=p*n+N,b<=v&&(b+=s);let P=a;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var x=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),o=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(o=o.filter(n=>n.opening_hours?.open_now===!0)),e.minRating&&(o=o.filter(n=>(n.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:o.map(n=>({name:n.name,place_id:n.place_id,address:n.formatted_address,location:n.geometry.location,rating:n.rating,total_ratings:n.user_ratings_total,open_now:n.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e,t=0){try{let r=await this.newPlacesService.getPlaceDetails(e),o;if(t>0&&r.photos?.length>0){let n=r.photos.slice(0,t);o=[];for(let s of n)try{let l=await this.newPlacesService.getPhotoUri(s.photo_reference);o.push({url:l,width:s.width,height:s.height})}catch{}}return{success:!0,data:{name:r.name,address:r.formatted_address,location:r.geometry?.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now,phone:r.formatted_phone_number,website:r.website,price_level:r.price_level,photo_count:r.photos?.length||0,...o&&o.length>0?{photos:o}:{},reviews:r.reviews?.map(n=>({rating:n.rating,text:n.text,time:n.time,author_name:n.author_name}))}}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,n){try{let s=o?new Date(o):new Date,l=n?new Date(n):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,l)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",o,n){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,o,n)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,o){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,o)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,o=e.topN||3,n=await this.geocode(e.location);if(!n.success||!n.data)throw new Error(n.error||"Geocode failed");let{lat:s,lng:l}=n.data.location,u=[];for(let a of t){let c=await this.searchNearby({center:{value:`${s},${l}`,isCoordinates:!0},keyword:a,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,o),h=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);h.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}u.push({type:a,count:c.data.length,top:h})}return{success:!0,data:{location:{address:n.data.formatted_address,lat:s,lng:l},radius:r,categories:u}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let o=[];for(let a of r){let c=await this.geocode(a);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${a}`);o.push({originalName:a,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let n=o;if(e.optimize!==!1&&o.length>2){let a=await this.calculateDistanceMatrix(r,r,"driving");if(a.success&&a.data){let c=new Set([0]),d=[0],h=0;for(;c.size<o.length;){let i=-1,g=1/0;for(let p=0;p<o.length;p++){if(c.has(p))continue;let w=a.data.durations[h]?.[p]?.value??1/0;w<g&&(g=w,i=p)}if(i===-1)break;c.add(i),d.push(i),h=i}n=d.map(i=>o[i])}}let s=[],l=0,u=0;for(let a=0;a<n.length-1;a++){let c=await this.getDirections(n[a].originalName,n[a+1].originalName,t);c.success&&c.data?(l+=c.data.total_distance.value,u+=c.data.total_duration.value,s.push({from:n[a].originalName,to:n[a+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:n[a].originalName,to:n[a+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&o.length>2,stops:n.map(a=>`${a.originalName} (${a.address})`),legs:s,total_distance:`${(l/1e3).toFixed(1)} km`,total_duration:`${Math.round(u/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let o=r.data.slice(0,t),n=[];for(let s of o){let l=await this.getPlaceDetails(s.place_id);n.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:l.data?.phone,website:l.data?.website,price_level:l.data?.price_level})}if(e.userLocation&&n.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,l=o.map(a=>`${a.location.lat},${a.location.lng}`),u=await this.calculateDistanceMatrix([s],l,"driving");if(u.success&&u.data)for(let a=0;a<n.length;a++)n[a].distance=u.data.distances[0]?.[a]?.text,n[a].drive_time=u.data.durations[0]?.[a]?.text}return{success:!0,data:n}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var m={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,x as b,m as c};