@cablate/mcp-google-map 0.0.37 → 0.0.38

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  Give your AI agent the ability to understand the physical world — geocode, route, search, and reason about locations.
8
8
 
9
- - **16 tools** — 13 atomic + 3 composite (explore-area, plan-route, compare-places)
9
+ - **17 tools** — 14 atomic + 3 composite (explore-area, plan-route, compare-places)
10
10
  - **3 modes** — stdio, StreamableHTTP, standalone exec CLI
11
11
  - **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/))
12
12
 
@@ -14,7 +14,7 @@ Give your AI agent the ability to understand the physical world — geocode, rou
14
14
 
15
15
  | | This project | [Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services) |
16
16
  |---|---|---|
17
- | Tools | **16** | 3 |
17
+ | Tools | **17** | 3 |
18
18
  | Geocoding | Yes | No |
19
19
  | Step-by-step directions | Yes | No |
20
20
  | Elevation | Yes | No |
@@ -63,6 +63,7 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
63
63
  | `maps_air_quality` | Get air quality index, pollutant concentrations, and health recommendations by demographic group. |
64
64
  | `maps_static_map` | Generate a map image with markers, paths, or routes — returned inline for the user to see directly. |
65
65
  | `maps_batch_geocode` | Geocode up to 50 addresses in one call — returns coordinates for each. |
66
+ | `maps_search_along_route` | Search for places along a route between two points — ranked by minimal detour time. |
66
67
  | **Composite Tools** | |
67
68
  | `maps_explore_area` | Explore what's around a location — searches multiple place types and gets details in one call. |
68
69
  | `maps_plan_route` | Plan an optimized multi-stop route — geocodes, finds best order, returns directions. |
@@ -116,7 +117,7 @@ Then configure your MCP client:
116
117
  ### Server Information
117
118
 
118
119
  - **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
119
- - **Tools**: 16 Google Maps tools (13 atomic + 3 composite)
120
+ - **Tools**: 16 Google Maps tools (14 atomic + 3 composite)
120
121
 
121
122
  ### CLI Exec Mode (Agent Skill)
122
123
 
@@ -127,7 +128,7 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
127
128
  npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
128
129
  ```
129
130
 
130
- All 16 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `explore-area`, `plan-route`, `compare-places`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
131
+ All 17 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `search-along-route`, `explore-area`, `plan-route`, `compare-places`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
131
132
 
132
133
  ### Batch Geocode
133
134
 
@@ -236,6 +237,7 @@ src/
236
237
  │ ├── airQuality.ts # maps_air_quality tool
237
238
  │ ├── staticMap.ts # maps_static_map tool
238
239
  │ ├── batchGeocode.ts # maps_batch_geocode tool
240
+ │ ├── searchAlongRoute.ts # maps_search_along_route tool
239
241
  │ ├── exploreArea.ts # maps_explore_area (composite)
240
242
  │ ├── planRoute.ts # maps_plan_route (composite)
241
243
  │ └── comparePlaces.ts # maps_compare_places (composite)
@@ -245,10 +247,18 @@ src/
245
247
  tests/
246
248
  └── smoke.test.ts # Smoke + E2E test suite
247
249
  skills/
248
- └── google-maps/
249
- ├── SKILL.md # Agent skill definition
250
+ ├── google-maps/ # Agent Skill — how to USE the tools
251
+ ├── SKILL.md # Tool map, recipes, invocation
252
+ │ └── references/
253
+ │ ├── tools-api.md # Tool parameters + scenario recipes
254
+ │ └── travel-planning.md # Travel planning methodology
255
+ └── project-docs/ # Project Skill — how to DEVELOP/MAINTAIN
256
+ ├── SKILL.md # Architecture overview + onboarding
250
257
  └── references/
251
- └── tools-api.md # Tool parameter reference
258
+ ├── architecture.md # System design, code map, 9-file checklist
259
+ ├── google-maps-api-guide.md # API endpoints, pricing, gotchas
260
+ ├── geo-domain-knowledge.md # GIS fundamentals, Japan context
261
+ └── decisions.md # 10 ADRs (design decisions + rationale)
252
262
  ```
253
263
 
254
264
  ## Tech Stack
@@ -0,0 +1 @@
1
+ import{Client as T,Language as R}from"@googlemaps/google-maps-services-js";import A from"dotenv";A.config();function f(y){let e=y?.response?.status,t=y?.response?.data?.error_message,r=y?.response?.data?.status;return e===403?"API key invalid or required API not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable the relevant API (Places, Geocoding, etc.)":e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":r==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":r==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":r==="REQUEST_DENIED"?`Request denied by Google Maps API. ${t||"Check your API key and enabled APIs."}`:r==="INVALID_REQUEST"?`Invalid request parameters. ${t||"Check your input values."}`:t?`${t} (HTTP ${e})`:y instanceof Error?y.message:String(y)}var E=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new T({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],o=r.geometry.location;return{lat:o.lat,lng:o.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw 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};
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{b as s,c as a}from"./chunk-TP4VNBCV.js";import{config as ae}from"dotenv";import{resolve as V}from"path";import wt from"yargs";import{hideBin as Tt}from"yargs/helpers";import{z as E}from"zod";import{AsyncLocalStorage as le}from"async_hooks";var ee=new le;function n(){return ee.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function B(r,e){return ee.run(r,e)}var pe="maps_search_nearby",de="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.",ue={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 me(r){try{let e=n(),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:pe,DESCRIPTION:de,SCHEMA:ue,ACTION:me};import{z as ye}from"zod";var ge="maps_place_details",fe="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.",he={placeId:ye.string().describe("Google Maps place ID")};async function Se(r){try{let e=n(),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:ge,DESCRIPTION:fe,SCHEMA:he,ACTION:Se};import{z as Ee}from"zod";var be="maps_geocode",Pe="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.",xe={address:Ee.string().describe("Address or place name to convert to coordinates")};async function Ae(r){try{let e=n(),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:be,DESCRIPTION:Pe,SCHEMA:xe,ACTION:Ae};import{z as te}from"zod";var ve="maps_reverse_geocode",Me="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.",Ce={latitude:te.number().describe("Latitude coordinate"),longitude:te.number().describe("Longitude coordinate")};async function Ne(r){try{let e=n(),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:ve,DESCRIPTION:Me,SCHEMA:Ce,ACTION:Ne};import{z as C}from"zod";var Oe="maps_distance_matrix",Ie="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.",we={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 Te(r){try{let e=n(),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 N={NAME:Oe,DESCRIPTION:Ie,SCHEMA:we,ACTION:Te};import{z as O}from"zod";var _e="maps_directions",Re="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.",ze={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 Ke(r){try{let e=n(),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:_e,DESCRIPTION:Re,SCHEMA:ze,ACTION:Ke};import{z as U}from"zod";var ke="maps_elevation",He="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.",De={locations:U.array(U.object({latitude:U.number().describe("Latitude coordinate"),longitude:U.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function $e(r){try{let e=n(),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:ke,DESCRIPTION:He,SCHEMA:De,ACTION:$e};import{z as f}from"zod";var Ge="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.",Le={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 qe(r){try{let e=n(),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:Ge,DESCRIPTION:Je,SCHEMA:Le,ACTION:qe};import{z as Y}from"zod";var je="maps_timezone",Ue="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.",Fe={latitude:Y.number().describe("Latitude coordinate"),longitude:Y.number().describe("Longitude coordinate"),timestamp:Y.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Ze(r){try{let e=n(),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:je,DESCRIPTION:Ue,SCHEMA:Fe,ACTION:Ze};import{z as R}from"zod";var We="maps_weather",Be="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.",Ye={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 Ve(r){try{let e=n(),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:We,DESCRIPTION:Be,SCHEMA:Ye,ACTION:Ve};import{z as K}from"zod";var Qe="maps_explore_area",Xe="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.",et={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 tt(r){try{let e=n(),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 k={NAME:Qe,DESCRIPTION:Xe,SCHEMA:et,ACTION:tt};import{z as F}from"zod";var rt="maps_plan_route",ot="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.",st={stops:F.array(F.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),mode:F.enum(["driving","walking","bicycling","transit"]).optional().describe("Travel mode (default: driving)"),optimize:F.boolean().optional().describe("Auto-optimize visit order by nearest-neighbor (default: true). Set false to keep original order.")};async function at(r){try{let e=n(),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 H={NAME:rt,DESCRIPTION:ot,SCHEMA:st,ACTION:at};import{z as D}from"zod";var nt="maps_compare_places",it="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.",ct={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 lt(r){try{let e=n(),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 $={NAME:nt,DESCRIPTION:it,SCHEMA:ct,ACTION:lt};import{z as Z}from"zod";var pt="maps_air_quality",dt="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.).",ut={latitude:Z.number().describe("Latitude coordinate"),longitude:Z.number().describe("Longitude coordinate"),includeHealthRecommendations:Z.boolean().optional().describe("Include health advice per demographic group (default: true)"),includePollutants:Z.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")};async function mt(r){try{let e=n(),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 G={NAME:pt,DESCRIPTION:dt,SCHEMA:ut,ACTION:mt};import{z as h}from"zod";var yt="maps_static_map",gt="Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. Use when the user says 'show me on a map', 'mark these locations', 'visualize the route', or after search/directions results to display them visually. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.",ft={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 ht(r){try{let e=n(),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 J={NAME:yt,DESCRIPTION:gt,SCHEMA:ft,ACTION:ht};import{z as re}from"zod";var St="maps_batch_geocode",Et="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.",bt={addresses:re.array(re.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")};async function Pt(r){try{let e=n(),o=new a(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:St,DESCRIPTION:Et,SCHEMA:bt,ACTION:Pt};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},xt=[{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)}]}],W=xt;import{McpServer as At}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as vt}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Mt}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Ct}from"@modelcontextprotocol/sdk/types.js";import oe from"express";import{randomUUID as Nt}from"crypto";import{z as Ot}from"zod";var q=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 It="0.0.1",j=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new At({name:this.serverName,version:It},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Ot.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),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=oe();o.use(oe.json()),o.post("/mcp",async(c,i)=>{let l=c.headers["mcp-session-id"],p,y=q.getInstance().getApiKey(c);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],y&&(p.apiKey=y);else if(!l&&Ct(c.body)){let g=new Mt({sessionIdGenerator:()=>Nt(),onsessioninitialized:P=>{this.sessions[P]=p,s.log(`[${this.serverName}] New session initialized: ${P}`)}});p={transport:g,apiKey:y},g.onclose=()=>{g.sessionId&&(delete this.sessions[g.sessionId],s.log(`[${this.serverName}] Session closed: ${g.sessionId}`))},await this.createMcpServer().connect(g)}else{i.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await B({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],y=q.getInstance().getApiKey(c);y&&(p.apiKey=y),await B({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,()=>{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 vt;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 c=Object.values(this.sessions).map(i=>(i.transport.sessionId&&delete this.sessions[i.transport.sessionId],Promise.resolve()));Promise.all(c).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(i=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,i),o(i)})})})}};import{fileURLToPath as _t}from"url";import{dirname as Rt}from"path";import{readFileSync as se,writeFileSync as zt,existsSync as Kt}from"fs";import{createInterface as kt}from"readline";var Ht=_t(import.meta.url),ne=Rt(Ht);ae({path:V(process.cwd(),".env")});ae({path:V(ne,"../.env")});async function Dt(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} Available tools: search_nearby, get_place_details, maps_geocode, maps_reverse_geocode, maps_distance_matrix, maps_directions, maps_elevation, echo"),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=W.map(async t=>{let c=process.env[t.portEnvVar];if(!c){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 i=Number(c);if(isNaN(i)||i<=0){s.error(`\u274C [${t.name}] Invalid port number "${c}" defined in ${t.portEnvVar}.`);return}try{let l=new j(t.name,t.tools);s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${i}...`),await l.startHttpServer(i),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${i}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${i}:`,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 ie=["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"];async function $t(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 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}}}default:throw new Error(`Unknown tool: ${r}. Available: ${ie.join(", ")}`)}}var Gt=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")),Jt=import.meta.url===`file://${process.argv[1]}`;if(Gt||Jt){let r="0.0.0";try{let e=V(ne,"../package.json");r=JSON.parse(se(e,"utf-8")).version}catch{r="0.0.0"}wt(Tt(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${ie.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 $t(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=kt({input:process.stdin});o=[];for await(let b of u){let m=b.trim();m&&o.push(m)}}else Kt(e.input)||(console.error(`Error: File not found: ${e.input}`),process.exit(1)),o=se(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),c=Math.min(Math.max(e.concurrency,1),50),i=[],l=0,p=async(u,b)=>{let m=[];for(let ce of u){let X=ce().then(()=>{m.splice(m.indexOf(X),1)});m.push(X),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 y=i.filter(u=>u.success).length,g=i.filter(u=>!u.success).length,Q={total:o.length,succeeded:y,failed:g,results:i},P=JSON.stringify(Q,null,2);e.output?(zt(e.output,P,"utf-8"),console.error(`Done: ${y}/${o.length} succeeded. Output: ${e.output}`)):console.log(P),process.exit(g>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 j(W[0].name,W[0].tools).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("")),Dt(e.port,e.apikey).catch(o=>{s.error("\u274C Failed to start server:",o),process.exit(1)}))}).version(r).alias("version","v").help().parse()}export{Dt as startServer};
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};
package/dist/index.d.ts CHANGED
@@ -138,6 +138,17 @@ declare class PlacesSearcher {
138
138
  markers?: string[];
139
139
  path?: string[];
140
140
  }): Promise<StaticMapResponse>;
141
+ searchAlongRoute(params: {
142
+ textQuery: string;
143
+ origin: string;
144
+ destination: string;
145
+ mode?: string;
146
+ maxResults?: number;
147
+ }): Promise<{
148
+ success: boolean;
149
+ error?: string;
150
+ data?: any;
151
+ }>;
141
152
  exploreArea(params: {
142
153
  location: string;
143
154
  types?: string[];
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{a,b,c}from"./chunk-TP4VNBCV.js";export{b as Logger,a as NewPlacesService,c as PlacesSearcher};
1
+ import{a,b,c}from"./chunk-5KQOCTQX.js";export{b as Logger,a as NewPlacesService,c as PlacesSearcher};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/mcp-google-map",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
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",
@@ -35,7 +35,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
35
35
 
36
36
  ## Tool Map
37
37
 
38
- 16 tools in five categories — pick by scenario:
38
+ 17 tools in five categories — pick by scenario:
39
39
 
40
40
  ### Place Discovery
41
41
  | Tool | When to use | Example |
@@ -52,6 +52,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
52
52
  |------|-------------|---------|
53
53
  | `directions` | How to get from A to B | "Route from Taipei Main Station to the airport" |
54
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" |
55
56
 
56
57
  ### Environment
57
58
  | Tool | When to use | Example |
@@ -102,3 +103,6 @@ npx @cablate/mcp-google-map exec <tool> '<json_params>' [-k API_KEY]
102
103
  | File | Content | When to read |
103
104
  |------|---------|--------------|
104
105
  | `references/tools-api.md` | Full parameter specs, response formats, 7 scenario recipes, and decision guide | When you need exact parameters, response shapes, or multi-tool workflow patterns |
106
+ | `references/travel-planning.md` | Travel planning methodology — 6-layer model, Search Along Route, anti-patterns | When planning multi-day trips — **read before Recipe 1** |
107
+
108
+ > For **project development** knowledge (architecture, API guide, GIS domain, design decisions), see `skills/project-docs/SKILL.md`.
@@ -294,6 +294,36 @@ Chaining patterns:
294
294
 
295
295
  ---
296
296
 
297
+ ## search-along-route
298
+
299
+ 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
+
301
+ ```bash
302
+ exec search-along-route '{"textQuery": "restaurant", "origin": "Fushimi Inari, Kyoto", "destination": "Kiyomizu-dera, Kyoto", "mode": "walking"}'
303
+ ```
304
+
305
+ | Param | Type | Required | Description |
306
+ |-------|------|----------|-------------|
307
+ | textQuery | string | yes | What to search for ("restaurant", "cafe", "temple") |
308
+ | origin | string | yes | Route start point |
309
+ | destination | string | yes | Route end point |
310
+ | mode | string | no | walking, driving, bicycling, transit (default: walking) |
311
+ | maxResults | number | no | Max results (default: 5, max: 20) |
312
+
313
+ Response:
314
+ ```json
315
+ {
316
+ "places": [
317
+ { "name": "SUSHI MATSUHIRO", "rating": 5.0, "location": { "lat": 34.968, "lng": 135.771 } }
318
+ ],
319
+ "route": { "distance": "4.0 km", "duration": "58 mins", "polyline": "..." }
320
+ }
321
+ ```
322
+
323
+ Key for trip planning: use this between consecutive anchors to find **along-the-way** stops instead of searching at endpoints.
324
+
325
+ ---
326
+
297
327
  ## explore-area (composite)
298
328
 
299
329
  Explore a neighborhood in one call. Internally chains geocode → search-nearby (per type) → place-details (top N).
@@ -392,23 +422,53 @@ Use these recipes when the user's question maps to a multi-step workflow. Think
392
422
 
393
423
  This is the most common complex scenario. The goal is a time-ordered itinerary with routes between stops.
394
424
 
425
+ > **Read `references/travel-planning.md` first** — it contains the full methodology, anti-patterns, and time budget guidelines.
426
+
395
427
  **Steps:**
396
- 1. `geocode` — Resolve all mentioned landmarks to coordinates
397
- 2. `search-nearby`Find restaurants/attractions near each landmark (use coordinates from step 1)
398
- 3. `place-details` — Get ratings, hours, reviews for top candidates (use place_id from step 2)
399
- 4. `distance-matrix` — Compare travel times between all candidate stops to find the optimal order
400
- 5. `directions` — Generate turn-by-turn routes between stops in the final order
428
+ 1. `search-places` — Search "top attractions in {city}" → geographically diverse **anchor points**
429
+ 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
401
435
 
402
436
  **Key decisions:**
403
- - If the user says "near X", use `search-nearby`. If they say "best Y in Z", use `search-places`.
404
- - Always check `opening_hours` from `place-details` before including in itinerary.
405
- - Use `distance-matrix` to order stops efficiently, THEN use `directions` for the final route.
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.
438
+ - **Never backtrack**: stops progress in one direction per day.
439
+ - Alternate activity types: temple food walk shrine cafe.
440
+ - Budget 5-7 stops per day max. Major temples = 90-120 min.
441
+ - Edge landmarks (geographically isolated) go at start or end of a day.
442
+ - **Always generate a map** for each day.
443
+
444
+ **Example flow (Kyoto 2-day):**
445
+ ```
446
+ search_places("top attractions in Kyoto")
447
+ → Fushimi Inari(south), Kiyomizu(east), Kinkaku-ji(north), Arashiyama(west)
448
+
449
+ Day 1 arc: south→center — Fushimi → Kiyomizu → Gion → Pontocho
450
+ Day 2 arc: center→west — Nishiki → Nijo Castle → Arashiyama
451
+
452
+ search_along_route("restaurant", "Fushimi Inari", "Kiyomizu-dera", "walking")
453
+ → finds lunch options ALONG the 4km route (not at endpoints)
454
+
455
+ search_along_route("kaiseki restaurant", "Gion, Kyoto", "Arashiyama, Kyoto")
456
+ → finds dinner along the afternoon route
457
+
458
+ plan_route(Day 1 stops, optimize:false) → static_map(Day 1)
459
+ plan_route(Day 2 stops, optimize:false) → static_map(Day 2)
460
+ ```
406
461
 
407
- **Example output shape:**
462
+ **Example output:**
408
463
  ```
409
- Morning: Tokyo Tower (9:00) 12 min walk Zojoji Temple (9:30)
410
- Lunch: Sushi Dai (11:30) ★4.6 2.1 km, 8 min by transit
411
- Afternoon: TeamLab (14:00) → Odaiba area
464
+ Day 1: SouthCenter arc
465
+ 08:30 Fushimi Inari (90 min) 25 min transit
466
+ 10:30 Kiyomizu-dera (90 min) → walk down Sannen-zaka
467
+ 12:30 [along-route find] Gion lunch ★4.7 (75 min)
468
+ 14:00 Yasaka Shrine (30 min) → 15 min walk
469
+ 14:45 Pontocho stroll + cafe (45 min)
470
+ 17:30 Dinner near Kawaramachi
471
+ [map with markers 1-6 and walking path]
412
472
  ```
413
473
 
414
474
  ---
@@ -0,0 +1,136 @@
1
+ # Travel Planning Best Practices
2
+
3
+ ## Core Principle
4
+
5
+ Real travel planning is **directional exploration**, not point collection.
6
+
7
+ A human plans: "Start south at Fushimi Inari, walk up to Kiyomizu-dera, eat lunch somewhere along the way in Gion, then head west to Arashiyama for evening."
8
+
9
+ An algorithm plans: "Here are the top 10 rated places. Optimizing shortest path..."
10
+
11
+ The difference is **arc thinking** — one direction per day, discovering things along the way.
12
+
13
+ ---
14
+
15
+ ## The 6-Layer Decision Model
16
+
17
+ Human travel planners think in layers. Each layer constrains the next.
18
+
19
+ ### Layer 1: Anchor Discovery
20
+ **What:** Find 4-6 must-visit landmarks spread across the city.
21
+ **Tool:** `search_places("top attractions in {city}")`
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
+
24
+ ### Layer 2: Arc Design
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.
27
+ **Rule:** Never backtrack. Each day sweeps one direction (south→north, east→west).
28
+
29
+ Example:
30
+ ```
31
+ Day 1: Fushimi(south) → Kiyomizu(east) → Gion → Pontocho(center) [south→center arc]
32
+ Day 2: Nishiki(center) → Nijo Castle → train → Arashiyama(west) [center→west arc]
33
+ ```
34
+
35
+ ### Layer 3: Time-Slot Matching
36
+ **What:** Assign anchors to times based on **experience quality**, not distance.
37
+ **Tool:** None — this is AI knowledge.
38
+ **Examples:**
39
+ - Fushimi Inari = **early morning** (fewer crowds, best light for photos, hiking needs fresh legs)
40
+ - Outdoor temples = **morning to afternoon** (natural light)
41
+ - Shopping districts = **afternoon** (all stores open)
42
+ - Scenic areas = **late afternoon** (golden hour light)
43
+ - Fine dining = **evening at the final stop** (no rush to next destination)
44
+
45
+ ### Layer 4: Along-Route Filling
46
+ **What:** Between two anchors, find what's **on the way** — not at the destination.
47
+ **Tool:** `search_along_route(textQuery, origin, destination)`
48
+ **This is the key differentiator.** Results are ranked by minimal detour time, not proximity to a point.
49
+
50
+ ```
51
+ search_along_route("restaurant", "Fushimi Inari, Kyoto", "Kiyomizu-dera, Kyoto", "walking")
52
+ → Finds restaurants ALONG the 4km walking route, not clustered at either end
53
+ ```
54
+
55
+ ### Layer 5: Meal Embedding
56
+ **What:** Meals appear where the traveler **will be** at mealtime, not where the "best restaurant" is.
57
+ **Logic:**
58
+ 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
+
62
+ **Anti-pattern:** "I searched for the best restaurant in the whole city" → it's 3km off the route.
63
+ **Correct:** "You'll be near Gion around noon — here are options along the way."
64
+
65
+ ### Layer 6: Rhythm Validation
66
+ **What:** Check the itinerary feels human, not robotic.
67
+ **Tool:** `plan_route(stops, optimize: false)` to get actual times, `weather` for conditions.
68
+ **Checklist:**
69
+ - [ ] Not 5 temples in a row (alternate: temple → food → walk → shrine → cafe)
70
+ - [ ] Major temples get 90-120 min, not 30 min
71
+ - [ ] Walking per day < 10km (suggest transit for >2km gaps)
72
+ - [ ] Lunch 11:30-13:00, dinner 17:30-19:30
73
+ - [ ] 5-7 stops per day max (including meals)
74
+ - [ ] Final stop: call `static_map` with markers + path to visualize
75
+
76
+ ---
77
+
78
+ ## Tool Call Sequence
79
+
80
+ ```
81
+ Phase 1 — Skeleton (2-3 calls)
82
+ search_places("top attractions in {city}") → anchors
83
+ distance_matrix(all anchors) → spatial relationships
84
+
85
+ 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
88
+
89
+ Phase 3 — Environment (2 calls)
90
+ weather(city coords) → rain → move indoor activities
91
+ air_quality(city coords) → bad AQI → reduce outdoor time
92
+
93
+ 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
96
+
97
+ Total: ~12-16 calls for a 2-day trip
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Anti-Patterns
103
+
104
+ | Anti-Pattern | Symptom | Fix |
105
+ |-------------|---------|-----|
106
+ | Single-point explosion | `explore_area("Kyoto")` → all within 1km | search_places for anchors first |
107
+ | Backtracking | east→west→east→west | One direction per day |
108
+ | No along-route search | Meals at endpoints only | `search_along_route` between stops |
109
+ | 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 |
111
+ | Over-scheduling | 12 stops in one day | Max 5-7 stops including meals |
112
+ | Same-type clustering | 5 temples consecutively | Alternate activity types |
113
+
114
+ ---
115
+
116
+ ## Time Budget
117
+
118
+ | Activity | Duration |
119
+ |----------|----------|
120
+ | Major temple/shrine | 60-120 min |
121
+ | Small shrine / photo spot | 15-30 min |
122
+ | Museum | 90-180 min |
123
+ | Market / shopping street | 60-90 min |
124
+ | Sit-down meal | 60-120 min |
125
+ | Quick meal / street food | 20-40 min |
126
+ | Cafe break | 30-45 min |
127
+ | Walking <1km | 10-15 min |
128
+ | Transit between areas | 20-40 min |
129
+
130
+ ---
131
+
132
+ ## When to Read This
133
+
134
+ - User says "plan a trip", "create an itinerary", "plan X days in Y"
135
+ - Trip plan clusters all stops in one area
136
+ - Building multi-day travel content