Merge pull request #10 from modelcontextprotocol/mahesh/add-gmaps

Add Google Maps!
This commit is contained in:
Mahesh Murag
2024-11-20 15:49:27 +01:00
committed by GitHub
5 changed files with 897 additions and 3 deletions

86
package-lock.json generated
View File

@@ -72,6 +72,10 @@
"resolved": "src/gdrive",
"link": true
},
"node_modules/@modelcontextprotocol/server-google-maps": {
"resolved": "src/google-maps",
"link": true
},
"node_modules/@modelcontextprotocol/server-postgres": {
"resolved": "src/postgres",
"link": true
@@ -190,11 +194,20 @@
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"devOptional": true,
"dependencies": {
"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": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
@@ -343,6 +356,12 @@
"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": {
"version": "1.6.7",
"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",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -697,6 +728,15 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -973,6 +1013,20 @@
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2659,8 +2713,7 @@
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"devOptional": true
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -2873,6 +2926,33 @@
"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": {
"name": "@modelcontextprotocol/server-postgres",
"version": "0.1.0",

65
src/google-maps/README.md Normal file
View 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
View 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);
});

View 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"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
]
}