From ff8e6a4019a39a9f6ee5f0e058889bb83be7ea15 Mon Sep 17 00:00:00 2001 From: lillythomas Date: Mon, 2 Feb 2026 12:49:55 -0800 Subject: [PATCH 1/4] MCP compatible geocode tool for EIE agent --- akd_ext/tools/__init__.py | 10 +++ akd_ext/tools/get_place.py | 151 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 + 3 files changed, 163 insertions(+) create mode 100644 akd_ext/tools/get_place.py diff --git a/akd_ext/tools/__init__.py b/akd_ext/tools/__init__.py index 173a47b..6179f85 100644 --- a/akd_ext/tools/__init__.py +++ b/akd_ext/tools/__init__.py @@ -14,6 +14,12 @@ RepositorySearchToolOutputSchema, RepositorySearchToolConfig, ) +from .get_place import ( + GetPlaceTool, + GetPlaceToolConfig, + GetPlaceInputSchema, + GetPlaceOutputSchema, +) __all__ = [ "DummyTool", @@ -28,4 +34,8 @@ "RepositorySearchToolInputSchema", "RepositorySearchToolOutputSchema", "RepositorySearchToolConfig", + "GetPlaceTool", + "GetPlaceToolConfig", + "GetPlaceInputSchema", + "GetPlaceOutputSchema", ] diff --git a/akd_ext/tools/get_place.py b/akd_ext/tools/get_place.py new file mode 100644 index 0000000..1080006 --- /dev/null +++ b/akd_ext/tools/get_place.py @@ -0,0 +1,151 @@ +""" +Geocoding tool for resolving place names to bounding boxes. + +This tool uses the Geodini geocoding service to convert natural language +place names (e.g., "California", "Los Angeles") into geographic bounding boxes +that can be used for spatial queries. +""" + +import os +import httpx +from akd._base import InputSchema, OutputSchema +from akd.tools import BaseTool, BaseToolConfig +from pydantic import Field +from loguru import logger + +from akd_ext.mcp import mcp_tool + + +class GetPlaceToolConfig(BaseToolConfig): + """Configuration for the GetPlace Tool.""" + + geodini_host: str = Field( + default=os.getenv("GEODINI_HOST", ""), + description="Base URL for the Geodini geocoding service", + ) + timeout: float = Field( + default=15.0, + description="HTTP request timeout in seconds", + ) + + +class GetPlaceInputSchema(InputSchema): + """Input schema for the GetPlace tool.""" + + query: str = Field( + ..., + description="A place name or location to geocode (e.g., 'Los Angeles', 'California', 'Amazon rainforest')", + ) + + +class GetPlaceOutputSchema(OutputSchema): + """Output schema for the GetPlace tool.""" + + place: str | None = Field( + None, + description="The resolved place name as returned by the geocoding service", + ) + bbox: list[float] | None = Field( + None, + description="Bounding box as [west, south, east, north] (i.e., [min_lon, min_lat, max_lon, max_lat])", + ) + error: str | None = Field( + None, + description="Error message if geocoding failed", + ) + + +@mcp_tool +class GetPlaceTool(BaseTool[GetPlaceInputSchema, GetPlaceOutputSchema]): + """ + Resolve a place name to a geographic bounding box via geocoding. + + This tool uses the Geodini geocoding service to convert natural language + place names into bounding boxes suitable for spatial queries against + geospatial data catalogs (e.g., STAC). + + Input parameters: + - query: Natural language place name (e.g., "I am interested in LA", "California", "Amazon basin") + + Configuration parameters: + - geodini_host: Base URL for the Geodini service (required) + - timeout: HTTP request timeout in seconds (default: 15.0) + + Returns: + - place: Resolved place name + - bbox: Bounding box as [west, south, east, north] + - error: Error message if resolution failed + """ + + input_schema = GetPlaceInputSchema + output_schema = GetPlaceOutputSchema + config_schema = GetPlaceToolConfig + + async def _arun(self, params: GetPlaceInputSchema) -> GetPlaceOutputSchema: + """Execute geocoding query and return bounding box.""" + # Validate configuration + if not self.config.geodini_host: + return GetPlaceOutputSchema( + place=None, + bbox=None, + error="geodini_host not configured", + ) + + try: + # Query the Geodini geocoding service + async with httpx.AsyncClient(timeout=self.config.timeout) as client: + response = await client.get( + f"{self.config.geodini_host.rstrip('/')}/search", + params={"query": params.query}, + ) + response.raise_for_status() + data = response.json() + + # Check if we got any results + if not data.get("results"): + return GetPlaceOutputSchema( + place=None, + bbox=None, + error=f"Could not resolve bbox for '{params.query}'", + ) + + # Extract the top result + top = data["results"][0] + name = top.get("name") or top.get("display_name") + + # Extract bounding box from geometry + # Geodini returns GeoJSON geometry, we need to compute bounds + geometry = top.get("geometry") + if geometry: + from shapely.geometry import shape + bbox = list(shape(geometry).bounds) # Returns (minx, miny, maxx, maxy) + else: + bbox = None + + if bbox: + return GetPlaceOutputSchema( + place=name, + bbox=bbox, + error=None, + ) + else: + return GetPlaceOutputSchema( + place=name, + bbox=None, + error=f"No geometry found for '{params.query}'", + ) + + except httpx.TimeoutException as e: + msg = f"Geodini request timed out after {self.config.timeout}s" + logger.error(msg) + return GetPlaceOutputSchema(place=None, bbox=None, error=msg) + + except httpx.HTTPStatusError as e: + msg = f"Geodini returned error status {e.response.status_code}" + logger.error(msg) + return GetPlaceOutputSchema(place=None, bbox=None, error=msg) + + except Exception as e: + msg = f"Geocoding failed: {e}" + logger.error(msg) + return GetPlaceOutputSchema(place=None, bbox=None, error=msg) diff --git a/pyproject.toml b/pyproject.toml index 043781c..761b84b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "fastmcp>=2.0.0", "openai-agents>=0.6.7", "PyGithub>=2.1.1", + "httpx>=0.27.0", + "shapely>=2.0.0", ] [project.urls] From f3c22399966a2c5e1de9d34a49e6490576cbd25e Mon Sep 17 00:00:00 2001 From: lillythomas Date: Mon, 9 Feb 2026 15:12:20 -0800 Subject: [PATCH 2/4] add geojson geometry to output --- akd_ext/tools/get_place.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/akd_ext/tools/get_place.py b/akd_ext/tools/get_place.py index 1080006..6fc2a56 100644 --- a/akd_ext/tools/get_place.py +++ b/akd_ext/tools/get_place.py @@ -49,6 +49,10 @@ class GetPlaceOutputSchema(OutputSchema): None, description="Bounding box as [west, south, east, north] (i.e., [min_lon, min_lat, max_lon, max_lat])", ) + geometry: dict | None = Field( + None, + description="GeoJSON geometry for the place", + ) error: str | None = Field( None, description="Error message if geocoding failed", @@ -126,26 +130,28 @@ async def _arun(self, params: GetPlaceInputSchema) -> GetPlaceOutputSchema: return GetPlaceOutputSchema( place=name, bbox=bbox, + geometry=geometry, error=None, ) else: return GetPlaceOutputSchema( place=name, bbox=None, + geometry=None, error=f"No geometry found for '{params.query}'", ) except httpx.TimeoutException as e: msg = f"Geodini request timed out after {self.config.timeout}s" logger.error(msg) - return GetPlaceOutputSchema(place=None, bbox=None, error=msg) + return GetPlaceOutputSchema(place=None, bbox=None, geometry=None, error=msg) except httpx.HTTPStatusError as e: msg = f"Geodini returned error status {e.response.status_code}" logger.error(msg) - return GetPlaceOutputSchema(place=None, bbox=None, error=msg) + return GetPlaceOutputSchema(place=None, bbox=None, geometry=None, error=msg) except Exception as e: msg = f"Geocoding failed: {e}" logger.error(msg) - return GetPlaceOutputSchema(place=None, bbox=None, error=msg) + return GetPlaceOutputSchema(place=None, bbox=None, geometry=None, error=msg) From e7fd8045d18336b319282cab44c8099ea5e98e92 Mon Sep 17 00:00:00 2001 From: lillythomas Date: Tue, 10 Feb 2026 11:10:04 -0800 Subject: [PATCH 3/4] add eie namespace --- akd_ext/tools/__init__.py | 2 +- akd_ext/tools/eie/__init__.py | 15 +++++++++++++++ akd_ext/tools/{ => eie}/get_place.py | 0 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 akd_ext/tools/eie/__init__.py rename akd_ext/tools/{ => eie}/get_place.py (100%) diff --git a/akd_ext/tools/__init__.py b/akd_ext/tools/__init__.py index 6179f85..cde0db7 100644 --- a/akd_ext/tools/__init__.py +++ b/akd_ext/tools/__init__.py @@ -14,7 +14,7 @@ RepositorySearchToolOutputSchema, RepositorySearchToolConfig, ) -from .get_place import ( +from .eie import ( GetPlaceTool, GetPlaceToolConfig, GetPlaceInputSchema, diff --git a/akd_ext/tools/eie/__init__.py b/akd_ext/tools/eie/__init__.py new file mode 100644 index 0000000..ed4ca59 --- /dev/null +++ b/akd_ext/tools/eie/__init__.py @@ -0,0 +1,15 @@ +"""EIE-specific tools for akd_ext.""" + +from .get_place import ( + GetPlaceTool, + GetPlaceToolConfig, + GetPlaceInputSchema, + GetPlaceOutputSchema, +) + +__all__ = [ + "GetPlaceTool", + "GetPlaceToolConfig", + "GetPlaceInputSchema", + "GetPlaceOutputSchema", +] diff --git a/akd_ext/tools/get_place.py b/akd_ext/tools/eie/get_place.py similarity index 100% rename from akd_ext/tools/get_place.py rename to akd_ext/tools/eie/get_place.py From 6128bcf3b07eb26b7ad791163d0c09e74cc4aa2c Mon Sep 17 00:00:00 2001 From: lillythomas Date: Mon, 13 Apr 2026 14:31:35 -0700 Subject: [PATCH 4/4] updates to get place tool --- akd_ext/tools/eie/get_place.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/akd_ext/tools/eie/get_place.py b/akd_ext/tools/eie/get_place.py index 6fc2a56..da45e16 100644 --- a/akd_ext/tools/eie/get_place.py +++ b/akd_ext/tools/eie/get_place.py @@ -24,9 +24,13 @@ class GetPlaceToolConfig(BaseToolConfig): description="Base URL for the Geodini geocoding service", ) timeout: float = Field( - default=15.0, + default=30.0, description="HTTP request timeout in seconds", ) + verify_ssl: bool = Field( + default=True, + description="Verify SSL certificates (set False for self-signed certs)", + ) class GetPlaceInputSchema(InputSchema): @@ -96,11 +100,16 @@ async def _arun(self, params: GetPlaceInputSchema) -> GetPlaceOutputSchema: ) try: + place_query = params.query + # Query the Geodini geocoding service - async with httpx.AsyncClient(timeout=self.config.timeout) as client: + async with httpx.AsyncClient( + timeout=self.config.timeout, + verify=self.config.verify_ssl, + ) as client: response = await client.get( f"{self.config.geodini_host.rstrip('/')}/search", - params={"query": params.query}, + params={"query": place_query}, ) response.raise_for_status() data = response.json()