Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
rover_nameYes
solNo
earth_dateNo
cameraNo
pageNo

Implementation Reference

  • 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)}"
  • 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"]
    }
  • 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)}

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/AnCode666/nasa-mcp'

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