@cablate/mcp-google-map 0.0.21 β 0.0.23
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 +73 -120
- package/dist/chunk-RIT3FLYG.js +1 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-Z5SWQKLS.js +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
>
|
|
11
|
+
> **Important Notice**
|
|
12
12
|
>
|
|
13
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
14
|
>
|
|
@@ -16,61 +16,41 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
A
|
|
19
|
+
A Model Context Protocol (MCP) server providing comprehensive Google Maps API integration with streamable HTTP transport support and multi-session capabilities.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Special Thanks
|
|
22
22
|
|
|
23
|
-
This project has received contributions from the community.
|
|
23
|
+
This project has received contributions from the community.
|
|
24
24
|
Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add support for `streamablehttp`.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Verified Compatibility
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
This MCP server has been tested and verified with:
|
|
29
29
|
|
|
30
30
|
- Claude Desktop
|
|
31
31
|
- Dive Desktop
|
|
32
32
|
- MCP protocol implementations
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
## Features
|
|
37
|
-
|
|
38
|
-
### π Latest Updates
|
|
39
|
-
|
|
40
|
-
- βΉοΈ **Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features.**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
### πΊοΈ Google Maps Integration
|
|
44
|
-
|
|
45
|
-
- **Location Search**
|
|
46
|
-
|
|
47
|
-
- Search for places near a specific location with customizable radius and filters
|
|
48
|
-
- Get detailed place information including ratings, opening hours, and contact details
|
|
49
|
-
|
|
50
|
-
- **Geocoding Services**
|
|
51
|
-
|
|
52
|
-
- Convert addresses to coordinates (geocoding)
|
|
53
|
-
- Convert coordinates to addresses (reverse geocoding)
|
|
54
|
-
|
|
55
|
-
- **Distance & Directions**
|
|
56
|
-
|
|
57
|
-
- Calculate distances and travel times between multiple origins and destinations
|
|
58
|
-
- Get detailed turn-by-turn directions between two points
|
|
59
|
-
- Support for different travel modes (driving, walking, bicycling, transit)
|
|
34
|
+
## Available Tools
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `search_nearby` | Find places near a location by type (restaurant, cafe, hotel, etc.). Supports filtering by radius, rating, and open status. |
|
|
39
|
+
| `maps_search_places` | Free-text place search (e.g., "sushi restaurants in Tokyo"). Supports location bias, rating, open-now filters. |
|
|
40
|
+
| `get_place_details` | Get full details for a place by its place_id β reviews, phone, website, hours, photos. |
|
|
41
|
+
| `maps_geocode` | Convert an address or landmark name into GPS coordinates. |
|
|
42
|
+
| `maps_reverse_geocode` | Convert GPS coordinates into a street address. |
|
|
43
|
+
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. |
|
|
44
|
+
| `maps_directions` | Get step-by-step navigation between two points with route details. |
|
|
45
|
+
| `maps_elevation` | Get elevation (meters above sea level) for geographic coordinates. |
|
|
63
46
|
|
|
64
|
-
|
|
47
|
+
All tools are annotated with `readOnlyHint: true` and `destructiveHint: false` β MCP clients can auto-approve these without user confirmation.
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
- **Session Management**: Stateful sessions with UUID-based identification
|
|
68
|
-
- **Multiple Connection Support**: Handle multiple concurrent client connections
|
|
69
|
-
- **Echo Service**: Built-in testing tool for MCP server functionality
|
|
49
|
+
> **Prerequisite**: Enable **Places API (New)** in [Google Cloud Console](https://console.cloud.google.com) before using place-related tools.
|
|
70
50
|
|
|
71
51
|
## Installation
|
|
72
52
|
|
|
73
|
-
>
|
|
53
|
+
> **Note**: This server uses HTTP transport, not stdio. Direct npx usage in MCP Server Settings is **NOT supported**.
|
|
74
54
|
|
|
75
55
|
### Method 1: Global Installation (Recommended)
|
|
76
56
|
|
|
@@ -87,7 +67,7 @@ mcp-google-map -p 3000 -k "your_api_key_here"
|
|
|
87
67
|
|
|
88
68
|
### Method 2: Using npx (Quick Start)
|
|
89
69
|
|
|
90
|
-
>
|
|
70
|
+
> Cannot be used directly in MCP Server Settings with stdio mode
|
|
91
71
|
|
|
92
72
|
**Step 1: Launch HTTP Server in Terminal**
|
|
93
73
|
|
|
@@ -110,7 +90,7 @@ GOOGLE_MAPS_API_KEY=YOUR_API_KEY npx @cablate/mcp-google-map
|
|
|
110
90
|
}
|
|
111
91
|
```
|
|
112
92
|
|
|
113
|
-
###
|
|
93
|
+
### Common Mistake to Avoid
|
|
114
94
|
|
|
115
95
|
```json
|
|
116
96
|
// This WILL NOT WORK - stdio mode not supported with npx
|
|
@@ -125,8 +105,8 @@ GOOGLE_MAPS_API_KEY=YOUR_API_KEY npx @cablate/mcp-google-map
|
|
|
125
105
|
### Server Information
|
|
126
106
|
|
|
127
107
|
- **Endpoint**: `http://localhost:3000/mcp`
|
|
128
|
-
- **Transport**: HTTP (not stdio)
|
|
129
|
-
- **Tools**: 8 Google Maps tools
|
|
108
|
+
- **Transport**: Streamable HTTP (not stdio)
|
|
109
|
+
- **Tools**: 8 Google Maps tools
|
|
130
110
|
|
|
131
111
|
### API Key Configuration
|
|
132
112
|
|
|
@@ -135,14 +115,12 @@ API keys can be provided in three ways (priority order):
|
|
|
135
115
|
1. **HTTP Headers** (Highest priority)
|
|
136
116
|
|
|
137
117
|
```json
|
|
138
|
-
// MCP Client config
|
|
139
118
|
{
|
|
140
119
|
"mcp-google-map": {
|
|
141
120
|
"transport": "streamableHttp",
|
|
142
121
|
"url": "http://localhost:3000/mcp",
|
|
143
|
-
// if your MCP Client support 'headers'
|
|
144
122
|
"headers": {
|
|
145
|
-
"X-Google-Maps-API-Key": "YOUR_API_KEY"
|
|
123
|
+
"X-Google-Maps-API-Key": "YOUR_API_KEY"
|
|
146
124
|
}
|
|
147
125
|
}
|
|
148
126
|
}
|
|
@@ -160,20 +138,6 @@ API keys can be provided in three ways (priority order):
|
|
|
160
138
|
MCP_SERVER_PORT=3000
|
|
161
139
|
```
|
|
162
140
|
|
|
163
|
-
## Available Tools
|
|
164
|
-
|
|
165
|
-
The server provides the following tools:
|
|
166
|
-
|
|
167
|
-
### Google Maps Tools
|
|
168
|
-
|
|
169
|
-
1. **search_nearby** - Search for nearby places based on location, with optional filtering by keywords, distance, rating, and operating hours
|
|
170
|
-
2. **get_place_details** - Get detailed information about a specific place including contact details, reviews, ratings, and operating hours
|
|
171
|
-
3. **maps_geocode** - Convert addresses or place names to geographic coordinates (latitude and longitude)
|
|
172
|
-
4. **maps_reverse_geocode** - Convert geographic coordinates to a human-readable address
|
|
173
|
-
5. **maps_distance_matrix** - Calculate travel distances and durations between multiple origins and destinations
|
|
174
|
-
6. **maps_directions** - Get detailed turn-by-turn navigation directions between two locations
|
|
175
|
-
7. **maps_elevation** - Get elevation data (height above sea level) for specific geographic locations
|
|
176
|
-
|
|
177
141
|
## Development
|
|
178
142
|
|
|
179
143
|
### Local Development
|
|
@@ -200,49 +164,68 @@ npm start
|
|
|
200
164
|
npm run dev
|
|
201
165
|
```
|
|
202
166
|
|
|
167
|
+
### Testing
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Run smoke tests (no API key required for basic tests)
|
|
171
|
+
npm test
|
|
172
|
+
|
|
173
|
+
# Run full E2E tests (requires GOOGLE_MAPS_API_KEY)
|
|
174
|
+
npm run test:e2e
|
|
175
|
+
```
|
|
176
|
+
|
|
203
177
|
### Project Structure
|
|
204
178
|
|
|
205
179
|
```
|
|
206
180
|
src/
|
|
207
|
-
βββ cli.ts
|
|
208
|
-
βββ config.ts
|
|
209
|
-
βββ index.ts
|
|
181
|
+
βββ cli.ts # CLI entry point
|
|
182
|
+
βββ config.ts # Tool registration and server config
|
|
183
|
+
βββ index.ts # Package exports
|
|
210
184
|
βββ core/
|
|
211
|
-
β βββ BaseMcpServer.ts
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
185
|
+
β βββ BaseMcpServer.ts # MCP server with streamable HTTP transport
|
|
186
|
+
βββ services/
|
|
187
|
+
β βββ NewPlacesService.ts # Google Places API (New) client
|
|
188
|
+
β βββ PlacesSearcher.ts # Service facade layer
|
|
189
|
+
β βββ toolclass.ts # Legacy Google Maps API client
|
|
190
|
+
βββ tools/
|
|
191
|
+
β βββ maps/
|
|
192
|
+
β βββ searchNearby.ts # search_nearby tool
|
|
193
|
+
β βββ searchPlaces.ts # maps_search_places tool
|
|
194
|
+
β βββ placeDetails.ts # get_place_details tool
|
|
195
|
+
β βββ geocode.ts # maps_geocode tool
|
|
196
|
+
β βββ reverseGeocode.ts # maps_reverse_geocode tool
|
|
197
|
+
β βββ distanceMatrix.ts # maps_distance_matrix tool
|
|
198
|
+
β βββ directions.ts # maps_directions tool
|
|
199
|
+
β βββ elevation.ts # maps_elevation tool
|
|
200
|
+
βββ utils/
|
|
201
|
+
βββ apiKeyManager.ts # API key management
|
|
202
|
+
βββ requestContext.ts # Per-request context (API key isolation)
|
|
203
|
+
tests/
|
|
204
|
+
βββ smoke.test.ts # Smoke + E2E test suite
|
|
223
205
|
```
|
|
224
206
|
|
|
225
207
|
## Tech Stack
|
|
226
208
|
|
|
227
209
|
- **TypeScript** - Type-safe development
|
|
228
210
|
- **Node.js** - Runtime environment
|
|
229
|
-
- **Google
|
|
230
|
-
- **
|
|
211
|
+
- **@googlemaps/places** - Google Places API (New) for place search and details
|
|
212
|
+
- **@googlemaps/google-maps-services-js** - Legacy API for geocoding, directions, distance matrix, elevation
|
|
213
|
+
- **@modelcontextprotocol/sdk** - MCP protocol implementation (v1.27+)
|
|
231
214
|
- **Express.js** - HTTP server framework
|
|
232
215
|
- **Zod** - Schema validation
|
|
233
216
|
|
|
234
|
-
## Security
|
|
217
|
+
## Security
|
|
235
218
|
|
|
236
|
-
- API keys are handled server-side
|
|
219
|
+
- API keys are handled server-side
|
|
220
|
+
- Per-session API key isolation for multi-tenant deployments
|
|
237
221
|
- DNS rebinding protection available for production
|
|
238
222
|
- Input validation using Zod schemas
|
|
239
|
-
- Error handling and logging
|
|
240
223
|
|
|
241
|
-
|
|
224
|
+
For enterprise security reviews, see [Security Assessment Clarifications](./SECURITY_ASSESSMENT.md) β a 23-item checklist covering licensing, data protection, credential management, tool contamination, and AI agent execution environment verification.
|
|
242
225
|
|
|
243
|
-
|
|
226
|
+
## Changelog
|
|
244
227
|
|
|
245
|
-
|
|
228
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
246
229
|
|
|
247
230
|
## License
|
|
248
231
|
|
|
@@ -250,46 +233,16 @@ MIT
|
|
|
250
233
|
|
|
251
234
|
## Contributing
|
|
252
235
|
|
|
253
|
-
Community participation and contributions are welcome!
|
|
236
|
+
Community participation and contributions are welcome!
|
|
254
237
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
- π Documentation: Help improve documentation
|
|
238
|
+
- Submit Issues: Report bugs or provide suggestions
|
|
239
|
+
- Create Pull Requests: Submit code improvements
|
|
240
|
+
- Documentation: Help improve documentation
|
|
259
241
|
|
|
260
242
|
## Contact
|
|
261
243
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
- π§ Email: [reahtuoo310109@gmail.com](mailto:reahtuoo310109@gmail.com)
|
|
265
|
-
- π» GitHub: [CabLate](https://github.com/cablate/)
|
|
266
|
-
- π€ Collaboration: Welcome to discuss project cooperation
|
|
267
|
-
- π Technical Guidance: Sincere welcome for suggestions and guidance
|
|
268
|
-
|
|
269
|
-
## Changelog
|
|
270
|
-
|
|
271
|
-
### v0.0.19 (Latest)
|
|
272
|
-
|
|
273
|
-
- **New Places API Integration**: Updated to use Google's new Places API (New) instead of the legacy API to resolve HTTP 403 errors and ensure continued functionality.
|
|
274
|
-
|
|
275
|
-
### v0.0.18
|
|
276
|
-
|
|
277
|
-
- **Error response improvements**: Now all error messages are in English with more detailed information (previously in Chinese)
|
|
278
|
-
|
|
279
|
-
### v0.0.17
|
|
280
|
-
|
|
281
|
-
- **Added HTTP Header Authentication**: Support for passing API keys via `X-Google-Maps-API-Key` header in MCP Client config
|
|
282
|
-
- **Fixed Concurrent User Issues**: Each session now uses its own API key without conflicts
|
|
283
|
-
- **Fixed npx Execution**: Resolved module bundling issues
|
|
284
|
-
- **Improved Documentation**: Clearer setup instructions
|
|
285
|
-
|
|
286
|
-
### v0.0.14
|
|
287
|
-
|
|
288
|
-
- Added streamable HTTP transport support
|
|
289
|
-
- Improved CLI interface with emoji indicators
|
|
290
|
-
- Enhanced error handling and logging
|
|
291
|
-
- Added comprehensive tool descriptions for LLM integration
|
|
292
|
-
- Updated to latest MCP SDK version
|
|
244
|
+
- Email: [reahtuoo310109@gmail.com](mailto:reahtuoo310109@gmail.com)
|
|
245
|
+
- GitHub: [CabLate](https://github.com/cablate/)
|
|
293
246
|
|
|
294
247
|
## Star History
|
|
295
248
|
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{b as s,c}from"./chunk-
|
|
3
|
-
`+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var y={NAME:G,DESCRIPTION:z,SCHEMA:k,ACTION:J};import{z as L}from"zod";var j="get_place_details",V="Get detailed information about a specific place including contact details, reviews, ratings, and operating hours",q={placeId:L.string().describe("Google Maps place ID")};async function F(t){try{let e=p(),r=await new c(e).getPlaceDetails(t.placeId);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var f={NAME:j,DESCRIPTION:V,SCHEMA:q,ACTION:F};import{z as W}from"zod";var Z="maps_geocode",Y="Convert addresses or place names to geographic coordinates (latitude and longitude)",B={address:W.string().describe("Address or place name to convert to coordinates")};async function U(t){try{let e=p(),r=await new c(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:Z,DESCRIPTION:Y,SCHEMA:B,ACTION:U};import{z as _}from"zod";var Q="maps_reverse_geocode",X="Convert geographic coordinates (latitude and longitude) to a human-readable address",ee={latitude:_.number().describe("Latitude coordinate"),longitude:_.number().describe("Longitude coordinate")};async function re(t){try{let e=p(),r=await new c(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var h={NAME:Q,DESCRIPTION:X,SCHEMA:ee,ACTION:re};import{z as S}from"zod";var te="maps_distance_matrix",oe="Calculate travel distances and durations between multiple origins and destinations for different travel modes",se={origins:S.array(S.string()).describe("List of origin addresses or coordinates"),destinations:S.array(S.string()).describe("List of destination addresses or coordinates"),mode:S.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ne(t){try{let e=p(),r=await new c(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var E={NAME:te,DESCRIPTION:oe,SCHEMA:se,ACTION:ne};import{z as P}from"zod";var ie="maps_directions",ae="Get detailed turn-by-turn navigation directions between two locations with route information",ce={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 pe(t){try{let e=p(),r=await new c(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:ie,DESCRIPTION:ae,SCHEMA:ce,ACTION:pe};import{z as x}from"zod";var le="maps_elevation",de="Get elevation data (height above sea level) for specific geographic locations",me={locations:x.array(x.object({latitude:x.number().describe("Latitude coordinate"),longitude:x.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function ge(t){try{let e=p(),r=await new c(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:le,DESCRIPTION:de,SCHEMA:me,ACTION:ge};var m={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},ue=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:y.NAME,description:y.DESCRIPTION,schema:y.SCHEMA,annotations:m,action:t=>y.ACTION(t)},{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:m,action:t=>f.ACTION(t)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:m,action:t=>v.ACTION(t)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:m,action:t=>h.ACTION(t)},{name:E.NAME,description:E.DESCRIPTION,schema:E.SCHEMA,annotations:m,action:t=>E.ACTION(t)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:m,action:t=>A.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:m,action:t=>M.ACTION(t)}]}],R=ue;import{McpServer as ye}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as fe}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as ve}from"@modelcontextprotocol/sdk/types.js";import w from"express";import{randomUUID as he}from"crypto";import{z as Se}from"zod";var b=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let 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 Ee="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 ye({name:this.serverName,version:Ee},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Se.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,n,i)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,n,i),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=w();o.use(w.json()),o.post("/mcp",async(n,i)=>{let a=n.headers["mcp-session-id"],l,g=b.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),a&&this.sessions[a])l=this.sessions[a],g&&(l.apiKey=g);else if(!a&&ve(n.body)){let u=new fe({sessionIdGenerator:()=>he(),onsessioninitialized:O=>{this.sessions[O]=l,s.log(`[${this.serverName}] New session initialized: ${O}`)}});l={transport:u,apiKey:g},u.onclose=()=>{u.sessionId&&(delete this.sessions[u.sessionId],s.log(`[${this.serverName}] Session closed: ${u.sessionId}`))},await this.createMcpServer().connect(u)}else{i.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await N({apiKey:l.apiKey,sessionId:a},async()=>{await l.transport.handleRequest(n,i,n.body)})});let r=async(n,i)=>{let a=n.headers["mcp-session-id"];if(!a||!this.sessions[a]){i.status(400).send("Invalid or missing session ID");return}let l=this.sessions[a],g=b.getInstance().getApiKey(n);g&&(l.apiKey=g),await N({apiKey:l.apiKey,sessionId:a},async()=>{await l.transport.handleRequest(n,i)})};o.get("/mcp",r),o.delete("/mcp",r),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 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(r=>{if(r){s.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(i=>(i.transport.sessionId&&delete this.sessions[i.transport.sessionId],Promise.resolve()));Promise.all(n).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 Me}from"url";import{dirname as be}from"path";import{readFileSync as xe}from"fs";var Ce=Me(import.meta.url),$=be(Ce);K({path:I(process.cwd(),".env")});K({path:I($,"../.env")});async function Ne(t,e){t&&(process.env.MCP_SERVER_PORT=t.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=R.map(async r=>{let n=process.env[r.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${r.name}] Port environment variable ${r.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${r.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${r.portEnvVar}=3000 or --port 3000`);return}let i=Number(n);if(isNaN(i)||i<=0){s.error(`\u274C [${r.name}] Invalid port number "${n}" defined in ${r.portEnvVar}.`);return}try{let a=new C(r.name,r.tools);s.log(`\u{1F527} [${r.name}] Initializing MCP Server in HTTP mode on port ${i}...`),await a.startHttpServer(i),s.log(`\u2705 [${r.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${i}/mcp`),s.log(` \u{1F4DA} Tools: ${r.tools.length} available`)}catch(a){s.error(`\u274C [${r.name}] Failed to start MCP Server on port ${i}:`,a)}});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=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")),Oe=import.meta.url===`file://${process.argv[1]}`;if(Ie||Oe){let t="0.0.0";try{let o=I($,"../package.json");t=JSON.parse(xe(o,"utf-8")).version}catch{t="0.0.0"}let e=Pe(Ae(process.argv)).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("help",{alias:"h",type:"boolean",description:"Show help"}).version(t).alias("version","v").example([["$0","Start server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start server with custom port and API key"],['$0 -p 3001 -k "your_api_key"',"Start server with short options"]]).help().parseSync();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(" Example: mcp-google-map --apikey your_api_key_here"),s.log(" Or: GOOGLE_MAPS_API_KEY=your_api_key_here"),s.log("")),Ne(e.port,e.apikey).catch(o=>{s.error("\u274C Failed to start server:",o),process.exit(1)})}export{Ne as startServer};
|
|
2
|
+
import{b as s,c as i}from"./chunk-RIT3FLYG.js";import{config as $}from"dotenv";import{resolve as T}from"path";import Ne from"yargs";import{hideBin as Ie}from"yargs/helpers";import{z as u}from"zod";import{AsyncLocalStorage as k}from"async_hooks";var w=new k;function c(){return w.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function O(t,e){return w.run(t,e)}var z="search_nearby",J="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.",L={center:u.object({value:u.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:u.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:u.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:u.number().default(1e3).describe("Search radius in meters"),openNow:u.boolean().default(!1).describe("Only show places that are currently open"),minRating:u.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function q(t){try{let e=c(),r=await new i(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
|
|
3
|
+
`+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var f={NAME:z,DESCRIPTION:J,SCHEMA:L,ACTION:q};import{z as j}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.",U={placeId:j.string().describe("Google Maps place ID")};async function W(t){try{let e=c(),r=await new i(e).getPlaceDetails(t.placeId);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var h={NAME:V,DESCRIPTION:F,SCHEMA:U,ACTION:W};import{z as Z}from"zod";var B="maps_geocode",Y="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:Z.string().describe("Address or place name to convert to coordinates")};async function X(t){try{let e=c(),r=await new i(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:B,DESCRIPTION:Y,SCHEMA:Q,ACTION:X};import{z as R}from"zod";var ee="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.",te={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate")};async function oe(t){try{let e=c(),r=await new i(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var S={NAME:ee,DESCRIPTION:re,SCHEMA:te,ACTION:oe};import{z as E}from"zod";var se="maps_distance_matrix",ne="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.",ae={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 ie(t){try{let e=c(),r=await new i(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var P={NAME:se,DESCRIPTION:ne,SCHEMA:ae,ACTION:ie};import{z as b}from"zod";var ce="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.",le={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 de(t){try{let e=c(),r=await new i(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:ce,DESCRIPTION:pe,SCHEMA:le,ACTION:de};import{z as N}from"zod";var me="maps_elevation",ue="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.",ge={locations:N.array(N.object({latitude:N.number().describe("Latitude coordinate"),longitude:N.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function ye(t){try{let e=c(),r=await new i(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var x={NAME:me,DESCRIPTION:ue,SCHEMA:ge,ACTION:ye};import{z as d}from"zod";var fe="maps_search_places",he="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.",ve={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 Se(t){try{let e=c(),r=await new i(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:fe,DESCRIPTION:he,SCHEMA:ve,ACTION:Se};var m={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Ee=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:m,action:t=>f.ACTION(t)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:m,action:t=>h.ACTION(t)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:m,action:t=>v.ACTION(t)},{name:S.NAME,description:S.DESCRIPTION,schema:S.SCHEMA,annotations:m,action:t=>S.ACTION(t)},{name:P.NAME,description:P.DESCRIPTION,schema:P.SCHEMA,annotations:m,action:t=>P.ACTION(t)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:m,action:t=>A.ACTION(t)},{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:m,action:t=>x.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:m,action:t=>M.ACTION(t)}]}],K=Ee;import{McpServer as Pe}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as be}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Ae}from"@modelcontextprotocol/sdk/types.js";import D from"express";import{randomUUID as xe}from"crypto";import{z as Me}from"zod";var C=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let 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 Ce="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 Pe({name:this.serverName,version:Ce},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Me.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,n,a)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,n,a),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=D();o.use(D.json()),o.post("/mcp",async(n,a)=>{let p=n.headers["mcp-session-id"],l,g=C.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),p&&this.sessions[p])l=this.sessions[p],g&&(l.apiKey=g);else if(!p&&Ae(n.body)){let y=new be({sessionIdGenerator:()=>xe(),onsessioninitialized:_=>{this.sessions[_]=l,s.log(`[${this.serverName}] New session initialized: ${_}`)}});l={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 O({apiKey:l.apiKey,sessionId:p},async()=>{await l.transport.handleRequest(n,a,n.body)})});let r=async(n,a)=>{let p=n.headers["mcp-session-id"];if(!p||!this.sessions[p]){a.status(400).send("Invalid or missing session ID");return}let l=this.sessions[p],g=C.getInstance().getApiKey(n);g&&(l.apiKey=g),await O({apiKey:l.apiKey,sessionId:p},async()=>{await l.transport.handleRequest(n,a)})};o.get("/mcp",r),o.delete("/mcp",r),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 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(r=>{if(r){s.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);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 Oe}from"url";import{dirname as Te}from"path";import{readFileSync as _e}from"fs";var we=Oe(import.meta.url),H=Te(we);$({path:T(process.cwd(),".env")});$({path:T(H,"../.env")});async function Re(t,e){t&&(process.env.MCP_SERVER_PORT=t.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=K.map(async r=>{let n=process.env[r.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${r.name}] Port environment variable ${r.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${r.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${r.portEnvVar}=3000 or --port 3000`);return}let a=Number(n);if(isNaN(a)||a<=0){s.error(`\u274C [${r.name}] Invalid port number "${n}" defined in ${r.portEnvVar}.`);return}try{let p=new I(r.name,r.tools);s.log(`\u{1F527} [${r.name}] Initializing MCP Server in HTTP mode on port ${a}...`),await p.startHttpServer(a),s.log(`\u2705 [${r.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${a}/mcp`),s.log(` \u{1F4DA} Tools: ${r.tools.length} available`)}catch(p){s.error(`\u274C [${r.name}] Failed to start MCP Server on port ${a}:`,p)}});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 Ke=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")),De=import.meta.url===`file://${process.argv[1]}`;if(Ke||De){let t="0.0.0";try{let o=T(H,"../package.json");t=JSON.parse(_e(o,"utf-8")).version}catch{t="0.0.0"}let e=Ne(Ie(process.argv)).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("help",{alias:"h",type:"boolean",description:"Show help"}).version(t).alias("version","v").example([["$0","Start server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start server with custom port and API key"],['$0 -p 3001 -k "your_api_key"',"Start server with short options"]]).help().parseSync();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(" Example: mcp-google-map --apikey your_api_key_here"),s.log(" Or: GOOGLE_MAPS_API_KEY=your_api_key_here"),s.log("")),Re(e.port,e.apikey).catch(o=>{s.error("\u274C Failed to start server:",o),process.exit(1)})}export{Re as startServer};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface
|
|
1
|
+
interface SearchResponse {
|
|
2
2
|
success: boolean;
|
|
3
3
|
error?: string;
|
|
4
4
|
data?: any[];
|
|
@@ -80,7 +80,18 @@ declare class PlacesSearcher {
|
|
|
80
80
|
radius?: number;
|
|
81
81
|
openNow?: boolean;
|
|
82
82
|
minRating?: number;
|
|
83
|
-
}): Promise<
|
|
83
|
+
}): Promise<SearchResponse>;
|
|
84
|
+
searchText(params: {
|
|
85
|
+
query: string;
|
|
86
|
+
locationBias?: {
|
|
87
|
+
latitude: number;
|
|
88
|
+
longitude: number;
|
|
89
|
+
radius?: number;
|
|
90
|
+
};
|
|
91
|
+
openNow?: boolean;
|
|
92
|
+
minRating?: number;
|
|
93
|
+
includedType?: string;
|
|
94
|
+
}): Promise<SearchResponse>;
|
|
84
95
|
getPlaceDetails(placeId: string): Promise<PlaceDetailsResponse>;
|
|
85
96
|
geocode(address: string): Promise<GeocodeResponse>;
|
|
86
97
|
reverseGeocode(latitude: number, longitude: number): Promise<ReverseGeocodeResponse>;
|
|
@@ -96,7 +107,29 @@ declare class NewPlacesService {
|
|
|
96
107
|
private client;
|
|
97
108
|
private readonly defaultLanguage;
|
|
98
109
|
private readonly placeFieldMask;
|
|
110
|
+
private readonly searchNearbyFieldMask;
|
|
99
111
|
constructor(apiKey?: string);
|
|
112
|
+
searchNearby(params: {
|
|
113
|
+
location: {
|
|
114
|
+
lat: number;
|
|
115
|
+
lng: number;
|
|
116
|
+
};
|
|
117
|
+
keyword?: string;
|
|
118
|
+
radius?: number;
|
|
119
|
+
maxResultCount?: number;
|
|
120
|
+
}): Promise<any[]>;
|
|
121
|
+
searchText(params: {
|
|
122
|
+
textQuery: string;
|
|
123
|
+
locationBias?: {
|
|
124
|
+
lat: number;
|
|
125
|
+
lng: number;
|
|
126
|
+
radius?: number;
|
|
127
|
+
};
|
|
128
|
+
openNow?: boolean;
|
|
129
|
+
minRating?: number;
|
|
130
|
+
includedType?: string;
|
|
131
|
+
maxResultCount?: number;
|
|
132
|
+
}): Promise<any[]>;
|
|
100
133
|
getPlaceDetails(placeId: string): Promise<{
|
|
101
134
|
name: any;
|
|
102
135
|
place_id: string;
|
|
@@ -119,6 +152,7 @@ declare class NewPlacesService {
|
|
|
119
152
|
reviews: any;
|
|
120
153
|
photos: any;
|
|
121
154
|
}>;
|
|
155
|
+
private transformSearchResult;
|
|
122
156
|
private transformPlaceResponse;
|
|
123
157
|
private extractLegacyPlaceId;
|
|
124
158
|
private isCurrentlyOpen;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b,c}from"./chunk-
|
|
1
|
+
import{a,b,c}from"./chunk-RIT3FLYG.js";export{b as Logger,a as NewPlacesService,c as PlacesSearcher};
|
package/package.json
CHANGED
package/dist/chunk-Z5SWQKLS.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Client as N,Language as x}from"@googlemaps/google-maps-services-js";import R from"dotenv";R.config();function y(c){let r=c?.response?.status,e=c?.response?.data?.error_message,t=c?.response?.data?.status;return r===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.)":r===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":t==="ZERO_RESULTS"?"No results found. Try broader search terms or a larger radius.":t==="OVER_QUERY_LIMIT"?"API quota exceeded. Wait and retry, or upgrade your billing plan.":t==="REQUEST_DENIED"?`Request denied by Google Maps API. ${e||"Check your API key and enabled APIs."}`:t==="INVALID_REQUEST"?`Invalid request parameters. ${e||"Check your input values."}`:e?`${e} (HTTP ${r})`:c instanceof Error?c.message:String(c)}var P=class{constructor(r){this.defaultLanguage=x.en;if(this.client=new N({}),this.apiKey=r||process.env.GOOGLE_MAPS_API_KEY||"",!this.apiKey)throw new Error("Google Maps API Key is required")}async searchNearbyPlaces(r){let e={location:r.location,radius:r.radius||1e3,keyword:r.keyword,opennow:r.openNow,language:this.defaultLanguage,key:this.apiKey};try{let n=(await this.client.placesNearby({params:e})).data.results;return r.minRating&&(n=n.filter(a=>(a.rating||0)>=(r.minRating||0))),n}catch(t){throw d.error("Error in searchNearbyPlaces:",t),new Error(`Failed to search nearby places: ${y(t)}`)}}async getPlaceDetails(r){try{return(await this.client.placeDetails({params:{place_id:r,fields:["name","rating","formatted_address","opening_hours","reviews","geometry","formatted_phone_number","website","price_level","photos"],language:this.defaultLanguage,key:this.apiKey}})).data.result}catch(e){throw d.error("Error in getPlaceDetails:",e),new Error(`Failed to get place details for ${r}: ${y(e)}`)}}async geocodeAddress(r){try{let e=await this.client.geocode({params:{address:r,key:this.apiKey,language:this.defaultLanguage}});if(e.data.results.length===0)throw new Error(`No location found for address: "${r}"`);let t=e.data.results[0],n=t.geometry.location;return{lat:n.lat,lng:n.lng,formatted_address:t.formatted_address,place_id:t.place_id}}catch(e){throw d.error("Error in geocodeAddress:",e),new Error(`Failed to geocode address "${r}": ${y(e)}`)}}parseCoordinates(r){let e=r.split(",").map(t=>parseFloat(t.trim()));if(e.length!==2||isNaN(e[0])||isNaN(e[1]))throw new Error(`Invalid coordinate format: "${r}". Please use "latitude,longitude" format (e.g., "25.033,121.564"`);return{lat:e[0],lng:e[1]}}async getLocation(r){return r.isCoordinates?this.parseCoordinates(r.value):this.geocodeAddress(r.value)}async geocode(r){try{let e=await this.geocodeAddress(r);return{location:{lat:e.lat,lng:e.lng},formatted_address:e.formatted_address||"",place_id:e.place_id||""}}catch(e){throw d.error("Error in geocode:",e),new Error(`Failed to geocode address "${r}": ${y(e)}`)}}async reverseGeocode(r,e){try{let t=await this.client.reverseGeocode({params:{latlng:{lat:r,lng:e},language:this.defaultLanguage,key:this.apiKey}});if(t.data.results.length===0)throw new Error(`No address found for coordinates: (${r}, ${e})`);let n=t.data.results[0];return{formatted_address:n.formatted_address,place_id:n.place_id,address_components:n.address_components}}catch(t){throw d.error("Error in reverseGeocode:",t),new Error(`Failed to reverse geocode coordinates (${r}, ${e}): ${y(t)}`)}}async calculateDistanceMatrix(r,e,t="driving"){try{let a=(await this.client.distancematrix({params:{origins:r,destinations:e,mode:t,language:this.defaultLanguage,key:this.apiKey}})).data;if(a.status!=="OK")throw new Error(`Distance matrix calculation failed with status: ${a.status}`);let o=[],l=[];return a.rows.forEach(h=>{let u=[],m=[];h.elements.forEach(i=>{i.status==="OK"?(u.push({value:i.distance.value,text:i.distance.text}),m.push({value:i.duration.value,text:i.duration.text})):(u.push(null),m.push(null))}),o.push(u),l.push(m)}),{distances:o,durations:l,origin_addresses:a.origin_addresses,destination_addresses:a.destination_addresses}}catch(n){throw d.error("Error in calculateDistanceMatrix:",n),new Error(`Failed to calculate distance matrix: ${y(n)}`)}}async getDirections(r,e,t="driving",n,a){try{let o;a&&(o=Math.floor(a.getTime()/1e3));let l;o||(n instanceof Date?l=Math.floor(n.getTime()/1e3):n?l=n:l="now");let u=(await this.client.directions({params:{origin:r,destination:e,mode:t,language:this.defaultLanguage,key:this.apiKey,arrival_time:o,departure_time:l}})).data;if(u.status!=="OK")throw new Error(`Failed to get directions with status: ${u.status} (arrival_time: ${o}, departure_time: ${l}`);if(u.routes.length===0)throw new Error(`No route found from "${r}" to "${e}" with mode: ${t}`);let m=u.routes[0],i=m.legs[0],f=s=>{if(!s||typeof s.value!="number")return"";let g=new Date(s.value*1e3),p={year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1};return s.time_zone&&typeof s.time_zone=="string"&&(p.timeZone=s.time_zone),g.toLocaleString(this.defaultLanguage.toString(),p)};return{routes:u.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 "${r}" to "${e}": ${y(o)}`)}}async getElevation(r){try{let e=r.map(a=>({lat:a.latitude,lng:a.longitude})),n=(await this.client.elevation({params:{locations:e,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,o)=>({elevation:a.elevation,location:e[o]}))}catch(e){throw d.error("Error in getElevation:",e),new Error(`Failed to get elevation data for ${r.length} location(s): ${y(e)}`)}}};import{PlacesClient as T}from"@googlemaps/places";var _=class{constructor(r){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(",");if(this.client=new T({apiKey:r||process.env.GOOGLE_MAPS_API_KEY||""}),!r&&!process.env.GOOGLE_MAPS_API_KEY)throw new Error("Google Maps API Key is required")}async getPlaceDetails(r){try{let e=`places/${r}`,[t]=await this.client.getPlace({name:e,languageCode:this.defaultLanguage},{otherArgs:{headers:{"X-Goog-FieldMask":this.placeFieldMask}}});return this.transformPlaceResponse(t)}catch(e){throw d.error("Error in getPlaceDetails (New API):",e),new Error(`Failed to get place details for ${r}: ${this.extractErrorMessage(e)}`)}}transformPlaceResponse(r){return{name:r.displayName?.text||r.name||"",place_id:this.extractLegacyPlaceId(r),formatted_address:r.formattedAddress||"",geometry:{location:{lat:r.location?.latitude||0,lng:r.location?.longitude||0}},rating:r.rating||0,user_ratings_total:r.userRatingCount||0,opening_hours:r.regularOpeningHours?{open_now:this.isCurrentlyOpen(r.regularOpeningHours,r.utcOffsetMinutes,r.currentOpeningHours),weekday_text:this.formatOpeningHours(r.regularOpeningHours)}:void 0,formatted_phone_number:r.nationalPhoneNumber||"",website:r.websiteUri||"",price_level:r.priceLevel||0,reviews:r.reviews?.map(e=>({rating:e.rating||0,text:e.text?.text||"",time:e.publishTime?.seconds||0,author_name:e.authorAttribution?.displayName||""}))||[],photos:r.photos?.map(e=>({photo_reference:e.name||"",height:e.heightPx||0,width:e.widthPx||0}))||[]}}extractLegacyPlaceId(r){let e=r?.name;if(typeof e=="string"&&e.startsWith("places/")){let t=e.substring(7);if(t)return t}return r?.id||""}isCurrentlyOpen(r,e,t){if(typeof t?.openNow=="boolean")return t.openNow;if(typeof r?.openNow=="boolean")return r.openNow;let n=r?.periods;if(!Array.isArray(n)||n.length===0)return!1;let a=1440,o=a*7,{day:l,minutes:h}=this.getLocalTimeComponents(e),u=l*a+h,m={SUNDAY:0,MONDAY:1,TUESDAY:2,WEDNESDAY:3,THURSDAY:4,FRIDAY:5,SATURDAY:6},i=s=>{if(typeof s=="number"&&s>=0&&s<=6)return s;if(typeof s=="string"){let g=s.toUpperCase();if(g in m)return m[g]}},f=s=>{if(!s)return;let g=typeof s.hours=="number"?s.hours:Number(s.hours??NaN),p=typeof s.minutes=="number"?s.minutes:Number(s.minutes??NaN);if(!(!Number.isFinite(g)||!Number.isFinite(p)))return g*60+p};for(let s of n){let g=i(s?.openDay),p=i(s?.closeDay??s?.openDay),D=f(s?.openTime),A=f(s?.closeTime);if(g===void 0||D===void 0)continue;let b=g*a+D,w;p===void 0||A===void 0?w=b+a:w=p*a+A,w<=b&&(w+=o);let v=u;for(;v<b;)v+=o;if(v>=b&&v<w)return!0}return!1}getLocalTimeComponents(r){let e=new Date;if(typeof r=="number"&&Number.isFinite(r)){let t=new Date(e.getTime()+r*6e4);return{day:t.getUTCDay(),minutes:t.getUTCHours()*60+t.getUTCMinutes()}}return{day:e.getDay(),minutes:e.getHours()*60+e.getMinutes()}}formatOpeningHours(r){return r?.weekdayDescriptions||[]}extractErrorMessage(r){let e=r?.code,t=r?.message||r?.details;return e===7||e===403?"API key invalid or Places API (New) not enabled. Check: console.cloud.google.com \u2192 APIs & Services \u2192 Enable 'Places API (New)'":e===8||e===429?"API quota exceeded. Wait and retry, or check quota at console.cloud.google.com \u2192 Quotas":t||(r instanceof Error?r.message:String(r))}};var E=class{constructor(r){this.mapsTools=new P(r),this.newPlacesService=new _(r)}async searchNearby(r){try{let e=await this.mapsTools.getLocation(r.center),t=await this.mapsTools.searchNearbyPlaces({location:e,keyword:r.keyword,radius:r.radius,openNow:r.openNow,minRating:r.minRating});return{location:e,success:!0,data:t.map(n=>({name:n.name,place_id:n.place_id,address:n.formatted_address,location:n.geometry.location,rating:n.rating,total_ratings:n.user_ratings_total,open_now:n.opening_hours?.open_now}))}}catch(e){return{success:!1,error:e instanceof Error?e.message:"An error occurred during search"}}}async getPlaceDetails(r){try{let e=await this.newPlacesService.getPlaceDetails(r);return{success:!0,data:{name:e.name,address:e.formatted_address,location:e.geometry?.location,rating:e.rating,total_ratings:e.user_ratings_total,open_now:e.opening_hours?.open_now,phone:e.formatted_phone_number,website:e.website,price_level:e.price_level,reviews:e.reviews?.map(t=>({rating:t.rating,text:t.text,time:t.time,author_name:t.author_name}))}}}catch(e){return{success:!1,error:e instanceof Error?e.message:"An error occurred while getting place details"}}}async geocode(r){try{return{success:!0,data:await this.mapsTools.geocode(r)}}catch(e){return{success:!1,error:e instanceof Error?e.message:"An error occurred while geocoding address"}}}async reverseGeocode(r,e){try{return{success:!0,data:await this.mapsTools.reverseGeocode(r,e)}}catch(t){return{success:!1,error:t instanceof Error?t.message:"An error occurred during reverse geocoding"}}}async calculateDistanceMatrix(r,e,t="driving"){try{return{success:!0,data:await this.mapsTools.calculateDistanceMatrix(r,e,t)}}catch(n){return{success:!1,error:n instanceof Error?n.message:"An error occurred while calculating distance matrix"}}}async getDirections(r,e,t="driving",n,a){try{let o=n?new Date(n):new Date,l=a?new Date(a):void 0;return{success:!0,data:await this.mapsTools.getDirections(r,e,t,o,l)}}catch(o){return{success:!1,error:o instanceof Error?o.message:"An error occurred while getting directions"}}}async getElevation(r){try{return{success:!0,data:await this.mapsTools.getElevation(r)}}catch(e){return{success:!1,error:e instanceof Error?e.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};
|