mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-26 15:55:25 +02:00
Merge pull request #10 from modelcontextprotocol/mahesh/add-gmaps
Add Google Maps!
This commit is contained in:
86
package-lock.json
generated
86
package-lock.json
generated
@@ -72,6 +72,10 @@
|
|||||||
"resolved": "src/gdrive",
|
"resolved": "src/gdrive",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/server-google-maps": {
|
||||||
|
"resolved": "src/google-maps",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@modelcontextprotocol/server-postgres": {
|
"node_modules/@modelcontextprotocol/server-postgres": {
|
||||||
"resolved": "src/postgres",
|
"resolved": "src/postgres",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -190,11 +194,20 @@
|
|||||||
"version": "22.9.0",
|
"version": "22.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.8"
|
"undici-types": "~6.19.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-fetch": {
|
||||||
|
"version": "2.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||||
|
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/pg": {
|
"node_modules/@types/pg": {
|
||||||
"version": "8.11.10",
|
"version": "8.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
|
||||||
@@ -343,6 +356,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
@@ -589,6 +608,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -697,6 +728,15 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -973,6 +1013,20 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -2659,8 +2713,7 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@@ -2873,6 +2926,33 @@
|
|||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"src/google-maps": {
|
||||||
|
"name": "@modelcontextprotocol/server-google-maps",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "0.6.0",
|
||||||
|
"@types/node-fetch": "^2.6.12"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mcp-server-google-maps": "dist/index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shx": "^0.3.4",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"src/google-maps/node_modules/@modelcontextprotocol/sdk": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"src/postgres": {
|
"src/postgres": {
|
||||||
"name": "@modelcontextprotocol/server-postgres",
|
"name": "@modelcontextprotocol/server-postgres",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
65
src/google-maps/README.md
Normal file
65
src/google-maps/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Google Maps MCP Server
|
||||||
|
|
||||||
|
MCP Server for the Google Maps API.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
1. `geocode`
|
||||||
|
- Convert address to coordinates
|
||||||
|
- Input: `address` (string)
|
||||||
|
- Returns: location, formatted_address, place_id
|
||||||
|
|
||||||
|
2. `reverse_geocode`
|
||||||
|
- Convert coordinates to address
|
||||||
|
- Inputs:
|
||||||
|
- `latitude` (number)
|
||||||
|
- `longitude` (number)
|
||||||
|
- Returns: formatted_address, place_id, address_components
|
||||||
|
|
||||||
|
3. `search_places`
|
||||||
|
- Search for places using text query
|
||||||
|
- Inputs:
|
||||||
|
- `query` (string)
|
||||||
|
- `location` (optional): { latitude: number, longitude: number }
|
||||||
|
- `radius` (optional): number (meters, max 50000)
|
||||||
|
- Returns: array of places with names, addresses, locations
|
||||||
|
|
||||||
|
4. `get_place_details`
|
||||||
|
- Get detailed information about a place
|
||||||
|
- Input: `place_id` (string)
|
||||||
|
- Returns: name, address, contact info, ratings, reviews, opening hours
|
||||||
|
|
||||||
|
5. `get_distance_matrix`
|
||||||
|
- Calculate distances and times between points
|
||||||
|
- Inputs:
|
||||||
|
- `origins` (string[])
|
||||||
|
- `destinations` (string[])
|
||||||
|
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||||
|
- Returns: distances and durations matrix
|
||||||
|
|
||||||
|
6. `get_elevation`
|
||||||
|
- Get elevation data for locations
|
||||||
|
- Input: `locations` (array of {latitude, longitude})
|
||||||
|
- Returns: elevation data for each point
|
||||||
|
|
||||||
|
7. `get_directions`
|
||||||
|
- Get directions between points
|
||||||
|
- Inputs:
|
||||||
|
- `origin` (string)
|
||||||
|
- `destination` (string)
|
||||||
|
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||||
|
- Returns: route details with steps, distance, duration
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Get a Google Maps API key by following the instructions [here](https://developers.google.com/maps/documentation/javascript/get-api-key#create-api-keys).
|
||||||
|
|
||||||
|
2. To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
|
||||||
|
```json
|
||||||
|
"mcp-server-google-maps": {
|
||||||
|
"command": "mcp-server-google-maps",
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_MAPS_API_KEY": "<YOUR_API_KEY>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
710
src/google-maps/index.ts
Normal file
710
src/google-maps/index.ts
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
Tool,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
// Response interfaces
|
||||||
|
interface GoogleMapsResponse {
|
||||||
|
status: string;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeocodeResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
address_components: Array<{
|
||||||
|
long_name: string;
|
||||||
|
short_name: string;
|
||||||
|
types: string[];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlacesSearchResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
name: string;
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rating?: number;
|
||||||
|
types: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlaceDetailsResponse extends GoogleMapsResponse {
|
||||||
|
result: {
|
||||||
|
name: string;
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
formatted_phone_number?: string;
|
||||||
|
website?: string;
|
||||||
|
rating?: number;
|
||||||
|
reviews?: Array<{
|
||||||
|
author_name: string;
|
||||||
|
rating: number;
|
||||||
|
text: string;
|
||||||
|
time: number;
|
||||||
|
}>;
|
||||||
|
opening_hours?: {
|
||||||
|
weekday_text: string[];
|
||||||
|
open_now: boolean;
|
||||||
|
};
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DistanceMatrixResponse extends GoogleMapsResponse {
|
||||||
|
origin_addresses: string[];
|
||||||
|
destination_addresses: string[];
|
||||||
|
rows: Array<{
|
||||||
|
elements: Array<{
|
||||||
|
status: string;
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElevationResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
elevation: number;
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
resolution: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DirectionsResponse extends GoogleMapsResponse {
|
||||||
|
routes: Array<{
|
||||||
|
summary: string;
|
||||||
|
legs: Array<{
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
steps: Array<{
|
||||||
|
html_instructions: string;
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
travel_mode: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiKey(): string {
|
||||||
|
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("GOOGLE_MAPS_API_KEY environment variable is not set");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOOGLE_MAPS_API_KEY = getApiKey();
|
||||||
|
|
||||||
|
// Tool definitions
|
||||||
|
const GEOCODE_TOOL: Tool = {
|
||||||
|
name: "maps_geocode",
|
||||||
|
description: "Convert an address into geographic coordinates",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
description: "The address to geocode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["address"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const REVERSE_GEOCODE_TOOL: Tool = {
|
||||||
|
name: "maps_reverse_geocode",
|
||||||
|
description: "Convert coordinates into an address",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: {
|
||||||
|
type: "number",
|
||||||
|
description: "Latitude coordinate"
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: "number",
|
||||||
|
description: "Longitude coordinate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["latitude", "longitude"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEARCH_PLACES_TOOL: Tool = {
|
||||||
|
name: "maps_search_places",
|
||||||
|
description: "Search for places using Google Places API",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "Search query"
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: { type: "number" },
|
||||||
|
longitude: { type: "number" }
|
||||||
|
},
|
||||||
|
description: "Optional center point for the search"
|
||||||
|
},
|
||||||
|
radius: {
|
||||||
|
type: "number",
|
||||||
|
description: "Search radius in meters (max 50000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["query"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLACE_DETAILS_TOOL: Tool = {
|
||||||
|
name: "maps_place_details",
|
||||||
|
description: "Get detailed information about a specific place",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
place_id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The place ID to get details for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["place_id"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DISTANCE_MATRIX_TOOL: Tool = {
|
||||||
|
name: "maps_distance_matrix",
|
||||||
|
description: "Calculate travel distance and time for multiple origins and destinations",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
origins: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Array of origin addresses or coordinates"
|
||||||
|
},
|
||||||
|
destinations: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Array of destination addresses or coordinates"
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: "string",
|
||||||
|
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||||
|
enum: ["driving", "walking", "bicycling", "transit"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["origins", "destinations"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ELEVATION_TOOL: Tool = {
|
||||||
|
name: "maps_elevation",
|
||||||
|
description: "Get elevation data for locations on the earth",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
locations: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: { type: "number" },
|
||||||
|
longitude: { type: "number" }
|
||||||
|
},
|
||||||
|
required: ["latitude", "longitude"]
|
||||||
|
},
|
||||||
|
description: "Array of locations to get elevation for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["locations"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIRECTIONS_TOOL: Tool = {
|
||||||
|
name: "maps_directions",
|
||||||
|
description: "Get directions between two points",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
origin: {
|
||||||
|
type: "string",
|
||||||
|
description: "Starting point address or coordinates"
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: "string",
|
||||||
|
description: "Ending point address or coordinates"
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: "string",
|
||||||
|
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||||
|
enum: ["driving", "walking", "bicycling", "transit"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["origin", "destination"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAPS_TOOLS = [
|
||||||
|
GEOCODE_TOOL,
|
||||||
|
REVERSE_GEOCODE_TOOL,
|
||||||
|
SEARCH_PLACES_TOOL,
|
||||||
|
PLACE_DETAILS_TOOL,
|
||||||
|
DISTANCE_MATRIX_TOOL,
|
||||||
|
ELEVATION_TOOL,
|
||||||
|
DIRECTIONS_TOOL,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// API handlers
|
||||||
|
async function handleGeocode(address: string) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||||
|
url.searchParams.append("address", address);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as GeocodeResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Geocoding failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
location: data.results[0].geometry.location,
|
||||||
|
formatted_address: data.results[0].formatted_address,
|
||||||
|
place_id: data.results[0].place_id
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReverseGeocode(latitude: number, longitude: number) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||||
|
url.searchParams.append("latlng", `${latitude},${longitude}`);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as GeocodeResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Reverse geocoding failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
formatted_address: data.results[0].formatted_address,
|
||||||
|
place_id: data.results[0].place_id,
|
||||||
|
address_components: data.results[0].address_components
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlaceSearch(
|
||||||
|
query: string,
|
||||||
|
location?: { latitude: number; longitude: number },
|
||||||
|
radius?: number
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/place/textsearch/json");
|
||||||
|
url.searchParams.append("query", query);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
if (location) {
|
||||||
|
url.searchParams.append("location", `${location.latitude},${location.longitude}`);
|
||||||
|
}
|
||||||
|
if (radius) {
|
||||||
|
url.searchParams.append("radius", radius.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as PlacesSearchResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Place search failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
places: data.results.map((place) => ({
|
||||||
|
name: place.name,
|
||||||
|
formatted_address: place.formatted_address,
|
||||||
|
location: place.geometry.location,
|
||||||
|
place_id: place.place_id,
|
||||||
|
rating: place.rating,
|
||||||
|
types: place.types
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlaceDetails(place_id: string) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/place/details/json");
|
||||||
|
url.searchParams.append("place_id", place_id);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as PlaceDetailsResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Place details request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
name: data.result.name,
|
||||||
|
formatted_address: data.result.formatted_address,
|
||||||
|
location: data.result.geometry.location,
|
||||||
|
formatted_phone_number: data.result.formatted_phone_number,
|
||||||
|
website: data.result.website,
|
||||||
|
rating: data.result.rating,
|
||||||
|
reviews: data.result.reviews,
|
||||||
|
opening_hours: data.result.opening_hours
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function handleDistanceMatrix(
|
||||||
|
origins: string[],
|
||||||
|
destinations: string[],
|
||||||
|
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/distancematrix/json");
|
||||||
|
url.searchParams.append("origins", origins.join("|"));
|
||||||
|
url.searchParams.append("destinations", destinations.join("|"));
|
||||||
|
url.searchParams.append("mode", mode);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as DistanceMatrixResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Distance matrix request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
origin_addresses: data.origin_addresses,
|
||||||
|
destination_addresses: data.destination_addresses,
|
||||||
|
results: data.rows.map((row) => ({
|
||||||
|
elements: row.elements.map((element) => ({
|
||||||
|
status: element.status,
|
||||||
|
duration: element.duration,
|
||||||
|
distance: element.distance
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleElevation(locations: Array<{ latitude: number; longitude: number }>) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/elevation/json");
|
||||||
|
const locationString = locations
|
||||||
|
.map((loc) => `${loc.latitude},${loc.longitude}`)
|
||||||
|
.join("|");
|
||||||
|
url.searchParams.append("locations", locationString);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as ElevationResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Elevation request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
results: data.results.map((result) => ({
|
||||||
|
elevation: result.elevation,
|
||||||
|
location: result.location,
|
||||||
|
resolution: result.resolution
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDirections(
|
||||||
|
origin: string,
|
||||||
|
destination: string,
|
||||||
|
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/directions/json");
|
||||||
|
url.searchParams.append("origin", origin);
|
||||||
|
url.searchParams.append("destination", destination);
|
||||||
|
url.searchParams.append("mode", mode);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as DirectionsResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Directions request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
routes: data.routes.map((route) => ({
|
||||||
|
summary: route.summary,
|
||||||
|
distance: route.legs[0].distance,
|
||||||
|
duration: route.legs[0].duration,
|
||||||
|
steps: route.legs[0].steps.map((step) => ({
|
||||||
|
instructions: step.html_instructions,
|
||||||
|
distance: step.distance,
|
||||||
|
duration: step.duration,
|
||||||
|
travel_mode: step.travel_mode
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server setup
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "mcp-server/google-maps",
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up request handlers
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: MAPS_TOOLS,
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
switch (request.params.name) {
|
||||||
|
case "maps_geocode": {
|
||||||
|
const { address } = request.params.arguments as { address: string };
|
||||||
|
return await handleGeocode(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_reverse_geocode": {
|
||||||
|
const { latitude, longitude } = request.params.arguments as {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
return await handleReverseGeocode(latitude, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_search_places": {
|
||||||
|
const { query, location, radius } = request.params.arguments as {
|
||||||
|
query: string;
|
||||||
|
location?: { latitude: number; longitude: number };
|
||||||
|
radius?: number;
|
||||||
|
};
|
||||||
|
return await handlePlaceSearch(query, location, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_place_details": {
|
||||||
|
const { place_id } = request.params.arguments as { place_id: string };
|
||||||
|
return await handlePlaceDetails(place_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_distance_matrix": {
|
||||||
|
const { origins, destinations, mode } = request.params.arguments as {
|
||||||
|
origins: string[];
|
||||||
|
destinations: string[];
|
||||||
|
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||||
|
};
|
||||||
|
return await handleDistanceMatrix(origins, destinations, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_elevation": {
|
||||||
|
const { locations } = request.params.arguments as {
|
||||||
|
locations: Array<{ latitude: number; longitude: number }>;
|
||||||
|
};
|
||||||
|
return await handleElevation(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_directions": {
|
||||||
|
const { origin, destination, mode } = request.params.arguments as {
|
||||||
|
origin: string;
|
||||||
|
destination: string;
|
||||||
|
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||||
|
};
|
||||||
|
return await handleDirections(origin, destination, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Unknown tool: ${request.params.name}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runServer() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("Google Maps MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
runServer().catch((error) => {
|
||||||
|
console.error("Fatal error running server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
29
src/google-maps/package.json
Normal file
29
src/google-maps/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@modelcontextprotocol/server-google-maps",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "MCP server for using the Google Maps API",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
|
"homepage": "https://modelcontextprotocol.io",
|
||||||
|
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mcp-server-google-maps": "dist/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && shx chmod +x dist/*.js",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "0.6.0",
|
||||||
|
"@types/node-fetch": "^2.6.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shx": "^0.3.4",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/google-maps/tsconfig.json
Normal file
10
src/google-maps/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user