mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-18 14:54:09 +02:00
679 lines
17 KiB
JavaScript
679 lines
17 KiB
JavaScript
#!/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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Geocoding failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Reverse geocoding failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Place search failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Place details request failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Distance matrix request failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Elevation request failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Directions request failed: ${data.error_message || data.status}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
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 {
|
|
content: [{
|
|
type: "text",
|
|
text: `Unknown tool: ${request.params.name}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
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);
|
|
});
|