@cablate/mcp-google-map 0.0.30 → 0.0.32
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 +38 -29
- package/dist/chunk-YHYT4LXI.js +1 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-RIT3FLYG.js +0 -1
package/README.md
CHANGED
|
@@ -1,47 +1,48 @@
|
|
|
1
|
-
[](https://www.npmjs.com/package/@cablate/mcp-google-map) [](./LICENSE) [](https://www.npmjs.com/package/@cablate/mcp-google-map)
|
|
2
2
|
|
|
3
|
-
<
|
|
4
|
-
<img width="380" height="200" src="https://glama.ai/mcp/servers/@cablate/mcp-google-map/badge" alt="Google Map Server MCP server" />
|
|
5
|
-
</a>
|
|
3
|
+
[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522google-maps%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540cablate%252Fmcp-google-map%2540latest%2522%252C%2522--stdio%2522%255D%252C%2522env%2522%253A%257B%2522GOOGLE_MAPS_API_KEY%2522%253A%2522YOUR_API_KEY%2522%257D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522google-maps%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540cablate%252Fmcp-google-map%2540latest%2522%252C%2522--stdio%2522%255D%252C%2522env%2522%253A%257B%2522GOOGLE_MAPS_API_KEY%2522%253A%2522YOUR_API_KEY%2522%257D%257D)
|
|
6
4
|
|
|
7
5
|
# MCP Google Map Server
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Give your AI agent the ability to understand the physical world — geocode, route, search, and reason about locations.
|
|
8
|
+
|
|
9
|
+
- **10 tools** — geocode, reverse-geocode, search-nearby, search-places, place-details, directions, distance-matrix, elevation, timezone, weather
|
|
10
|
+
- **3 modes** — stdio, StreamableHTTP, standalone exec CLI
|
|
11
|
+
- **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/))
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
>
|
|
13
|
-
> Google officially announced MCP support for Google Maps on December 10, 2025, introducing **[Maps Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services)** - a fully-managed MCP server for geospatial data and routing.
|
|
14
|
-
>
|
|
15
|
-
> This community project remains actively maintained as an alternative with different features and deployment options.
|
|
13
|
+
### vs Google Grounding Lite
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
| | This project | [Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services) |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Tools | **10** | 3 |
|
|
18
|
+
| Geocoding | Yes | No |
|
|
19
|
+
| Step-by-step directions | Yes | No |
|
|
20
|
+
| Elevation | Yes | No |
|
|
21
|
+
| Distance matrix | Yes | No |
|
|
22
|
+
| Place details | Yes | No |
|
|
23
|
+
| Timezone | Yes | No |
|
|
24
|
+
| Weather | Yes | Yes |
|
|
25
|
+
| Open source | MIT | No |
|
|
26
|
+
| Self-hosted | Yes | Google-managed only |
|
|
27
|
+
| Agent Skill | Yes | No |
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
### Quick Start
|
|
20
30
|
|
|
21
31
|
```bash
|
|
22
|
-
#
|
|
32
|
+
# stdio (Claude Desktop, Cursor, etc.)
|
|
33
|
+
npx @cablate/mcp-google-map --stdio
|
|
34
|
+
|
|
35
|
+
# exec CLI — no server needed
|
|
23
36
|
npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
|
|
24
|
-
npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
|
|
25
37
|
|
|
26
|
-
#
|
|
38
|
+
# HTTP server
|
|
27
39
|
npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
|
|
28
40
|
```
|
|
29
41
|
|
|
30
|
-
All 8 tools available in both modes. See [`skills/google-maps/`](./skills/google-maps/) for the full agent skill definition.
|
|
31
|
-
|
|
32
42
|
## Special Thanks
|
|
33
43
|
|
|
34
|
-
This project has received contributions from the community.
|
|
35
44
|
Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add support for `streamablehttp`.
|
|
36
45
|
|
|
37
|
-
## Verified Compatibility
|
|
38
|
-
|
|
39
|
-
This MCP server has been tested and verified with:
|
|
40
|
-
|
|
41
|
-
- Claude Desktop
|
|
42
|
-
- Dive Desktop
|
|
43
|
-
- MCP protocol implementations
|
|
44
|
-
|
|
45
46
|
## Available Tools
|
|
46
47
|
|
|
47
48
|
| Tool | Description |
|
|
@@ -54,6 +55,8 @@ This MCP server has been tested and verified with:
|
|
|
54
55
|
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. |
|
|
55
56
|
| `maps_directions` | Get step-by-step navigation between two points with route details. |
|
|
56
57
|
| `maps_elevation` | Get elevation (meters above sea level) for geographic coordinates. |
|
|
58
|
+
| `maps_timezone` | Get timezone ID, name, UTC/DST offsets, and local time for coordinates. |
|
|
59
|
+
| `maps_weather` | Get current weather conditions — temperature, humidity, wind, UV, precipitation. |
|
|
57
60
|
|
|
58
61
|
All tools are annotated with `readOnlyHint: true` and `destructiveHint: false` — MCP clients can auto-approve these without user confirmation.
|
|
59
62
|
|
|
@@ -103,7 +106,7 @@ Then configure your MCP client:
|
|
|
103
106
|
### Server Information
|
|
104
107
|
|
|
105
108
|
- **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
|
|
106
|
-
- **Tools**:
|
|
109
|
+
- **Tools**: 10 Google Maps tools
|
|
107
110
|
|
|
108
111
|
### CLI Exec Mode (Agent Skill)
|
|
109
112
|
|
|
@@ -114,7 +117,7 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
|
|
|
114
117
|
npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
All
|
|
120
|
+
All 10 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
|
|
118
121
|
|
|
119
122
|
### API Key Configuration
|
|
120
123
|
|
|
@@ -204,7 +207,9 @@ src/
|
|
|
204
207
|
│ ├── reverseGeocode.ts # maps_reverse_geocode tool
|
|
205
208
|
│ ├── distanceMatrix.ts # maps_distance_matrix tool
|
|
206
209
|
│ ├── directions.ts # maps_directions tool
|
|
207
|
-
│
|
|
210
|
+
│ ├── elevation.ts # maps_elevation tool
|
|
211
|
+
│ ├── timezone.ts # maps_timezone tool
|
|
212
|
+
│ └── weather.ts # maps_weather tool
|
|
208
213
|
└── utils/
|
|
209
214
|
├── apiKeyManager.ts # API key management
|
|
210
215
|
└── requestContext.ts # Per-request context (API key isolation)
|
|
@@ -259,4 +264,8 @@ Community participation and contributions are welcome!
|
|
|
259
264
|
|
|
260
265
|
## Star History
|
|
261
266
|
|
|
267
|
+
<a href="https://glama.ai/mcp/servers/@cablate/mcp-google-map">
|
|
268
|
+
<img width="380" height="200" src="https://glama.ai/mcp/servers/@cablate/mcp-google-map/badge" alt="Google Map Server MCP server" />
|
|
269
|
+
</a>
|
|
270
|
+
|
|
262
271
|
[](https://www.star-history.com/#cablate/mcp-google-map&Date)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Client as N,Language as A}from"@googlemaps/google-maps-services-js";import R from"dotenv";R.config();function p(d){let e=d?.response?.status,t=d?.response?.data?.error_message,r=d?.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})`:d instanceof Error?d.message:String(d)}var P=class{constructor(e){this.defaultLanguage=A.en;if(this.client=new N({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],n=r.geometry.location;return{lat:n.lat,lng:n.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw g.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${p(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 g.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${p(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let n=r.data.results[0];return{formatted_address:n.formatted_address,place_id:n.place_id,address_components:n.address_components}}catch(r){throw g.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${p(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=[],c=[];return a.rows.forEach(m=>{let i=[],l=[];m.elements.forEach(u=>{u.status==="OK"?(i.push({value:u.distance.value,text:u.distance.text}),l.push({value:u.duration.value,text:u.duration.text})):(i.push(null),l.push(null))}),s.push(i),c.push(l)}),{distances:s,durations:c,origin_addresses:a.origin_addresses,destination_addresses:a.destination_addresses}}catch(n){throw g.error("Error in calculateDistanceMatrix:",n),new Error(`Failed to calculate distance matrix: ${p(n)}`)}}async getDirections(e,t,r="driving",n,a){try{let s;a&&(s=Math.floor(a.getTime()/1e3));let c;s||(n instanceof Date?c=Math.floor(n.getTime()/1e3):n?c=n:c="now");let i=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:s,departure_time:c}})).data;if(i.status!=="OK")throw new Error(`Failed to get directions with status: ${i.status} (arrival_time: ${s}, departure_time: ${c}`);if(i.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let l=i.routes[0],u=l.legs[0],f=o=>{if(!o||typeof o.value!="number")return"";let h=new Date(o.value*1e3),y={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return o.time_zone&&typeof o.time_zone=="string"&&(y.timeZone=o.time_zone),h.toLocaleString(this.defaultLanguage.toString(),y)};return{routes:i.routes,summary:l.summary,total_distance:{value:u.distance.value,text:u.distance.text},total_duration:{value:u.duration.value,text:u.duration.text},arrival_time:f(u.arrival_time),departure_time:f(u.departure_time)}}catch(s){throw g.error("Error in getDirections:",s),new Error(`Failed to get directions from "${e}" to "${t}": ${p(s)}`)}}async getWeather(e,t,r="current",n,a){try{let s=`key=${this.apiKey}&location.latitude=${e}&location.longitude=${t}`,c;switch(r){case"forecast_daily":{let l=Math.min(Math.max(n||5,1),10);c=`https://weather.googleapis.com/v1/forecast/days:lookup?${s}&days=${l}`;break}case"forecast_hourly":{let l=Math.min(Math.max(a||24,1),240);c=`https://weather.googleapis.com/v1/forecast/hours:lookup?${s}&hours=${l}`;break}default:c=`https://weather.googleapis.com/v1/currentConditions:lookup?${s}`}let m=await fetch(c);if(!m.ok){let u=(await m.json().catch(()=>({})))?.error?.message||`HTTP ${m.status}`;throw u.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(u)}let i=await m.json();return r==="current"?{temperature:i.temperature,feelsLike:i.feelsLikeTemperature,humidity:i.relativeHumidity,wind:i.wind,conditions:i.weatherCondition?.description?.text||i.weatherCondition?.type,uvIndex:i.uvIndex,precipitation:i.precipitation,visibility:i.visibility,pressure:i.airPressure,cloudCover:i.cloudCover,isDayTime:i.isDaytime}:i}catch(s){throw g.error("Error in getWeather:",s),new Error(s.message||`Failed to get weather for (${e}, ${t})`)}}async getTimezone(e,t,r){try{let n=Math.floor(r?r/1e3:Date.now()/1e3),s=(await this.client.timezone({params:{location:{lat:e,lng:t},timestamp:n,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Timezone API returned status: ${s.status}`);let c=(s.rawOffset+s.dstOffset)*1e3,m=new Date(n*1e3+c).toISOString().replace("Z","");return{timeZoneId:s.timeZoneId,timeZoneName:s.timeZoneName,utcOffset:s.rawOffset,dstOffset:s.dstOffset,localTime:m}}catch(n){throw g.error("Error in getTimezone:",n),new Error(`Failed to get timezone for (${e}, ${t}): ${p(n)}`)}}async getElevation(e){try{let t=e.map(a=>({lat:a.latitude,lng:a.longitude})),n=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Failed to get elevation data with status: ${n.status}`);return n.results.map((a,s)=>({elevation:a.elevation,location:t[s]}))}catch(t){throw g.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${p(t)}`)}}};import{PlacesClient as D}from"@googlemaps/places";var b=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 D({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(n=>this.transformSearchResult(n))}catch(t){throw g.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(n=>this.transformSearchResult(n))}catch(t){throw g.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 g.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 n=e?.periods;if(!Array.isArray(n)||n.length===0)return!1;let a=1440,s=a*7,{day:c,minutes:m}=this.getLocalTimeComponents(t),i=c*a+m,l={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},u=o=>{if(typeof o=="number"&&o>=0&&o<=6)return o;if(typeof o=="string"){let h=o.toUpperCase();if(h in l)return l[h]}},f=o=>{if(!o)return;let h=typeof o.hours=="number"?o.hours:Number(o.hours??NaN),y=typeof o.minutes=="number"?o.minutes:Number(o.minutes??NaN);if(!(!Number.isFinite(h)||!Number.isFinite(y)))return h*60+y};for(let o of n){let h=u(o?.openDay),y=u(o?.closeDay??o?.openDay),T=f(o?.openTime),x=f(o?.closeTime);if(h===void 0||T===void 0)continue;let _=h*a+T,w;y===void 0||x===void 0?w=_+a:w=y*a+x,w<=_&&(w+=s);let v=i;for(;v<_;)v+=s;if(v>=_&&v<w)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 E=class{constructor(e){this.mapsTools=new P(e),this.newPlacesService=new b(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),n=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(n=n.filter(a=>a.opening_hours?.open_now===!0)),e.minRating&&(n=n.filter(a=>(a.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:n.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(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",n,a){try{let s=n?new Date(n):new Date,c=a?new Date(a):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,s,c)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting directions"}}}async getTimezone(e,t,r){try{return{success:!0,data:await this.mapsTools.getTimezone(e,t,r)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while getting timezone"}}}async getWeather(e,t,r="current",n,a){try{return{success:!0,data:await this.mapsTools.getWeather(e,t,r,n,a)}}catch(s){return{success:!1,error:s instanceof Error?s.message:"An error occurred while getting weather"}}}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 g={log:(...d)=>{console.error("[INFO]",...d)},error:(...d)=>{console.error("[ERROR]",...d)}};export{b as a,g as b,E as c};
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{b as s,c as
|
|
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 f={NAME:J,DESCRIPTION:L,SCHEMA:q,ACTION:j};import{z as U}from"zod";var V="get_place_details",F="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.",B={placeId:U.string().describe("Google Maps place ID")};async function W(r){try{let e=c(),t=await new i(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 h={NAME:V,DESCRIPTION:F,SCHEMA:B,ACTION:W};import{z as Y}from"zod";var Z="maps_geocode",X="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.",Q={address:Y.string().describe("Address or place name to convert to coordinates")};async function ee(r){try{let e=c(),t=await new i(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:Z,DESCRIPTION:X,SCHEMA:Q,ACTION:ee};import{z as K}from"zod";var te="maps_reverse_geocode",re="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:K.number().describe("Latitude coordinate"),longitude:K.number().describe("Longitude coordinate")};async function se(r){try{let e=c(),t=await new i(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 S={NAME:te,DESCRIPTION:re,SCHEMA:oe,ACTION:se};import{z as E}from"zod";var ne="maps_distance_matrix",ae="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.",ie={origins:E.array(E.string()).describe("List of origin addresses or coordinates"),destinations:E.array(E.string()).describe("List of destination addresses or coordinates"),mode:E.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ce(r){try{let e=c(),t=await new i(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 P={NAME:ne,DESCRIPTION:ae,SCHEMA:ie,ACTION:ce};import{z as b}from"zod";var le="maps_directions",pe="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.",de={origin:b.string().describe("Starting point address or coordinates"),destination:b.string().describe("Destination address or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:b.string().optional().describe("Departure time (ISO string format)"),arrival_time:b.string().optional().describe("Arrival time (ISO string format)")};async function ue(r){try{let e=c(),t=await new i(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 x={NAME:le,DESCRIPTION:pe,SCHEMA:de,ACTION:ue};import{z as O}from"zod";var me="maps_elevation",ge="Get elevation (height above sea level in meters) for one or more geographic coordinates. Use for terrain analysis, hiking/cycling route planning, or when the user asks about altitude at specific locations.",ye={locations:O.array(O.object({latitude:O.number().describe("Latitude coordinate"),longitude:O.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function fe(r){try{let e=c(),t=await new i(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 A={NAME:me,DESCRIPTION:ge,SCHEMA:ye,ACTION:fe};import{z as d}from"zod";var he="maps_search_places",ve="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.",Se={query:d.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:d.object({latitude:d.number().describe("Latitude to bias results toward"),longitude:d.number().describe("Longitude to bias results toward"),radius:d.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:d.boolean().optional().describe("Only return places that are currently open"),minRating:d.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:d.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Ee(r){try{let e=c(),t=await new i(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 M={NAME:he,DESCRIPTION:ve,SCHEMA:Se,ACTION:Ee};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Pe=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:u,action:r=>f.ACTION(r)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:u,action:r=>h.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:u,action:r=>v.ACTION(r)},{name:S.NAME,description:S.DESCRIPTION,schema:S.SCHEMA,annotations:u,action:r=>S.ACTION(r)},{name:P.NAME,description:P.DESCRIPTION,schema:P.SCHEMA,annotations:u,action:r=>P.ACTION(r)},{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:u,action:r=>x.ACTION(r)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:u,action:r=>A.ACTION(r)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:r=>M.ACTION(r)}]}],T=Pe;import{McpServer as be}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as xe}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as Ae}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Me}from"@modelcontextprotocol/sdk/types.js";import $ from"express";import{randomUUID as Ne}from"crypto";import{z as Ce}from"zod";var N=class r{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return r.instance||(r.instance=new r),r.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let t=e.headers["x-google-maps-api-key"];if(t)return t;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Oe="0.0.1",C=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new be({name:this.serverName,version:Oe},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Ce.object(o.schema),annotations:o.annotations},async t=>o.action(t))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(t,n,a)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,n,a),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=$();o.use($.json()),o.post("/mcp",async(n,a)=>{let l=n.headers["mcp-session-id"],p,g=N.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Me(n.body)){let y=new Ae({sessionIdGenerator:()=>Ne(),onsessioninitialized:w=>{this.sessions[w]=p,s.log(`[${this.serverName}] New session initialized: ${w}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{a.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await I({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,a,n.body)})});let t=async(n,a)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){a.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=N.getInstance().getApiKey(n);g&&(p.apiKey=g),await I({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,a)})};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 xe;await this.connect(e)}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(t=>{if(t){s.error(`[${this.serverName}] Error stopping HTTP server:`,t),o(t);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(a=>(a.transport.sessionId&&delete this.sessions[a.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(a=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,a),o(a)})})})}};import{fileURLToPath as _e}from"url";import{dirname as we}from"path";import{readFileSync as Re}from"fs";var Ke=_e(import.meta.url),G=we(Ke);D({path:_(process.cwd(),".env")});D({path:_(G,"../.env")});async function $e(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=T.map(async t=>{let n=process.env[t.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${t.name}] Port environment variable ${t.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${t.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${t.portEnvVar}=3000 or --port 3000`);return}let a=Number(n);if(isNaN(a)||a<=0){s.error(`\u274C [${t.name}] Invalid port number "${n}" defined in ${t.portEnvVar}.`);return}try{let l=new C(t.name,t.tools);s.log(`\u{1F527} [${t.name}] Initializing MCP Server in HTTP mode on port ${a}...`),await l.startHttpServer(a),s.log(`\u2705 [${t.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${a}/mcp`),s.log(` \u{1F4DA} Tools: ${t.tools.length} available`)}catch(l){s.error(`\u274C [${t.name}] Failed to start MCP Server on port ${a}:`,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 H=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation"];async function De(r,e,o){let t=new i(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":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":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);default:throw new Error(`Unknown tool: ${r}. Available: ${H.join(", ")}`)}}var Ge=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")),He=import.meta.url===`file://${process.argv[1]}`;if(Ge||He){let r="0.0.0";try{let e=_(G,"../package.json");r=JSON.parse(Re(e,"utf-8")).version}catch{r="0.0.0"}Te(Ie(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${H.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 De(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("$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 C(T[0].name,T[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("")),$e(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{$e as startServer};
|
|
2
|
+
import{b as s,c as n}from"./chunk-YHYT4LXI.js";import{config as k}from"dotenv";import{resolve as z}from"path";import Je from"yargs";import{hideBin as Le}from"yargs/helpers";import{z as m}from"zod";import{AsyncLocalStorage as j}from"async_hooks";var H=new j;function c(){return H.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function R(r,e){return H.run(r,e)}var U="search_nearby",W="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.",F={center:m.object({value:m.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:m.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:m.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:m.number().default(1e3).describe("Search radius in meters"),openNow:m.boolean().default(!1).describe("Only show places that are currently open"),minRating:m.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function V(r){try{let e=c(),t=await new n(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 f={NAME:U,DESCRIPTION:W,SCHEMA:F,ACTION:V};import{z as Z}from"zod";var B="get_place_details",Y="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.",X={placeId:Z.string().describe("Google Maps place ID")};async function Q(r){try{let e=c(),t=await new n(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 h={NAME:B,DESCRIPTION:Y,SCHEMA:X,ACTION:Q};import{z as ee}from"zod";var te="maps_geocode",re="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.",oe={address:ee.string().describe("Address or place name to convert to coordinates")};async function se(r){try{let e=c(),t=await new n(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 S={NAME:te,DESCRIPTION:re,SCHEMA:oe,ACTION:se};import{z as $}from"zod";var ne="maps_reverse_geocode",ae="Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.",ie={latitude:$.number().describe("Latitude coordinate"),longitude:$.number().describe("Longitude coordinate")};async function ce(r){try{let e=c(),t=await new n(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 v={NAME:ne,DESCRIPTION:ae,SCHEMA:ie,ACTION:ce};import{z as E}from"zod";var le="maps_distance_matrix",pe="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.",de={origins:E.array(E.string()).describe("List of origin addresses or coordinates"),destinations:E.array(E.string()).describe("List of destination addresses or coordinates"),mode:E.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ue(r){try{let e=c(),t=await new n(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 b={NAME:le,DESCRIPTION:pe,SCHEMA:de,ACTION:ue};import{z as P}from"zod";var me="maps_directions",ge="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.",ye={origin:P.string().describe("Starting point address or coordinates"),destination:P.string().describe("Destination address or coordinates"),mode:P.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:P.string().optional().describe("Departure time (ISO string format)"),arrival_time:P.string().optional().describe("Arrival time (ISO string format)")};async function fe(r){try{let e=c(),t=await new n(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 x={NAME:me,DESCRIPTION:ge,SCHEMA:ye,ACTION:fe};import{z as w}from"zod";var he="maps_elevation",Se="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.",ve={locations:w.array(w.object({latitude:w.number().describe("Latitude coordinate"),longitude:w.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function Ee(r){try{let e=c(),t=await new n(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 A={NAME:he,DESCRIPTION:Se,SCHEMA:ve,ACTION:Ee};import{z as u}from"zod";var be="maps_search_places",Pe="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.",xe={query:u.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:u.object({latitude:u.number().describe("Latitude to bias results toward"),longitude:u.number().describe("Longitude to bias results toward"),radius:u.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:u.boolean().optional().describe("Only return places that are currently open"),minRating:u.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:u.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Ae(r){try{let e=c(),t=await new n(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 M={NAME:be,DESCRIPTION:Pe,SCHEMA:xe,ACTION:Ae};import{z as K}from"zod";var Me="maps_timezone",Ne="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.",Ce={latitude:K.number().describe("Latitude coordinate"),longitude:K.number().describe("Longitude coordinate"),timestamp:K.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")};async function Te(r){try{let e=c(),t=await new n(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 N={NAME:Me,DESCRIPTION:Ne,SCHEMA:Ce,ACTION:Te};import{z as C}from"zod";var Oe="maps_weather",Ie="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.",we={latitude:C.number().describe("Latitude coordinate"),longitude:C.number().describe("Longitude coordinate"),type:C.enum(["current","forecast_daily","forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),forecastDays:C.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),forecastHours:C.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")};async function _e(r){try{let e=c(),t=await new n(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 T={NAME:Oe,DESCRIPTION:Ie,SCHEMA:we,ACTION:_e};var d={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Re=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:d,action:r=>f.ACTION(r)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:d,action:r=>h.ACTION(r)},{name:S.NAME,description:S.DESCRIPTION,schema:S.SCHEMA,annotations:d,action:r=>S.ACTION(r)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:d,action:r=>v.ACTION(r)},{name:b.NAME,description:b.DESCRIPTION,schema:b.SCHEMA,annotations:d,action:r=>b.ACTION(r)},{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: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:T.NAME,description:T.DESCRIPTION,schema:T.SCHEMA,annotations:d,action:r=>T.ACTION(r)}]}],_=Re;import{McpServer as Ke}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as ze}from"@modelcontextprotocol/sdk/server/stdio.js";import{StreamableHTTPServerTransport as De}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as He}from"@modelcontextprotocol/sdk/types.js";import G from"express";import{randomUUID as $e}from"crypto";import{z as Ge}from"zod";var O=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 a=e.headers.authorization;if(a&&a.startsWith("Bearer "))return a.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 ke="0.0.1",I=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Ke({name:this.serverName,version:ke},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Ge.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,a,i)=>typeof t=="string"&&!t.startsWith("{")?!0:o(t,a,i),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=G();o.use(G.json()),o.post("/mcp",async(a,i)=>{let l=a.headers["mcp-session-id"],p,g=O.getInstance().getApiKey(a);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&He(a.body)){let y=new De({sessionIdGenerator:()=>$e(),onsessioninitialized:D=>{this.sessions[D]=p,s.log(`[${this.serverName}] New session initialized: ${D}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{i.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await R({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(a,i,a.body)})});let t=async(a,i)=>{let l=a.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=O.getInstance().getApiKey(a);g&&(p.apiKey=g),await R({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(a,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 ze;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 a=Object.values(this.sessions).map(i=>(i.transport.sessionId&&delete this.sessions[i.transport.sessionId],Promise.resolve()));Promise.all(a).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 qe}from"url";import{dirname as je}from"path";import{readFileSync as Ue}from"fs";var We=qe(import.meta.url),J=je(We);k({path:z(process.cwd(),".env")});k({path:z(J,"../.env")});async function Fe(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=_.map(async t=>{let a=process.env[t.portEnvVar];if(!a){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(a);if(isNaN(i)||i<=0){s.error(`\u274C [${t.name}] Invalid port number "${a}" defined in ${t.portEnvVar}.`);return}try{let l=new I(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 L=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation","timezone","weather"];async function Ve(r,e,o){let t=new n(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":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":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);default:throw new Error(`Unknown tool: ${r}. Available: ${L.join(", ")}`)}}var Ze=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")),Be=import.meta.url===`file://${process.argv[1]}`;if(Ze||Be){let r="0.0.0";try{let e=z(J,"../package.json");r=JSON.parse(Ue(e,"utf-8")).version}catch{r="0.0.0"}Je(Le(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${L.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 Ve(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("$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 I(_[0].name,_[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("")),Fe(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{Fe as startServer};
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,22 @@ interface DirectionsResponse {
|
|
|
56
56
|
};
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
+
interface TimezoneResponse {
|
|
60
|
+
success: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
data?: {
|
|
63
|
+
timeZoneId: string;
|
|
64
|
+
timeZoneName: string;
|
|
65
|
+
utcOffset: number;
|
|
66
|
+
dstOffset: number;
|
|
67
|
+
localTime: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
interface WeatherResponse {
|
|
71
|
+
success: boolean;
|
|
72
|
+
error?: string;
|
|
73
|
+
data?: any;
|
|
74
|
+
}
|
|
59
75
|
interface ElevationResponse {
|
|
60
76
|
success: boolean;
|
|
61
77
|
error?: string;
|
|
@@ -97,6 +113,8 @@ declare class PlacesSearcher {
|
|
|
97
113
|
reverseGeocode(latitude: number, longitude: number): Promise<ReverseGeocodeResponse>;
|
|
98
114
|
calculateDistanceMatrix(origins: string[], destinations: string[], mode?: "driving" | "walking" | "bicycling" | "transit"): Promise<DistanceMatrixResponse>;
|
|
99
115
|
getDirections(origin: string, destination: string, mode?: "driving" | "walking" | "bicycling" | "transit", departure_time?: string, arrival_time?: string): Promise<DirectionsResponse>;
|
|
116
|
+
getTimezone(latitude: number, longitude: number, timestamp?: number): Promise<TimezoneResponse>;
|
|
117
|
+
getWeather(latitude: number, longitude: number, type?: "current" | "forecast_daily" | "forecast_hourly", forecastDays?: number, forecastHours?: number): Promise<WeatherResponse>;
|
|
100
118
|
getElevation(locations: Array<{
|
|
101
119
|
latitude: number;
|
|
102
120
|
longitude: number;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b,c}from"./chunk-
|
|
1
|
+
import{a,b,c}from"./chunk-YHYT4LXI.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.
|
|
3
|
+
"version": "0.0.32",
|
|
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",
|
package/dist/chunk-RIT3FLYG.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Client as A,Language as R}from"@googlemaps/google-maps-services-js";import T from"dotenv";T.config();function p(c){let e=c?.response?.status,t=c?.response?.data?.error_message,r=c?.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})`:c instanceof Error?c.message:String(c)}var P=class{constructor(e){this.defaultLanguage=R.en;if(this.client=new A({}),this.apiKey=e||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async geocodeAddress(e){try{let t=await this.client.geocode({params:{address:e,key:this.apiKey,language:this.defaultLanguage}});if(t.data.results.length===0)throw new Error(`No location found for address: "${e}"`);let r=t.data.results[0],n=r.geometry.location;return{lat:n.lat,lng:n.lng,formatted_address:r.formatted_address,place_id:r.place_id}}catch(t){throw d.error("Error in geocodeAddress:",t),new Error(`Failed to geocode address "${e}": ${p(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 d.error("Error in geocode:",t),new Error(`Failed to geocode address "${e}": ${p(t)}`)}}async reverseGeocode(e,t){try{let r=await this.client.reverseGeocode({params:{latlng:{lat:e,lng:t},language:this.defaultLanguage,key:this.apiKey}});if(r.data.results.length===0)throw new Error(`No address found for coordinates: (${e}, ${t})`);let n=r.data.results[0];return{formatted_address:n.formatted_address,place_id:n.place_id,address_components:n.address_components}}catch(r){throw d.error("Error in reverseGeocode:",r),new Error(`Failed to reverse geocode coordinates (${e}, ${t}): ${p(r)}`)}}async calculateDistanceMatrix(e,t,r="driving"){try{let s=(await this.client.distancematrix({params:{origins:e,destinations:t,mode:r,language:this.defaultLanguage,key:this.apiKey}})).data;if(s.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${s.status}`);let o=[],u=[];return s.rows.forEach(h=>{let l=[],m=[];h.elements.forEach(i=>{i.status==="OK"?(l.push({value:i.distance.value,text:i.distance.text}),m.push({value:i.duration.value,text:i.duration.text})):(l.push(null),m.push(null))}),o.push(l),u.push(m)}),{distances:o,durations:u,origin_addresses:s.origin_addresses,destination_addresses:s.destination_addresses}}catch(n){throw d.error("Error in calculateDistanceMatrix:",n),new Error(`Failed to calculate distance matrix: ${p(n)}`)}}async getDirections(e,t,r="driving",n,s){try{let o;s&&(o=Math.floor(s.getTime()/1e3));let u;o||(n instanceof Date?u=Math.floor(n.getTime()/1e3):n?u=n:u="now");let l=(await this.client.directions({params:{origin:e,destination:t,mode:r,language:this.defaultLanguage,key:this.apiKey,arrival_time:o,departure_time:u}})).data;if(l.status!=="OK")throw new Error(`Failed to get directions with status: ${l.status} (arrival_time: ${o}, departure_time: ${u}`);if(l.routes.length===0)throw new Error(`No route found from "${e}" to "${t}" with mode: ${r}`);let m=l.routes[0],i=m.legs[0],f=a=>{if(!a||typeof a.value!="number")return"";let g=new Date(a.value*1e3),y={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return a.time_zone&&typeof a.time_zone=="string"&&(y.timeZone=a.time_zone),g.toLocaleString(this.defaultLanguage.toString(),y)};return{routes:l.routes,summary:m.summary,total_distance:{value:i.distance.value,text:i.distance.text},total_duration:{value:i.duration.value,text:i.duration.text},arrival_time:f(i.arrival_time),departure_time:f(i.departure_time)}}catch(o){throw d.error("Error in getDirections:",o),new Error(`Failed to get directions from "${e}" to "${t}": ${p(o)}`)}}async getElevation(e){try{let t=e.map(s=>({lat:s.latitude,lng:s.longitude})),n=(await this.client.elevation({params:{locations:t,key:this.apiKey}})).data;if(n.status!=="OK")throw new Error(`Failed to get elevation data with status: ${n.status}`);return n.results.map((s,o)=>({elevation:s.elevation,location:t[o]}))}catch(t){throw d.error("Error in getElevation:",t),new Error(`Failed to get elevation data for ${e.length} location(s): ${p(t)}`)}}};import{PlacesClient as D}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 D({apiKey:e||process.env.GOOGLE_MAPS_API_KEY||""}),!e&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async searchNearby(e){try{let t={locationRestriction:{circle:{center:{latitude:e.location.lat,longitude:e.location.lng},radius:e.radius||1e3}},maxResultCount:Math.min(e.maxResultCount||20,20),languageCode:this.defaultLanguage};e.keyword&&(t.includedTypes=[e.keyword]);let[r]=await this.client.searchNearby(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(n=>this.transformSearchResult(n))}catch(t){throw d.error("Error in searchNearby (New API):",t),new Error(`Failed to search nearby places: ${this.extractErrorMessage(t)}`)}}async searchText(e){try{let t={textQuery:e.textQuery,languageCode:this.defaultLanguage,maxResultCount:Math.min(e.maxResultCount||10,20)};e.locationBias&&(t.locationBias={circle:{center:{latitude:e.locationBias.lat,longitude:e.locationBias.lng},radius:e.locationBias.radius||5e3}}),e.openNow&&(t.openNow=!0),e.minRating&&(t.minRating=e.minRating),e.includedType&&(t.includedType=e.includedType);let[r]=await this.client.searchText(t,{otherArgs:{headers:{"X-Goog-FieldMask":this.searchNearbyFieldMask}}});return(r.places||[]).map(n=>this.transformSearchResult(n))}catch(t){throw d.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 d.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 n=e?.periods;if(!Array.isArray(n)||n.length===0)return!1;let s=1440,o=s*7,{day:u,minutes:h}=this.getLocalTimeComponents(t),l=u*s+h,m={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},i=a=>{if(typeof a=="number"&&a>=0&&a<=6)return a;if(typeof a=="string"){let g=a.toUpperCase();if(g in m)return m[g]}},f=a=>{if(!a)return;let g=typeof a.hours=="number"?a.hours:Number(a.hours??NaN),y=typeof a.minutes=="number"?a.minutes:Number(a.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(y)))return g*60+y};for(let a of n){let g=i(a?.openDay),y=i(a?.closeDay??a?.openDay),x=f(a?.openTime),N=f(a?.closeTime);if(g===void 0||x===void 0)continue;let b=g*s+x,w;y===void 0||N===void 0?w=b+s:w=y*s+N,w<=b&&(w+=o);let v=l;for(;v<b;)v+=o;if(v>=b&&v<w)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 E=class{constructor(e){this.mapsTools=new P(e),this.newPlacesService=new _(e)}async searchNearby(e){try{let t=await this.mapsTools.getLocation(e.center),n=await this.newPlacesService.searchNearby({location:t,keyword:e.keyword,radius:e.radius});return e.openNow&&(n=n.filter(s=>s.opening_hours?.open_now===!0)),e.minRating&&(n=n.filter(s=>(s.rating||0)>=(e.minRating||0))),{location:t,success:!0,data:n.map(s=>({name:s.name,place_id:s.place_id,address:s.formatted_address,location:s.geometry.location,rating:s.rating,total_ratings:s.user_ratings_total,open_now:s.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(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while calculating distance matrix"}}}async getDirections(e,t,r="driving",n,s){try{let o=n?new Date(n):new Date,u=s?new Date(s):void 0;return{success:!0,data:await this.mapsTools.getDirections(e,t,r,o,u)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting directions"}}}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 d={log:(...c)=>{console.error("[INFO]",...c)},error:(...c)=>{console.error("[ERROR]",...c)}};export{_ as a,d as b,E as c};
|