Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ dist/
node_modules/
credentials.json
.env
.agents/*
.agents/*
.mcpregistry_*
.mcp.json
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@

Give your AI agent the ability to understand the physical world — geocode, route, search, and reason about locations.

- **8 tools** — geocode, reverse-geocode, search-nearby, search-places, place-details, directions, distance-matrix, elevation
- **10 tools** — geocode, reverse-geocode, search-nearby, search-places, place-details, directions, distance-matrix, elevation, timezone, weather
- **3 modes** — stdio, StreamableHTTP, standalone exec CLI
- **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/))

### vs Google Grounding Lite

| | This project | [Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services) |
|---|---|---|
| Tools | **8** | 3 |
| Tools | **10** | 3 |
| Geocoding | Yes | No |
| Step-by-step directions | Yes | No |
| Elevation | Yes | No |
| Distance matrix | Yes | No |
| Place details | Yes | No |
| Timezone | Yes | No |
| Weather | Yes | Yes |
| Open source | MIT | No |
| Self-hosted | Yes | Google-managed only |
| Agent Skill | Yes | No |
Expand Down Expand Up @@ -53,6 +55,8 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. |
| `maps_directions` | Get step-by-step navigation between two points with route details. |
| `maps_elevation` | Get elevation (meters above sea level) for geographic coordinates. |
| `maps_timezone` | Get timezone ID, name, UTC/DST offsets, and local time for coordinates. |
| `maps_weather` | Get current weather conditions — temperature, humidity, wind, UV, precipitation. |

All tools are annotated with `readOnlyHint: true` and `destructiveHint: false` — MCP clients can auto-approve these without user confirmation.

Expand Down Expand Up @@ -102,7 +106,7 @@ Then configure your MCP client:
### Server Information

- **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
- **Tools**: 8 Google Maps tools
- **Tools**: 10 Google Maps tools

### CLI Exec Mode (Agent Skill)

Expand All @@ -113,7 +117,7 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
```

All 8 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
All 10 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.

### API Key Configuration

Expand Down Expand Up @@ -203,7 +207,9 @@ src/
│ ├── reverseGeocode.ts # maps_reverse_geocode tool
│ ├── distanceMatrix.ts # maps_distance_matrix tool
│ ├── directions.ts # maps_directions tool
│ └── elevation.ts # maps_elevation tool
│ ├── elevation.ts # maps_elevation tool
│ ├── timezone.ts # maps_timezone tool
│ └── weather.ts # maps_weather tool
└── utils/
├── apiKeyManager.ts # API key management
└── requestContext.ts # Per-request context (API key isolation)
Expand Down
16 changes: 16 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@
"directions",
"distance-matrix",
"elevation",
"timezone",
"weather",
] as const;

async function execTool(toolName: string, params: any, apiKey: string): Promise<any> {

Check warning on line 91 in src/cli.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

Check warning on line 91 in src/cli.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
const searcher = new PlacesSearcher(apiKey);

switch (toolName) {
Expand Down Expand Up @@ -134,6 +136,20 @@
case "maps_elevation":
return searcher.getElevation(params.locations);

case "timezone":
case "maps_timezone":
return searcher.getTimezone(params.latitude, params.longitude, params.timestamp);

case "weather":
case "maps_weather":
return searcher.getWeather(
params.latitude,
params.longitude,
params.type,
params.forecastDays,
params.forecastHours
);

default:
throw new Error(`Unknown tool: ${toolName}. Available: ${EXEC_TOOLS.join(", ")}`);
}
Expand All @@ -157,7 +173,7 @@
const packageJsonPath = resolve(__dirname, "../package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
packageVersion = packageJson.version;
} catch (e) {

Check warning on line 176 in src/cli.ts

View workflow job for this annotation

GitHub Actions / ci

'e' is defined but never used
packageVersion = "0.0.0";
}

Expand Down Expand Up @@ -208,7 +224,7 @@
const result = await execTool(argv.tool as string, params, argv.apikey as string);
console.log(JSON.stringify(result, null, 2));
process.exit(0);
} catch (error: any) {

Check warning on line 227 in src/cli.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
console.error(JSON.stringify({ error: error.message }, null, 2));
process.exit(1);
}
Expand Down
16 changes: 16 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { DistanceMatrix, DistanceMatrixParams } from "./tools/maps/distanceMatri
import { Directions, DirectionsParams } from "./tools/maps/directions.js";
import { Elevation, ElevationParams } from "./tools/maps/elevation.js";
import { SearchPlaces, SearchPlacesParams } from "./tools/maps/searchPlaces.js";
import { Timezone, TimezoneParams } from "./tools/maps/timezone.js";
import { Weather, WeatherParams } from "./tools/maps/weather.js";

// All Google Maps tools are read-only API queries
const MAPS_TOOL_ANNOTATIONS = {
Expand Down Expand Up @@ -85,6 +87,20 @@ const serverConfigs: ServerInstanceConfig[] = [
annotations: MAPS_TOOL_ANNOTATIONS,
action: (params: SearchPlacesParams) => SearchPlaces.ACTION(params),
},
{
name: Timezone.NAME,
description: Timezone.DESCRIPTION,
schema: Timezone.SCHEMA,
annotations: MAPS_TOOL_ANNOTATIONS,
action: (params: TimezoneParams) => Timezone.ACTION(params),
},
{
name: Weather.NAME,
description: Weather.DESCRIPTION,
schema: Weather.SCHEMA,
annotations: MAPS_TOOL_ANNOTATIONS,
action: (params: WeatherParams) => Weather.ACTION(params),
},
],
},
];
Expand Down
48 changes: 48 additions & 0 deletions src/services/PlacesSearcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ interface DirectionsResponse {
};
}

interface TimezoneResponse {
success: boolean;
error?: string;
data?: {
timeZoneId: string;
timeZoneName: string;
utcOffset: number;
dstOffset: number;
localTime: string;
};
}

interface WeatherResponse {
success: boolean;
error?: string;
data?: any;
}

interface ElevationResponse {
success: boolean;
error?: string;
Expand Down Expand Up @@ -268,6 +286,36 @@ export class PlacesSearcher {
}
}

async getTimezone(latitude: number, longitude: number, timestamp?: number): Promise<TimezoneResponse> {
try {
const result = await this.mapsTools.getTimezone(latitude, longitude, timestamp);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "An error occurred while getting timezone",
};
}
}

async getWeather(
latitude: number,
longitude: number,
type: "current" | "forecast_daily" | "forecast_hourly" = "current",
forecastDays?: number,
forecastHours?: number
): Promise<WeatherResponse> {
try {
const result = await this.mapsTools.getWeather(latitude, longitude, type, forecastDays, forecastHours);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "An error occurred while getting weather",
};
}
}

async getElevation(locations: Array<{ latitude: number; longitude: number }>): Promise<ElevationResponse> {
try {
const result = await this.mapsTools.getElevation(locations);
Expand Down
107 changes: 107 additions & 0 deletions src/services/toolclass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,113 @@ export class GoogleMapsTools {
}
}

async getWeather(
latitude: number,
longitude: number,
type: "current" | "forecast_daily" | "forecast_hourly" = "current",
forecastDays?: number,
forecastHours?: number
): Promise<any> {
try {
const baseParams = `key=${this.apiKey}&location.latitude=${latitude}&location.longitude=${longitude}`;
let url: string;

switch (type) {
case "forecast_daily": {
const days = Math.min(Math.max(forecastDays || 5, 1), 10);
url = `https://weather.googleapis.com/v1/forecast/days:lookup?${baseParams}&days=${days}`;
break;
}
case "forecast_hourly": {
const hours = Math.min(Math.max(forecastHours || 24, 1), 240);
url = `https://weather.googleapis.com/v1/forecast/hours:lookup?${baseParams}&hours=${hours}`;
break;
}
default:
url = `https://weather.googleapis.com/v1/currentConditions:lookup?${baseParams}`;
}

const response = await fetch(url);

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const msg = errorData?.error?.message || `HTTP ${response.status}`;

if (msg.includes("not supported for this location")) {
throw new Error(
`Weather data is not available for this location (${latitude}, ${longitude}). ` +
"The Google Weather API has limited coverage — China, Japan, South Korea, Cuba, Iran, North Korea, and Syria are unsupported. " +
"Try a location in North America, Europe, or Oceania."
);
}

throw new Error(msg);
}

const data = await response.json();

if (type === "current") {
return {
temperature: data.temperature,
feelsLike: data.feelsLikeTemperature,
humidity: data.relativeHumidity,
wind: data.wind,
conditions: data.weatherCondition?.description?.text || data.weatherCondition?.type,
uvIndex: data.uvIndex,
precipitation: data.precipitation,
visibility: data.visibility,
pressure: data.airPressure,
cloudCover: data.cloudCover,
isDayTime: data.isDaytime,
};
}

// forecast_daily or forecast_hourly — return as-is with light cleanup
return data;
} catch (error: any) {
Logger.error("Error in getWeather:", error);
throw new Error(error.message || `Failed to get weather for (${latitude}, ${longitude})`);
}
}

async getTimezone(
latitude: number,
longitude: number,
timestamp?: number
): Promise<{ timeZoneId: string; timeZoneName: string; utcOffset: number; dstOffset: number; localTime: string }> {
try {
const ts = timestamp ? Math.floor(timestamp / 1000) : Math.floor(Date.now() / 1000);

const response = await this.client.timezone({
params: {
location: { lat: latitude, lng: longitude },
timestamp: ts,
key: this.apiKey,
},
});

const result = response.data;

if (result.status !== "OK") {
throw new Error(`Timezone API returned status: ${result.status}`);
}

const totalOffset = (result.rawOffset + result.dstOffset) * 1000;
const localTime = new Date(ts * 1000 + totalOffset).toISOString().replace("Z", "");

return {
timeZoneId: result.timeZoneId,
timeZoneName: result.timeZoneName,
utcOffset: result.rawOffset,
dstOffset: result.dstOffset,
localTime,
};
} catch (error: any) {
Logger.error("Error in getTimezone:", error);
throw new Error(`Failed to get timezone for (${latitude}, ${longitude}): ${extractErrorMessage(error)}`);
}
}

async getElevation(
locations: Array<{ latitude: number; longitude: number }>
): Promise<Array<{ elevation: number; location: { lat: number; lng: number } }>> {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/maps/elevation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getCurrentApiKey } from "../../utils/requestContext.js";

const NAME = "maps_elevation";
const DESCRIPTION =
"Get elevation (height above sea level in meters) for one or more geographic coordinates. Use for terrain analysis, hiking/cycling route planning, or when the user asks about altitude at specific locations.";
"Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment — low elevation near water suggests flood risk.";

const SCHEMA = {
locations: z
Expand Down
51 changes: 51 additions & 0 deletions src/tools/maps/timezone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";
import { PlacesSearcher } from "../../services/PlacesSearcher.js";
import { getCurrentApiKey } from "../../utils/requestContext.js";

const NAME = "maps_timezone";
const DESCRIPTION =
"Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.";

const SCHEMA = {
latitude: z.number().describe("Latitude coordinate"),
longitude: z.number().describe("Longitude coordinate"),
timestamp: z
.number()
.optional()
.describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)"),
};

export type TimezoneParams = z.infer<z.ZodObject<typeof SCHEMA>>;

async function ACTION(params: any): Promise<{ content: any[]; isError?: boolean }> {
try {
const apiKey = getCurrentApiKey();
const placesSearcher = new PlacesSearcher(apiKey);
const result = await placesSearcher.getTimezone(params.latitude, params.longitude, params.timestamp);

if (!result.success) {
return {
content: [{ type: "text", text: result.error || "Failed to get timezone data" }],
isError: true,
};
}

return {
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
isError: false,
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
return {
isError: true,
content: [{ type: "text", text: `Error getting timezone: ${errorMessage}` }],
};
}
}

export const Timezone = {
NAME,
DESCRIPTION,
SCHEMA,
ACTION,
};
Loading
Loading