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)}
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses basic behavioral traits like the exclusive choice between sol and earth_date, and pagination (25 photos per page), but lacks details on rate limits, error handling, or authentication needs. This is adequate but has gaps for a tool with no annotation support.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, followed by a structured list of parameters with clear explanations. Every sentence adds value without redundancy, making it efficient and easy for an AI agent to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (5 parameters, no annotations, no output schema), the description is largely complete, covering purpose, usage rules, and parameter details. However, it lacks information on return values or error cases, which would enhance completeness for a tool with no output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description compensates well by explaining each parameter's purpose and constraints (e.g., sol vs. earth_date exclusivity, camera abbreviations, page usage). It adds meaningful semantics beyond the bare schema, though it could provide more on default behaviors or validation rules.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Get photos') and resource ('from a Mars rover'), and it distinguishes the tool from siblings by specifying the rovers (Curiosity, Opportunity, Spirit). This is precise and actionable for an AI agent.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit usage guidance by stating 'Specify either sol (Martian day) or earth_date (YYYY-MM-DD), but not both,' which helps the agent avoid conflicts. It also lists camera options and pagination details, offering clear context for when to use parameters.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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