Skip to main content
Glama
server.py21.4 kB
""" GeoSight MCP Server - Main Entry Point This module implements the MCP server that exposes satellite imagery analysis tools through the Model Context Protocol. """ import asyncio import logging import sys from contextlib import asynccontextmanager from typing import Any import structlog from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import ( CallToolResult, ListToolsResult, TextContent, ImageContent, Tool, ) from geosight.config import settings from geosight.tools import ( search_imagery, calculate_ndvi, calculate_ndwi, calculate_ndbi, detect_land_cover, detect_changes, detect_objects, generate_report, get_location_info, ) from geosight.utils.logging import setup_logging # Setup structured logging setup_logging(settings.log_level) logger = structlog.get_logger(__name__) def create_server() -> Server: """Create and configure the MCP server instance.""" server = Server("geosight") @server.list_tools() async def list_tools() -> ListToolsResult: """List all available satellite imagery analysis tools.""" tools = [ Tool( name="search_imagery", description="""Search for available satellite imagery for a given location and date range. Use this tool to find what imagery is available before running analysis. Supports Sentinel-2, Landsat 8/9, and Sentinel-1 (radar) data. Example queries: - "Find imagery for New Delhi from last month" - "Search for cloud-free Sentinel-2 images of Mumbai" """, inputSchema={ "type": "object", "properties": { "latitude": { "type": "number", "description": "Latitude of the center point (-90 to 90)", "minimum": -90, "maximum": 90, }, "longitude": { "type": "number", "description": "Longitude of the center point (-180 to 180)", "minimum": -180, "maximum": 180, }, "location_name": { "type": "string", "description": "Name of the location (e.g., 'Mumbai, India'). Will be geocoded if lat/lon not provided.", }, "start_date": { "type": "string", "description": "Start date in YYYY-MM-DD format", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", }, "end_date": { "type": "string", "description": "End date in YYYY-MM-DD format", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", }, "max_cloud_cover": { "type": "number", "description": "Maximum cloud cover percentage (0-100)", "minimum": 0, "maximum": 100, "default": 20, }, "data_source": { "type": "string", "enum": ["sentinel-2", "landsat-8", "landsat-9", "sentinel-1"], "description": "Satellite data source", "default": "sentinel-2", }, }, "required": ["start_date", "end_date"], }, ), Tool( name="calculate_ndvi", description="""Calculate NDVI (Normalized Difference Vegetation Index) for vegetation health analysis. NDVI values range from -1 to 1: - 0.6 to 1.0: Dense, healthy vegetation - 0.3 to 0.6: Moderate vegetation - 0.1 to 0.3: Sparse vegetation - -0.1 to 0.1: Bare soil, rocks, sand - -1 to -0.1: Water, snow, clouds Use cases: - Agricultural monitoring and crop health - Deforestation tracking - Drought assessment - Urban green space analysis """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": { "type": "number", "description": "Analysis radius in kilometers", "minimum": 1, "maximum": 100, "default": 10, }, "start_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "end_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "resolution": { "type": "integer", "description": "Output resolution in meters", "enum": [10, 20, 60], "default": 10, }, }, "required": ["start_date", "end_date"], }, ), Tool( name="calculate_ndwi", description="""Calculate NDWI (Normalized Difference Water Index) for water body detection. NDWI values: - > 0.3: Open water - 0.0 to 0.3: Flooding, wetlands - < 0.0: Non-water surfaces Use cases: - Flood mapping and monitoring - Water body extent tracking - Wetland mapping - Coastal change analysis """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 1, "maximum": 100, "default": 10}, "start_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "end_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "water_threshold": { "type": "number", "description": "Threshold for water classification", "default": 0.3, }, }, "required": ["start_date", "end_date"], }, ), Tool( name="calculate_ndbi", description="""Calculate NDBI (Normalized Difference Built-up Index) for urban area detection. NDBI helps identify built-up/urban areas: - High values: Dense urban areas, roads, buildings - Low values: Vegetation, water, bare soil Use cases: - Urban expansion tracking - Infrastructure development monitoring - Land use change analysis """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 1, "maximum": 100, "default": 10}, "start_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "end_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, }, "required": ["start_date", "end_date"], }, ), Tool( name="detect_land_cover", description="""Classify land cover into categories using machine learning. Categories detected: - Forest (deciduous, evergreen, mixed) - Water (rivers, lakes, reservoirs) - Urban (residential, commercial, industrial) - Agriculture (cropland, pasture) - Barren (desert, rock, sand) - Wetlands - Snow/Ice Returns a classification map with area statistics for each category. """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 1, "maximum": 50, "default": 10}, "date": { "type": "string", "description": "Target date for classification", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", }, "model": { "type": "string", "enum": ["eurosat", "deepglobe", "custom"], "default": "eurosat", }, }, "required": ["date"], }, ), Tool( name="detect_changes", description="""Detect changes between two time periods using bi-temporal analysis. Identifies: - Deforestation / Reforestation - Urban expansion / Construction - Flood events - Agricultural changes - Mining / Excavation Returns a change map highlighting areas of significant change with statistics. """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 1, "maximum": 50, "default": 10}, "date_before": { "type": "string", "description": "Earlier date for comparison", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", }, "date_after": { "type": "string", "description": "Later date for comparison", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", }, "change_type": { "type": "string", "enum": ["all", "vegetation", "urban", "water"], "default": "all", }, "sensitivity": { "type": "string", "enum": ["low", "medium", "high"], "default": "medium", }, }, "required": ["date_before", "date_after"], }, ), Tool( name="detect_objects", description="""Detect specific objects in satellite imagery using deep learning. Detectable objects: - Ships and vessels - Aircraft - Vehicles - Buildings and structures - Solar panels / Solar farms - Storage tanks - Sports fields Returns bounding boxes with confidence scores for detected objects. """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 0.5, "maximum": 20, "default": 5}, "date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "object_types": { "type": "array", "items": { "type": "string", "enum": [ "ship", "aircraft", "vehicle", "building", "solar_panel", "storage_tank", "sports_field", ], }, "description": "Types of objects to detect", "default": ["ship", "aircraft", "building"], }, "confidence_threshold": { "type": "number", "description": "Minimum confidence score (0-1)", "minimum": 0, "maximum": 1, "default": 0.5, }, }, "required": ["date"], }, ), Tool( name="generate_report", description="""Generate a comprehensive analysis report with maps and statistics. Report includes: - Location overview and map - Satellite imagery preview - Analysis results (NDVI, land cover, etc.) - Change detection if applicable - Statistical summaries - Recommendations Output formats: PDF, HTML, or Markdown """, inputSchema={ "type": "object", "properties": { "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, "location_name": {"type": "string"}, "radius_km": {"type": "number", "minimum": 1, "maximum": 50, "default": 10}, "start_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "end_date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "analyses": { "type": "array", "items": { "type": "string", "enum": ["ndvi", "ndwi", "land_cover", "change_detection"], }, "description": "Analyses to include in report", "default": ["ndvi", "land_cover"], }, "output_format": { "type": "string", "enum": ["pdf", "html", "markdown"], "default": "pdf", }, "report_title": { "type": "string", "description": "Custom title for the report", }, }, "required": ["start_date", "end_date"], }, ), Tool( name="get_location_info", description="""Get geographic information about a location. Geocodes location names to coordinates and provides: - Coordinates (latitude, longitude) - Administrative boundaries - Elevation data - Climate zone - Time zone Use this to convert place names to coordinates for other tools. """, inputSchema={ "type": "object", "properties": { "location_name": { "type": "string", "description": "Name of the location to geocode (e.g., 'Mumbai, India')", }, "latitude": {"type": "number", "minimum": -90, "maximum": 90}, "longitude": {"type": "number", "minimum": -180, "maximum": 180}, }, }, ), ] return ListToolsResult(tools=tools) @server.call_tool() async def call_tool(name: str, arguments: dict[str, Any]) -> CallToolResult: """Execute a satellite imagery analysis tool.""" logger.info("tool_called", tool=name, arguments=arguments) try: # Route to appropriate tool handler tool_handlers = { "search_imagery": search_imagery, "calculate_ndvi": calculate_ndvi, "calculate_ndwi": calculate_ndwi, "calculate_ndbi": calculate_ndbi, "detect_land_cover": detect_land_cover, "detect_changes": detect_changes, "detect_objects": detect_objects, "generate_report": generate_report, "get_location_info": get_location_info, } if name not in tool_handlers: return CallToolResult( content=[TextContent(type="text", text=f"Unknown tool: {name}")], isError=True, ) handler = tool_handlers[name] result = await handler(**arguments) # Format response based on result type content = [] if isinstance(result, dict): # Add text summary if "summary" in result: content.append(TextContent(type="text", text=result["summary"])) # Add statistics as formatted text if "statistics" in result: stats_text = format_statistics(result["statistics"]) content.append(TextContent(type="text", text=stats_text)) # Add image if available if "image_base64" in result: content.append( ImageContent( type="image", data=result["image_base64"], mimeType=result.get("image_mime_type", "image/png"), ) ) # Add map URL if available if "map_url" in result: content.append( TextContent( type="text", text=f"\n📍 Interactive Map: {result['map_url']}" ) ) # Add any additional text content if "details" in result: content.append(TextContent(type="text", text=result["details"])) else: content.append(TextContent(type="text", text=str(result))) logger.info("tool_completed", tool=name, success=True) return CallToolResult(content=content) except Exception as e: logger.error("tool_error", tool=name, error=str(e), exc_info=True) return CallToolResult( content=[TextContent(type="text", text=f"Error executing {name}: {str(e)}")], isError=True, ) return server def format_statistics(stats: dict[str, Any]) -> str: """Format statistics dictionary as readable text.""" lines = ["\n📊 **Statistics:**"] for key, value in stats.items(): # Format key formatted_key = key.replace("_", " ").title() # Format value if isinstance(value, float): if "percent" in key.lower() or "pct" in key.lower(): formatted_value = f"{value:.1f}%" else: formatted_value = f"{value:.4f}" elif isinstance(value, dict): formatted_value = ", ".join(f"{k}: {v}" for k, v in value.items()) else: formatted_value = str(value) lines.append(f" • {formatted_key}: {formatted_value}") return "\n".join(lines) async def run_server(): """Run the MCP server.""" server = create_server() logger.info( "starting_server", mode=settings.server.mode, environment=settings.environment, ) if settings.server.mode == "stdio": async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options(), ) else: # HTTP/SSE mode would be implemented here raise NotImplementedError(f"Server mode {settings.server.mode} not yet implemented") def main(): """Main entry point.""" try: asyncio.run(run_server()) except KeyboardInterrupt: logger.info("server_shutdown", reason="keyboard_interrupt") except Exception as e: logger.error("server_crash", error=str(e), exc_info=True) sys.exit(1) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/armaasinghn/geosight-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server