@cablate/mcp-google-map 0.0.1 → 0.0.3
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 +30 -3
- package/dist/index.cjs +487 -10
- package/dist/index.js +197 -0
- package/dist/maps-tools/mapsTools.js +167 -0
- package/dist/maps-tools/searchPlaces.js +144 -0
- package/dist/maps-tools/toolclass.js +248 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,24 @@ A powerful Model Context Protocol (MCP) server providing comprehensive Google Ma
|
|
|
6
6
|
|
|
7
7
|
### Google Maps Features
|
|
8
8
|
|
|
9
|
-
- Location
|
|
10
|
-
|
|
11
|
-
-
|
|
9
|
+
- **Location Search**
|
|
10
|
+
|
|
11
|
+
- Search for places near a specific location with customizable radius and filters
|
|
12
|
+
- Get detailed place information including ratings, opening hours, and contact details
|
|
13
|
+
|
|
14
|
+
- **Geocoding Services**
|
|
15
|
+
|
|
16
|
+
- Convert addresses to coordinates (geocoding)
|
|
17
|
+
- Convert coordinates to addresses (reverse geocoding)
|
|
18
|
+
|
|
19
|
+
- **Distance & Directions**
|
|
20
|
+
|
|
21
|
+
- Calculate distances and travel times between multiple origins and destinations
|
|
22
|
+
- Get detailed directions between two points with step-by-step instructions
|
|
23
|
+
- Support for different travel modes (driving, walking, bicycling, transit)
|
|
24
|
+
|
|
25
|
+
- **Elevation Data**
|
|
26
|
+
- Retrieve elevation data for specific locations
|
|
12
27
|
|
|
13
28
|
## Installation
|
|
14
29
|
|
|
@@ -48,6 +63,18 @@ mcp-google-map
|
|
|
48
63
|
|
|
49
64
|
3. Click "Save" to complete the installation
|
|
50
65
|
|
|
66
|
+
## Available Tools
|
|
67
|
+
|
|
68
|
+
The server provides the following tools:
|
|
69
|
+
|
|
70
|
+
1. **search_nearby** - Search for places near a specific location
|
|
71
|
+
2. **get_place_details** - Get detailed information about a specific place
|
|
72
|
+
3. **maps_geocode** - Convert an address to coordinates
|
|
73
|
+
4. **maps_reverse_geocode** - Convert coordinates to an address
|
|
74
|
+
5. **maps_distance_matrix** - Calculate distances and times between multiple origins and destinations
|
|
75
|
+
6. **maps_directions** - Get directions between two points
|
|
76
|
+
7. **maps_elevation** - Get elevation data for specific locations
|
|
77
|
+
|
|
51
78
|
## Google Maps API Setup
|
|
52
79
|
|
|
53
80
|
To use this service, you need to:
|
package/dist/index.cjs
CHANGED
|
@@ -17127,13 +17127,13 @@ var require_common = __commonJS({
|
|
|
17127
17127
|
Language3["zh_CN"] = "zh-CN";
|
|
17128
17128
|
Language3["zh_TW"] = "zh-TW";
|
|
17129
17129
|
})(Language2 || (exports2.Language = Language2 = {}));
|
|
17130
|
-
var
|
|
17131
|
-
(function(
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
})(
|
|
17130
|
+
var TravelMode2;
|
|
17131
|
+
(function(TravelMode3) {
|
|
17132
|
+
TravelMode3["driving"] = "driving";
|
|
17133
|
+
TravelMode3["walking"] = "walking";
|
|
17134
|
+
TravelMode3["bicycling"] = "bicycling";
|
|
17135
|
+
TravelMode3["transit"] = "transit";
|
|
17136
|
+
})(TravelMode2 || (exports2.TravelMode = TravelMode2 = {}));
|
|
17137
17137
|
var TravelRestriction;
|
|
17138
17138
|
(function(TravelRestriction2) {
|
|
17139
17139
|
TravelRestriction2["tolls"] = "tolls";
|
|
@@ -23407,6 +23407,129 @@ var SEARCH_NEARBY_TOOL = {
|
|
|
23407
23407
|
required: ["center"]
|
|
23408
23408
|
}
|
|
23409
23409
|
};
|
|
23410
|
+
var GEOCODE_TOOL = {
|
|
23411
|
+
name: "maps_geocode",
|
|
23412
|
+
description: "\u5C07\u5730\u5740\u8F49\u63DB\u70BA\u5EA7\u6A19",
|
|
23413
|
+
inputSchema: {
|
|
23414
|
+
type: "object",
|
|
23415
|
+
properties: {
|
|
23416
|
+
address: {
|
|
23417
|
+
type: "string",
|
|
23418
|
+
description: "\u8981\u8F49\u63DB\u7684\u5730\u5740\u6216\u5730\u6A19\u540D\u7A31"
|
|
23419
|
+
}
|
|
23420
|
+
},
|
|
23421
|
+
required: ["address"]
|
|
23422
|
+
}
|
|
23423
|
+
};
|
|
23424
|
+
var REVERSE_GEOCODE_TOOL = {
|
|
23425
|
+
name: "maps_reverse_geocode",
|
|
23426
|
+
description: "\u5C07\u5EA7\u6A19\u8F49\u63DB\u70BA\u5730\u5740",
|
|
23427
|
+
inputSchema: {
|
|
23428
|
+
type: "object",
|
|
23429
|
+
properties: {
|
|
23430
|
+
latitude: {
|
|
23431
|
+
type: "number",
|
|
23432
|
+
description: "\u7DEF\u5EA6"
|
|
23433
|
+
},
|
|
23434
|
+
longitude: {
|
|
23435
|
+
type: "number",
|
|
23436
|
+
description: "\u7D93\u5EA6"
|
|
23437
|
+
}
|
|
23438
|
+
},
|
|
23439
|
+
required: ["latitude", "longitude"]
|
|
23440
|
+
}
|
|
23441
|
+
};
|
|
23442
|
+
var DISTANCE_MATRIX_TOOL = {
|
|
23443
|
+
name: "maps_distance_matrix",
|
|
23444
|
+
description: "\u8A08\u7B97\u591A\u500B\u8D77\u9EDE\u548C\u7D42\u9EDE\u4E4B\u9593\u7684\u8DDD\u96E2\u548C\u6642\u9593",
|
|
23445
|
+
inputSchema: {
|
|
23446
|
+
type: "object",
|
|
23447
|
+
properties: {
|
|
23448
|
+
origins: {
|
|
23449
|
+
type: "array",
|
|
23450
|
+
items: {
|
|
23451
|
+
type: "string"
|
|
23452
|
+
},
|
|
23453
|
+
description: "\u8D77\u9EDE\u5730\u5740\u6216\u5EA7\u6A19\u5217\u8868"
|
|
23454
|
+
},
|
|
23455
|
+
destinations: {
|
|
23456
|
+
type: "array",
|
|
23457
|
+
items: {
|
|
23458
|
+
type: "string"
|
|
23459
|
+
},
|
|
23460
|
+
description: "\u7D42\u9EDE\u5730\u5740\u6216\u5EA7\u6A19\u5217\u8868"
|
|
23461
|
+
},
|
|
23462
|
+
mode: {
|
|
23463
|
+
type: "string",
|
|
23464
|
+
enum: ["driving", "walking", "bicycling", "transit"],
|
|
23465
|
+
description: "\u4EA4\u901A\u6A21\u5F0F",
|
|
23466
|
+
default: "driving"
|
|
23467
|
+
}
|
|
23468
|
+
},
|
|
23469
|
+
required: ["origins", "destinations"]
|
|
23470
|
+
}
|
|
23471
|
+
};
|
|
23472
|
+
var DIRECTIONS_TOOL = {
|
|
23473
|
+
name: "maps_directions",
|
|
23474
|
+
description: "\u7372\u53D6\u5169\u9EDE\u4E4B\u9593\u7684\u8A73\u7D30\u5C0E\u822A\u8DEF\u7DDA",
|
|
23475
|
+
inputSchema: {
|
|
23476
|
+
type: "object",
|
|
23477
|
+
properties: {
|
|
23478
|
+
origin: {
|
|
23479
|
+
type: "string",
|
|
23480
|
+
description: "\u8D77\u9EDE\u5730\u5740\u6216\u5EA7\u6A19"
|
|
23481
|
+
},
|
|
23482
|
+
destination: {
|
|
23483
|
+
type: "string",
|
|
23484
|
+
description: "\u7D42\u9EDE\u5730\u5740\u6216\u5EA7\u6A19"
|
|
23485
|
+
},
|
|
23486
|
+
mode: {
|
|
23487
|
+
type: "string",
|
|
23488
|
+
enum: ["driving", "walking", "bicycling", "transit"],
|
|
23489
|
+
description: "\u4EA4\u901A\u6A21\u5F0F",
|
|
23490
|
+
default: "driving"
|
|
23491
|
+
},
|
|
23492
|
+
departure_time: {
|
|
23493
|
+
type: "string",
|
|
23494
|
+
description: "\u51FA\u767C\u6642\u9593",
|
|
23495
|
+
default: (/* @__PURE__ */ new Date()).toISOString()
|
|
23496
|
+
},
|
|
23497
|
+
arrival_time: {
|
|
23498
|
+
type: "string",
|
|
23499
|
+
description: "\u62B5\u9054\u6642\u9593"
|
|
23500
|
+
}
|
|
23501
|
+
},
|
|
23502
|
+
required: ["origin", "destination"]
|
|
23503
|
+
}
|
|
23504
|
+
};
|
|
23505
|
+
var ELEVATION_TOOL = {
|
|
23506
|
+
name: "maps_elevation",
|
|
23507
|
+
description: "\u7372\u53D6\u4F4D\u7F6E\u7684\u6D77\u62D4\u6578\u64DA",
|
|
23508
|
+
inputSchema: {
|
|
23509
|
+
type: "object",
|
|
23510
|
+
properties: {
|
|
23511
|
+
locations: {
|
|
23512
|
+
type: "array",
|
|
23513
|
+
items: {
|
|
23514
|
+
type: "object",
|
|
23515
|
+
properties: {
|
|
23516
|
+
latitude: {
|
|
23517
|
+
type: "number",
|
|
23518
|
+
description: "\u7DEF\u5EA6"
|
|
23519
|
+
},
|
|
23520
|
+
longitude: {
|
|
23521
|
+
type: "number",
|
|
23522
|
+
description: "\u7D93\u5EA6"
|
|
23523
|
+
}
|
|
23524
|
+
},
|
|
23525
|
+
required: ["latitude", "longitude"]
|
|
23526
|
+
},
|
|
23527
|
+
description: "\u8981\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u7684\u4F4D\u7F6E\u5217\u8868"
|
|
23528
|
+
}
|
|
23529
|
+
},
|
|
23530
|
+
required: ["locations"]
|
|
23531
|
+
}
|
|
23532
|
+
};
|
|
23410
23533
|
var GET_PLACE_DETAILS_TOOL = {
|
|
23411
23534
|
name: "get_place_details",
|
|
23412
23535
|
description: "\u7372\u53D6\u7279\u5B9A\u5730\u9EDE\u7684\u8A73\u7D30\u8CC7\u8A0A",
|
|
@@ -23485,10 +23608,13 @@ var GoogleMapsTools = class {
|
|
|
23485
23608
|
if (response.data.results.length === 0) {
|
|
23486
23609
|
throw new Error("\u627E\u4E0D\u5230\u8A72\u5730\u5740\u7684\u4F4D\u7F6E");
|
|
23487
23610
|
}
|
|
23488
|
-
const
|
|
23611
|
+
const result = response.data.results[0];
|
|
23612
|
+
const location = result.geometry.location;
|
|
23489
23613
|
return {
|
|
23490
23614
|
lat: location.lat,
|
|
23491
|
-
lng: location.lng
|
|
23615
|
+
lng: location.lng,
|
|
23616
|
+
formatted_address: result.formatted_address,
|
|
23617
|
+
place_id: result.place_id
|
|
23492
23618
|
};
|
|
23493
23619
|
} catch (error) {
|
|
23494
23620
|
console.error("Error in geocodeAddress:", error);
|
|
@@ -23508,6 +23634,190 @@ var GoogleMapsTools = class {
|
|
|
23508
23634
|
}
|
|
23509
23635
|
return this.geocodeAddress(center.value);
|
|
23510
23636
|
}
|
|
23637
|
+
// 新增公開方法用於地址轉座標
|
|
23638
|
+
async geocode(address) {
|
|
23639
|
+
try {
|
|
23640
|
+
const result = await this.geocodeAddress(address);
|
|
23641
|
+
return {
|
|
23642
|
+
location: { lat: result.lat, lng: result.lng },
|
|
23643
|
+
formatted_address: result.formatted_address || "",
|
|
23644
|
+
place_id: result.place_id || ""
|
|
23645
|
+
};
|
|
23646
|
+
} catch (error) {
|
|
23647
|
+
console.error("Error in geocode:", error);
|
|
23648
|
+
throw new Error("\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u6642\u767C\u751F\u932F\u8AA4");
|
|
23649
|
+
}
|
|
23650
|
+
}
|
|
23651
|
+
async reverseGeocode(latitude, longitude) {
|
|
23652
|
+
try {
|
|
23653
|
+
const response = await this.client.reverseGeocode({
|
|
23654
|
+
params: {
|
|
23655
|
+
latlng: { lat: latitude, lng: longitude },
|
|
23656
|
+
language: this.defaultLanguage,
|
|
23657
|
+
key: process.env.GOOGLE_MAPS_API_KEY || ""
|
|
23658
|
+
}
|
|
23659
|
+
});
|
|
23660
|
+
if (response.data.results.length === 0) {
|
|
23661
|
+
throw new Error("\u627E\u4E0D\u5230\u8A72\u5EA7\u6A19\u7684\u5730\u5740");
|
|
23662
|
+
}
|
|
23663
|
+
const result = response.data.results[0];
|
|
23664
|
+
return {
|
|
23665
|
+
formatted_address: result.formatted_address,
|
|
23666
|
+
place_id: result.place_id,
|
|
23667
|
+
address_components: result.address_components
|
|
23668
|
+
};
|
|
23669
|
+
} catch (error) {
|
|
23670
|
+
console.error("Error in reverseGeocode:", error);
|
|
23671
|
+
throw new Error("\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u6642\u767C\u751F\u932F\u8AA4");
|
|
23672
|
+
}
|
|
23673
|
+
}
|
|
23674
|
+
async calculateDistanceMatrix(origins, destinations, mode = "driving") {
|
|
23675
|
+
try {
|
|
23676
|
+
const response = await this.client.distancematrix({
|
|
23677
|
+
params: {
|
|
23678
|
+
origins,
|
|
23679
|
+
destinations,
|
|
23680
|
+
mode,
|
|
23681
|
+
language: this.defaultLanguage,
|
|
23682
|
+
key: process.env.GOOGLE_MAPS_API_KEY || ""
|
|
23683
|
+
}
|
|
23684
|
+
});
|
|
23685
|
+
const result = response.data;
|
|
23686
|
+
if (result.status !== "OK") {
|
|
23687
|
+
throw new Error(`\u8DDD\u96E2\u77E9\u9663\u8A08\u7B97\u5931\u6557: ${result.status}`);
|
|
23688
|
+
}
|
|
23689
|
+
const distances = [];
|
|
23690
|
+
const durations = [];
|
|
23691
|
+
result.rows.forEach((row) => {
|
|
23692
|
+
const distanceRow = [];
|
|
23693
|
+
const durationRow = [];
|
|
23694
|
+
row.elements.forEach((element) => {
|
|
23695
|
+
if (element.status === "OK") {
|
|
23696
|
+
distanceRow.push({
|
|
23697
|
+
value: element.distance.value,
|
|
23698
|
+
text: element.distance.text
|
|
23699
|
+
});
|
|
23700
|
+
durationRow.push({
|
|
23701
|
+
value: element.duration.value,
|
|
23702
|
+
text: element.duration.text
|
|
23703
|
+
});
|
|
23704
|
+
} else {
|
|
23705
|
+
distanceRow.push(null);
|
|
23706
|
+
durationRow.push(null);
|
|
23707
|
+
}
|
|
23708
|
+
});
|
|
23709
|
+
distances.push(distanceRow);
|
|
23710
|
+
durations.push(durationRow);
|
|
23711
|
+
});
|
|
23712
|
+
return {
|
|
23713
|
+
distances,
|
|
23714
|
+
durations,
|
|
23715
|
+
origin_addresses: result.origin_addresses,
|
|
23716
|
+
destination_addresses: result.destination_addresses
|
|
23717
|
+
};
|
|
23718
|
+
} catch (error) {
|
|
23719
|
+
console.error("Error in calculateDistanceMatrix:", error);
|
|
23720
|
+
throw new Error("\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u6642\u767C\u751F\u932F\u8AA4");
|
|
23721
|
+
}
|
|
23722
|
+
}
|
|
23723
|
+
async getDirections(origin, destination, mode = "driving", departure_time, arrival_time) {
|
|
23724
|
+
try {
|
|
23725
|
+
let apiArrivalTime = void 0;
|
|
23726
|
+
if (arrival_time) {
|
|
23727
|
+
apiArrivalTime = Math.floor(arrival_time.getTime() / 1e3);
|
|
23728
|
+
}
|
|
23729
|
+
let apiDepartureTime = void 0;
|
|
23730
|
+
if (!apiArrivalTime) {
|
|
23731
|
+
if (departure_time instanceof Date) {
|
|
23732
|
+
apiDepartureTime = Math.floor(departure_time.getTime() / 1e3);
|
|
23733
|
+
} else if (departure_time) {
|
|
23734
|
+
apiDepartureTime = departure_time;
|
|
23735
|
+
} else {
|
|
23736
|
+
apiDepartureTime = "now";
|
|
23737
|
+
}
|
|
23738
|
+
}
|
|
23739
|
+
const response = await this.client.directions({
|
|
23740
|
+
params: {
|
|
23741
|
+
origin,
|
|
23742
|
+
destination,
|
|
23743
|
+
mode,
|
|
23744
|
+
language: this.defaultLanguage,
|
|
23745
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
23746
|
+
arrival_time: apiArrivalTime,
|
|
23747
|
+
departure_time: apiDepartureTime
|
|
23748
|
+
}
|
|
23749
|
+
});
|
|
23750
|
+
const result = response.data;
|
|
23751
|
+
if (result.status !== "OK") {
|
|
23752
|
+
throw new Error(`\u8DEF\u7DDA\u6307\u5F15\u7372\u53D6\u5931\u6557: ${result.status} (arrival_time: ${apiArrivalTime}, departure_time: ${apiDepartureTime})`);
|
|
23753
|
+
}
|
|
23754
|
+
if (result.routes.length === 0) {
|
|
23755
|
+
throw new Error("\u627E\u4E0D\u5230\u8DEF\u7DDA");
|
|
23756
|
+
}
|
|
23757
|
+
const route = result.routes[0];
|
|
23758
|
+
const legs = route.legs[0];
|
|
23759
|
+
const formatTime = (timeInfo) => {
|
|
23760
|
+
if (!timeInfo || typeof timeInfo.value !== "number") return "";
|
|
23761
|
+
const date = new Date(timeInfo.value * 1e3);
|
|
23762
|
+
const options = {
|
|
23763
|
+
year: "numeric",
|
|
23764
|
+
month: "2-digit",
|
|
23765
|
+
day: "2-digit",
|
|
23766
|
+
hour: "2-digit",
|
|
23767
|
+
minute: "2-digit",
|
|
23768
|
+
second: "2-digit",
|
|
23769
|
+
hour12: false
|
|
23770
|
+
// Use 24-hour format
|
|
23771
|
+
};
|
|
23772
|
+
if (timeInfo.time_zone && typeof timeInfo.time_zone === "string") {
|
|
23773
|
+
options.timeZone = timeInfo.time_zone;
|
|
23774
|
+
}
|
|
23775
|
+
return date.toLocaleString(this.defaultLanguage.toString(), options);
|
|
23776
|
+
};
|
|
23777
|
+
return {
|
|
23778
|
+
routes: result.routes,
|
|
23779
|
+
summary: route.summary,
|
|
23780
|
+
total_distance: {
|
|
23781
|
+
value: legs.distance.value,
|
|
23782
|
+
text: legs.distance.text
|
|
23783
|
+
},
|
|
23784
|
+
total_duration: {
|
|
23785
|
+
value: legs.duration.value,
|
|
23786
|
+
text: legs.duration.text
|
|
23787
|
+
},
|
|
23788
|
+
arrival_time: formatTime(legs.arrival_time),
|
|
23789
|
+
departure_time: formatTime(legs.departure_time)
|
|
23790
|
+
};
|
|
23791
|
+
} catch (error) {
|
|
23792
|
+
console.error("Error in getDirections:", error);
|
|
23793
|
+
throw new Error("\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u6642\u767C\u751F\u932F\u8AA4" + error);
|
|
23794
|
+
}
|
|
23795
|
+
}
|
|
23796
|
+
async getElevation(locations) {
|
|
23797
|
+
try {
|
|
23798
|
+
const formattedLocations = locations.map((loc) => ({
|
|
23799
|
+
lat: loc.latitude,
|
|
23800
|
+
lng: loc.longitude
|
|
23801
|
+
}));
|
|
23802
|
+
const response = await this.client.elevation({
|
|
23803
|
+
params: {
|
|
23804
|
+
locations: formattedLocations,
|
|
23805
|
+
key: process.env.GOOGLE_MAPS_API_KEY || ""
|
|
23806
|
+
}
|
|
23807
|
+
});
|
|
23808
|
+
const result = response.data;
|
|
23809
|
+
if (result.status !== "OK") {
|
|
23810
|
+
throw new Error(`\u6D77\u62D4\u6578\u64DA\u7372\u53D6\u5931\u6557: ${result.status}`);
|
|
23811
|
+
}
|
|
23812
|
+
return result.results.map((item, index) => ({
|
|
23813
|
+
elevation: item.elevation,
|
|
23814
|
+
location: formattedLocations[index]
|
|
23815
|
+
}));
|
|
23816
|
+
} catch (error) {
|
|
23817
|
+
console.error("Error in getElevation:", error);
|
|
23818
|
+
throw new Error("\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u6642\u767C\u751F\u932F\u8AA4");
|
|
23819
|
+
}
|
|
23820
|
+
}
|
|
23511
23821
|
};
|
|
23512
23822
|
|
|
23513
23823
|
// src/maps-tools/searchPlaces.ts
|
|
@@ -23576,10 +23886,82 @@ var PlacesSearcher = class {
|
|
|
23576
23886
|
};
|
|
23577
23887
|
}
|
|
23578
23888
|
}
|
|
23889
|
+
async geocode(address) {
|
|
23890
|
+
try {
|
|
23891
|
+
const result = await this.mapsTools.geocode(address);
|
|
23892
|
+
return {
|
|
23893
|
+
success: true,
|
|
23894
|
+
data: result
|
|
23895
|
+
};
|
|
23896
|
+
} catch (error) {
|
|
23897
|
+
return {
|
|
23898
|
+
success: false,
|
|
23899
|
+
error: error instanceof Error ? error.message : "\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u6642\u767C\u751F\u932F\u8AA4"
|
|
23900
|
+
};
|
|
23901
|
+
}
|
|
23902
|
+
}
|
|
23903
|
+
async reverseGeocode(latitude, longitude) {
|
|
23904
|
+
try {
|
|
23905
|
+
const result = await this.mapsTools.reverseGeocode(latitude, longitude);
|
|
23906
|
+
return {
|
|
23907
|
+
success: true,
|
|
23908
|
+
data: result
|
|
23909
|
+
};
|
|
23910
|
+
} catch (error) {
|
|
23911
|
+
return {
|
|
23912
|
+
success: false,
|
|
23913
|
+
error: error instanceof Error ? error.message : "\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u6642\u767C\u751F\u932F\u8AA4"
|
|
23914
|
+
};
|
|
23915
|
+
}
|
|
23916
|
+
}
|
|
23917
|
+
async calculateDistanceMatrix(origins, destinations, mode = "driving") {
|
|
23918
|
+
try {
|
|
23919
|
+
const result = await this.mapsTools.calculateDistanceMatrix(origins, destinations, mode);
|
|
23920
|
+
return {
|
|
23921
|
+
success: true,
|
|
23922
|
+
data: result
|
|
23923
|
+
};
|
|
23924
|
+
} catch (error) {
|
|
23925
|
+
return {
|
|
23926
|
+
success: false,
|
|
23927
|
+
error: error instanceof Error ? error.message : "\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u6642\u767C\u751F\u932F\u8AA4"
|
|
23928
|
+
};
|
|
23929
|
+
}
|
|
23930
|
+
}
|
|
23931
|
+
async getDirections(origin, destination, mode = "driving", departure_time, arrival_time) {
|
|
23932
|
+
try {
|
|
23933
|
+
const departureTime = departure_time ? new Date(departure_time) : /* @__PURE__ */ new Date();
|
|
23934
|
+
const arrivalTime = arrival_time ? new Date(arrival_time) : void 0;
|
|
23935
|
+
const result = await this.mapsTools.getDirections(origin, destination, mode, departureTime, arrivalTime);
|
|
23936
|
+
return {
|
|
23937
|
+
success: true,
|
|
23938
|
+
data: result
|
|
23939
|
+
};
|
|
23940
|
+
} catch (error) {
|
|
23941
|
+
return {
|
|
23942
|
+
success: false,
|
|
23943
|
+
error: error instanceof Error ? error.message : "\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u6642\u767C\u751F\u932F\u8AA4"
|
|
23944
|
+
};
|
|
23945
|
+
}
|
|
23946
|
+
}
|
|
23947
|
+
async getElevation(locations) {
|
|
23948
|
+
try {
|
|
23949
|
+
const result = await this.mapsTools.getElevation(locations);
|
|
23950
|
+
return {
|
|
23951
|
+
success: true,
|
|
23952
|
+
data: result
|
|
23953
|
+
};
|
|
23954
|
+
} catch (error) {
|
|
23955
|
+
return {
|
|
23956
|
+
success: false,
|
|
23957
|
+
error: error instanceof Error ? error.message : "\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u6642\u767C\u751F\u932F\u8AA4"
|
|
23958
|
+
};
|
|
23959
|
+
}
|
|
23960
|
+
}
|
|
23579
23961
|
};
|
|
23580
23962
|
|
|
23581
23963
|
// src/index.ts
|
|
23582
|
-
var tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL];
|
|
23964
|
+
var tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL, GEOCODE_TOOL, REVERSE_GEOCODE_TOOL, DISTANCE_MATRIX_TOOL, DIRECTIONS_TOOL, ELEVATION_TOOL];
|
|
23583
23965
|
var placesSearcher = new PlacesSearcher();
|
|
23584
23966
|
var server = new Server(
|
|
23585
23967
|
{
|
|
@@ -23647,6 +24029,101 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
23647
24029
|
isError: false
|
|
23648
24030
|
};
|
|
23649
24031
|
}
|
|
24032
|
+
if (name === "maps_geocode") {
|
|
24033
|
+
const { address } = args;
|
|
24034
|
+
const result = await placesSearcher.geocode(address);
|
|
24035
|
+
if (!result.success) {
|
|
24036
|
+
return {
|
|
24037
|
+
content: [{ type: "text", text: result.error || "\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u5931\u6557" }],
|
|
24038
|
+
isError: true
|
|
24039
|
+
};
|
|
24040
|
+
}
|
|
24041
|
+
return {
|
|
24042
|
+
content: [
|
|
24043
|
+
{
|
|
24044
|
+
type: "text",
|
|
24045
|
+
text: JSON.stringify(result.data, null, 2)
|
|
24046
|
+
}
|
|
24047
|
+
],
|
|
24048
|
+
isError: false
|
|
24049
|
+
};
|
|
24050
|
+
}
|
|
24051
|
+
if (name === "maps_reverse_geocode") {
|
|
24052
|
+
const { latitude, longitude } = args;
|
|
24053
|
+
const result = await placesSearcher.reverseGeocode(latitude, longitude);
|
|
24054
|
+
if (!result.success) {
|
|
24055
|
+
return {
|
|
24056
|
+
content: [{ type: "text", text: result.error || "\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u5931\u6557" }],
|
|
24057
|
+
isError: true
|
|
24058
|
+
};
|
|
24059
|
+
}
|
|
24060
|
+
return {
|
|
24061
|
+
content: [
|
|
24062
|
+
{
|
|
24063
|
+
type: "text",
|
|
24064
|
+
text: JSON.stringify(result.data, null, 2)
|
|
24065
|
+
}
|
|
24066
|
+
],
|
|
24067
|
+
isError: false
|
|
24068
|
+
};
|
|
24069
|
+
}
|
|
24070
|
+
if (name === "maps_distance_matrix") {
|
|
24071
|
+
const { origins, destinations, mode } = args;
|
|
24072
|
+
const result = await placesSearcher.calculateDistanceMatrix(origins, destinations, mode || "driving");
|
|
24073
|
+
if (!result.success) {
|
|
24074
|
+
return {
|
|
24075
|
+
content: [{ type: "text", text: result.error || "\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u5931\u6557" }],
|
|
24076
|
+
isError: true
|
|
24077
|
+
};
|
|
24078
|
+
}
|
|
24079
|
+
return {
|
|
24080
|
+
content: [
|
|
24081
|
+
{
|
|
24082
|
+
type: "text",
|
|
24083
|
+
text: JSON.stringify(result.data, null, 2)
|
|
24084
|
+
}
|
|
24085
|
+
],
|
|
24086
|
+
isError: false
|
|
24087
|
+
};
|
|
24088
|
+
}
|
|
24089
|
+
if (name === "maps_directions") {
|
|
24090
|
+
const { origin, destination, mode, arrival_time, departure_time } = args;
|
|
24091
|
+
const result = await placesSearcher.getDirections(origin, destination, mode, departure_time, arrival_time);
|
|
24092
|
+
if (!result.success) {
|
|
24093
|
+
return {
|
|
24094
|
+
content: [{ type: "text", text: result.error || "\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u5931\u6557" }],
|
|
24095
|
+
isError: true
|
|
24096
|
+
};
|
|
24097
|
+
}
|
|
24098
|
+
return {
|
|
24099
|
+
content: [
|
|
24100
|
+
{
|
|
24101
|
+
type: "text",
|
|
24102
|
+
text: JSON.stringify(result.data, null, 2)
|
|
24103
|
+
}
|
|
24104
|
+
],
|
|
24105
|
+
isError: false
|
|
24106
|
+
};
|
|
24107
|
+
}
|
|
24108
|
+
if (name === "maps_elevation") {
|
|
24109
|
+
const { locations } = args;
|
|
24110
|
+
const result = await placesSearcher.getElevation(locations);
|
|
24111
|
+
if (!result.success) {
|
|
24112
|
+
return {
|
|
24113
|
+
content: [{ type: "text", text: result.error || "\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u5931\u6557" }],
|
|
24114
|
+
isError: true
|
|
24115
|
+
};
|
|
24116
|
+
}
|
|
24117
|
+
return {
|
|
24118
|
+
content: [
|
|
24119
|
+
{
|
|
24120
|
+
type: "text",
|
|
24121
|
+
text: JSON.stringify(result.data, null, 2)
|
|
24122
|
+
}
|
|
24123
|
+
],
|
|
24124
|
+
isError: false
|
|
24125
|
+
};
|
|
24126
|
+
}
|
|
23650
24127
|
return {
|
|
23651
24128
|
content: [{ type: "text", text: `\u932F\u8AA4\uFF1A\u672A\u77E5\u7684\u5DE5\u5177 ${name}` }],
|
|
23652
24129
|
isError: true
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { DIRECTIONS_TOOL, DISTANCE_MATRIX_TOOL, ELEVATION_TOOL, GEOCODE_TOOL, GET_PLACE_DETAILS_TOOL, REVERSE_GEOCODE_TOOL, SEARCH_NEARBY_TOOL } from "./maps-tools/mapsTools.js";
|
|
6
|
+
import { PlacesSearcher } from "./maps-tools/searchPlaces.js";
|
|
7
|
+
const tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL, GEOCODE_TOOL, REVERSE_GEOCODE_TOOL, DISTANCE_MATRIX_TOOL, DIRECTIONS_TOOL, ELEVATION_TOOL];
|
|
8
|
+
const placesSearcher = new PlacesSearcher();
|
|
9
|
+
const server = new Server({
|
|
10
|
+
name: "mcp-server/maps_executor",
|
|
11
|
+
version: "0.0.1",
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
description: "An MCP server providing Google Maps integration!",
|
|
15
|
+
tools: {},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
19
|
+
tools,
|
|
20
|
+
}));
|
|
21
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
22
|
+
try {
|
|
23
|
+
const { name, arguments: args } = request.params;
|
|
24
|
+
if (!args) {
|
|
25
|
+
throw new Error("No parameters provided");
|
|
26
|
+
}
|
|
27
|
+
if (name === "search_nearby") {
|
|
28
|
+
const { center, keyword, radius, openNow, minRating } = args;
|
|
29
|
+
const result = await placesSearcher.searchNearby({
|
|
30
|
+
center,
|
|
31
|
+
keyword,
|
|
32
|
+
radius,
|
|
33
|
+
openNow,
|
|
34
|
+
minRating,
|
|
35
|
+
});
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: result.error || "搜尋失敗" }],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `location: ${JSON.stringify(result.location, null, 2)}\n` + JSON.stringify(result.data, null, 2),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
isError: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (name === "get_place_details") {
|
|
53
|
+
const { placeId } = args;
|
|
54
|
+
const result = await placesSearcher.getPlaceDetails(placeId);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: result.error || "獲取詳細資訊失敗" }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify(result.data, null, 2),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
isError: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (name === "maps_geocode") {
|
|
72
|
+
const { address } = args;
|
|
73
|
+
const result = await placesSearcher.geocode(address);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: result.error || "地址轉換座標失敗" }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: JSON.stringify(result.data, null, 2),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
isError: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (name === "maps_reverse_geocode") {
|
|
91
|
+
const { latitude, longitude } = args;
|
|
92
|
+
const result = await placesSearcher.reverseGeocode(latitude, longitude);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: result.error || "座標轉換地址失敗" }],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: JSON.stringify(result.data, null, 2),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
isError: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (name === "maps_distance_matrix") {
|
|
110
|
+
const { origins, destinations, mode } = args;
|
|
111
|
+
const result = await placesSearcher.calculateDistanceMatrix(origins, destinations, mode || "driving");
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: result.error || "計算距離矩陣失敗" }],
|
|
115
|
+
isError: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: JSON.stringify(result.data, null, 2),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
isError: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (name === "maps_directions") {
|
|
129
|
+
const { origin, destination, mode } = args;
|
|
130
|
+
const result = await placesSearcher.getDirections(origin, destination, mode || "driving");
|
|
131
|
+
if (!result.success) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: "text", text: result.error || "獲取路線指引失敗" }],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify(result.data, null, 2),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
isError: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (name === "maps_elevation") {
|
|
148
|
+
const { locations } = args;
|
|
149
|
+
const result = await placesSearcher.getElevation(locations);
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: result.error || "獲取海拔數據失敗" }],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify(result.data, null, 2),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
isError: false,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: `錯誤:未知的工具 ${name}` }],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: `錯誤:${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
isError: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
async function runServer() {
|
|
184
|
+
try {
|
|
185
|
+
const transport = new StdioServerTransport();
|
|
186
|
+
await server.connect(transport);
|
|
187
|
+
console.log("MCP Maps Server started");
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error("Server startup failed:", error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
runServer().catch((error) => {
|
|
195
|
+
console.error("Server encountered a critical error:", error);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const SEARCH_NEARBY_TOOL = {
|
|
2
|
+
name: "search_nearby",
|
|
3
|
+
description: "搜尋附近的地點",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
center: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
value: { type: "string", description: "地址、地標名稱或經緯度座標(經緯度座標格式: lat,lng)" },
|
|
11
|
+
isCoordinates: { type: "boolean", description: "是否為經緯度座標", default: false },
|
|
12
|
+
},
|
|
13
|
+
required: ["value"],
|
|
14
|
+
description: "搜尋中心點",
|
|
15
|
+
},
|
|
16
|
+
keyword: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "搜尋關鍵字(例如:餐廳、咖啡廳)",
|
|
19
|
+
},
|
|
20
|
+
radius: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "搜尋半徑(公尺)",
|
|
23
|
+
default: 1000,
|
|
24
|
+
},
|
|
25
|
+
openNow: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "是否只顯示營業中的地點",
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
minRating: {
|
|
31
|
+
type: "number",
|
|
32
|
+
description: "最低評分要求(0-5)",
|
|
33
|
+
minimum: 0,
|
|
34
|
+
maximum: 5,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["center"],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const GEOCODE_TOOL = {
|
|
41
|
+
name: "maps_geocode",
|
|
42
|
+
description: "將地址轉換為座標",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
address: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "要轉換的地址或地標名稱",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ["address"],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export const REVERSE_GEOCODE_TOOL = {
|
|
55
|
+
name: "maps_reverse_geocode",
|
|
56
|
+
description: "將座標轉換為地址",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
latitude: {
|
|
61
|
+
type: "number",
|
|
62
|
+
description: "緯度",
|
|
63
|
+
},
|
|
64
|
+
longitude: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: "經度",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ["latitude", "longitude"],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const DISTANCE_MATRIX_TOOL = {
|
|
73
|
+
name: "maps_distance_matrix",
|
|
74
|
+
description: "計算多個起點和終點之間的距離和時間",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
origins: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: {
|
|
81
|
+
type: "string",
|
|
82
|
+
},
|
|
83
|
+
description: "起點地址或座標列表",
|
|
84
|
+
},
|
|
85
|
+
destinations: {
|
|
86
|
+
type: "array",
|
|
87
|
+
items: {
|
|
88
|
+
type: "string",
|
|
89
|
+
},
|
|
90
|
+
description: "終點地址或座標列表",
|
|
91
|
+
},
|
|
92
|
+
mode: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: ["driving", "walking", "bicycling", "transit"],
|
|
95
|
+
description: "交通模式",
|
|
96
|
+
default: "driving",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["origins", "destinations"],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
export const DIRECTIONS_TOOL = {
|
|
103
|
+
name: "maps_directions",
|
|
104
|
+
description: "獲取兩點之間的路線指引",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
origin: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: "起點地址或座標",
|
|
111
|
+
},
|
|
112
|
+
destination: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "終點地址或座標",
|
|
115
|
+
},
|
|
116
|
+
mode: {
|
|
117
|
+
type: "string",
|
|
118
|
+
enum: ["driving", "walking", "bicycling", "transit"],
|
|
119
|
+
description: "交通模式",
|
|
120
|
+
default: "driving",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["origin", "destination"],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
export const ELEVATION_TOOL = {
|
|
127
|
+
name: "maps_elevation",
|
|
128
|
+
description: "獲取位置的海拔數據",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
locations: {
|
|
133
|
+
type: "array",
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
latitude: {
|
|
138
|
+
type: "number",
|
|
139
|
+
description: "緯度",
|
|
140
|
+
},
|
|
141
|
+
longitude: {
|
|
142
|
+
type: "number",
|
|
143
|
+
description: "經度",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
required: ["latitude", "longitude"],
|
|
147
|
+
},
|
|
148
|
+
description: "要獲取海拔數據的位置列表",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ["locations"],
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
export const GET_PLACE_DETAILS_TOOL = {
|
|
155
|
+
name: "get_place_details",
|
|
156
|
+
description: "獲取特定地點的詳細資訊",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
placeId: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Google Maps 地點 ID",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
required: ["placeId"],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { GoogleMapsTools } from "./toolclass.js";
|
|
2
|
+
export class PlacesSearcher {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.mapsTools = new GoogleMapsTools();
|
|
5
|
+
}
|
|
6
|
+
async searchNearby(params) {
|
|
7
|
+
try {
|
|
8
|
+
const location = await this.mapsTools.getLocation(params.center);
|
|
9
|
+
console.error(location);
|
|
10
|
+
const places = await this.mapsTools.searchNearbyPlaces({
|
|
11
|
+
location,
|
|
12
|
+
keyword: params.keyword,
|
|
13
|
+
radius: params.radius,
|
|
14
|
+
openNow: params.openNow,
|
|
15
|
+
minRating: params.minRating,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
location: location,
|
|
19
|
+
success: true,
|
|
20
|
+
data: places.map((place) => ({
|
|
21
|
+
name: place.name,
|
|
22
|
+
place_id: place.place_id,
|
|
23
|
+
address: place.formatted_address,
|
|
24
|
+
location: place.geometry.location,
|
|
25
|
+
rating: place.rating,
|
|
26
|
+
total_ratings: place.user_ratings_total,
|
|
27
|
+
open_now: place.opening_hours?.open_now,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: error instanceof Error ? error.message : "搜尋時發生錯誤",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async getPlaceDetails(placeId) {
|
|
39
|
+
try {
|
|
40
|
+
const details = await this.mapsTools.getPlaceDetails(placeId);
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
data: {
|
|
44
|
+
name: details.name,
|
|
45
|
+
address: details.formatted_address,
|
|
46
|
+
location: details.geometry?.location,
|
|
47
|
+
rating: details.rating,
|
|
48
|
+
total_ratings: details.user_ratings_total,
|
|
49
|
+
open_now: details.opening_hours?.open_now,
|
|
50
|
+
phone: details.formatted_phone_number,
|
|
51
|
+
website: details.website,
|
|
52
|
+
price_level: details.price_level,
|
|
53
|
+
reviews: details.reviews?.map((review) => ({
|
|
54
|
+
rating: review.rating,
|
|
55
|
+
text: review.text,
|
|
56
|
+
time: review.time,
|
|
57
|
+
author_name: review.author_name,
|
|
58
|
+
})),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: error instanceof Error ? error.message : "獲取詳細資訊時發生錯誤",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async geocode(address) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.mapsTools.geocode(address);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: result,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
error: error instanceof Error ? error.message : "地址轉換座標時發生錯誤",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async reverseGeocode(latitude, longitude) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.mapsTools.reverseGeocode(latitude, longitude);
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
data: result,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: error instanceof Error ? error.message : "座標轉換地址時發生錯誤",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async calculateDistanceMatrix(origins, destinations, mode = "driving") {
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.mapsTools.calculateDistanceMatrix(origins, destinations, mode);
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
data: result,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: error instanceof Error ? error.message : "計算距離矩陣時發生錯誤",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async getDirections(origin, destination, mode = "driving") {
|
|
115
|
+
try {
|
|
116
|
+
const result = await this.mapsTools.getDirections(origin, destination, mode);
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
data: result,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: error instanceof Error ? error.message : "獲取路線指引時發生錯誤",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async getElevation(locations) {
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.mapsTools.getElevation(locations);
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
data: result,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: error instanceof Error ? error.message : "獲取海拔數據時發生錯誤",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Client, Language } from "@googlemaps/google-maps-services-js";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
// 確保環境變數被載入
|
|
4
|
+
dotenv.config();
|
|
5
|
+
export class GoogleMapsTools {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.defaultLanguage = Language.zh_TW;
|
|
8
|
+
this.client = new Client({});
|
|
9
|
+
if (!process.env.GOOGLE_MAPS_API_KEY) {
|
|
10
|
+
throw new Error("Google Maps API Key is required");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async searchNearbyPlaces(params) {
|
|
14
|
+
const searchParams = {
|
|
15
|
+
location: params.location,
|
|
16
|
+
radius: params.radius || 1000,
|
|
17
|
+
keyword: params.keyword,
|
|
18
|
+
opennow: params.openNow,
|
|
19
|
+
language: this.defaultLanguage,
|
|
20
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const response = await this.client.placesNearby({
|
|
24
|
+
params: searchParams,
|
|
25
|
+
});
|
|
26
|
+
let results = response.data.results;
|
|
27
|
+
// 如果有最低評分要求,進行過濾
|
|
28
|
+
if (params.minRating) {
|
|
29
|
+
results = results.filter((place) => (place.rating || 0) >= (params.minRating || 0));
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error("Error in searchNearbyPlaces:", error);
|
|
35
|
+
throw new Error("搜尋附近地點時發生錯誤");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async getPlaceDetails(placeId) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await this.client.placeDetails({
|
|
41
|
+
params: {
|
|
42
|
+
place_id: placeId,
|
|
43
|
+
fields: ["name", "rating", "formatted_address", "opening_hours", "reviews", "geometry", "formatted_phone_number", "website", "price_level", "photos"],
|
|
44
|
+
language: this.defaultLanguage,
|
|
45
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return response.data.result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("Error in getPlaceDetails:", error);
|
|
52
|
+
throw new Error("獲取地點詳細資訊時發生錯誤");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async geocodeAddress(address) {
|
|
56
|
+
try {
|
|
57
|
+
const response = await this.client.geocode({
|
|
58
|
+
params: {
|
|
59
|
+
address: address,
|
|
60
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
61
|
+
language: this.defaultLanguage,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (response.data.results.length === 0) {
|
|
65
|
+
throw new Error("找不到該地址的位置");
|
|
66
|
+
}
|
|
67
|
+
const result = response.data.results[0];
|
|
68
|
+
const location = result.geometry.location;
|
|
69
|
+
return {
|
|
70
|
+
lat: location.lat,
|
|
71
|
+
lng: location.lng,
|
|
72
|
+
formatted_address: result.formatted_address,
|
|
73
|
+
place_id: result.place_id,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error("Error in geocodeAddress:", error);
|
|
78
|
+
throw new Error("地址轉換座標時發生錯誤");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
parseCoordinates(coordString) {
|
|
82
|
+
const coords = coordString.split(",").map((c) => parseFloat(c.trim()));
|
|
83
|
+
if (coords.length !== 2 || isNaN(coords[0]) || isNaN(coords[1])) {
|
|
84
|
+
throw new Error("無效的座標格式,請使用「緯度,經度」格式");
|
|
85
|
+
}
|
|
86
|
+
return { lat: coords[0], lng: coords[1] };
|
|
87
|
+
}
|
|
88
|
+
async getLocation(center) {
|
|
89
|
+
if (center.isCoordinates) {
|
|
90
|
+
return this.parseCoordinates(center.value);
|
|
91
|
+
}
|
|
92
|
+
return this.geocodeAddress(center.value);
|
|
93
|
+
}
|
|
94
|
+
// 新增公開方法用於地址轉座標
|
|
95
|
+
async geocode(address) {
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.geocodeAddress(address);
|
|
98
|
+
return {
|
|
99
|
+
location: { lat: result.lat, lng: result.lng },
|
|
100
|
+
formatted_address: result.formatted_address || "",
|
|
101
|
+
place_id: result.place_id || "",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("Error in geocode:", error);
|
|
106
|
+
throw new Error("地址轉換座標時發生錯誤");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async reverseGeocode(latitude, longitude) {
|
|
110
|
+
try {
|
|
111
|
+
const response = await this.client.reverseGeocode({
|
|
112
|
+
params: {
|
|
113
|
+
latlng: { lat: latitude, lng: longitude },
|
|
114
|
+
language: this.defaultLanguage,
|
|
115
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
if (response.data.results.length === 0) {
|
|
119
|
+
throw new Error("找不到該座標的地址");
|
|
120
|
+
}
|
|
121
|
+
const result = response.data.results[0];
|
|
122
|
+
return {
|
|
123
|
+
formatted_address: result.formatted_address,
|
|
124
|
+
place_id: result.place_id,
|
|
125
|
+
address_components: result.address_components,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error("Error in reverseGeocode:", error);
|
|
130
|
+
throw new Error("座標轉換地址時發生錯誤");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async calculateDistanceMatrix(origins, destinations, mode = "driving") {
|
|
134
|
+
try {
|
|
135
|
+
const response = await this.client.distancematrix({
|
|
136
|
+
params: {
|
|
137
|
+
origins: origins,
|
|
138
|
+
destinations: destinations,
|
|
139
|
+
mode: mode,
|
|
140
|
+
language: this.defaultLanguage,
|
|
141
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const result = response.data;
|
|
145
|
+
if (result.status !== "OK") {
|
|
146
|
+
throw new Error(`距離矩陣計算失敗: ${result.status}`);
|
|
147
|
+
}
|
|
148
|
+
const distances = [];
|
|
149
|
+
const durations = [];
|
|
150
|
+
result.rows.forEach((row) => {
|
|
151
|
+
const distanceRow = [];
|
|
152
|
+
const durationRow = [];
|
|
153
|
+
row.elements.forEach((element) => {
|
|
154
|
+
if (element.status === "OK") {
|
|
155
|
+
distanceRow.push({
|
|
156
|
+
value: element.distance.value,
|
|
157
|
+
text: element.distance.text,
|
|
158
|
+
});
|
|
159
|
+
durationRow.push({
|
|
160
|
+
value: element.duration.value,
|
|
161
|
+
text: element.duration.text,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
distanceRow.push(null);
|
|
166
|
+
durationRow.push(null);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
distances.push(distanceRow);
|
|
170
|
+
durations.push(durationRow);
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
distances: distances,
|
|
174
|
+
durations: durations,
|
|
175
|
+
origin_addresses: result.origin_addresses,
|
|
176
|
+
destination_addresses: result.destination_addresses,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error("Error in calculateDistanceMatrix:", error);
|
|
181
|
+
throw new Error("計算距離矩陣時發生錯誤");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async getDirections(origin, destination, mode = "driving") {
|
|
185
|
+
try {
|
|
186
|
+
const response = await this.client.directions({
|
|
187
|
+
params: {
|
|
188
|
+
origin: origin,
|
|
189
|
+
destination: destination,
|
|
190
|
+
mode: mode,
|
|
191
|
+
language: this.defaultLanguage,
|
|
192
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
const result = response.data;
|
|
196
|
+
if (result.status !== "OK") {
|
|
197
|
+
throw new Error(`路線指引獲取失敗: ${result.status}`);
|
|
198
|
+
}
|
|
199
|
+
if (result.routes.length === 0) {
|
|
200
|
+
throw new Error("找不到路線");
|
|
201
|
+
}
|
|
202
|
+
const route = result.routes[0];
|
|
203
|
+
const legs = route.legs[0];
|
|
204
|
+
return {
|
|
205
|
+
routes: result.routes,
|
|
206
|
+
summary: route.summary,
|
|
207
|
+
total_distance: {
|
|
208
|
+
value: legs.distance.value,
|
|
209
|
+
text: legs.distance.text,
|
|
210
|
+
},
|
|
211
|
+
total_duration: {
|
|
212
|
+
value: legs.duration.value,
|
|
213
|
+
text: legs.duration.text,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error("Error in getDirections:", error);
|
|
219
|
+
throw new Error("獲取路線指引時發生錯誤");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async getElevation(locations) {
|
|
223
|
+
try {
|
|
224
|
+
const formattedLocations = locations.map((loc) => ({
|
|
225
|
+
lat: loc.latitude,
|
|
226
|
+
lng: loc.longitude,
|
|
227
|
+
}));
|
|
228
|
+
const response = await this.client.elevation({
|
|
229
|
+
params: {
|
|
230
|
+
locations: formattedLocations,
|
|
231
|
+
key: process.env.GOOGLE_MAPS_API_KEY || "",
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
const result = response.data;
|
|
235
|
+
if (result.status !== "OK") {
|
|
236
|
+
throw new Error(`海拔數據獲取失敗: ${result.status}`);
|
|
237
|
+
}
|
|
238
|
+
return result.results.map((item, index) => ({
|
|
239
|
+
elevation: item.elevation,
|
|
240
|
+
location: formattedLocations[index],
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error("Error in getElevation:", error);
|
|
245
|
+
throw new Error("獲取海拔數據時發生錯誤");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
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.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --external:pdfreader --external:jsdom --external:mammoth --external:csv-parse --external:libreoffice-convert && shx chmod +x dist/index.cjs",
|