@cablate/mcp-google-map 0.0.38 → 0.0.39

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
@@ -93,6 +93,19 @@ Works with Claude Desktop, Cursor, VS Code, and any MCP client that supports std
93
93
  }
94
94
  ```
95
95
 
96
+ **Reduce context usage** — If you only need a subset of tools, set `GOOGLE_MAPS_ENABLED_TOOLS` to limit which tools are registered:
97
+
98
+ ```json
99
+ {
100
+ "env": {
101
+ "GOOGLE_MAPS_API_KEY": "YOUR_API_KEY",
102
+ "GOOGLE_MAPS_ENABLED_TOOLS": "maps_geocode,maps_directions,maps_search_places"
103
+ }
104
+ }
105
+ ```
106
+
107
+ Omit or set to `*` for all 17 tools (default).
108
+
96
109
  ### Method 2: HTTP Server
97
110
 
98
111
  For multi-session deployments, per-request API key isolation, or remote access:
@@ -117,7 +130,7 @@ Then configure your MCP client:
117
130
  ### Server Information
118
131
 
119
132
  - **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
120
- - **Tools**: 16 Google Maps tools (14 atomic + 3 composite)
133
+ - **Tools**: 17 Google Maps tools (14 atomic + 3 composite) — filterable via `GOOGLE_MAPS_ENABLED_TOOLS`
121
134
 
122
135
  ### CLI Exec Mode (Agent Skill)
123
136
 
@@ -282,22 +295,26 @@ For enterprise security reviews, see [Security Assessment Clarifications](./SECU
282
295
 
283
296
  ## Roadmap
284
297
 
285
- ### New Tools
298
+ ### Recent Additions
286
299
 
287
- | Tool | What it unlocks | Status |
300
+ | Tool / Feature | What it unlocks | Status |
288
301
  |------|----------------|--------|
289
- | `maps_static_map` | Return map images with pins/routes — multimodal AI can "see" the map | **Done** |
290
- | `maps_air_quality` | AQI, pollutants — health-aware travel, outdoor planning, real estate | **Done** |
291
- | `maps_validate_address` | Standardize and verify addresses — logistics/e-commerce | Planned |
292
- | `maps_isochrone` | "Show me everything within 30 min drive"reachability analysis | Planned |
293
- | `maps_batch_geocode` | Geocode hundreds of addresses in one call data enrichment | **Done** (CLI) |
302
+ | `maps_static_map` | Map images with pins/routes — multimodal AI can "see" the map | **Done** |
303
+ | `maps_air_quality` | AQI, pollutants — health-aware travel, outdoor planning | **Done** |
304
+ | `maps_batch_geocode` | Geocode up to 50 addresses in one call data enrichment | **Done** |
305
+ | `maps_search_along_route` | Find places along a route ranked by detour time trip planning | **Done** |
306
+ | `maps_explore_area` | One-call neighborhood overview (composite) | **Done** |
307
+ | `maps_plan_route` | Optimized multi-stop itinerary (composite) | **Done** |
308
+ | `maps_compare_places` | Side-by-side place comparison (composite) | **Done** |
309
+ | `GOOGLE_MAPS_ENABLED_TOOLS` | Filter tools to reduce context usage | **Done** |
294
310
 
295
- ### Capabilities
311
+ ### Planned
296
312
 
297
313
  | Feature | What it unlocks | Status |
298
314
  |---------|----------------|--------|
299
- | Spatial Context | Agent remembers "the area we were just looking at" across turns | Research |
300
- | Geo Agent Template | One command to spin up a full geo-aware AI agent | Research |
315
+ | `maps_place_photo` | Place photos for multimodal AI "see" the restaurant ambiance | Planned |
316
+ | Language parameter | Multi-language responses (ISO 639-1) across all tools | Planned |
317
+ | MCP Prompt Templates | `/travel-planner`, `/neighborhood-scout` slash commands in Claude Desktop | Planned |
301
318
  | Geo-Reasoning Benchmark | 10-scenario test suite measuring LLM geospatial reasoning accuracy | Research |
302
319
 
303
320
  ### Use Cases We're Building Toward
@@ -1 +1 @@
1
- import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw p.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw p.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let o=r.data.results[0];return{formatted_address:o.formatted_address,place_id:o.place_id,address_components:o.address_components}}catch(r){throw p.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let a=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(a.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${a.status}`);let s=[],l=[];return a.rows.forEach(u=>{let n=[],c=[];u.elements.forEach(d=>{d.status==="OK"?(n.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(n.push(null),c.push(null))}),s.push(n),l.push(c)}),{distances:s,durations:l,origin_addresses:a.origin_addresses,destination_addresses:a.destination_addresses}}catch(o){throw p.error("Error in calculateDistanceMatrix:",o),new Error(`Failed to calculate distance matrix: ${f(o)}`)}}async getDirections(e,t,r="driving",o,a){try{let s;a&&(s=Math.floor(a.getTime()/1e3));let l;s||(o instanceof Date?l=Math.floor(o.getTime()/1e3):o?l=o:l="now");let n=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:l}})).data;if(n.status!=="OK")throw new Error(`Failed to get directions with status: ${n.status} (arrival_time: ${s}, departure_time: ${l}`);if(n.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=n.routes[0],d=c.legs[0],m=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),h={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(h.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),h)};return{routes:n.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:m(d.arrival_time),departure_time:m(d.departure_time)}}catch(s){throw p.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async searchAlongRoute(e){try{let t=await this.getDirections(e.origin,e.destination,e.mode||"walking"),r=t.routes[0]?.overview_polyline?.points;if(!r)throw new Error("Could not get route polyline");let o=Math.min(e.maxResults||5,20),s=await fetch("https://places.googleapis.com/v1/places:searchText",{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":"places.displayName,places.id,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.currentOpeningHours.openNow"},body:JSON.stringify({textQuery:e.textQuery,searchAlongRouteParameters:{polyline:{encodedPolyline:r}},maxResultCount:o})});if(!s.ok){let n=await s.json().catch(()=>({}));throw new Error(n?.error?.message||`HTTP ${s.status}`)}return{places:((await s.json()).places||[]).map(n=>({name:n.displayName?.text||"",place_id:n.id||"",formatted_address:n.formattedAddress||"",location:{lat:n.location?.latitude||0,lng:n.location?.longitude||0},rating:n.rating||0,user_ratings_total:n.userRatingCount||0,open_now:n.currentOpeningHours?.openNow??null})),route:{distance:t.total_distance.text,duration:t.total_duration.text,polyline:r}}}catch(t){throw p.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",o,a){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,l;switch(r){case"forecast_daily":{let c=Math.min(Math.max(o||5,1),10);l=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(a||24,1),240);l=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:l=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(l);if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let n=await u.json();return r==="current"?{temperature:n.temperature,feelsLike:n.feelsLikeTemperature,humidity:n.relativeHumidity,wind:n.wind,conditions:n.weatherCondition?.description?.text||n.weatherCondition?.type,uvIndex:n.uvIndex,precipitation:n.precipitation,visibility:n.visibility,pressure:n.airPressure,cloudCover:n.cloudCover,isDayTime:n.isDaytime}:n}catch(s){throw p.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,o=!1){try{let a=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),o&&s.push("POLLUTANT_CONCENTRATION");let l={location:{latitude:e,longitude:t}};s.length>0&&(l.extraComputations=s);let u=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!u.ok){let g=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(g)}let n=await u.json(),c=n.indexes||[],d=c[0],m={dateTime:n.dateTime,regionCode:n.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(m.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),n.healthRecommendations&&(m.healthRecommendations=n.healthRecommendations),n.pollutants&&(m.pollutants=n.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),m}catch(a){throw p.error("Error in getAirQuality:",a),new Error(a.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let n of e.markers)r.push(`markers=${encodeURIComponent(n)}`);if(e.path)for(let n of e.path)r.push(`path=${encodeURIComponent(n)}`);let o=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(o.length>16384)throw new Error(`URL exceeds 16,384 character limit (${o.length}). Reduce markers or path points.`);let a=await fetch(o);if(!a.ok){let n=a.headers.get("content-type")||"";if(n.includes("application/json")||n.includes("text/")){let c=await a.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${a.status}`)}let s=await a.arrayBuffer(),l=Buffer.from(s);return{base64:l.toString("base64"),size:l.length,dimensions:t}}catch(t){throw p.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let o=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:o,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let l=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+l).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw p.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${f(o)}`)}}async getElevation(e){try{let t=e.map(a=>({lat:a.latitude,lng:a.longitude})),o=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(o.status!=="OK")throw new Error(`Failed to get elevation data with status: ${o.status}`);return o.results.map((a,s)=>({elevation:a.elevation,location:t[s]}))}catch(t){throw p.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw p.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let o=e?.periods;if(!Array.isArray(o)||o.length===0)return!1;let a=1440,s=a*7,{day:l,minutes:u}=this.getLocalTimeComponents(t),n=l*a+u,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},m=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),h=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(h)))return g*60+h};for(let i of o){let g=d(i?.openDay),h=d(i?.closeDay??i?.openDay),w=m(i?.openTime),N=m(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*a+w,b;h===void 0||N===void 0?b=v+a:b=h*a+N,b<=v&&(b+=s);let P=n;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var x=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),o=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(o=o.filter(a=>a.opening_hours?.open_now===!0)),e.minRating&&(o=o.filter(a=>(a.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:o.map(a=>({name:a.name,place_id:a.place_id,address:a.formatted_address,location:a.geometry.location,rating:a.rating,total_ratings:a.user_ratings_total,open_now:a.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e){try{let t=await this.newPlacesService.getPlaceDetails(e);return{success:!0,data:{name:t.name,address:t.formatted_address,location:t.geometry?.location,rating:t.rating,total_ratings:t.user_ratings_total,open_now:t.opening_hours?.open_now,phone:t.formatted_phone_number,website:t.website,price_level:t.price_level,reviews:t.reviews?.map(r=>({rating:r.rating,text:r.text,time:r.time,author_name:r.author_name}))}}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,a){try{let s=o?new Date(o):new Date,l=a?new Date(a):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,l)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",o,a){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,o,a)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,o){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,o)}}catch(a){return{success:!1,error:a instanceof Error?a.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,o=e.topN||3,a=await this.geocode(e.location);if(!a.success||!a.data)throw new Error(a.error||"Geocode failed");let{lat:s,lng:l}=a.data.location,u=[];for(let n of t){let c=await this.searchNearby({center:{value:`${s},${l}`,isCoordinates:!0},keyword:n,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,o),m=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);m.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}u.push({type:n,count:c.data.length,top:m})}return{success:!0,data:{location:{address:a.data.formatted_address,lat:s,lng:l},radius:r,categories:u}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let o=[];for(let n of r){let c=await this.geocode(n);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${n}`);o.push({originalName:n,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let a=o;if(e.optimize!==!1&&o.length>2){let n=await this.calculateDistanceMatrix(r,r,"driving");if(n.success&&n.data){let c=new Set([0]),d=[0],m=0;for(;c.size<o.length;){let i=-1,g=1/0;for(let h=0;h<o.length;h++){if(c.has(h))continue;let w=n.data.durations[m]?.[h]?.value??1/0;w<g&&(g=w,i=h)}if(i===-1)break;c.add(i),d.push(i),m=i}a=d.map(i=>o[i])}}let s=[],l=0,u=0;for(let n=0;n<a.length-1;n++){let c=await this.getDirections(a[n].originalName,a[n+1].originalName,t);c.success&&c.data?(l+=c.data.total_distance.value,u+=c.data.total_duration.value,s.push({from:a[n].originalName,to:a[n+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:a[n].originalName,to:a[n+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&o.length>2,stops:a.map(n=>`${n.originalName} (${n.address})`),legs:s,total_distance:`${(l/1e3).toFixed(1)} km`,total_duration:`${Math.round(u/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let o=r.data.slice(0,t),a=[];for(let s of o){let l=await this.getPlaceDetails(s.place_id);a.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:l.data?.phone,website:l.data?.website,price_level:l.data?.price_level})}if(e.userLocation&&a.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,l=o.map(n=>`${n.location.lat},${n.location.lng}`),u=await this.calculateDistanceMatrix([s],l,"driving");if(u.success&&u.data)for(let n=0;n<a.length;n++)a[n].distance=u.data.distances[0]?.[n]?.text,a[n].drive_time=u.data.durations[0]?.[n]?.text}return{success:!0,data:a}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var p={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,p as b,x as c};
1
+ import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw p.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}parseCoordinates(e){let t=e.split(",").map(r=>parseFloat(r.trim()));if(t.length!==2||isNaN(t[0])||isNaN(t[1]))throw new Error(`Invalid coordinate format: "${e}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:t[0],lng:t[1]}}async getLocation(e){return e.isCoordinates?this.parseCoordinates(e.value):this.geocodeAddress(e.value)}async geocode(e){try{let t=await this.geocodeAddress(e);return{location:{lat:t.lat,lng:t.lng},formatted_address:t.formatted_address||"",place_id:t.place_id||""}}catch(t){throw p.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${f(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let o=r.data.results[0];return{formatted_address:o.formatted_address,place_id:o.place_id,address_components:o.address_components}}catch(r){throw p.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${f(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let a=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(a.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${a.status}`);let s=[],l=[];return a.rows.forEach(u=>{let n=[],c=[];u.elements.forEach(d=>{d.status==="OK"?(n.push({value:d.distance.value,text:d.distance.text}),c.push({value:d.duration.value,text:d.duration.text})):(n.push(null),c.push(null))}),s.push(n),l.push(c)}),{distances:s,durations:l,origin_addresses:a.origin_addresses,destination_addresses:a.destination_addresses}}catch(o){throw p.error("Error in calculateDistanceMatrix:",o),new Error(`Failed to calculate distance matrix: ${f(o)}`)}}async getDirections(e,t,r="driving",o,a){try{let s;a&&(s=Math.floor(a.getTime()/1e3));let l;s||(o instanceof Date?l=Math.floor(o.getTime()/1e3):o?l=o:l="now");let n=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:l}})).data;if(n.status!=="OK")throw new Error(`Failed to get directions with status: ${n.status} (arrival_time: ${s}, departure_time: ${l}`);if(n.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let c=n.routes[0],d=c.legs[0],m=i=>{if(!i||typeof i.value!="number")return"";let g=new Date(i.value*1e3),h={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return i.time_zone&&typeof i.time_zone=="string"&&(h.timeZone=i.time_zone),g.toLocaleString(this.defaultLanguage.toString(),h)};return{routes:n.routes,summary:c.summary,total_distance:{value:d.distance.value,text:d.distance.text},total_duration:{value:d.duration.value,text:d.duration.text},arrival_time:m(d.arrival_time),departure_time:m(d.departure_time)}}catch(s){throw p.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${f(s)}`)}}async searchAlongRoute(e){try{let t=await this.getDirections(e.origin,e.destination,e.mode||"walking"),r=t.routes[0]?.overview_polyline?.points;if(!r)throw new Error("Could not get route polyline");let o=Math.min(e.maxResults||5,20),s=await fetch("https://places.googleapis.com/v1/places:searchText",{method:"POST",headers:{"Content-Type":"application/json","X-Goog-Api-Key":this.apiKey,"X-Goog-FieldMask":"places.displayName,places.id,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.currentOpeningHours.openNow"},body:JSON.stringify({textQuery:e.textQuery,searchAlongRouteParameters:{polyline:{encodedPolyline:r}},maxResultCount:o})});if(!s.ok){let n=await s.json().catch(()=>({}));throw new Error(n?.error?.message||`HTTP ${s.status}`)}return{places:((await s.json()).places||[]).map(n=>({name:n.displayName?.text||"",place_id:n.id||"",formatted_address:n.formattedAddress||"",location:{lat:n.location?.latitude||0,lng:n.location?.longitude||0},rating:n.rating||0,user_ratings_total:n.userRatingCount||0,open_now:n.currentOpeningHours?.openNow??null})),route:{distance:t.total_distance.text,duration:t.total_duration.text,polyline:r}}}catch(t){throw p.error("Error in searchAlongRoute:",t),new Error(t.message||"Failed to search along route")}}async getWeather(e,t,r="current",o,a){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,l;switch(r){case"forecast_daily":{let c=Math.min(Math.max(o||5,1),10);l=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${c}`;break}case"forecast_hourly":{let c=Math.min(Math.max(a||24,1),240);l=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${c}`;break}default:l=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let u=await fetch(l);if(!u.ok){let d=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw d.includes("not supported for this location")?new Error(`Weather data is not available for this location (${e}, ${t}). The Google Weather API has limited coverage \u2014 China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. Try a location in North America, Europe, or Oceania.`):new Error(d)}let n=await u.json();return r==="current"?{temperature:n.temperature,feelsLike:n.feelsLikeTemperature,humidity:n.relativeHumidity,wind:n.wind,conditions:n.weatherCondition?.description?.text||n.weatherCondition?.type,uvIndex:n.uvIndex,precipitation:n.precipitation,visibility:n.visibility,pressure:n.airPressure,cloudCover:n.cloudCover,isDayTime:n.isDaytime}:n}catch(s){throw p.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getAirQuality(e,t,r=!0,o=!1){try{let a=`https://airquality.googleapis.com/v1/currentConditions:lookup?key=${this.apiKey}`,s=[];r&&s.push("HEALTH_RECOMMENDATIONS"),o&&s.push("POLLUTANT_CONCENTRATION");let l={location:{latitude:e,longitude:t}};s.length>0&&(l.extraComputations=s);let u=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!u.ok){let g=(await u.json().catch(()=>({})))?.error?.message||`HTTP ${u.status}`;throw new Error(g)}let n=await u.json(),c=n.indexes||[],d=c[0],m={dateTime:n.dateTime,regionCode:n.regionCode,aqi:d?.aqi,category:d?.category,dominantPollutant:d?.dominantPollutant,color:d?.color};return c.length>1&&(m.indexes=c.map(i=>({code:i.code,displayName:i.displayName,aqi:i.aqi,category:i.category,dominantPollutant:i.dominantPollutant}))),n.healthRecommendations&&(m.healthRecommendations=n.healthRecommendations),n.pollutants&&(m.pollutants=n.pollutants.map(i=>({code:i.code,displayName:i.displayName,concentration:i.concentration,additionalInfo:i.additionalInfo}))),m}catch(a){throw p.error("Error in getAirQuality:",a),new Error(a.message||`Failed to get air quality for (${e}, ${t})`)}}async getStaticMap(e){try{let t=e.size||"600x400",r=[`key=${this.apiKey}`,`size=${t}`,`maptype=${e.maptype||"roadmap"}`];if(e.center&&r.push(`center=${encodeURIComponent(e.center)}`),e.zoom!==void 0&&r.push(`zoom=${e.zoom}`),e.markers)for(let n of e.markers)r.push(`markers=${encodeURIComponent(n)}`);if(e.path)for(let n of e.path)r.push(`path=${encodeURIComponent(n)}`);let o=`https://maps.googleapis.com/maps/api/staticmap?${r.join("&")}`;if(o.length>16384)throw new Error(`URL exceeds 16,384 character limit (${o.length}). Reduce markers or path points.`);let a=await fetch(o);if(!a.ok){let n=a.headers.get("content-type")||"";if(n.includes("application/json")||n.includes("text/")){let c=await a.text();throw new Error(`Static Maps API error: ${c}`)}throw new Error(`Static Maps API returned HTTP ${a.status}`)}let s=await a.arrayBuffer(),l=Buffer.from(s);return{base64:l.toString("base64"),size:l.length,dimensions:t}}catch(t){throw p.error("Error in getStaticMap:",t),new Error(t.message||"Failed to generate static map")}}async getTimezone(e,t,r){try{let o=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:o,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let l=(s.rawOffset+s.dstOffset)*1e3,u=new Date(o*1e3+l).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:u}}catch(o){throw p.error("Error in getTimezone:",o),new Error(`Failed to get timezone for (${e}, ${t}): ${f(o)}`)}}async getElevation(e){try{let t=e.map(a=>({lat:a.latitude,lng:a.longitude})),o=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(o.status!=="OK")throw new Error(`Failed to get elevation data with status: ${o.status}`);return o.results.map((a,s)=>({elevation:a.elevation,location:t[s]}))}catch(t){throw p.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${f(t)}`)}}};import{PlacesClient as $}from"@googlemaps/places";var _=class{constructor(e){this.defaultLanguage="en";this.placeFieldMask=["displayName","name","id","formattedAddress","location","utcOffsetMinutes","regularOpeningHours.periods","regularOpeningHours.weekdayDescriptions","currentOpeningHours.openNow","nationalPhoneNumber","websiteUri","priceLevel","rating","userRatingCount","reviews.rating","reviews.text","reviews.publishTime","reviews.authorAttribution.displayName","photos.heightPx","photos.widthPx","photos.name"].join(",");this.searchNearbyFieldMask=["places.displayName","places.name","places.id","places.formattedAddress","places.location","places.rating","places.userRatingCount","places.currentOpeningHours.openNow"].join(",");if(this.client=new $({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(o=>this.transformSearchResult(o))}catch(t){throw p.error("Error in searchText (New API):",t),new Error(`Failed to search places: ${this.extractErrorMessage(t)}`)}}async getPlaceDetails(e){try{let t=`places/${e}`,[r]=await this.client.getPlace({name:t,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(r)}catch(t){throw p.error("Error in getPlaceDetails (New API):",t),new Error(`Failed to get place details for ${e}: ${this.extractErrorMessage(t)}`)}}transformSearchResult(e){return{name:e.displayName?.text||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:{open_now:e.currentOpeningHours?.openNow??null}}}transformPlaceResponse(e){return{name:e.displayName?.text||e.name||"",place_id:this.extractLegacyPlaceId(e),formatted_address:e.formattedAddress||"",geometry:{location:{lat:e.location?.latitude||0,lng:e.location?.longitude||0}},rating:e.rating||0,user_ratings_total:e.userRatingCount||0,opening_hours:e.regularOpeningHours?{open_now:this.isCurrentlyOpen(e.regularOpeningHours,e.utcOffsetMinutes,e.currentOpeningHours),weekday_text:this.formatOpeningHours(e.regularOpeningHours)}:void 0,formatted_phone_number:e.nationalPhoneNumber||"",website:e.websiteUri||"",price_level:e.priceLevel||0,reviews:e.reviews?.map(t=>({rating:t.rating||0,text:t.text?.text||"",time:t.publishTime?.seconds||0,author_name:t.authorAttribution?.displayName||""}))||[],photos:e.photos?.map(t=>({photo_reference:t.name||"",height:t.heightPx||0,width:t.widthPx||0}))||[]}}extractLegacyPlaceId(e){let t=e?.name;if(typeof t=="string"&&t.startsWith("places/")){let r=t.substring(7);if(r)return r}return e?.id||""}isCurrentlyOpen(e,t,r){if(typeof r?.openNow=="boolean")return r.openNow;if(typeof e?.openNow=="boolean")return e.openNow;let o=e?.periods;if(!Array.isArray(o)||o.length===0)return!1;let a=1440,s=a*7,{day:l,minutes:u}=this.getLocalTimeComponents(t),n=l*a+u,c={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},d=i=>{if(typeof i=="number"&&i>=0&&i<=6)return i;if(typeof i=="string"){let g=i.toUpperCase();if(g in c)return c[g]}},m=i=>{if(!i)return;let g=typeof i.hours=="number"?i.hours:Number(i.hours??NaN),h=typeof i.minutes=="number"?i.minutes:Number(i.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(h)))return g*60+h};for(let i of o){let g=d(i?.openDay),h=d(i?.closeDay??i?.openDay),w=m(i?.openTime),N=m(i?.closeTime);if(g===void 0||w===void 0)continue;let v=g*a+w,b;h===void 0||N===void 0?b=v+a:b=h*a+N,b<=v&&(b+=s);let P=n;for(;P<v;)P+=s;if(P>=v&&P<b)return!0}return!1}getLocalTimeComponents(e){let t=new Date;if(typeof e=="number"&&Number.isFinite(e)){let r=new Date(t.getTime()+e*6e4);return{day:r.getUTCDay(),minutes:r.getUTCHours()*60+r.getUTCMinutes()}}return{day:t.getDay(),minutes:t.getHours()*60+t.getMinutes()}}formatOpeningHours(e){return e?.weekdayDescriptions||[]}extractErrorMessage(e){let t=e?.code,r=e?.message||e?.details;return t===7||t===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":t===8||t===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r||(e instanceof Error?e.message:String(e))}};var x=class{constructor(e){this.mapsTools=new E(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),o=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(o=o.filter(a=>a.opening_hours?.open_now===!0)),e.minRating&&(o=o.filter(a=>(a.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:o.map(a=>({name:a.name,place_id:a.place_id,address:a.formatted_address,location:a.geometry.location,rating:a.rating,total_ratings:a.user_ratings_total,open_now:a.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during search"}}}async searchText(e){try{return{success:!0,data:(await this.newPlacesService.searchText({textQuery:e.query,locationBias:e.locationBias?{lat:e.locationBias.latitude,lng:e.locationBias.longitude,radius:e.locationBias.radius}:void 0,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType})).map(r=>({name:r.name,place_id:r.place_id,address:r.formatted_address,location:r.geometry.location,rating:r.rating,total_ratings:r.user_ratings_total,open_now:r.opening_hours?.open_now}))}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during text search"}}}async getPlaceDetails(e){try{let t=await this.newPlacesService.getPlaceDetails(e);return{success:!0,data:{name:t.name,address:t.formatted_address,location:t.geometry?.location,rating:t.rating,total_ratings:t.user_ratings_total,open_now:t.opening_hours?.open_now,phone:t.formatted_phone_number,website:t.website,price_level:t.price_level,reviews:t.reviews?.map(r=>({rating:r.rating,text:r.text,time:r.time,author_name:r.author_name}))}}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting place details"}}}async geocode(e){try{return{success:!0,data:await this.mapsTools.geocode(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while geocoding address"}}}async reverseGeocode(e,t){try{return{success:!0,data:await this.mapsTools.reverseGeocode(e,t)}}catch(r){return{success:!1,error:r instanceof Error?r.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(e,t,r="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",o,a){try{let s=o?new Date(o):new Date,l=a?new Date(a):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,l)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",o,a){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,o,a)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}async getAirQuality(e,t,r,o){try{return{success:!0,data:await this.mapsTools.getAirQuality(e,t,r,o)}}catch(a){return{success:!1,error:a instanceof Error?a.message:"An error occurred while getting air quality"}}}async getStaticMap(e){try{return{success:!0,data:await this.mapsTools.getStaticMap(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while generating static map"}}}async searchAlongRoute(e){try{return{success:!0,data:await this.mapsTools.searchAlongRoute(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while searching along route"}}}async exploreArea(e){let t=e.types||["restaurant","cafe","attraction"],r=e.radius||1e3,o=e.topN||3,a=await this.geocode(e.location);if(!a.success||!a.data)throw new Error(a.error||"Geocode failed");let{lat:s,lng:l}=a.data.location,u=[];for(let n of t){let c=await this.searchNearby({center:{value:`${s},${l}`,isCoordinates:!0},keyword:n,radius:r});if(!c.success||!c.data)continue;let d=c.data.slice(0,o),m=[];for(let i of d){if(!i.place_id)continue;let g=await this.getPlaceDetails(i.place_id);m.push({name:i.name,address:i.address,rating:i.rating,total_ratings:i.total_ratings,open_now:i.open_now,phone:g.data?.phone,website:g.data?.website})}u.push({type:n,count:c.data.length,top:m})}return{success:!0,data:{location:{address:a.data.formatted_address,lat:s,lng:l},radius:r,categories:u}}}async planRoute(e){let t=e.mode||"driving",r=e.stops;if(r.length<2)throw new Error("Need at least 2 stops");let o=[];for(let n of r){let c=await this.geocode(n);if(!c.success||!c.data)throw new Error(`Failed to geocode: ${n}`);o.push({originalName:n,address:c.data.formatted_address,lat:c.data.location.lat,lng:c.data.location.lng})}let a=o;if(e.optimize!==!1&&o.length>2){let n=await this.calculateDistanceMatrix(r,r,"driving");if(n.success&&n.data){let c=new Set([0]),d=[0],m=0;for(;c.size<o.length;){let i=-1,g=1/0;for(let h=0;h<o.length;h++){if(c.has(h))continue;let w=n.data.durations[m]?.[h]?.value??1/0;w<g&&(g=w,i=h)}if(i===-1)break;c.add(i),d.push(i),m=i}a=d.map(i=>o[i])}}let s=[],l=0,u=0;for(let n=0;n<a.length-1;n++){let c=await this.getDirections(a[n].originalName,a[n+1].originalName,t);c.success&&c.data?(l+=c.data.total_distance.value,u+=c.data.total_duration.value,s.push({from:a[n].originalName,to:a[n+1].originalName,distance:c.data.total_distance.text,duration:c.data.total_duration.text})):s.push({from:a[n].originalName,to:a[n+1].originalName,distance:"unknown",duration:"unknown",note:c.error||"Directions unavailable for this segment"})}return{success:!0,data:{mode:t,optimized:e.optimize!==!1&&o.length>2,stops:a.map(n=>`${n.originalName} (${n.address})`),legs:s,total_distance:`${(l/1e3).toFixed(1)} km`,total_duration:`${Math.round(u/60)} min`}}}async comparePlaces(e){let t=e.limit||5,r=await this.searchText({query:e.query});if(!r.success||!r.data)throw new Error(r.error||"Search failed");let o=r.data.slice(0,t),a=[];for(let s of o){let l=await this.getPlaceDetails(s.place_id);a.push({name:s.name,address:s.address,rating:s.rating,total_ratings:s.total_ratings,open_now:s.open_now,phone:l.data?.phone,website:l.data?.website,price_level:l.data?.price_level})}if(e.userLocation&&a.length>0){let s=`${e.userLocation.latitude},${e.userLocation.longitude}`,l=o.map(n=>`${n.location.lat},${n.location.lng}`),u=await this.calculateDistanceMatrix([s],l,"driving");if(u.success&&u.data)for(let n=0;n<a.length;n++)a[n].distance=u.data.distances[0]?.[n]?.text,a[n].drive_time=u.data.durations[0]?.[n]?.text}return{success:!0,data:a}}async getElevation(e){try{return{success:!0,data:await this.mapsTools.getElevation(e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred while getting elevation data"}}}};var p={log:(...y)=>{console.error("[INFO]",...y)},error:(...y)=>{console.error("[ERROR]",...y)}};export{_ as a,x as b,p as c};
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-5KQOCTQX.js";import{config as ie}from"dotenv";import{resolve as X}from"path";import Kt from"yargs";import{hideBin as Ht}from"yargs/helpers";import{z as E}from"zod";import{AsyncLocalStorage as de}from"async_hooks";var re=new de;function n(){return re.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function V(r,e){return re.run(r,e)}var ue="maps_search_nearby",me="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.",ge={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 ye(r){try{let e=n(),t=await new s(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:ue,DESCRIPTION:me,SCHEMA:ge,ACTION:ye};import{z as fe}from"zod";var he="maps_place_details",Se="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, opening hours, and photos. Returns everything needed to evaluate or contact a business.",Ee={placeId:fe.string().describe("Google Maps place ID")};async function be(r){try{let e=n(),t=await new s(e).getPlaceDetails(r.placeId);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:he,DESCRIPTION:Se,SCHEMA:Ee,ACTION:be};import{z as Pe}from"zod";var xe="maps_geocode",Ae="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.",ve={address:Pe.string().describe("Address or place name to convert to coordinates")};async function Me(r){try{let e=n(),t=await new s(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:xe,DESCRIPTION:Ae,SCHEMA:ve,ACTION:Me};import{z as oe}from"zod";var Ce="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.",Oe={latitude:oe.number().describe("Latitude coordinate"),longitude:oe.number().describe("Longitude coordinate")};async function Ie(r){try{let e=n(),t=await new s(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:Ce,DESCRIPTION:Ne,SCHEMA:Oe,ACTION:Ie};import{z as C}from"zod";var we="maps_distance_matrix",Te="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.",_e={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 Re(r){try{let e=n(),t=await new s(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 N={NAME:we,DESCRIPTION:Te,SCHEMA:_e,ACTION:Re};import{z as O}from"zod";var ze="maps_directions",ke="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",Ke={origin:O.string().describe("Starting point address or coordinates"),destination:O.string().describe("Destination address or coordinates"),mode:O.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:O.string().optional().describe("Departure time (ISO string format)"),arrival_time:O.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=n(),t=await new s(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:ze,DESCRIPTION:ke,SCHEMA:Ke,ACTION:He};import{z as Z}from"zod";var De="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.",Ge={locations:Z.array(Z.object({latitude:Z.number().describe("Latitude coordinate"),longitude:Z.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=n(),t=await new s(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:De,DESCRIPTION:$e,SCHEMA:Ge,ACTION:Je};import{z as f}from"zod";var Le="maps_search_places",qe="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.",je={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 Ue(r){try{let e=n(),t=await new s(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:Le,DESCRIPTION:qe,SCHEMA:je,ACTION:Ue};import{z as Q}from"zod";var Fe="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 Be(r){try{let e=n(),t=await new s(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:Fe,DESCRIPTION:Ze,SCHEMA:We,ACTION:Be};import{z as R}from"zod";var Ye="maps_weather",Ve="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.",Qe={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 Xe(r){try{let e=n(),t=await new s(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:Ye,DESCRIPTION:Ve,SCHEMA:Qe,ACTION:Xe};import{z as k}from"zod";var et="maps_explore_area",tt="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.",rt={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 ot(r){try{let e=n(),t=await new s(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 K={NAME:et,DESCRIPTION:tt,SCHEMA:rt,ACTION:ot};import{z as W}from"zod";var st="maps_plan_route",at="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.",nt={stops:W.array(W.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:W.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:W.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function it(r){try{let e=n(),t=await new s(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 H={NAME:st,DESCRIPTION:at,SCHEMA:nt,ACTION:it};import{z as D}from"zod";var ct="maps_compare_places",lt="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.",pt={query:D.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:D.object({latitude:D.number().describe("Your latitude"),longitude:D.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:D.number().optional().describe("Max places to compare (default: 5)")};async function dt(r){try{let e=n(),t=await new s(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 $={NAME:ct,DESCRIPTION:lt,SCHEMA:pt,ACTION:dt};import{z as B}from"zod";var ut="maps_air_quality",mt="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.).",gt={latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate"),includeHealthRecommendations:B.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:B.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function yt(r){try{let e=n(),t=await new s(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 G={NAME:ut,DESCRIPTION:mt,SCHEMA:gt,ACTION:yt};import{z as h}from"zod";var ft="maps_static_map",ht="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.",St={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 Et(r){try{let e=n(),t=await new s(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 J={NAME:ft,DESCRIPTION:ht,SCHEMA:St,ACTION:Et};import{z as se}from"zod";var bt="maps_batch_geocode",Pt="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.",xt={addresses:se.array(se.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function At(r){try{let e=n(),o=new s(e),t=r.addresses,c=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}}})),i=c.filter(p=>p.success).length,l=c.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:i,failed:l,results:c},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 L={NAME:bt,DESCRIPTION:Pt,SCHEMA:xt,ACTION:At};import{z as q}from"zod";var vt="maps_search_along_route",Mt="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Ct={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=n(),t=await new s(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:vt,DESCRIPTION:Mt,SCHEMA:Ct,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Ot=[{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:N.NAME,description:N.DESCRIPTION,schema:N.SCHEMA,annotations:d,action:r=>N.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:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:H.NAME,description:H.DESCRIPTION,schema:H.SCHEMA,annotations:d,action:r=>H.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.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)}]}],Y=Ot;import{McpServer as It}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as wt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Tt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as _t}from"@modelcontextprotocol/sdk/types.js";import ae from"express";import{randomUUID as Rt}from"crypto";import{z as zt}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 c=e.headers.authorization;if(c&&c.startsWith("Bearer "))return c.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 kt="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 It({name:this.serverName,version:kt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:zt.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,c,i)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,c,i),a.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ae();o.use(ae.json()),o.post("/mcp",async(c,i)=>{let l=c.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(c);if(a.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&_t(c.body)){let y=new Tt({sessionIdGenerator:()=>Rt(),onsessioninitialized:P=>{this.sessions[P]=p,a.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],a.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{i.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(c,i,c.body)})});let t=async(c,i)=>{let l=c.headers["mcp-session-id"];if(!l||!this.sessions[l]){i.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(c);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(c,i)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{a.log(`[${this.serverName}] HTTP server listening on port ${e}`),a.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new wt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){a.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){a.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}a.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let c=Object.values(this.sessions).map(i=>(i.transport.sessionId&&delete this.sessions[i.transport.sessionId],Promise.resolve()));Promise.all(c).then(()=>{a.log(`[${this.serverName}] All transports closed.`),e()}).catch(i=>{a.error(`[${this.serverName}] Error during bulk transport closing:`,i),o(i)})})})}};import{fileURLToPath as Dt}from"url";import{dirname as $t}from"path";import{readFileSync as ne,writeFileSync as Gt,existsSync as Jt}from"fs";import{createInterface as Lt}from"readline";var qt=Dt(import.meta.url),ce=$t(qt);ie({path:X(process.cwd(),".env")});ie({path:X(ce,"../.env")});async function jt(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),a.log("\u{1F680} Starting Google Maps MCP Server..."),a.log("\u{1F4CD} Available tools: search_nearby, get_place_details, maps_geocode, maps_reverse_geocode, maps_distance_matrix, maps_directions, maps_elevation, echo"),a.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),a.log("");let o=Y.map(async t=>{let c=process.env[t.portEnvVar];if(!c){a.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),a.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),a.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let i=Number(c);if(isNaN(i)||i<=0){a.error(`\u274C [${t.name}] Invalid port number "${c}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,t.tools);a.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${i}...`),await l.startHttpServer(i),a.log(`\u2705 [${t.name}] MCP Server started successfully!`),a.log(` \u{1F310} Endpoint: http://localhost:${i}/mcp`),a.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){a.error(`\u274C [${t.name}] Failed to start MCP Server on port ${i}:`,l)}});await Promise.allSettled(o),a.log(""),a.log("\u{1F389} Server initialization completed!"),a.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var le=["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 Ut(r,e,o){let t=new s(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return t.getPlaceDetails(e.placeId);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let c=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}}})),i=c.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:i,failed:e.addresses.length-i,results:c}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${le.join(", ")}`)}}var Ft=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(Ft||Zt){let r="0.0.0";try{let e=X(ce,"../package.json");r=JSON.parse(ne(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: ${le.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 Ut(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=Lt({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=ne(e.input,"utf-8").split(`
4
- `).map(u=>u.trim()).filter(u=>u.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let t=new s(e.apikey),c=Math.min(Math.max(e.concurrency,1),50),i=[],l=0,p=async(u,b)=>{let m=[];for(let pe of u){let te=pe().then(()=>{m.splice(m.indexOf(te),1)});m.push(te),m.length>=b&&await Promise.race(m)}await Promise.all(m)},S=o.map((u,b)=>async()=>{try{let m=await t.geocode(u);i[b]={address:u,...m}}catch(m){i[b]={address:u,success:!1,error:m.message}}l++,e.output&&process.stderr.write(`\r ${l}/${o.length} geocoded`)});await p(S,c),e.output&&process.stderr.write(`
5
- `);let g=i.filter(u=>u.success).length,y=i.filter(u=>!u.success).length,ee={total:o.length,succeeded:g,failed:y,results:i},P=JSON.stringify(ee,null,2);e.output?(Gt(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),e.stdio?await new F(Y[0].name,Y[0].tools).startStdio():(a.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),a.log(" A Model Context Protocol server for Google Maps services"),a.log(""),e.apikey||(a.log("\u26A0\uFE0F Google Maps API Key not found!"),a.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),a.log("")),jt(e.port,e.apikey).catch(o=>{a.error("\u274C Failed to start server:",o),process.exit(1)}))}).version(r).alias("version","v").help().parse()}export{jt as startServer};
2
+ import{b as a,c as s}from"./chunk-72XPXAUJ.js";import{config as ce}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 ue}from"async_hooks";var oe=new ue;function i(){return oe.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function V(r,e){return oe.run(r,e)}var me="maps_search_nearby",ge="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.",ye={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 fe(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:me,DESCRIPTION:ge,SCHEMA:ye,ACTION:fe};import{z as he}from"zod";var Se="maps_place_details",Ee="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, opening hours, and photos. Returns everything needed to evaluate or contact a business.",be={placeId:he.string().describe("Google Maps place ID")};async function Pe(r){try{let e=i(),t=await new a(e).getPlaceDetails(r.placeId);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:Se,DESCRIPTION:Ee,SCHEMA:be,ACTION:Pe};import{z as xe}from"zod";var Ae="maps_geocode",ve="Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.",Me={address:xe.string().describe("Address or place name to convert to coordinates")};async function Ce(r){try{let e=i(),t=await new a(e).geocode(r.address);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:Ae,DESCRIPTION:ve,SCHEMA:Me,ACTION:Ce};import{z as se}from"zod";var Oe="maps_reverse_geocode",Ne="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",Ie={latitude:se.number().describe("Latitude coordinate"),longitude:se.number().describe("Longitude coordinate")};async function we(r){try{let e=i(),t=await new a(e).reverseGeocode(r.latitude,r.longitude);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:Oe,DESCRIPTION:Ne,SCHEMA:Ie,ACTION:we};import{z as C}from"zod";var Te="maps_distance_matrix",_e="Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.",Re={origins:C.array(C.string()).describe("List of origin addresses or coordinates"),destinations:C.array(C.string()).describe("List of destination addresses or coordinates"),mode:C.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ze(r){try{let e=i(),t=await new a(e).calculateDistanceMatrix(r.origins,r.destinations,r.mode);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var O={NAME:Te,DESCRIPTION:_e,SCHEMA:Re,ACTION:ze};import{z as N}from"zod";var ke="maps_directions",De="Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.",Ke={origin:N.string().describe("Starting point address or coordinates"),destination:N.string().describe("Destination address or coordinates"),mode:N.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:N.string().optional().describe("Departure time (ISO string format)"),arrival_time:N.string().optional().describe("Arrival time (ISO string format)")};async function He(r){try{let e=i(),t=await new a(e).getDirections(r.origin,r.destination,r.mode,r.departure_time,r.arrival_time);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var I={NAME:ke,DESCRIPTION:De,SCHEMA:Ke,ACTION:He};import{z as B}from"zod";var Ge="maps_elevation",$e="Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.",Le={locations:B.array(B.object({latitude:B.number().describe("Latitude coordinate"),longitude:B.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Je(r){try{let e=i(),t=await new a(e).getElevation(r.locations);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var w={NAME:Ge,DESCRIPTION:$e,SCHEMA:Le,ACTION:Je};import{z as f}from"zod";var qe="maps_search_places",je="Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Ue={query:f.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:f.object({latitude:f.number().describe("Latitude to bias results toward"),longitude:f.number().describe("Longitude to bias results toward"),radius:f.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:f.boolean().optional().describe("Only return places that are currently open"),minRating:f.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:f.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Fe(r){try{let e=i(),t=await new a(e).searchText({query:r.query,locationBias:r.locationBias,openNow:r.openNow,minRating:r.minRating,includedType:r.includedType});return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var T={NAME:qe,DESCRIPTION:je,SCHEMA:Ue,ACTION:Fe};import{z as Q}from"zod";var Be="maps_timezone",Ze="Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.",We={latitude:Q.number().describe("Latitude coordinate"),longitude:Q.number().describe("Longitude coordinate"),timestamp:Q.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ye(r){try{let e=i(),t=await new a(e).getTimezone(r.latitude,r.longitude,r.timestamp);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get timezone data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting timezone: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var _={NAME:Be,DESCRIPTION:Ze,SCHEMA:We,ACTION:Ye};import{z as R}from"zod";var Ve="maps_weather",Qe="Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.",Xe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate"),type:R.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:R.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:R.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function et(r){try{let e=i(),t=await new a(e).getWeather(r.latitude,r.longitude,r.type||"current",r.forecastDays,r.forecastHours);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get weather data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting weather: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var z={NAME:Ve,DESCRIPTION:Qe,SCHEMA:Xe,ACTION:et};import{z as k}from"zod";var tt="maps_explore_area",rt="Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.",ot={location:k.string().describe("Address or landmark to explore around"),types:k.array(k.string()).optional().describe("Place types to search (default: restaurant, cafe, attraction). Examples: hotel, bar, park, museum"),radius:k.number().optional().describe("Search radius in meters (default: 1000)"),topN:k.number().optional().describe("Number of top results per type to get details for (default: 3)")};async function st(r){try{let e=i(),t=await new a(e).exploreArea(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error exploring area: ${e.message}`}]}}}var D={NAME:tt,DESCRIPTION:rt,SCHEMA:ot,ACTION:st};import{z as Z}from"zod";var at="maps_plan_route",nt="Plan an optimized multi-stop route in one call \u2014 geocodes all stops, finds the most efficient visit order using distance-matrix, and returns step-by-step directions between each stop. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.",it={stops:Z.array(Z.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:Z.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:Z.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function ct(r){try{let e=i(),t=await new a(e).planRoute(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error planning route: ${e.message}`}]}}}var K={NAME:at,DESCRIPTION:nt,SCHEMA:it,ACTION:ct};import{z as H}from"zod";var lt="maps_compare_places",pt="Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.",dt={query:H.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),userLocation:H.object({latitude:H.number().describe("Your latitude"),longitude:H.number().describe("Your longitude")}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),limit:H.number().optional().describe("Max places to compare (default: 5)")};async function ut(r){try{let e=i(),t=await new a(e).comparePlaces(r);return{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error comparing places: ${e.message}`}]}}}var G={NAME:lt,DESCRIPTION:pt,SCHEMA:dt,ACTION:ut};import{z as W}from"zod";var mt="maps_air_quality",gt="Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).",yt={latitude:W.number().describe("Latitude coordinate"),longitude:W.number().describe("Longitude coordinate"),includeHealthRecommendations:W.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:W.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function ft(r){try{let e=i(),t=await new a(e).getAirQuality(r.latitude,r.longitude,r.includeHealthRecommendations,r.includePollutants);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to get air quality data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting air quality: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var $={NAME:mt,DESCRIPTION:gt,SCHEMA:yt,ACTION:ft};import{z as h}from"zod";var ht="maps_static_map",St="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",Et={center:h.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),zoom:h.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),size:h.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),maptype:h.enum(["roadmap","satellite","terrain","hybrid"]).optional().describe("Map style. Default: roadmap."),markers:h.array(h.string()).optional().describe('Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'),path:h.array(h.string()).optional().describe('Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.')};async function bt(r){try{let e=i(),t=await new a(e).getStaticMap(r);return t.success?{content:[{type:"image",data:t.data.base64,mimeType:"image/png"},{type:"text",text:`Map generated (${t.data.size} bytes, ${t.data.dimensions})`}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to generate static map"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error generating static map: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var L={NAME:ht,DESCRIPTION:St,SCHEMA:Et,ACTION:bt};import{z as ae}from"zod";var Pt="maps_batch_geocode",xt="Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.",At={addresses:ae.array(ae.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function vt(r){try{let e=i(),o=new a(e),t=r.addresses,n=await Promise.all(t.map(async p=>{try{let S=await o.geocode(p);return{address:p,...S}}catch(S){return{address:p,success:!1,error:S.message}}})),c=n.filter(p=>p.success).length,l=n.filter(p=>!p.success).length;return{content:[{type:"text",text:JSON.stringify({total:t.length,succeeded:c,failed:l,results:n},null,2)}],isError:!1}}catch(e){return{isError:!0,content:[{type:"text",text:`Error batch geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var J={NAME:Pt,DESCRIPTION:xt,SCHEMA:At,ACTION:vt};import{z as q}from"zod";var Mt="maps_search_along_route",Ct="Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.",Ot={textQuery:q.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),origin:q.string().describe("Route start point \u2014 address or landmark name"),destination:q.string().describe("Route end point \u2014 address or landmark name"),mode:q.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode for the route (default: walking)"),maxResults:q.number().optional().describe("Max results to return (default: 5, max: 20)")};async function Nt(r){try{let e=i(),t=await new a(e).searchAlongRoute(r);return t.success?{content:[{type:"text",text:JSON.stringify(t.data,null,2)}],isError:!1}:{content:[{type:"text",text:t.error||"Failed to search along route"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching along route: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var j={NAME:Mt,DESCRIPTION:Ct,SCHEMA:Ot,ACTION:Nt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},It=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:d,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:d,action:r=>A.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:d,action:r=>M.ACTION(r)},{name:O.NAME,description:O.DESCRIPTION,schema:O.SCHEMA,annotations:d,action:r=>O.ACTION(r)},{name:I.NAME,description:I.DESCRIPTION,schema:I.SCHEMA,annotations:d,action:r=>I.ACTION(r)},{name:w.NAME,description:w.DESCRIPTION,schema:w.SCHEMA,annotations:d,action:r=>w.ACTION(r)},{name:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)},{name:_.NAME,description:_.DESCRIPTION,schema:_.SCHEMA,annotations:d,action:r=>_.ACTION(r)},{name:z.NAME,description:z.DESCRIPTION,schema:z.SCHEMA,annotations:d,action:r=>z.ACTION(r)},{name:D.NAME,description:D.DESCRIPTION,schema:D.SCHEMA,annotations:d,action:r=>D.ACTION(r)},{name:K.NAME,description:K.DESCRIPTION,schema:K.SCHEMA,annotations:d,action:r=>K.ACTION(r)},{name:G.NAME,description:G.DESCRIPTION,schema:G.SCHEMA,annotations:d,action:r=>G.ACTION(r)},{name:$.NAME,description:$.DESCRIPTION,schema:$.SCHEMA,annotations:d,action:r=>$.ACTION(r)},{name:L.NAME,description:L.DESCRIPTION,schema:L.SCHEMA,annotations:d,action:r=>L.ACTION(r)},{name:J.NAME,description:J.DESCRIPTION,schema:J.SCHEMA,annotations:d,action:r=>J.ACTION(r)},{name:j.NAME,description:j.DESCRIPTION,schema:j.SCHEMA,annotations:d,action:r=>j.ACTION(r)}]}];function X(r){let e=process.env.GOOGLE_MAPS_ENABLED_TOOLS?.trim();if(!e||e==="*")return r;let o=new Set(e.split(",").map(n=>n.trim()).filter(Boolean)),t=r.filter(n=>o.has(n.name));return t.length===0?(s.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${r.map(n=>n.name).join(", ")}`),r):(s.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${t.length}/${r.length} tools active`),t)}var Y=It;import{McpServer as wt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as _t}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Rt}from"@modelcontextprotocol/sdk/types.js";import ne from"express";import{randomUUID as zt}from"crypto";import{z as kt}from"zod";var U=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Dt="0.0.1",F=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new wt({name:this.serverName,version:Dt},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:kt.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,c)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,c),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=ne();o.use(ne.json()),o.post("/mcp",async(n,c)=>{let l=n.headers["mcp-session-id"],p,g=U.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Rt(n.body)){let y=new _t({sessionIdGenerator:()=>zt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{c.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c,n.body)})});let t=async(n,c)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){c.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=U.getInstance().getApiKey(n);g&&(p.apiKey=g),await V({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,c)})};o.get("/mcp",t),o.delete("/mcp",t),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async startStdio(){let e=new Tt;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(c=>(c.transport.sessionId&&delete this.sessions[c.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(c=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,c),o(c)})})})}};import{fileURLToPath as Gt}from"url";import{dirname as $t}from"path";import{readFileSync as ie,writeFileSync as Lt,existsSync as Jt}from"fs";import{createInterface as qt}from"readline";var jt=Gt(import.meta.url),le=$t(jt);ce({path:ee(process.cwd(),".env")});ce({path:ee(le,"../.env")});async function Ut(r,e){r&&(process.env.MCP_SERVER_PORT=r.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} 17 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=Y.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let c=Number(n);if(isNaN(c)||c<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new F(t.name,X(t.tools));s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${c}...`),await l.startHttpServer(c),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${c}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${c}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var pe=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather","explore-area","plan-route","compare-places","air-quality","static-map","batch-geocode-tool","search-along-route"];async function Ft(r,e,o){let t=new a(o);switch(r){case"geocode":case"maps_geocode":return t.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return t.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":case"maps_search_nearby":return t.searchNearby(e);case"search-places":case"maps_search_places":return t.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":case"maps_place_details":return t.getPlaceDetails(e.placeId);case"directions":case"maps_directions":return t.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return t.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return t.getElevation(e.locations);case"timezone":case"maps_timezone":return t.getTimezone(e.latitude,e.longitude,e.timestamp);case"weather":case"maps_weather":return t.getWeather(e.latitude,e.longitude,e.type,e.forecastDays,e.forecastHours);case"explore-area":case"maps_explore_area":return t.exploreArea(e);case"plan-route":case"maps_plan_route":return t.planRoute(e);case"compare-places":case"maps_compare_places":return t.comparePlaces(e);case"air-quality":case"maps_air_quality":return t.getAirQuality(e.latitude,e.longitude,e.includeHealthRecommendations,e.includePollutants);case"static-map":case"maps_static_map":return t.getStaticMap(e);case"batch-geocode-tool":case"maps_batch_geocode":{let n=await Promise.all(e.addresses.map(async l=>{try{let p=await t.geocode(l);return{address:l,...p}}catch(p){return{address:l,success:!1,error:p.message}}})),c=n.filter(l=>l.success).length;return{success:!0,data:{total:e.addresses.length,succeeded:c,failed:e.addresses.length-c,results:n}}}case"search-along-route":case"maps_search_along_route":return t.searchAlongRoute(e);default:throw new Error(`Unknown tool: ${r}. Available: ${pe.join(", ")}`)}}var Bt=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),Zt=import.meta.url===`file://${process.argv[1]}`;if(Bt||Zt){let r="0.0.0";try{let e=ee(le,"../package.json");r=JSON.parse(ie(e,"utf-8")).version}catch{r="0.0.0"}Kt(Ht(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${pe.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},t=await Ft(e.tool,o,e.apikey);console.log(JSON.stringify(t,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(1)}}).command("batch-geocode","Geocode multiple addresses from a file (one address per line)",e=>e.option("input",{alias:"i",type:"string",describe:"Input file path (one address per line). Use - for stdin.",demandOption:!0}).option("output",{alias:"o",type:"string",describe:"Output file path (JSON). Defaults to stdout."}).option("concurrency",{alias:"c",type:"number",describe:"Max parallel requests",default:20}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0 batch-geocode -i addresses.txt","Geocode to stdout"],["$0 batch-geocode -i addresses.txt -o results.json","Geocode to file"],["cat addresses.txt | $0 batch-geocode -i -","Geocode from stdin"]]),async e=>{e.apikey||(console.error("Error: GOOGLE_MAPS_API_KEY not set. Use --apikey or set env var."),process.exit(1));let o;if(e.input==="-"){let u=qt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Jt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=ie(e.input,"utf-8").split(`
4
+ `).map(u=>u.trim()).filter(u=>u.length>0);o.length===0&&(console.error("Error: No addresses found in input."),process.exit(1));let t=new a(e.apikey),n=Math.min(Math.max(e.concurrency,1),50),c=[],l=0,p=async(u,b)=>{let m=[];for(let de of u){let re=de().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
+ `);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.js CHANGED
@@ -1 +1 @@
1
- import{a,b,c}from"./chunk-5KQOCTQX.js";export{b as Logger,a as NewPlacesService,c as PlacesSearcher};
1
+ import{a,b,c}from"./chunk-72XPXAUJ.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.38",
3
+ "version": "0.0.39",
4
4
  "mcpName": "io.github.cablate/google-map",
5
5
  "description": "Google Maps tools for AI agents — geocode, search, directions, elevation via MCP server or standalone CLI",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  name: google-maps
3
3
  description: Geospatial query capabilities — geocoding, nearby search, routing, place details, elevation. Trigger when the user mentions locations, addresses, coordinates, navigation, "what's nearby", "how to get there", distance/duration, or any question that inherently involves geographic information — even if they don't explicitly say "map". Update when new tools are added or tool parameters change.
4
4
  license: MIT
5
- version: 0.0.25
5
+ version: 0.0.38
6
6
  compatibility:
7
7
  - claude-code
8
8
  - cursor
@@ -40,39 +40,49 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
40
40
  ### Place Discovery
41
41
  | Tool | When to use | Example |
42
42
  |------|-------------|---------|
43
- | `geocode` | Have an address/landmark, need coordinates | "What are the coordinates of Tokyo Tower?" |
44
- | `reverse-geocode` | Have coordinates, need an address | "What's at 35.65, 139.74?" |
45
- | `search-nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" |
46
- | `search-places` | Natural language place search | "Best ramen in Tokyo" |
47
- | `place-details` | Have a place_id, need full info | "Opening hours and reviews for this restaurant?" |
48
- | `batch-geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" |
43
+ | `maps_geocode` | Have an address/landmark, need coordinates | "What are the coordinates of Tokyo Tower?" |
44
+ | `maps_reverse_geocode` | Have coordinates, need an address | "What's at 35.65, 139.74?" |
45
+ | `maps_search_nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" |
46
+ | `maps_search_places` | Natural language place search | "Best ramen in Tokyo" |
47
+ | `maps_place_details` | Have a place_id, need full info | "Opening hours and reviews for this restaurant?" |
48
+ | `maps_batch_geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" |
49
49
 
50
50
  ### Routing & Distance
51
51
  | Tool | When to use | Example |
52
52
  |------|-------------|---------|
53
- | `directions` | How to get from A to B | "Route from Taipei Main Station to the airport" |
54
- | `distance-matrix` | Compare distances across multiple points | "Which of these 3 hotels is closest to the airport?" |
55
- | `search-along-route` | Find places along a route (meals, stops) ranked by detour time | "Restaurants between Fushimi Inari and Kiyomizu-dera" |
53
+ | `maps_directions` | How to get from A to B | "Route from Taipei Main Station to the airport" |
54
+ | `maps_distance_matrix` | Compare distances across multiple points | "Which of these 3 hotels is closest to the airport?" |
55
+ | `maps_search_along_route` | Find places along a route (meals, stops) ranked by detour time | "Restaurants between Fushimi Inari and Kiyomizu-dera" |
56
56
 
57
57
  ### Environment
58
58
  | Tool | When to use | Example |
59
59
  |------|-------------|---------|
60
- | `elevation` | Query altitude | "Elevation profile along this hiking trail" |
61
- | `timezone` | Need local time at a destination | "What time is it in Tokyo?" |
62
- | `weather` | Weather at a location (current or forecast) | "What's the weather in Paris?" |
63
- | `air-quality` | AQI, pollutants, health recommendations | "Is the air safe for jogging?" |
60
+ | `maps_elevation` | Query altitude | "Elevation profile along this hiking trail" |
61
+ | `maps_timezone` | Need local time at a destination | "What time is it in Tokyo?" |
62
+ | `maps_weather` | Weather at a location (current or forecast) | "What's the weather in Paris?" |
63
+ | `maps_air_quality` | AQI, pollutants, health recommendations | "Is the air safe for jogging?" |
64
64
 
65
65
  ### Visualization
66
66
  | Tool | When to use | Example |
67
67
  |------|-------------|---------|
68
- | `static-map` | Show locations/routes on a map image | "Show me these places on a map" |
68
+ | `maps_static_map` | Show locations/routes on a map image | "Show me these places on a map" |
69
69
 
70
70
  ### Composite (one-call shortcuts)
71
71
  | Tool | When to use | Example |
72
72
  |------|-------------|---------|
73
- | `explore-area` | Overview of a neighborhood | "What's around Tokyo Tower?" |
74
- | `plan-route` | Multi-stop optimized itinerary | "Visit these 5 places efficiently" |
75
- | `compare-places` | Side-by-side comparison | "Which ramen shop near Shibuya?" |
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" |
75
+ | `maps_compare_places` | Side-by-side comparison | "Which ramen shop near Shibuya?" |
76
+
77
+ ---
78
+
79
+ ## Known API Limitations
80
+
81
+ | Tool | Limitation | Workaround |
82
+ |------|-----------|------------|
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 |
85
+ | `maps_air_quality` | Works globally including Japan (unlike weather) | — |
76
86
 
77
87
  ---
78
88
 
@@ -85,6 +95,8 @@ npx @cablate/mcp-google-map exec <tool> '<json_params>' [-k API_KEY]
85
95
  - **API Key**: `-k` flag or `GOOGLE_MAPS_API_KEY` environment variable
86
96
  - **Output**: JSON to stdout, errors to stderr
87
97
  - **Stateless**: each call is independent
98
+ - **Tool names**: CLI accepts both `maps_geocode` and `geocode` short forms
99
+ - **Tool filtering**: Set `GOOGLE_MAPS_ENABLED_TOOLS` env var to a comma-separated list of tool names to limit registered tools (reduces MCP client context usage). Omit or set to `*` for all tools.
88
100
 
89
101
  ---
90
102
 
@@ -1,11 +1,11 @@
1
1
  # Google Maps Tools - Parameter & Response Reference
2
2
 
3
- ## geocode
3
+ ## maps_geocode
4
4
 
5
5
  Convert an address or landmark name to GPS coordinates.
6
6
 
7
7
  ```bash
8
- exec geocode '{"address": "Tokyo Tower"}'
8
+ exec maps_geocode '{"address": "Tokyo Tower"}'
9
9
  ```
10
10
 
11
11
  | Param | Type | Required | Description |
@@ -26,12 +26,12 @@ Response:
26
26
 
27
27
  ---
28
28
 
29
- ## batch-geocode
29
+ ## maps_batch_geocode
30
30
 
31
31
  Geocode multiple addresses in one call (max 50).
32
32
 
33
33
  ```bash
34
- exec batch-geocode-tool '{"addresses": ["Tokyo Tower", "Eiffel Tower", "Statue of Liberty"]}'
34
+ exec maps_batch_geocode '{"addresses": ["Tokyo Tower", "Eiffel Tower", "Statue of Liberty"]}'
35
35
  ```
36
36
 
37
37
  | Param | Type | Required | Description |
@@ -53,12 +53,12 @@ Response:
53
53
 
54
54
  ---
55
55
 
56
- ## reverse-geocode
56
+ ## maps_reverse_geocode
57
57
 
58
58
  Convert GPS coordinates to a street address.
59
59
 
60
60
  ```bash
61
- exec reverse-geocode '{"latitude": 35.6586, "longitude": 139.7454}'
61
+ exec maps_reverse_geocode '{"latitude": 35.6586, "longitude": 139.7454}'
62
62
  ```
63
63
 
64
64
  | Param | Type | Required | Description |
@@ -80,12 +80,12 @@ Response:
80
80
 
81
81
  ---
82
82
 
83
- ## search-nearby
83
+ ## maps_search_nearby
84
84
 
85
85
  Find places near a location by type.
86
86
 
87
87
  ```bash
88
- exec search-nearby '{"center": {"value": "35.6586,139.7454", "isCoordinates": true}, "keyword": "restaurant", "radius": 500}'
88
+ exec maps_search_nearby '{"center": {"value": "35.6586,139.7454", "isCoordinates": true}, "keyword": "restaurant", "radius": 500}'
89
89
  ```
90
90
 
91
91
  | Param | Type | Required | Description |
@@ -100,12 +100,12 @@ Response: `{ success, location, data: [{ name, place_id, formatted_address, geom
100
100
 
101
101
  ---
102
102
 
103
- ## search-places
103
+ ## maps_search_places
104
104
 
105
- Free-text place search. More flexible than search-nearby.
105
+ Free-text place search. More flexible than maps_search_nearby.
106
106
 
107
107
  ```bash
108
- exec search-places '{"query": "ramen in Tokyo"}'
108
+ exec maps_search_places '{"query": "ramen in Tokyo"}'
109
109
  ```
110
110
 
111
111
  | Param | Type | Required | Description |
@@ -120,12 +120,12 @@ Response: `{ success, data: [{ name, place_id, address, location, rating, total_
120
120
 
121
121
  ---
122
122
 
123
- ## place-details
123
+ ## maps_place_details
124
124
 
125
125
  Get full details for a place by its place_id (from search results). Returns reviews, phone, website, hours, photos.
126
126
 
127
127
  ```bash
128
- exec place-details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg"}'
128
+ exec maps_place_details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg"}'
129
129
  ```
130
130
 
131
131
  | Param | Type | Required | Description |
@@ -134,12 +134,12 @@ exec place-details '{"placeId": "ChIJCewJkL2LGGAR3Qmk0vCTGkg"}'
134
134
 
135
135
  ---
136
136
 
137
- ## directions
137
+ ## maps_directions
138
138
 
139
139
  Get step-by-step navigation between two points.
140
140
 
141
141
  ```bash
142
- exec directions '{"origin": "Tokyo Tower", "destination": "Shibuya Station", "mode": "transit"}'
142
+ exec maps_directions '{"origin": "Tokyo Tower", "destination": "Shibuya Station", "mode": "transit"}'
143
143
  ```
144
144
 
145
145
  | Param | Type | Required | Description |
@@ -152,12 +152,14 @@ exec directions '{"origin": "Tokyo Tower", "destination": "Shibuya Station", "mo
152
152
 
153
153
  ---
154
154
 
155
- ## distance-matrix
155
+ ## maps_distance_matrix
156
156
 
157
157
  Calculate travel distances and times between multiple origins and destinations.
158
158
 
159
+ > **Known limitation:** Transit mode returns null in some regions (notably Japan). Fall back to `driving` or `walking` mode if transit returns no results.
160
+
159
161
  ```bash
160
- exec distance-matrix '{"origins": ["Tokyo Tower"], "destinations": ["Shibuya Station", "Shinjuku Station"], "mode": "driving"}'
162
+ exec maps_distance_matrix '{"origins": ["Tokyo Tower"], "destinations": ["Shibuya Station", "Shinjuku Station"], "mode": "driving"}'
161
163
  ```
162
164
 
163
165
  | Param | Type | Required | Description |
@@ -168,12 +170,12 @@ exec distance-matrix '{"origins": ["Tokyo Tower"], "destinations": ["Shibuya Sta
168
170
 
169
171
  ---
170
172
 
171
- ## elevation
173
+ ## maps_elevation
172
174
 
173
175
  Get elevation data for geographic coordinates.
174
176
 
175
177
  ```bash
176
- exec elevation '{"locations": [{"latitude": 35.6586, "longitude": 139.7454}]}'
178
+ exec maps_elevation '{"locations": [{"latitude": 35.6586, "longitude": 139.7454}]}'
177
179
  ```
178
180
 
179
181
  | Param | Type | Required | Description |
@@ -187,12 +189,12 @@ Response:
187
189
 
188
190
  ---
189
191
 
190
- ## timezone
192
+ ## maps_timezone
191
193
 
192
194
  Get timezone and local time for coordinates.
193
195
 
194
196
  ```bash
195
- exec timezone '{"latitude": 35.6586, "longitude": 139.7454}'
197
+ exec maps_timezone '{"latitude": 35.6586, "longitude": 139.7454}'
196
198
  ```
197
199
 
198
200
  | Param | Type | Required | Description |
@@ -208,13 +210,15 @@ Response:
208
210
 
209
211
  ---
210
212
 
211
- ## weather
213
+ ## maps_weather
214
+
215
+ Get current weather or forecast.
212
216
 
213
- Get current weather or forecast. Coverage: most regions, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unsupported.
217
+ > **Known limitation:** Unsupported regions: China, Japan, South Korea, Cuba, Iran, North Korea, Syria. For these regions, use web search as fallback. `maps_air_quality` works in these regions (different API).
214
218
 
215
219
  ```bash
216
- exec weather '{"latitude": 37.4220, "longitude": -122.0841}'
217
- exec weather '{"latitude": 37.4220, "longitude": -122.0841, "type": "forecast_daily", "forecastDays": 3}'
220
+ exec maps_weather '{"latitude": 37.4220, "longitude": -122.0841}'
221
+ exec maps_weather '{"latitude": 37.4220, "longitude": -122.0841, "type": "forecast_daily", "forecastDays": 3}'
218
222
  ```
219
223
 
220
224
  | Param | Type | Required | Description |
@@ -227,13 +231,13 @@ exec weather '{"latitude": 37.4220, "longitude": -122.0841, "type": "forecast_da
227
231
 
228
232
  ---
229
233
 
230
- ## air-quality
234
+ ## maps_air_quality
231
235
 
232
236
  Get air quality index, pollutant concentrations, and health recommendations for a location.
233
237
 
234
238
  ```bash
235
- exec air-quality '{"latitude": 35.6762, "longitude": 139.6503}'
236
- exec air-quality '{"latitude": 35.6762, "longitude": 139.6503, "includePollutants": true}'
239
+ exec maps_air_quality '{"latitude": 35.6762, "longitude": 139.6503}'
240
+ exec maps_air_quality '{"latitude": 35.6762, "longitude": 139.6503, "includePollutants": true}'
237
241
  ```
238
242
 
239
243
  | Param | Type | Required | Description |
@@ -261,18 +265,18 @@ Response:
261
265
  }
262
266
  ```
263
267
 
264
- Chaining: `geocode` → `air-quality` when the user gives an address instead of coordinates.
268
+ Chaining: `maps_geocode` → `maps_air_quality` when the user gives an address instead of coordinates.
265
269
 
266
270
  ---
267
271
 
268
- ## static-map
272
+ ## maps_static_map
269
273
 
270
274
  Generate a map image with markers, paths, or routes. Returns an inline PNG image.
271
275
 
272
276
  ```bash
273
- exec static-map '{"center": "Tokyo Tower", "zoom": 14}'
274
- exec static-map '{"markers": ["color:red|label:A|35.6586,139.7454", "color:blue|label:B|35.6595,139.7006"]}'
275
- exec static-map '{"markers": ["color:red|35.6586,139.7454"], "maptype": "satellite", "zoom": 16}'
277
+ exec maps_static_map '{"center": "Tokyo Tower", "zoom": 14}'
278
+ exec maps_static_map '{"markers": ["color:red|label:A|35.6586,139.7454", "color:blue|label:B|35.6595,139.7006"]}'
279
+ exec maps_static_map '{"markers": ["color:red|35.6586,139.7454"], "maptype": "satellite", "zoom": 16}'
276
280
  ```
277
281
 
278
282
  | Param | Type | Required | Description |
@@ -287,19 +291,19 @@ exec static-map '{"markers": ["color:red|35.6586,139.7454"], "maptype": "satelli
287
291
  Response: MCP image content (inline PNG) + size metadata.
288
292
 
289
293
  Chaining patterns:
290
- - `search-nearby` → `static-map` (mark found places on map)
291
- - `plan-route` / `directions` → `static-map` (draw the route with path + markers)
292
- - `explore-area` → `static-map` (visualize neighborhood search results)
293
- - `compare-places` → `static-map` (show compared places side by side)
294
+ - `maps_search_nearby` → `maps_static_map` (mark found places on map)
295
+ - `maps_plan_route` / `maps_directions` → `maps_static_map` (draw the route with path + markers)
296
+ - `maps_explore_area` → `maps_static_map` (visualize neighborhood search results)
297
+ - `maps_compare_places` → `maps_static_map` (show compared places side by side)
294
298
 
295
299
  ---
296
300
 
297
- ## search-along-route
301
+ ## maps_search_along_route
298
302
 
299
303
  Search for places along a route between two points. Results ranked by minimal detour time — perfect for finding meals, cafes, or attractions "on the way" between landmarks.
300
304
 
301
305
  ```bash
302
- exec search-along-route '{"textQuery": "restaurant", "origin": "Fushimi Inari, Kyoto", "destination": "Kiyomizu-dera, Kyoto", "mode": "walking"}'
306
+ exec maps_search_along_route '{"textQuery": "restaurant", "origin": "Fushimi Inari, Kyoto", "destination": "Kiyomizu-dera, Kyoto", "mode": "walking"}'
303
307
  ```
304
308
 
305
309
  | Param | Type | Required | Description |
@@ -324,12 +328,12 @@ Key for trip planning: use this between consecutive anchors to find **along-the-
324
328
 
325
329
  ---
326
330
 
327
- ## explore-area (composite)
331
+ ## maps_explore_area (composite)
328
332
 
329
333
  Explore a neighborhood in one call. Internally chains geocode → search-nearby (per type) → place-details (top N).
330
334
 
331
335
  ```bash
332
- exec explore-area '{"location": "Tokyo Tower", "types": ["restaurant", "cafe"], "topN": 2}'
336
+ exec maps_explore_area '{"location": "Tokyo Tower", "types": ["restaurant", "cafe"], "topN": 2}'
333
337
  ```
334
338
 
335
339
  | Param | Type | Required | Description |
@@ -341,12 +345,12 @@ exec explore-area '{"location": "Tokyo Tower", "types": ["restaurant", "cafe"],
341
345
 
342
346
  ---
343
347
 
344
- ## plan-route (composite)
348
+ ## maps_plan_route (composite)
345
349
 
346
350
  Plan an optimized multi-stop route. Internally chains geocode → distance-matrix → nearest-neighbor → directions.
347
351
 
348
352
  ```bash
349
- exec plan-route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Station", "Ueno Park"], "mode": "driving"}'
353
+ exec maps_plan_route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Station", "Ueno Park"], "mode": "driving"}'
350
354
  ```
351
355
 
352
356
  | Param | Type | Required | Description |
@@ -357,12 +361,12 @@ exec plan-route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Station"
357
361
 
358
362
  ---
359
363
 
360
- ## compare-places (composite)
364
+ ## maps_compare_places (composite)
361
365
 
362
366
  Compare places side-by-side. Internally chains search-places → place-details → distance-matrix.
363
367
 
364
368
  ```bash
365
- exec compare-places '{"query": "ramen near Shibuya", "limit": 3}'
369
+ exec maps_compare_places '{"query": "ramen near Shibuya", "limit": 3}'
366
370
  ```
367
371
 
368
372
  | Param | Type | Required | Description |
@@ -379,37 +383,37 @@ exec compare-places '{"query": "ramen near Shibuya", "limit": 3}'
379
383
 
380
384
  **Search → Details** — Find places, then get full info on the best ones.
381
385
  ```
382
- search-places {"query":"Michelin restaurants in Taipei"}
383
- place-details {"placeId":"ChIJ..."} ← use place_id from results
386
+ maps_search_places {"query":"Michelin restaurants in Taipei"}
387
+ maps_place_details {"placeId":"ChIJ..."} ← use place_id from results
384
388
  ```
385
389
 
386
390
  **Geocode → Nearby** — Turn a landmark into coordinates, then explore the area.
387
391
  ```
388
- geocode {"address":"Taipei 101"}
389
- search-nearby {"center":{"value":"25.033,121.564","isCoordinates":true},"keyword":"cafe","radius":500}
392
+ maps_geocode {"address":"Taipei 101"}
393
+ maps_search_nearby {"center":{"value":"25.033,121.564","isCoordinates":true},"keyword":"cafe","radius":500}
390
394
  ```
391
395
 
392
396
  **Multi-point Comparison** — Compare distances across multiple origins and destinations in one call.
393
397
  ```
394
- distance-matrix {"origins":["Taipei Main Station","Banqiao Station"],"destinations":["Taoyuan Airport","Songshan Airport"],"mode":"driving"}
398
+ maps_distance_matrix {"origins":["Taipei Main Station","Banqiao Station"],"destinations":["Taoyuan Airport","Songshan Airport"],"mode":"driving"}
395
399
  ```
396
400
 
397
401
  **Geocode → Air Quality** — Check air quality at a named location.
398
402
  ```
399
- geocode {"address":"Tokyo"}
400
- air-quality {"latitude":35.6762,"longitude":139.6503}
403
+ maps_geocode {"address":"Tokyo"}
404
+ maps_air_quality {"latitude":35.6762,"longitude":139.6503}
401
405
  ```
402
406
 
403
407
  **Search → Map** — Find places, then show them on a map.
404
408
  ```
405
- search-nearby {"center":{"value":"35.6586,139.7454","isCoordinates":true},"keyword":"cafe","radius":500}
406
- static-map {"markers":["color:red|label:1|lat1,lng1","color:red|label:2|lat2,lng2"]}
409
+ maps_search_nearby {"center":{"value":"35.6586,139.7454","isCoordinates":true},"keyword":"cafe","radius":500}
410
+ maps_static_map {"markers":["color:red|label:1|lat1,lng1","color:red|label:2|lat2,lng2"]}
407
411
  ```
408
412
 
409
413
  **Directions → Map** — Get a route, then visualize it.
410
414
  ```
411
- directions {"origin":"Tokyo Tower","destination":"Shibuya Station","mode":"walking"}
412
- static-map {"path":["color:0x4285F4|weight:4|lat1,lng1|lat2,lng2|..."],"markers":["color:green|label:A|origin","color:red|label:B|dest"]}
415
+ maps_directions {"origin":"Tokyo Tower","destination":"Shibuya Station","mode":"walking"}
416
+ maps_static_map {"path":["color:0x4285F4|weight:4|lat1,lng1|lat2,lng2|..."],"markers":["color:green|label:A|origin","color:red|label:B|dest"]}
413
417
  ```
414
418
 
415
419
  ---
@@ -425,16 +429,16 @@ This is the most common complex scenario. The goal is a time-ordered itinerary w
425
429
  > **Read `references/travel-planning.md` first** — it contains the full methodology, anti-patterns, and time budget guidelines.
426
430
 
427
431
  **Steps:**
428
- 1. `search-places` — Search "top attractions in {city}" → geographically diverse **anchor points**
432
+ 1. `maps_search_places` — Search "top attractions in {city}" → geographically diverse **anchor points**
429
433
  2. **Design arcs** — Group nearby anchors into same-day arcs. One direction per day (south→north).
430
- 3. `search-along-route` — Between each pair of anchors, find restaurants/cafes **along the walking route** (ranked by minimal detour)
431
- 4. `place-details` — Get ratings, hours for top candidates
432
- 5. `plan-route` — Validate each day's route. Use `optimize: false` (you already know the geographic order).
433
- 6. `weather` + `air-quality` — Adjust for conditions
434
- 7. `static-map` — **Always** visualize each day with numbered markers + path
434
+ 3. `maps_search_along_route` — Between each pair of anchors, find restaurants/cafes **along the walking route** (ranked by minimal detour)
435
+ 4. `maps_place_details` — Get ratings, hours for top candidates
436
+ 5. `maps_plan_route` — Validate each day's route. Use `optimize: false` (you already know the geographic order).
437
+ 6. `maps_weather` + `maps_air_quality` — Adjust for conditions. **Note:** `maps_weather` is unavailable in Japan/China/Korea — use web search fallback.
438
+ 7. `maps_static_map` — **Always** visualize each day with numbered markers + path
435
439
 
436
440
  **Key decisions:**
437
- - **Use `search-along-route` for meals and breaks** — not explore_area or search_nearby. Along-route results are on the path, not random nearby points.
441
+ - **Use `maps_search_along_route` for meals and breaks** — not maps_explore_area or maps_search_nearby. Along-route results are on the path, not random nearby points.
438
442
  - **Never backtrack**: stops progress in one direction per day.
439
443
  - Alternate activity types: temple → food → walk → shrine → cafe.
440
444
  - Budget 5-7 stops per day max. Major temples = 90-120 min.
@@ -443,20 +447,20 @@ This is the most common complex scenario. The goal is a time-ordered itinerary w
443
447
 
444
448
  **Example flow (Kyoto 2-day):**
445
449
  ```
446
- search_places("top attractions in Kyoto")
450
+ maps_search_places("top attractions in Kyoto")
447
451
  → Fushimi Inari(south), Kiyomizu(east), Kinkaku-ji(north), Arashiyama(west)
448
452
 
449
453
  Day 1 arc: south→center — Fushimi → Kiyomizu → Gion → Pontocho
450
454
  Day 2 arc: center→west — Nishiki → Nijo Castle → Arashiyama
451
455
 
452
- search_along_route("restaurant", "Fushimi Inari", "Kiyomizu-dera", "walking")
456
+ maps_search_along_route("restaurant", "Fushimi Inari", "Kiyomizu-dera", "walking")
453
457
  → finds lunch options ALONG the 4km route (not at endpoints)
454
458
 
455
- search_along_route("kaiseki restaurant", "Gion, Kyoto", "Arashiyama, Kyoto")
459
+ maps_search_along_route("kaiseki restaurant", "Gion, Kyoto", "Arashiyama, Kyoto")
456
460
  → finds dinner along the afternoon route
457
461
 
458
- plan_route(Day 1 stops, optimize:false) → static_map(Day 1)
459
- plan_route(Day 2 stops, optimize:false) → static_map(Day 2)
462
+ maps_plan_route(Day 1 stops, optimize:false) → maps_static_map(Day 1)
463
+ maps_plan_route(Day 2 stops, optimize:false) → maps_static_map(Day 2)
460
464
  ```
461
465
 
462
466
  **Example output:**
@@ -478,9 +482,9 @@ Day 1: South → Center arc
478
482
  User asks about places around a location. May or may not specify what type.
479
483
 
480
484
  **Steps:**
481
- 1. `geocode` — Resolve the location (skip if user gave coordinates)
482
- 2. `search-nearby` — Search with keyword + radius. Use `openNow: true` if the user implies "right now"
483
- 3. `place-details` — Get details for the top 3-5 results (ratings, reviews, hours)
485
+ 1. `maps_geocode` — Resolve the location (skip if user gave coordinates)
486
+ 2. `maps_search_nearby` — Search with keyword + radius. Use `openNow: true` if the user implies "right now"
487
+ 3. `maps_place_details` — Get details for the top 3-5 results (ratings, reviews, hours)
484
488
 
485
489
  **Key decisions:**
486
490
  - If no keyword specified, search multiple types: restaurant, cafe, attraction
@@ -494,9 +498,9 @@ User asks about places around a location. May or may not specify what type.
494
498
  User wants to compare travel options between two points.
495
499
 
496
500
  **Steps:**
497
- 1. `directions` with `mode: "driving"` — Get driving route
498
- 2. `directions` with `mode: "transit"` — Get transit route
499
- 3. `directions` with `mode: "walking"` — Get walking route (if distance < 5 km)
501
+ 1. `maps_directions` with `mode: "driving"` — Get driving route
502
+ 2. `maps_directions` with `mode: "transit"` — Get transit route
503
+ 3. `maps_directions` with `mode: "walking"` — Get walking route (if distance < 5 km)
500
504
 
501
505
  **Present as comparison table:**
502
506
  ```
@@ -514,15 +518,15 @@ User wants to compare travel options between two points.
514
518
  User wants to evaluate a location for living, working, or investing.
515
519
 
516
520
  **Steps:**
517
- 1. `geocode` — Resolve the address
518
- 2. `search-nearby` — Run multiple searches from the same center:
521
+ 1. `maps_geocode` — Resolve the address
522
+ 2. `maps_search_nearby` — Run multiple searches from the same center:
519
523
  - `keyword: "school"` radius 2000
520
524
  - `keyword: "hospital"` radius 3000
521
525
  - `keyword: "supermarket"` radius 1000
522
526
  - `keyword: "restaurant"` radius 500
523
527
  - `keyword: "park"` radius 1000
524
- 3. `distance-matrix` — Calculate commute time to important locations (office, airport, city center)
525
- 4. `elevation` — Check if the area is in a low-elevation flood zone
528
+ 3. `maps_distance_matrix` — Calculate commute time to important locations (office, airport, city center)
529
+ 4. `maps_elevation` — Check if the area is in a low-elevation flood zone
526
530
 
527
531
  **Present as scorecard:**
528
532
  ```
@@ -541,14 +545,14 @@ Elevation: 45m (not a flood risk)
541
545
  User has a list of places and wants the optimal visit order.
542
546
 
543
547
  **Steps:**
544
- 1. `geocode` — Resolve all addresses to coordinates
545
- 2. `distance-matrix` — Calculate NxN matrix (all origins × all destinations)
548
+ 1. `maps_geocode` — Resolve all addresses to coordinates
549
+ 2. `maps_distance_matrix` — Calculate NxN matrix (all origins × all destinations)
546
550
  3. Use the matrix to determine the nearest-neighbor route order
547
- 4. `directions` — Generate route for the final order (chain waypoints)
551
+ 4. `maps_directions` — Generate route for the final order (chain waypoints)
548
552
 
549
553
  **Key decisions:**
550
554
  - For ≤ 5 stops, nearest-neighbor heuristic is good enough
551
- - For the `directions` call, set origin = first stop, destination = last stop, and mention intermediate stops in conversation
555
+ - For the `maps_directions` call, set origin = first stop, destination = last stop, and mention intermediate stops in conversation
552
556
  - If the user says "return to start", plan a round trip
553
557
 
554
558
  ---
@@ -558,9 +562,9 @@ User has a list of places and wants the optimal visit order.
558
562
  User is choosing between specific places.
559
563
 
560
564
  **Steps:**
561
- 1. `search-places` — Find each place (or use place_id if already known)
562
- 2. `place-details` — Get full details for each candidate
563
- 3. `distance-matrix` — Calculate distance from user's location to each candidate
565
+ 1. `maps_search_places` — Find each place (or use place_id if already known)
566
+ 2. `maps_place_details` — Get full details for each candidate
567
+ 3. `maps_distance_matrix` — Calculate distance from user's location to each candidate
564
568
 
565
569
  **Present as comparison:**
566
570
  ```
@@ -578,13 +582,17 @@ User is choosing between specific places.
578
582
  User wants to find things along a route (gas stations, rest stops, food).
579
583
 
580
584
  **Steps:**
581
- 1. `directions` — Get the route first, extract key waypoints from the steps
582
- 2. `search-nearby` — Search near 2-3 midpoints along the route
583
- 3. `place-details` — Get details for top results at each midpoint
585
+ 1. `maps_search_along_route` — Search directly along the route (preferred results ranked by minimal detour time)
586
+ 2. `maps_place_details` — Get details for top results
587
+
588
+ **Fallback** (if maps_search_along_route is unavailable):
589
+ 1. `maps_directions` — Get the route first, extract key waypoints from the steps
590
+ 2. `maps_search_nearby` — Search near 2-3 midpoints along the route
591
+ 3. `maps_place_details` — Get details for top results at each midpoint
584
592
 
585
593
  **Key decisions:**
586
- - Extract waypoints at roughly equal intervals along the route
587
- - Use the `start_location` of route steps at ~1/3 and ~2/3 of the total distance
594
+ - Prefer `maps_search_along_route` it uses Google's Routes API to rank results by actual detour time, not just proximity
595
+ - If using the fallback, extract waypoints at roughly equal intervals along the route
588
596
  - Set `radius` based on road type: 1000m for highways, 500m for city streets
589
597
 
590
598
  ---
@@ -593,22 +601,10 @@ User wants to find things along a route (gas stations, rest stops, food).
593
601
 
594
602
  | User says... | Recipe | First tool |
595
603
  |-------------|--------|------------|
596
- | "Plan a trip / itinerary / day in X" | Trip Planning | `geocode` |
597
- | "What's near X / around X" | Local Discovery | `geocode` → `search-nearby` |
598
- | "How do I get to X" / "route from A to B" | Route Comparison | `directions` |
599
- | "Is X a good neighborhood" / "analyze this area" | Neighborhood Analysis | `geocode` |
600
- | "Visit A, B, C, D efficiently" | Multi-Stop Route | `geocode` → `distance-matrix` |
601
- | "Which X should I pick" / "compare these" | Place Comparison | `search-places` |
602
- | "Find gas stations on the way to X" | Along the Route | `directions` → `search-nearby` |
603
-
604
- ---
605
-
606
- ## Future Composite Tools (Planned)
607
-
608
- These high-frequency scenarios are candidates for single-call composite tools in a future version:
609
-
610
- | Composite Tool | What it would do | Replaces |
611
- |---------------|-----------------|----------|
612
- | `maps_explore_area` | geocode + multi-type search-nearby + place-details for top results | Recipe 2 (3-call → 1-call) |
613
- | `maps_plan_route` | geocode all stops + distance-matrix + directions in optimal order | Recipe 5 (4-call → 1-call) |
614
- | `maps_compare_places` | search + details + distance for N candidates | Recipe 6 (3-call → 1-call) |
604
+ | "Plan a trip / itinerary / day in X" | Trip Planning | `maps_search_places` |
605
+ | "What's near X / around X" | Local Discovery | `maps_geocode` → `maps_search_nearby` |
606
+ | "How do I get to X" / "route from A to B" | Route Comparison | `maps_directions` |
607
+ | "Is X a good neighborhood" / "analyze this area" | Neighborhood Analysis | `maps_geocode` |
608
+ | "Visit A, B, C, D efficiently" | Multi-Stop Route | `maps_geocode` → `maps_distance_matrix` |
609
+ | "Which X should I pick" / "compare these" | Place Comparison | `maps_search_places` |
610
+ | "Find gas stations on the way to X" | Along the Route | `maps_search_along_route` |
@@ -18,14 +18,16 @@ Human travel planners think in layers. Each layer constrains the next.
18
18
 
19
19
  ### Layer 1: Anchor Discovery
20
20
  **What:** Find 4-6 must-visit landmarks spread across the city.
21
- **Tool:** `search_places("top attractions in {city}")`
21
+ **Tool:** `maps_search_places("top attractions in {city}")`
22
22
  **Why it works:** Google's algorithm returns geographically diverse results. Kyoto → Fushimi(south), Kiyomizu(east), Kinkaku-ji(north), Arashiyama(west) — natural 8km×10km spread.
23
23
 
24
24
  ### Layer 2: Arc Design
25
25
  **What:** Connect anchors into one-directional arcs per day. Edge landmarks go at start/end of a day.
26
- **Tool:** `distance_matrix` between anchors to understand spatial relationships.
26
+ **Tool:** `maps_distance_matrix` between anchors to understand spatial relationships.
27
27
  **Rule:** Never backtrack. Each day sweeps one direction (south→north, east→west).
28
28
 
29
+ > **Known limitation:** `maps_distance_matrix` with transit mode returns null in some regions (notably Japan). Use `driving` or `walking` mode instead, or reason from coordinates directly.
30
+
29
31
  Example:
30
32
  ```
31
33
  Day 1: Fushimi(south) → Kiyomizu(east) → Gion → Pontocho(center) [south→center arc]
@@ -44,11 +46,11 @@ Day 2: Nishiki(center) → Nijo Castle → train → Arashiyama(west) [cente
44
46
 
45
47
  ### Layer 4: Along-Route Filling
46
48
  **What:** Between two anchors, find what's **on the way** — not at the destination.
47
- **Tool:** `search_along_route(textQuery, origin, destination)`
49
+ **Tool:** `maps_search_along_route(textQuery, origin, destination)`
48
50
  **This is the key differentiator.** Results are ranked by minimal detour time, not proximity to a point.
49
51
 
50
52
  ```
51
- search_along_route("restaurant", "Fushimi Inari, Kyoto", "Kiyomizu-dera, Kyoto", "walking")
53
+ maps_search_along_route("restaurant", "Fushimi Inari, Kyoto", "Kiyomizu-dera, Kyoto", "walking")
52
54
  → Finds restaurants ALONG the 4km walking route, not clustered at either end
53
55
  ```
54
56
 
@@ -56,22 +58,24 @@ search_along_route("restaurant", "Fushimi Inari, Kyoto", "Kiyomizu-dera, Kyoto",
56
58
  **What:** Meals appear where the traveler **will be** at mealtime, not where the "best restaurant" is.
57
59
  **Logic:**
58
60
  1. Estimate what time the traveler reaches each arc segment
59
- 2. Lunch (~12:00) → search_along_route("lunch restaurant", previous_stop, next_stop)
60
- 3. Dinner (~18:00) → search_nearby at the day's final area (no rush)
61
+ 2. Lunch (~12:00) → `maps_search_along_route("lunch restaurant", previous_stop, next_stop)`
62
+ 3. Dinner (~18:00) → `maps_search_nearby` at the day's final area (no rush)
61
63
 
62
64
  **Anti-pattern:** "I searched for the best restaurant in the whole city" → it's 3km off the route.
63
65
  **Correct:** "You'll be near Gion around noon — here are options along the way."
64
66
 
65
67
  ### Layer 6: Rhythm Validation
66
68
  **What:** Check the itinerary feels human, not robotic.
67
- **Tool:** `plan_route(stops, optimize: false)` to get actual times, `weather` for conditions.
69
+ **Tool:** `maps_plan_route(stops, optimize: false)` to get actual times, `maps_weather` for conditions.
68
70
  **Checklist:**
69
71
  - [ ] Not 5 temples in a row (alternate: temple → food → walk → shrine → cafe)
70
72
  - [ ] Major temples get 90-120 min, not 30 min
71
73
  - [ ] Walking per day < 10km (suggest transit for >2km gaps)
72
74
  - [ ] Lunch 11:30-13:00, dinner 17:30-19:30
73
75
  - [ ] 5-7 stops per day max (including meals)
74
- - [ ] Final stop: call `static_map` with markers + path to visualize
76
+ - [ ] Final stop: call `maps_static_map` with markers + path to visualize
77
+
78
+ > **Known limitation:** `maps_weather` is unsupported in Japan, China, South Korea, and several other regions. Use web search as fallback for weather in these areas. `maps_air_quality` works globally including Japan.
75
79
 
76
80
  ---
77
81
 
@@ -79,20 +83,20 @@ search_along_route("restaurant", "Fushimi Inari, Kyoto", "Kiyomizu-dera, Kyoto",
79
83
 
80
84
  ```
81
85
  Phase 1 — Skeleton (2-3 calls)
82
- search_places("top attractions in {city}") → anchors
83
- distance_matrix(all anchors) → spatial relationships
86
+ maps_search_places("top attractions in {city}") → anchors
87
+ maps_distance_matrix(all anchors) → spatial relationships
84
88
 
85
89
  Phase 2 — Arc + Fill (2-4 calls per day)
86
- search_along_route("restaurant", stop_A, stop_B) → along-route meals
87
- search_along_route("cafe", stop_B, stop_C) → along-route breaks
90
+ maps_search_along_route("restaurant", stop_A, stop_B) → along-route meals
91
+ maps_search_along_route("cafe", stop_B, stop_C) → along-route breaks
88
92
 
89
93
  Phase 3 — Environment (2 calls)
90
- weather(city coords) → rain → move indoor activities
91
- air_quality(city coords) → bad AQI → reduce outdoor time
94
+ maps_weather(city coords) → rain → move indoor activities
95
+ maps_air_quality(city coords) → bad AQI → reduce outdoor time
92
96
 
93
97
  Phase 4 — Validate + Visualize (2 calls per day)
94
- plan_route(day_stops, optimize: false) → verify times/distances
95
- static_map(markers + path) → map for each day
98
+ maps_plan_route(day_stops, optimize: false) → verify times/distances
99
+ maps_static_map(markers + path) → map for each day
96
100
 
97
101
  Total: ~12-16 calls for a 2-day trip
98
102
  ```
@@ -103,11 +107,11 @@ Total: ~12-16 calls for a 2-day trip
103
107
 
104
108
  | Anti-Pattern | Symptom | Fix |
105
109
  |-------------|---------|-----|
106
- | Single-point explosion | `explore_area("Kyoto")` → all within 1km | search_places for anchors first |
110
+ | Single-point explosion | `maps_explore_area("Kyoto")` → all within 1km | `maps_search_places` for anchors first |
107
111
  | Backtracking | east→west→east→west | One direction per day |
108
- | No along-route search | Meals at endpoints only | `search_along_route` between stops |
112
+ | No along-route search | Meals at endpoints only | `maps_search_along_route` between stops |
109
113
  | Distance-optimal ordering | Ignores time-of-day quality | AI assigns time slots before routing |
110
- | No map output | Text/JSON only | Always `static_map` after each day's route |
114
+ | No map output | Text/JSON only | Always `maps_static_map` after each day's route |
111
115
  | Over-scheduling | 12 stops in one day | Max 5-7 stops including meals |
112
116
  | Same-type clustering | 5 temples consecutively | Alternate activity types |
113
117