mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-18 16:13:22 +02:00
Add Google Maps
This commit is contained in:
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);
|
||||
});
|
||||
Reference in New Issue
Block a user