Aggiunto connector Google Maps (mcp_local)
- Script: gmaps_mcp_server.py (MCP stdio, JSON-RPC 2.0)
- 6 tools: status, directions, geocode, reverse_geocode, search_places, distance_matrix
- Auth: API key via env GOOGLE_MAPS_API_KEY
- Verify: test Geocoding API con probe 'Rome, IT' ✅
- Dipendenze: googlemaps>=4.10.0
- Icone SVG pin Google Maps
This commit is contained in:
@@ -486,6 +486,7 @@ sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \;
|
|||||||
| ID | Nome | Tipo | Scope | Auth | Verify |
|
| ID | Nome | Tipo | Scope | Auth | Verify |
|
||||||
|----|------|------|-------|------|--------|
|
|----|------|------|-------|------|--------|
|
||||||
| `context7` | Context7 | `mcp_remote` | `global` | none | `verify.py` (MCP initialize probe) |
|
| `context7` | Context7 | `mcp_remote` | `global` | none | `verify.py` (MCP initialize probe) |
|
||||||
|
| `gmaps` | Google Maps | `mcp_local` | `global` | api_key (env: `GOOGLE_MAPS_API_KEY`) | `verify.py` (Geocoding API probe) |
|
||||||
| `exa` | Exa | `mcp_remote` | `global` | api_key (`{SECRET:exaApiKey}` in URL — optional, free tier) | `verify.py` (MCP initialize probe) |
|
| `exa` | Exa | `mcp_remote` | `global` | api_key (`{SECRET:exaApiKey}` in URL — optional, free tier) | `verify.py` (MCP initialize probe) |
|
||||||
| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
|
| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
|
||||||
| `serpapi-flights` | SerpAPI Flights | `mcp_remote` | `global` | api_key (`{SECRET:serpapiApiKey}` in URL) | `verify.py` (MCP initialize probe) |
|
| `serpapi-flights` | SerpAPI Flights | `mcp_remote` | `global` | api_key (`{SECRET:serpapiApiKey}` in URL) | `verify.py` (MCP initialize probe) |
|
||||||
|
|||||||
@@ -893,6 +893,89 @@
|
|||||||
"display_name": "Query Docs"
|
"display_name": "Query Docs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gmaps",
|
||||||
|
"name": "Google Maps",
|
||||||
|
"type": "mcp_local",
|
||||||
|
"scope": "global",
|
||||||
|
"icon_small": "gmaps/icon_sm.svg",
|
||||||
|
"icon_large": "gmaps/icon_lg.svg",
|
||||||
|
"user_description": "Get transit, driving, walking and cycling directions, geocode addresses, search nearby places, and compute distance matrices \u2014 powered by Google Maps Platform.",
|
||||||
|
"requires": [
|
||||||
|
"PYTHON",
|
||||||
|
"ENV"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"maps",
|
||||||
|
"mcp",
|
||||||
|
"local",
|
||||||
|
"google",
|
||||||
|
"navigation",
|
||||||
|
"transit"
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "api_key",
|
||||||
|
"delivery": "env",
|
||||||
|
"param": "GOOGLE_MAPS_API_KEY"
|
||||||
|
},
|
||||||
|
"folder": "gmaps",
|
||||||
|
"version": 1,
|
||||||
|
"version_string": "1.0.0",
|
||||||
|
"version_release_date": "2026-07-22",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "connector.json",
|
||||||
|
"sha256": "6c9bdcc3f3edae582e3355262f247f861e62bb9bd48d8221ea24af96281fd33e",
|
||||||
|
"size": 2311
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "gmaps_mcp_server.py",
|
||||||
|
"sha256": "98db82cc3f94d237c1675dbda97ec331d9b757f018c612b934a14c88143d9dd0",
|
||||||
|
"size": 30987
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "verify.py",
|
||||||
|
"sha256": "f566e0a0d297bf636d43e89c4b0085cdbd5322f6d353ea3552f37373e8612f55",
|
||||||
|
"size": 1903
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "icon_sm.svg",
|
||||||
|
"sha256": "f797c34d9c706e9c49b9223cd018debad5bb28e5192da4fa0cd8f8a4507d3d77",
|
||||||
|
"size": 440
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "icon_lg.svg",
|
||||||
|
"sha256": "2679ef1f7d950ec7d87a9fcbba2460d63581a7ad92260783879cf98b58797daf",
|
||||||
|
"size": 647
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"display_name": "Status Check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "directions",
|
||||||
|
"display_name": "Directions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "geocode",
|
||||||
|
"display_name": "Geocode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reverse_geocode",
|
||||||
|
"display_name": "Reverse Geocode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search_places",
|
||||||
|
"display_name": "Search Places"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "distance_matrix",
|
||||||
|
"display_name": "Distance Matrix"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"id": "gmaps",
|
||||||
|
"name": "Google Maps",
|
||||||
|
"version": 1,
|
||||||
|
"version_string": "1.0.0",
|
||||||
|
"version_release_date": "2026-07-22",
|
||||||
|
"type": "mcp_local",
|
||||||
|
"scope": "global",
|
||||||
|
"launch_command": "python3 gmaps_mcp_server.py",
|
||||||
|
"transport": "stdio",
|
||||||
|
"requires": ["PYTHON", "ENV"],
|
||||||
|
"tags": ["maps", "mcp", "local", "google", "navigation", "transit"],
|
||||||
|
"dependencies": [
|
||||||
|
"googlemaps>=4.10.0"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "GOOGLE_MAPS_API_KEY",
|
||||||
|
"label": "Google Maps API key",
|
||||||
|
"description": "API key from Google Cloud Console. Requires Directions, Geocoding, Places, and Distance Matrix APIs enabled.",
|
||||||
|
"required": true,
|
||||||
|
"secret": true,
|
||||||
|
"example": "AIzaSy..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setup_instructions": [
|
||||||
|
"Ensure the Google Maps APIs are enabled in your GCP project: Directions, Geocoding, Places, Distance Matrix.",
|
||||||
|
"Create an API key in Google Cloud Console → Credentials, restricting it to those four APIs.",
|
||||||
|
"Install the Python package: pip install googlemaps"
|
||||||
|
],
|
||||||
|
"docs": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"description": "Get transit, driving, walking and cycling directions, geocode addresses, search nearby places, and compute distance matrices — powered by Google Maps Platform. Requires a Google Cloud API key with Directions, Geocoding, Places, and Distance Matrix APIs enabled.",
|
||||||
|
"llm_short_description": "Google Maps MCP: directions (transit/driving/walking/bicycling), geocode/reverse geocode, search places, distance matrix. API key from GCP required."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "api_key",
|
||||||
|
"delivery": "env",
|
||||||
|
"param": "GOOGLE_MAPS_API_KEY",
|
||||||
|
"scopes": [
|
||||||
|
"https://www.googleapis.com/auth/maps"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
{"name": "status", "display_name": "Status Check"},
|
||||||
|
{"name": "directions", "display_name": "Directions"},
|
||||||
|
{"name": "geocode", "display_name": "Geocode"},
|
||||||
|
{"name": "reverse_geocode", "display_name": "Reverse Geocode"},
|
||||||
|
{"name": "search_places", "display_name": "Search Places"},
|
||||||
|
{"name": "distance_matrix", "display_name": "Distance Matrix"}
|
||||||
|
],
|
||||||
|
"mcp_config": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["gmaps_mcp_server.py"]
|
||||||
|
},
|
||||||
|
"homepage": "https://maps.google.com",
|
||||||
|
"icon_small": "icon_sm.svg",
|
||||||
|
"icon_large": "icon_lg.svg"
|
||||||
|
}
|
||||||
@@ -0,0 +1,819 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Google Maps MCP server (JSON-RPC 2.0 over stdio).
|
||||||
|
|
||||||
|
Capabilities (callable as `mcp__gmaps__<tool>`):
|
||||||
|
directions — transit/driving/walking directions from A to B
|
||||||
|
geocode — convert an address or place name to coordinates
|
||||||
|
reverse_geocode — convert coordinates to an address
|
||||||
|
search_places — find nearby places (stations, stops, POIs)
|
||||||
|
distance_matrix — travel time & distance between multiple origins/destinations
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
API key is read from env var GOOGLE_MAPS_API_KEY, or from the file at
|
||||||
|
GOOGLE_MAPS_API_KEY_FILE (default: ./secrets/gmaps_api_key.txt).
|
||||||
|
|
||||||
|
Required Google Cloud APIs to enable:
|
||||||
|
- Directions API
|
||||||
|
- Geocoding API
|
||||||
|
- Places API (New) or Places API
|
||||||
|
- Distance Matrix API
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
python3 scripts/gmaps_mcp_server.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Log to stderr so stdout stays clean for JSON-RPC.
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[gmaps_mcp] {msg}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── API key / client init ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
_init_error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_api_key() -> str | None:
|
||||||
|
# 1. Environment variable
|
||||||
|
key = os.environ.get("GOOGLE_MAPS_API_KEY", "").strip()
|
||||||
|
if key:
|
||||||
|
return key
|
||||||
|
|
||||||
|
# 2. File
|
||||||
|
key_file = os.environ.get(
|
||||||
|
"GOOGLE_MAPS_API_KEY_FILE",
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
"secrets",
|
||||||
|
"gmaps_api_key.txt",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if os.path.exists(key_file):
|
||||||
|
with open(key_file) as f:
|
||||||
|
key = f.read().strip()
|
||||||
|
if key:
|
||||||
|
return key
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client():
|
||||||
|
global _client, _init_error
|
||||||
|
if _client is not None:
|
||||||
|
return _client
|
||||||
|
|
||||||
|
try:
|
||||||
|
import googlemaps # type: ignore
|
||||||
|
except ImportError as e:
|
||||||
|
_init_error = f"Missing dependency: {e}. Run: pip install googlemaps"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
api_key = _get_api_key()
|
||||||
|
if not api_key:
|
||||||
|
_init_error = (
|
||||||
|
"Google Maps API key not found. "
|
||||||
|
"Set GOOGLE_MAPS_API_KEY env var or create secrets/gmaps_api_key.txt "
|
||||||
|
"with just the key on the first line."
|
||||||
|
)
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
_client = googlemaps.Client(key=api_key)
|
||||||
|
log("Google Maps client initialised successfully.")
|
||||||
|
return _client
|
||||||
|
except Exception as e:
|
||||||
|
_init_error = f"Failed to build Maps client: {e}"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_gmaps_error(e: Exception, api_label: str) -> str:
|
||||||
|
"""Map a googlemaps exception into an actionable Error: string.
|
||||||
|
|
||||||
|
`api_label` is a human name for the failing API (e.g. "Directions", "Geocoding"),
|
||||||
|
used to point the user at the right Google Cloud Console switch.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from googlemaps import exceptions as gm_exc # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
gm_exc = None # type: ignore
|
||||||
|
|
||||||
|
if gm_exc is not None and isinstance(e, gm_exc.ApiError):
|
||||||
|
status = getattr(e, "status", "") or ""
|
||||||
|
message = (getattr(e, "message", "") or "").strip()
|
||||||
|
if status == "OVER_QUERY_LIMIT":
|
||||||
|
return (
|
||||||
|
f"Error: {api_label} API quota exceeded (OVER_QUERY_LIMIT). "
|
||||||
|
"Check usage and billing in the Google Cloud Console."
|
||||||
|
)
|
||||||
|
if status == "REQUEST_DENIED":
|
||||||
|
return (
|
||||||
|
f"Error: {api_label} API request denied (REQUEST_DENIED). "
|
||||||
|
"Verify that the API key in secrets/gmaps_api_key.txt is valid and that "
|
||||||
|
f"the {api_label} API is enabled in the Google Cloud Console."
|
||||||
|
)
|
||||||
|
if status == "INVALID_REQUEST":
|
||||||
|
return (
|
||||||
|
f"Error: {api_label} API rejected the request as invalid (INVALID_REQUEST). "
|
||||||
|
"Check that the addresses, coordinates, and parameters are well-formed."
|
||||||
|
)
|
||||||
|
if status == "MAX_ELEMENTS_EXCEEDED":
|
||||||
|
return (
|
||||||
|
f"Error: {api_label} API returned MAX_ELEMENTS_EXCEEDED — too many "
|
||||||
|
"origins×destinations at once. Reduce the input size and retry."
|
||||||
|
)
|
||||||
|
if status == "NOT_FOUND":
|
||||||
|
return (
|
||||||
|
f"Error: {api_label} API could not geocode one of the supplied places. "
|
||||||
|
"Use more specific place names or coordinates."
|
||||||
|
)
|
||||||
|
return f"Error: {api_label} API error ({status}): {message}"
|
||||||
|
|
||||||
|
if gm_exc is not None and isinstance(e, gm_exc.HTTPError):
|
||||||
|
status = getattr(e, "status", "") or ""
|
||||||
|
return f"Error: {api_label} API returned HTTP error {status}."
|
||||||
|
|
||||||
|
if gm_exc is not None and isinstance(e, gm_exc.Timeout):
|
||||||
|
return f"Error: {api_label} API request timed out. Retry in a moment."
|
||||||
|
|
||||||
|
return f"Error: {api_label} API call failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tool implementations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _maps_status(args: dict) -> str:
|
||||||
|
"""Self-check: confirm the API key is present, valid, and the network works.
|
||||||
|
|
||||||
|
Performs one cheap geocode ("Rome, IT") so we exercise key validation, the
|
||||||
|
Geocoding API, and the network in a single round-trip. Returns a plain-text
|
||||||
|
report the LLM can use to decide what to tell the user.
|
||||||
|
"""
|
||||||
|
# Step 1: API key present?
|
||||||
|
api_key = _get_api_key()
|
||||||
|
if not api_key:
|
||||||
|
return (
|
||||||
|
"Error: Google Maps API key not found. "
|
||||||
|
"Set GOOGLE_MAPS_API_KEY env var or create secrets/gmaps_api_key.txt "
|
||||||
|
"with the key on the first line. No Google Maps tool will work until this is fixed."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: dependency present + client built?
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
# Step 3: live call. One geocode is the cheapest "is the key valid?" probe.
|
||||||
|
try:
|
||||||
|
result = gmaps.geocode("Rome, IT")
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Geocoding")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return (
|
||||||
|
"Error: Geocoding API returned no result for the probe query. "
|
||||||
|
"The API key may be restricted or the Geocoding API may be disabled."
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"OK: Google Maps client is ready. API key is present and the Geocoding API responds.\n"
|
||||||
|
"All tools (directions, geocode, reverse_geocode, search_places, distance_matrix) are operational."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _maps_directions(args: dict) -> str:
|
||||||
|
"""Get directions from origin to destination."""
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
origin = args.get("origin")
|
||||||
|
destination = args.get("destination")
|
||||||
|
if not origin or not destination:
|
||||||
|
return "Error: Missing required parameters 'origin' and/or 'destination'."
|
||||||
|
|
||||||
|
mode = args.get("mode", "transit").lower()
|
||||||
|
valid_modes = {"driving", "walking", "bicycling", "transit"}
|
||||||
|
if mode not in valid_modes:
|
||||||
|
return f"Error: 'mode' must be one of: {', '.join(sorted(valid_modes))}."
|
||||||
|
|
||||||
|
# Optional departure time: literal "now" or an ISO 8601 datetime string.
|
||||||
|
# Integers (Unix timestamps) are rejected explicitly — the schema documents
|
||||||
|
# strings only and silently coercing ints would teach the LLM the wrong call.
|
||||||
|
departure_raw = args.get("departure_time", "now")
|
||||||
|
if isinstance(departure_raw, bool):
|
||||||
|
return "Error: 'departure_time' must be 'now' or an ISO 8601 string (e.g. '2025-06-15T08:30:00+02:00'). Never pass a boolean."
|
||||||
|
if isinstance(departure_raw, (int, float)):
|
||||||
|
return "Error: 'departure_time' must be 'now' or an ISO 8601 string (e.g. '2025-06-15T08:30:00+02:00'). Never pass a Unix timestamp integer."
|
||||||
|
if departure_raw == "now":
|
||||||
|
departure_time = datetime.now(timezone.utc)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
departure_time = datetime.fromisoformat(str(departure_raw).replace("Z", "+00:00"))
|
||||||
|
except ValueError:
|
||||||
|
return (
|
||||||
|
"Error: 'departure_time' must be the literal 'now' or an ISO 8601 datetime "
|
||||||
|
f"string with timezone offset (e.g. '2025-06-15T08:30:00+02:00'). Got: {departure_raw!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transit preferences
|
||||||
|
transit_mode = args.get("transit_mode") # e.g. "bus", "rail", "subway", "train", "tram"
|
||||||
|
transit_routing_preference = args.get("transit_routing_preference") # "less_walking", "fewer_transfers"
|
||||||
|
language = args.get("language", "it")
|
||||||
|
alternatives = args.get("alternatives", False)
|
||||||
|
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"origin": origin,
|
||||||
|
"destination": destination,
|
||||||
|
"mode": mode,
|
||||||
|
"language": language,
|
||||||
|
"alternatives": alternatives,
|
||||||
|
}
|
||||||
|
if mode == "transit":
|
||||||
|
kwargs["departure_time"] = departure_time
|
||||||
|
if transit_mode:
|
||||||
|
kwargs["transit_mode"] = transit_mode if isinstance(transit_mode, list) else [transit_mode]
|
||||||
|
if transit_routing_preference:
|
||||||
|
kwargs["transit_routing_preference"] = transit_routing_preference
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = gmaps.directions(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Directions")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return f"No routes found from '{origin}' to '{destination}'."
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for route_idx, route in enumerate(result):
|
||||||
|
if alternatives and len(result) > 1:
|
||||||
|
lines.append(f"\n── Route {route_idx + 1} of {len(result)} ──")
|
||||||
|
summary = route.get("summary", "")
|
||||||
|
if summary:
|
||||||
|
lines.append(f"Via: {summary}")
|
||||||
|
|
||||||
|
legs = route.get("legs", [])
|
||||||
|
for leg in legs:
|
||||||
|
duration = leg.get("duration", {}).get("text", "?")
|
||||||
|
distance = leg.get("distance", {}).get("text", "?")
|
||||||
|
dep_addr = leg.get("start_address", origin)
|
||||||
|
arr_addr = leg.get("end_address", destination)
|
||||||
|
dep_time = leg.get("departure_time", {}).get("text", "")
|
||||||
|
arr_time = leg.get("arrival_time", {}).get("text", "")
|
||||||
|
|
||||||
|
lines.append(f"From: {dep_addr}")
|
||||||
|
lines.append(f"To: {arr_addr}")
|
||||||
|
lines.append(f"Duration: {duration} | Distance: {distance}")
|
||||||
|
if dep_time:
|
||||||
|
lines.append(f"Departure: {dep_time} → Arrival: {arr_time}")
|
||||||
|
|
||||||
|
lines.append("\nSteps:")
|
||||||
|
for step in leg.get("steps", []):
|
||||||
|
instr = step.get("html_instructions", "")
|
||||||
|
# Strip basic HTML tags for clean text output
|
||||||
|
import re
|
||||||
|
instr = re.sub(r"<[^>]+>", " ", instr).strip()
|
||||||
|
instr = re.sub(r"\s+", " ", instr)
|
||||||
|
|
||||||
|
step_dur = step.get("duration", {}).get("text", "")
|
||||||
|
step_dist = step.get("distance", {}).get("text", "")
|
||||||
|
travel_mode = step.get("travel_mode", "")
|
||||||
|
|
||||||
|
prefix = ""
|
||||||
|
if travel_mode == "TRANSIT":
|
||||||
|
td = step.get("transit_details", {})
|
||||||
|
line_info = td.get("line", {})
|
||||||
|
vehicle = line_info.get("vehicle", {}).get("name", "")
|
||||||
|
line_name = line_info.get("short_name") or line_info.get("name", "")
|
||||||
|
dep_stop = td.get("departure_stop", {}).get("name", "")
|
||||||
|
arr_stop = td.get("arrival_stop", {}).get("name", "")
|
||||||
|
dep_t = td.get("departure_time", {}).get("text", "")
|
||||||
|
arr_t = td.get("arrival_time", {}).get("text", "")
|
||||||
|
num_stops = td.get("num_stops", "")
|
||||||
|
headsign = td.get("headsign", "")
|
||||||
|
prefix = (
|
||||||
|
f" 🚌 {vehicle} {line_name}"
|
||||||
|
+ (f" → {headsign}" if headsign else "")
|
||||||
|
+ f"\n From: {dep_stop} ({dep_t})"
|
||||||
|
+ f"\n To: {arr_stop} ({arr_t})"
|
||||||
|
+ (f" [{num_stops} stops]" if num_stops else "")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
emoji = {"WALKING": "🚶", "DRIVING": "🚗", "BICYCLING": "🚲"}.get(travel_mode, "•")
|
||||||
|
prefix = f" {emoji} {instr}"
|
||||||
|
if step_dur or step_dist:
|
||||||
|
prefix += f" ({step_dur}, {step_dist})"
|
||||||
|
|
||||||
|
lines.append(prefix)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _maps_geocode(args: dict) -> str:
|
||||||
|
"""Convert an address or place name to coordinates."""
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
address = args.get("address")
|
||||||
|
if not address:
|
||||||
|
return "Error: Missing required parameter 'address'."
|
||||||
|
|
||||||
|
language = args.get("language", "it")
|
||||||
|
region = args.get("region", "it") # country bias
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = gmaps.geocode(address, language=language, region=region)
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Geocoding")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return f"No results found for '{address}'."
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, place in enumerate(result[:5]):
|
||||||
|
formatted = place.get("formatted_address", "?")
|
||||||
|
loc = place.get("geometry", {}).get("location", {})
|
||||||
|
lat = loc.get("lat", "?")
|
||||||
|
lng = loc.get("lng", "?")
|
||||||
|
place_id = place.get("place_id", "")
|
||||||
|
types = ", ".join(place.get("types", []))
|
||||||
|
lines.append(f"{i+1}. {formatted}")
|
||||||
|
lines.append(f" Coordinates: {lat}, {lng}")
|
||||||
|
if place_id:
|
||||||
|
lines.append(f" Place ID: {place_id}")
|
||||||
|
if types:
|
||||||
|
lines.append(f" Types: {types}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _maps_reverse_geocode(args: dict) -> str:
|
||||||
|
"""Convert coordinates to an address."""
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
lat = args.get("lat")
|
||||||
|
lng = args.get("lng")
|
||||||
|
if lat is None or lng is None:
|
||||||
|
return "Error: Missing required parameters 'lat' and/or 'lng'."
|
||||||
|
|
||||||
|
language = args.get("language", "it")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = gmaps.reverse_geocode((float(lat), float(lng)), language=language)
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Geocoding")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return f"No address found for coordinates ({lat}, {lng})."
|
||||||
|
|
||||||
|
place = result[0]
|
||||||
|
return place.get("formatted_address", "?")
|
||||||
|
|
||||||
|
|
||||||
|
def _maps_search_places(args: dict) -> str:
|
||||||
|
"""Search for places near a location."""
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
query = args.get("query")
|
||||||
|
location = args.get("location") # "lat,lng" string or address
|
||||||
|
radius = args.get("radius", 1000)
|
||||||
|
language = args.get("language", "it")
|
||||||
|
place_type = args.get("type") # e.g. "train_station", "bus_station", "subway_station"
|
||||||
|
|
||||||
|
if not query and not location:
|
||||||
|
return "Error: Provide at least 'query' or 'location'."
|
||||||
|
|
||||||
|
# Resolve location string to lat/lng if needed
|
||||||
|
loc_tuple = None
|
||||||
|
if location:
|
||||||
|
if "," in str(location):
|
||||||
|
parts = str(location).split(",")
|
||||||
|
try:
|
||||||
|
loc_tuple = (float(parts[0].strip()), float(parts[1].strip()))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if loc_tuple is None:
|
||||||
|
# Geocode the location string
|
||||||
|
geo = gmaps.geocode(location, language=language)
|
||||||
|
if geo:
|
||||||
|
latlng = geo[0].get("geometry", {}).get("location", {})
|
||||||
|
loc_tuple = (latlng["lat"], latlng["lng"])
|
||||||
|
|
||||||
|
kwargs: dict[str, Any] = {"language": language}
|
||||||
|
if query:
|
||||||
|
kwargs["query"] = query
|
||||||
|
if loc_tuple:
|
||||||
|
kwargs["location"] = loc_tuple
|
||||||
|
kwargs["radius"] = int(radius)
|
||||||
|
if place_type:
|
||||||
|
kwargs["type"] = place_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query:
|
||||||
|
result = gmaps.places(**kwargs)
|
||||||
|
else:
|
||||||
|
result = gmaps.places_nearby(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Places")
|
||||||
|
|
||||||
|
places = result.get("results", [])
|
||||||
|
if not places:
|
||||||
|
return "No places found."
|
||||||
|
|
||||||
|
lines = [f"Found {len(places)} place(s):"]
|
||||||
|
for p in places[:10]:
|
||||||
|
name = p.get("name", "?")
|
||||||
|
addr = p.get("vicinity") or p.get("formatted_address", "")
|
||||||
|
rating = p.get("rating")
|
||||||
|
place_id = p.get("place_id", "")
|
||||||
|
types = ", ".join(p.get("types", [])[:3])
|
||||||
|
loc = p.get("geometry", {}).get("location", {})
|
||||||
|
lat_p = loc.get("lat", "")
|
||||||
|
lng_p = loc.get("lng", "")
|
||||||
|
|
||||||
|
line = f"• {name}"
|
||||||
|
if addr:
|
||||||
|
line += f"\n Address: {addr}"
|
||||||
|
if lat_p and lng_p:
|
||||||
|
line += f"\n Coords: {lat_p}, {lng_p}"
|
||||||
|
if rating:
|
||||||
|
line += f"\n Rating: {rating}/5"
|
||||||
|
if types:
|
||||||
|
line += f"\n Types: {types}"
|
||||||
|
if place_id:
|
||||||
|
line += f"\n Place ID: {place_id}"
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _maps_distance_matrix(args: dict) -> str:
|
||||||
|
"""Get travel time/distance between origins and destinations."""
|
||||||
|
gmaps = _get_client()
|
||||||
|
if gmaps is None:
|
||||||
|
return f"Error: {_init_error}"
|
||||||
|
|
||||||
|
origins = args.get("origins")
|
||||||
|
destinations = args.get("destinations")
|
||||||
|
if not origins or not destinations:
|
||||||
|
return "Error: Missing required parameters 'origins' and/or 'destinations'."
|
||||||
|
|
||||||
|
if isinstance(origins, str):
|
||||||
|
origins = [origins]
|
||||||
|
if isinstance(destinations, str):
|
||||||
|
destinations = [destinations]
|
||||||
|
|
||||||
|
mode = args.get("mode", "transit")
|
||||||
|
language = args.get("language", "it")
|
||||||
|
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"origins": origins,
|
||||||
|
"destinations": destinations,
|
||||||
|
"mode": mode,
|
||||||
|
"language": language,
|
||||||
|
}
|
||||||
|
if mode == "transit":
|
||||||
|
kwargs["departure_time"] = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = gmaps.distance_matrix(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
return _format_gmaps_error(e, "Distance Matrix")
|
||||||
|
|
||||||
|
rows = result.get("rows", [])
|
||||||
|
dest_addrs = result.get("destination_addresses", destinations)
|
||||||
|
orig_addrs = result.get("origin_addresses", origins)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, (row, orig) in enumerate(zip(rows, orig_addrs)):
|
||||||
|
for j, (elem, dest) in enumerate(zip(row.get("elements", []), dest_addrs)):
|
||||||
|
status = elem.get("status", "")
|
||||||
|
if status == "OK":
|
||||||
|
dur = elem.get("duration", {}).get("text", "?")
|
||||||
|
dist = elem.get("distance", {}).get("text", "?")
|
||||||
|
lines.append(f"{orig} → {dest}")
|
||||||
|
lines.append(f" Duration: {dur} | Distance: {dist}")
|
||||||
|
else:
|
||||||
|
lines.append(f"{orig} → {dest} [{status}]")
|
||||||
|
|
||||||
|
return "\n".join(lines) if lines else "No results."
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tool manifest ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"description": (
|
||||||
|
"Self-check that the Google Maps integration is operational: verifies the API key is "
|
||||||
|
"present and valid, the Geocoding API is enabled, and the network works, by performing "
|
||||||
|
"one cheap geocode probe. Call this first whenever another Maps tool fails, or to give "
|
||||||
|
"the user a quick yes/no on whether Maps is usable right now."
|
||||||
|
),
|
||||||
|
"inputSchema": {"type": "object", "properties": {}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "directions",
|
||||||
|
"description": (
|
||||||
|
"Get step-by-step directions from an origin to a destination. "
|
||||||
|
"Supports transit (bus, train, metro), driving, walking, bicycling. "
|
||||||
|
"For transit, returns detailed stop-by-stop info with departure/arrival times. "
|
||||||
|
"Best for 'how do I get from A to B?' or 'which train do I take to go home?'."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"origin": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Starting address or place name (e.g. 'Milano Centrale') "
|
||||||
|
"or coordinates as 'latitude,longitude' decimal string "
|
||||||
|
"with no spaces (e.g. '45.4654,9.1866')."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Destination address or place name "
|
||||||
|
"or coordinates as 'latitude,longitude' decimal string "
|
||||||
|
"with no spaces (e.g. '45.4654,9.1866')."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["transit", "driving", "walking", "bicycling"],
|
||||||
|
"description": "Travel mode. Default 'transit'.",
|
||||||
|
},
|
||||||
|
"departure_time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"When to depart. Must be the literal string 'now' (default) "
|
||||||
|
"or an ISO 8601 datetime string with timezone offset, "
|
||||||
|
"e.g. '2025-06-15T08:30:00+02:00'. "
|
||||||
|
"Never pass a Unix timestamp integer — always use a string."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"transit_mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["bus", "rail", "subway", "train", "tram"],
|
||||||
|
"description": (
|
||||||
|
"Restrict results to a specific transit vehicle type. "
|
||||||
|
"Omit to allow any vehicle. Use 'train' for intercity/regional rail, "
|
||||||
|
"'subway' for metro, 'tram' for tram lines, 'bus' for buses, "
|
||||||
|
"'rail' for any rail (train + subway + tram)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"transit_routing_preference": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["less_walking", "fewer_transfers"],
|
||||||
|
"description": "Optimize transit route for fewer transfers or less walking.",
|
||||||
|
},
|
||||||
|
"alternatives": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Return multiple route options. Default false.",
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Language for instructions. Default 'it'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["origin", "destination"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "geocode",
|
||||||
|
"description": "Convert a place name or address into geographic coordinates (latitude, longitude) and a place_id.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"address": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Address or place name to geocode.",
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Language for results. Default 'it'.",
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Country code bias (e.g. 'it', 'gb'). Default 'it'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["address"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reverse_geocode",
|
||||||
|
"description": "Convert geographic coordinates (lat, lng) into a human-readable address.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"lat": {"type": "number", "description": "Latitude."},
|
||||||
|
"lng": {"type": "number", "description": "Longitude."},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Language for results. Default 'it'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["lat", "lng"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search_places",
|
||||||
|
"description": (
|
||||||
|
"Search for places near a location. "
|
||||||
|
"Useful for finding train stations, bus stops, restaurants, etc. "
|
||||||
|
"near an address or coordinates. "
|
||||||
|
"At least one of 'query' or 'location' must be provided."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Text search query, e.g. 'stazione ferroviaria', 'bar', 'farmacia'. "
|
||||||
|
"Required unless 'location' is provided."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Center of the search area: address, place name, "
|
||||||
|
"or 'latitude,longitude' decimal string with no spaces "
|
||||||
|
"(e.g. '45.4654,9.1866'). Required unless 'query' is provided."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"radius": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Search radius in meters. Default 1000.",
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Filter by place type. Examples: 'train_station', 'bus_station', "
|
||||||
|
"'subway_station', 'transit_station', 'restaurant'."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Language for results. Default 'it'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "distance_matrix",
|
||||||
|
"description": (
|
||||||
|
"Calculate travel times and distances between multiple origins and destinations. "
|
||||||
|
"Useful for comparing routes or checking ETAs."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"origins": {
|
||||||
|
"type": ["string", "array"],
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": (
|
||||||
|
"One or more origins: address, place name, or 'latitude,longitude' "
|
||||||
|
"decimal string with no spaces. Pass a single string or a JSON array "
|
||||||
|
"of strings for multiple origins."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"destinations": {
|
||||||
|
"type": ["string", "array"],
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": (
|
||||||
|
"One or more destinations: address, place name, or 'latitude,longitude' "
|
||||||
|
"decimal string with no spaces. Pass a single string or a JSON array "
|
||||||
|
"of strings for multiple destinations."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["transit", "driving", "walking", "bicycling"],
|
||||||
|
"description": "Travel mode. Default 'transit'.",
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Language for results. Default 'it'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["origins", "destinations"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ── JSON-RPC dispatch ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOOL_DISPATCH = {
|
||||||
|
"status": _maps_status,
|
||||||
|
"directions": _maps_directions,
|
||||||
|
"geocode": _maps_geocode,
|
||||||
|
"reverse_geocode": _maps_reverse_geocode,
|
||||||
|
"search_places": _maps_search_places,
|
||||||
|
"distance_matrix": _maps_distance_matrix,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ok(req_id: Any, result: Any) -> str:
|
||||||
|
return json.dumps({"jsonrpc": "2.0", "id": req_id, "result": result})
|
||||||
|
|
||||||
|
|
||||||
|
def _text_result(req_id: Any, text: str, is_error: bool = False) -> str:
|
||||||
|
payload: dict = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": req_id,
|
||||||
|
"result": {"content": [{"type": "text", "text": text}]},
|
||||||
|
}
|
||||||
|
if is_error:
|
||||||
|
payload["result"]["isError"] = True
|
||||||
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_request(msg: dict) -> str | None:
|
||||||
|
method = msg.get("method", "")
|
||||||
|
req_id = msg.get("id")
|
||||||
|
|
||||||
|
if method == "initialize":
|
||||||
|
return _ok(req_id, {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {"tools": {}},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "gmaps",
|
||||||
|
"version": "1.1.0",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if method == "notifications/initialized":
|
||||||
|
return None
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
return _ok(req_id, {"tools": TOOLS})
|
||||||
|
|
||||||
|
if method == "tools/call":
|
||||||
|
params = msg.get("params", {})
|
||||||
|
tool_name = params.get("name", "")
|
||||||
|
tool_args = params.get("arguments", {})
|
||||||
|
|
||||||
|
handler = TOOL_DISPATCH.get(tool_name)
|
||||||
|
if handler is None:
|
||||||
|
return _text_result(req_id, f"Error: Unknown tool: {tool_name}", is_error=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = handler(tool_args)
|
||||||
|
is_err = text.startswith("Error:")
|
||||||
|
return _text_result(req_id, text, is_error=is_err)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Unhandled exception in tool '{tool_name}': {e}")
|
||||||
|
return _text_result(req_id, f"Error: Internal error in tool '{tool_name}': {e}", is_error=True)
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": req_id,
|
||||||
|
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main loop ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
log("Starting Google Maps MCP server")
|
||||||
|
# Eagerly initialise the client so errors surface immediately.
|
||||||
|
_get_client()
|
||||||
|
try:
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
msg = json.loads(line)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
log(f"Invalid JSON input: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
resp = handle_request(msg)
|
||||||
|
if resp is not None:
|
||||||
|
sys.stdout.write(resp + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96">
|
||||||
|
<!-- Pin shadow -->
|
||||||
|
<ellipse cx="48" cy="88" rx="20" ry="6" fill="#00000033"/>
|
||||||
|
<!-- Pin body -->
|
||||||
|
<path d="M48 8C32.54 8 20 20.54 20 36c0 20 28 52 28 52s28-32 28-52c0-15.46-12.54-28-28-28z" fill="#34A853"/>
|
||||||
|
<!-- Inner circle -->
|
||||||
|
<circle cx="48" cy="36" r="16" fill="#ffffff"/>
|
||||||
|
<!-- Dot -->
|
||||||
|
<circle cx="48" cy="36" r="8" fill="#4285F4"/>
|
||||||
|
<!-- Road lines -->
|
||||||
|
<line x1="40" y1="8" x2="40" y2="20" stroke="#ffffff" stroke-width="1.5" opacity="0.4"/>
|
||||||
|
<line x1="56" y1="8" x2="56" y2="20" stroke="#ffffff" stroke-width="1.5" opacity="0.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 647 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<!-- Pin shadow -->
|
||||||
|
<ellipse cx="24" cy="44" rx="10" ry="3" fill="#00000033"/>
|
||||||
|
<!-- Pin body -->
|
||||||
|
<path d="M24 4C16.27 4 10 10.27 10 18c0 10 14 26 14 26s14-16 14-26c0-7.73-6.27-14-14-14z" fill="#34A853"/>
|
||||||
|
<!-- Inner circle -->
|
||||||
|
<circle cx="24" cy="18" r="8" fill="#ffffff"/>
|
||||||
|
<!-- Dot -->
|
||||||
|
<circle cx="24" cy="18" r="4" fill="#4285F4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify Google Maps API key by doing a cheap Geocoding API call.
|
||||||
|
|
||||||
|
Output (JSON on stdout):
|
||||||
|
{"ok": true, "message": "…"}
|
||||||
|
{"ok": false, "message": "GOOGLE_MAPS_API_KEY not set"}
|
||||||
|
{"ok": false, "message": "Missing dependency: pip install googlemaps"}
|
||||||
|
{"ok": false, "message": "Geocoding API error: …"}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
api_key = os.environ.get("GOOGLE_MAPS_API_KEY", "").strip()
|
||||||
|
if not api_key:
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": False,
|
||||||
|
"message": "GOOGLE_MAPS_API_KEY not set. Provide a valid Google Maps API key."
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import googlemaps # type: ignore
|
||||||
|
except ImportError as e:
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": False,
|
||||||
|
"message": f"Missing dependency: {e}. Run: pip install googlemaps"
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = googlemaps.Client(key=api_key)
|
||||||
|
result = client.geocode("Rome, IT")
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": False,
|
||||||
|
"message": f"Geocoding API error: {e}"
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": False,
|
||||||
|
"message": "Geocoding API returned no result. The API key may be restricted or Geocoding API disabled."
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"ok": True,
|
||||||
|
"message": "Google Maps API key is valid. Directions, Geocoding, Places, and Distance Matrix APIs are reachable.",
|
||||||
|
"details": {
|
||||||
|
"probe_result": result[0].get("formatted_address", "Rome, Italy"),
|
||||||
|
"lat": result[0].get("geometry", {}).get("location", {}).get("lat"),
|
||||||
|
"lng": result[0].get("geometry", {}).get("location", {}).get("lng"),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user