get_mars_rover_photos
Retrieve Mars rover photos from Curiosity, Opportunity, or Spirit by specifying either Martian sol or Earth date and optional camera filters.
Instructions
Get photos from a Mars rover (Curiosity, Opportunity, Spirit). Specify either sol (Martian day) or earth_date (YYYY-MM-DD), but not both.
Args: rover_name: Name of the rover (curiosity, opportunity, spirit). sol: Martian sol (day number, starting from landing). Use if not using earth_date. earth_date: Earth date in YYYY-MM-DD format. Use if not using sol. camera: Filter by camera abbreviation (e.g., FHAZ, RHAZ, MAST, NAVCAM, PANCAM). See documentation for full list per rover. page: Page number for results (25 photos per page).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rover_name | Yes | ||
| sol | No | ||
| earth_date | No | ||
| camera | No | ||
| page | No |
Implementation Reference
- src/nasa_mcp/server.py:1208-1286 (handler)The handler function implementing the get_mars_rover_photos tool. It validates inputs, queries the NASA Mars Rover Photos API, and formats the response with photo details including IDs, dates, cameras, and image URLs.@mcp.tool() async def get_mars_rover_photos(rover_name: str, sol: int = None, earth_date: str = None, camera: str = None, page: int = 1) -> str: """Get photos from a Mars rover (Curiosity, Opportunity, Spirit). Specify either sol (Martian day) or earth_date (YYYY-MM-DD), but not both. Args: rover_name: Name of the rover (curiosity, opportunity, spirit). sol: Martian sol (day number, starting from landing). Use if not using earth_date. earth_date: Earth date in YYYY-MM-DD format. Use if not using sol. camera: Filter by camera abbreviation (e.g., FHAZ, RHAZ, MAST, NAVCAM, PANCAM). See documentation for full list per rover. page: Page number for results (25 photos per page). """ rover_name = rover_name.lower() if rover_name not in ROVER_CAMERAS: return f"Invalid rover name. Available rovers: {', '.join(ROVER_CAMERAS.keys())}" if sol is not None and earth_date is not None: return "Error: Specify either sol or earth_date, but not both." if sol is None and earth_date is None: return "Error: Specify either sol or earth_date." params = {"page": page} if sol is not None: params["sol"] = sol if earth_date is not None: params["earth_date"] = earth_date if camera: camera = camera.upper() if camera not in ROVER_CAMERAS[rover_name]: return f"Invalid camera '{camera}' for rover '{rover_name}'. Available cameras: {', '.join(ROVER_CAMERAS[rover_name])}" params["camera"] = camera url = f"{NASA_API_BASE}/mars-photos/api/v1/rovers/{rover_name}/photos" data = await make_nasa_request(url, params) if not data: return f"Could not retrieve Mars Rover photos for {rover_name} due to a connection error." # Check for error response (must be a dictionary) if isinstance(data, dict) and "error" in data: return f"API Error: {data.get('error')} - Details: {data.get('details', 'N/A')}" if isinstance(data, dict) and data.get("binary_content"): return f"Received unexpected binary content from Mars Rover Photos API. URL: {data.get('url')}" # The response should be a dictionary containing a 'photos' list if not isinstance(data, dict) or "photos" not in data: logger.error(f"Unexpected response format from Mars Rover Photos API: {data}") return "Received unexpected data format from Mars Rover Photos API." try: photos = data.get("photos", []) if not photos: query_details = f"sol={sol}" if sol is not None else f"earth_date={earth_date}" if camera: query_details += f", camera={camera}" return f"No photos found for rover '{rover_name}' with criteria: {query_details}, page {page}." result = [f"Mars Rover Photos for '{rover_name}' (Page {page}): {len(photos)} found on this page."] display_limit = 10 # Limit display per page in the result string count = 0 for photo in photos: if count >= display_limit: result.append(f"n... and {len(photos) - display_limit} more photos on this page.") break result.append(f"nPhoto ID: {photo.get('id', 'Unknown')}") result.append(f"Sol: {photo.get('sol', 'Unknown')}") result.append(f"Earth Date: {photo.get('earth_date', 'Unknown')}") result.append(f"Camera: {photo.get('camera', {}).get('name', 'Unknown')} ({photo.get('camera', {}).get('full_name', 'N/A')})") result.append(f"Image URL: {photo.get('img_src', 'Not available')}") result.append(f"Rover: {photo.get('rover', {}).get('name', 'Unknown')} (Status: {photo.get('rover', {}).get('status', 'N/A')}) ") result.append("-" * 40) count += 1 return "n".join(result) except Exception as e: logger.error(f"Error processing Mars Rover Photos data: {str(e)}") return f"Error processing Mars Rover Photos data: {str(e)}"
- src/nasa_mcp/server.py:1202-1206 (helper)Helper dictionary defining valid rover names and their available camera types, used for input validation in the get_mars_rover_photos tool.ROVER_CAMERAS = { "curiosity": ["FHAZ", "RHAZ", "MAST", "CHEMCAM", "MAHLI", "MARDI", "NAVCAM"], "opportunity": ["FHAZ", "RHAZ", "NAVCAM", "PANCAM", "MINITES"], "spirit": ["FHAZ", "RHAZ", "NAVCAM", "PANCAM", "MINITES"] }
- src/nasa_mcp/server.py:28-83 (helper)Shared helper function make_nasa_request used by get_mars_rover_photos (and other tools) to make HTTP requests to NASA APIs with error handling, API key injection, and support for JSON/binary responses.async def make_nasa_request(url: str, params: dict = None) -> Union[dict[str, Any], List[Any], None]: """Make a request to the NASA API with proper error handling. Handles both JSON and binary (image) responses. """ logger.info(f"Making request to: {url} with params: {params}") if params is None: params = {} # Ensure API key is included in parameters if "api_key" not in params: params["api_key"] = API_KEY async with httpx.AsyncClient() as client: try: response = await client.get(url, params=params, timeout=30.0, follow_redirects=True) response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) content_type = response.headers.get("Content-Type", "").lower() if "application/json" in content_type: try: return response.json() except json.JSONDecodeError as json_err: logger.error(f"JSON decode error for URL {response.url}: {json_err}") logger.error(f"Response text: {response.text[:500]}") # Log beginning of text return {"error": "Failed to decode JSON response", "details": str(json_err)} elif content_type.startswith("image/"): logger.info(f"Received binary image content ({content_type}) from {response.url}") # Return a dictionary indicating binary content was received return { "binary_content": True, "content_type": content_type, "url": str(response.url) # Return the final URL after redirects } else: # Handle other unexpected content types logger.warning(f"Unexpected content type '{content_type}' received from {response.url}") return {"error": f"Unexpected content type: {content_type}", "content": response.text[:500]} except httpx.HTTPStatusError as http_err: logger.error(f"HTTP error occurred: {http_err} - {http_err.response.status_code} for URL {http_err.request.url}") try: # Try to get more details from response body if possible error_details = http_err.response.json() except Exception: error_details = http_err.response.text[:500] return {"error": f"HTTP error: {http_err.response.status_code}", "details": error_details} except httpx.RequestError as req_err: logger.error(f"Request error occurred: {req_err} for URL {req_err.request.url}") return {"error": "Request failed", "details": str(req_err)} except Exception as e: logger.error(f"An unexpected error occurred: {str(e)}") return {"error": "An unexpected error occurred", "details": str(e)}