@cablate/mcp-google-map 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,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 search and information retrieval
10
- - Geocoding and reverse geocoding
11
- - Detailed place information
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 TravelMode;
17131
- (function(TravelMode2) {
17132
- TravelMode2["driving"] = "driving";
17133
- TravelMode2["walking"] = "walking";
17134
- TravelMode2["bicycling"] = "bicycling";
17135
- TravelMode2["transit"] = "transit";
17136
- })(TravelMode || (exports2.TravelMode = TravelMode = {}));
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,120 @@ 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\u8DEF\u7DDA\u6307\u5F15",
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
+ },
23493
+ required: ["origin", "destination"]
23494
+ }
23495
+ };
23496
+ var ELEVATION_TOOL = {
23497
+ name: "maps_elevation",
23498
+ description: "\u7372\u53D6\u4F4D\u7F6E\u7684\u6D77\u62D4\u6578\u64DA",
23499
+ inputSchema: {
23500
+ type: "object",
23501
+ properties: {
23502
+ locations: {
23503
+ type: "array",
23504
+ items: {
23505
+ type: "object",
23506
+ properties: {
23507
+ latitude: {
23508
+ type: "number",
23509
+ description: "\u7DEF\u5EA6"
23510
+ },
23511
+ longitude: {
23512
+ type: "number",
23513
+ description: "\u7D93\u5EA6"
23514
+ }
23515
+ },
23516
+ required: ["latitude", "longitude"]
23517
+ },
23518
+ description: "\u8981\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u7684\u4F4D\u7F6E\u5217\u8868"
23519
+ }
23520
+ },
23521
+ required: ["locations"]
23522
+ }
23523
+ };
23410
23524
  var GET_PLACE_DETAILS_TOOL = {
23411
23525
  name: "get_place_details",
23412
23526
  description: "\u7372\u53D6\u7279\u5B9A\u5730\u9EDE\u7684\u8A73\u7D30\u8CC7\u8A0A",
@@ -23485,10 +23599,13 @@ var GoogleMapsTools = class {
23485
23599
  if (response.data.results.length === 0) {
23486
23600
  throw new Error("\u627E\u4E0D\u5230\u8A72\u5730\u5740\u7684\u4F4D\u7F6E");
23487
23601
  }
23488
- const location = response.data.results[0].geometry.location;
23602
+ const result = response.data.results[0];
23603
+ const location = result.geometry.location;
23489
23604
  return {
23490
23605
  lat: location.lat,
23491
- lng: location.lng
23606
+ lng: location.lng,
23607
+ formatted_address: result.formatted_address,
23608
+ place_id: result.place_id
23492
23609
  };
23493
23610
  } catch (error) {
23494
23611
  console.error("Error in geocodeAddress:", error);
@@ -23508,6 +23625,154 @@ var GoogleMapsTools = class {
23508
23625
  }
23509
23626
  return this.geocodeAddress(center.value);
23510
23627
  }
23628
+ // 新增公開方法用於地址轉座標
23629
+ async geocode(address) {
23630
+ try {
23631
+ const result = await this.geocodeAddress(address);
23632
+ return {
23633
+ location: { lat: result.lat, lng: result.lng },
23634
+ formatted_address: result.formatted_address || "",
23635
+ place_id: result.place_id || ""
23636
+ };
23637
+ } catch (error) {
23638
+ console.error("Error in geocode:", error);
23639
+ throw new Error("\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u6642\u767C\u751F\u932F\u8AA4");
23640
+ }
23641
+ }
23642
+ async reverseGeocode(latitude, longitude) {
23643
+ try {
23644
+ const response = await this.client.reverseGeocode({
23645
+ params: {
23646
+ latlng: { lat: latitude, lng: longitude },
23647
+ language: this.defaultLanguage,
23648
+ key: process.env.GOOGLE_MAPS_API_KEY || ""
23649
+ }
23650
+ });
23651
+ if (response.data.results.length === 0) {
23652
+ throw new Error("\u627E\u4E0D\u5230\u8A72\u5EA7\u6A19\u7684\u5730\u5740");
23653
+ }
23654
+ const result = response.data.results[0];
23655
+ return {
23656
+ formatted_address: result.formatted_address,
23657
+ place_id: result.place_id,
23658
+ address_components: result.address_components
23659
+ };
23660
+ } catch (error) {
23661
+ console.error("Error in reverseGeocode:", error);
23662
+ throw new Error("\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u6642\u767C\u751F\u932F\u8AA4");
23663
+ }
23664
+ }
23665
+ async calculateDistanceMatrix(origins, destinations, mode = "driving") {
23666
+ try {
23667
+ const response = await this.client.distancematrix({
23668
+ params: {
23669
+ origins,
23670
+ destinations,
23671
+ mode,
23672
+ language: this.defaultLanguage,
23673
+ key: process.env.GOOGLE_MAPS_API_KEY || ""
23674
+ }
23675
+ });
23676
+ const result = response.data;
23677
+ if (result.status !== "OK") {
23678
+ throw new Error(`\u8DDD\u96E2\u77E9\u9663\u8A08\u7B97\u5931\u6557: ${result.status}`);
23679
+ }
23680
+ const distances = [];
23681
+ const durations = [];
23682
+ result.rows.forEach((row) => {
23683
+ const distanceRow = [];
23684
+ const durationRow = [];
23685
+ row.elements.forEach((element) => {
23686
+ if (element.status === "OK") {
23687
+ distanceRow.push({
23688
+ value: element.distance.value,
23689
+ text: element.distance.text
23690
+ });
23691
+ durationRow.push({
23692
+ value: element.duration.value,
23693
+ text: element.duration.text
23694
+ });
23695
+ } else {
23696
+ distanceRow.push(null);
23697
+ durationRow.push(null);
23698
+ }
23699
+ });
23700
+ distances.push(distanceRow);
23701
+ durations.push(durationRow);
23702
+ });
23703
+ return {
23704
+ distances,
23705
+ durations,
23706
+ origin_addresses: result.origin_addresses,
23707
+ destination_addresses: result.destination_addresses
23708
+ };
23709
+ } catch (error) {
23710
+ console.error("Error in calculateDistanceMatrix:", error);
23711
+ throw new Error("\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u6642\u767C\u751F\u932F\u8AA4");
23712
+ }
23713
+ }
23714
+ async getDirections(origin, destination, mode = "driving") {
23715
+ try {
23716
+ const response = await this.client.directions({
23717
+ params: {
23718
+ origin,
23719
+ destination,
23720
+ mode,
23721
+ language: this.defaultLanguage,
23722
+ key: process.env.GOOGLE_MAPS_API_KEY || ""
23723
+ }
23724
+ });
23725
+ const result = response.data;
23726
+ if (result.status !== "OK") {
23727
+ throw new Error(`\u8DEF\u7DDA\u6307\u5F15\u7372\u53D6\u5931\u6557: ${result.status}`);
23728
+ }
23729
+ if (result.routes.length === 0) {
23730
+ throw new Error("\u627E\u4E0D\u5230\u8DEF\u7DDA");
23731
+ }
23732
+ const route = result.routes[0];
23733
+ const legs = route.legs[0];
23734
+ return {
23735
+ routes: result.routes,
23736
+ summary: route.summary,
23737
+ total_distance: {
23738
+ value: legs.distance.value,
23739
+ text: legs.distance.text
23740
+ },
23741
+ total_duration: {
23742
+ value: legs.duration.value,
23743
+ text: legs.duration.text
23744
+ }
23745
+ };
23746
+ } catch (error) {
23747
+ console.error("Error in getDirections:", error);
23748
+ throw new Error("\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u6642\u767C\u751F\u932F\u8AA4");
23749
+ }
23750
+ }
23751
+ async getElevation(locations) {
23752
+ try {
23753
+ const formattedLocations = locations.map((loc) => ({
23754
+ lat: loc.latitude,
23755
+ lng: loc.longitude
23756
+ }));
23757
+ const response = await this.client.elevation({
23758
+ params: {
23759
+ locations: formattedLocations,
23760
+ key: process.env.GOOGLE_MAPS_API_KEY || ""
23761
+ }
23762
+ });
23763
+ const result = response.data;
23764
+ if (result.status !== "OK") {
23765
+ throw new Error(`\u6D77\u62D4\u6578\u64DA\u7372\u53D6\u5931\u6557: ${result.status}`);
23766
+ }
23767
+ return result.results.map((item, index) => ({
23768
+ elevation: item.elevation,
23769
+ location: formattedLocations[index]
23770
+ }));
23771
+ } catch (error) {
23772
+ console.error("Error in getElevation:", error);
23773
+ throw new Error("\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u6642\u767C\u751F\u932F\u8AA4");
23774
+ }
23775
+ }
23511
23776
  };
23512
23777
 
23513
23778
  // src/maps-tools/searchPlaces.ts
@@ -23576,10 +23841,80 @@ var PlacesSearcher = class {
23576
23841
  };
23577
23842
  }
23578
23843
  }
23844
+ async geocode(address) {
23845
+ try {
23846
+ const result = await this.mapsTools.geocode(address);
23847
+ return {
23848
+ success: true,
23849
+ data: result
23850
+ };
23851
+ } catch (error) {
23852
+ return {
23853
+ success: false,
23854
+ error: error instanceof Error ? error.message : "\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u6642\u767C\u751F\u932F\u8AA4"
23855
+ };
23856
+ }
23857
+ }
23858
+ async reverseGeocode(latitude, longitude) {
23859
+ try {
23860
+ const result = await this.mapsTools.reverseGeocode(latitude, longitude);
23861
+ return {
23862
+ success: true,
23863
+ data: result
23864
+ };
23865
+ } catch (error) {
23866
+ return {
23867
+ success: false,
23868
+ error: error instanceof Error ? error.message : "\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u6642\u767C\u751F\u932F\u8AA4"
23869
+ };
23870
+ }
23871
+ }
23872
+ async calculateDistanceMatrix(origins, destinations, mode = "driving") {
23873
+ try {
23874
+ const result = await this.mapsTools.calculateDistanceMatrix(origins, destinations, mode);
23875
+ return {
23876
+ success: true,
23877
+ data: result
23878
+ };
23879
+ } catch (error) {
23880
+ return {
23881
+ success: false,
23882
+ error: error instanceof Error ? error.message : "\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u6642\u767C\u751F\u932F\u8AA4"
23883
+ };
23884
+ }
23885
+ }
23886
+ async getDirections(origin, destination, mode = "driving") {
23887
+ try {
23888
+ const result = await this.mapsTools.getDirections(origin, destination, mode);
23889
+ return {
23890
+ success: true,
23891
+ data: result
23892
+ };
23893
+ } catch (error) {
23894
+ return {
23895
+ success: false,
23896
+ error: error instanceof Error ? error.message : "\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u6642\u767C\u751F\u932F\u8AA4"
23897
+ };
23898
+ }
23899
+ }
23900
+ async getElevation(locations) {
23901
+ try {
23902
+ const result = await this.mapsTools.getElevation(locations);
23903
+ return {
23904
+ success: true,
23905
+ data: result
23906
+ };
23907
+ } catch (error) {
23908
+ return {
23909
+ success: false,
23910
+ error: error instanceof Error ? error.message : "\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u6642\u767C\u751F\u932F\u8AA4"
23911
+ };
23912
+ }
23913
+ }
23579
23914
  };
23580
23915
 
23581
23916
  // src/index.ts
23582
- var tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL];
23917
+ var tools = [SEARCH_NEARBY_TOOL, GET_PLACE_DETAILS_TOOL, GEOCODE_TOOL, REVERSE_GEOCODE_TOOL, DISTANCE_MATRIX_TOOL, DIRECTIONS_TOOL, ELEVATION_TOOL];
23583
23918
  var placesSearcher = new PlacesSearcher();
23584
23919
  var server = new Server(
23585
23920
  {
@@ -23647,6 +23982,101 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
23647
23982
  isError: false
23648
23983
  };
23649
23984
  }
23985
+ if (name === "maps_geocode") {
23986
+ const { address } = args;
23987
+ const result = await placesSearcher.geocode(address);
23988
+ if (!result.success) {
23989
+ return {
23990
+ content: [{ type: "text", text: result.error || "\u5730\u5740\u8F49\u63DB\u5EA7\u6A19\u5931\u6557" }],
23991
+ isError: true
23992
+ };
23993
+ }
23994
+ return {
23995
+ content: [
23996
+ {
23997
+ type: "text",
23998
+ text: JSON.stringify(result.data, null, 2)
23999
+ }
24000
+ ],
24001
+ isError: false
24002
+ };
24003
+ }
24004
+ if (name === "maps_reverse_geocode") {
24005
+ const { latitude, longitude } = args;
24006
+ const result = await placesSearcher.reverseGeocode(latitude, longitude);
24007
+ if (!result.success) {
24008
+ return {
24009
+ content: [{ type: "text", text: result.error || "\u5EA7\u6A19\u8F49\u63DB\u5730\u5740\u5931\u6557" }],
24010
+ isError: true
24011
+ };
24012
+ }
24013
+ return {
24014
+ content: [
24015
+ {
24016
+ type: "text",
24017
+ text: JSON.stringify(result.data, null, 2)
24018
+ }
24019
+ ],
24020
+ isError: false
24021
+ };
24022
+ }
24023
+ if (name === "maps_distance_matrix") {
24024
+ const { origins, destinations, mode } = args;
24025
+ const result = await placesSearcher.calculateDistanceMatrix(origins, destinations, mode || "driving");
24026
+ if (!result.success) {
24027
+ return {
24028
+ content: [{ type: "text", text: result.error || "\u8A08\u7B97\u8DDD\u96E2\u77E9\u9663\u5931\u6557" }],
24029
+ isError: true
24030
+ };
24031
+ }
24032
+ return {
24033
+ content: [
24034
+ {
24035
+ type: "text",
24036
+ text: JSON.stringify(result.data, null, 2)
24037
+ }
24038
+ ],
24039
+ isError: false
24040
+ };
24041
+ }
24042
+ if (name === "maps_directions") {
24043
+ const { origin, destination, mode } = args;
24044
+ const result = await placesSearcher.getDirections(origin, destination, mode || "driving");
24045
+ if (!result.success) {
24046
+ return {
24047
+ content: [{ type: "text", text: result.error || "\u7372\u53D6\u8DEF\u7DDA\u6307\u5F15\u5931\u6557" }],
24048
+ isError: true
24049
+ };
24050
+ }
24051
+ return {
24052
+ content: [
24053
+ {
24054
+ type: "text",
24055
+ text: JSON.stringify(result.data, null, 2)
24056
+ }
24057
+ ],
24058
+ isError: false
24059
+ };
24060
+ }
24061
+ if (name === "maps_elevation") {
24062
+ const { locations } = args;
24063
+ const result = await placesSearcher.getElevation(locations);
24064
+ if (!result.success) {
24065
+ return {
24066
+ content: [{ type: "text", text: result.error || "\u7372\u53D6\u6D77\u62D4\u6578\u64DA\u5931\u6557" }],
24067
+ isError: true
24068
+ };
24069
+ }
24070
+ return {
24071
+ content: [
24072
+ {
24073
+ type: "text",
24074
+ text: JSON.stringify(result.data, null, 2)
24075
+ }
24076
+ ],
24077
+ isError: false
24078
+ };
24079
+ }
23650
24080
  return {
23651
24081
  content: [{ type: "text", text: `\u932F\u8AA4\uFF1A\u672A\u77E5\u7684\u5DE5\u5177 ${name}` }],
23652
24082
  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.1",
3
+ "version": "0.0.2",
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",