#
tokens: 1200/50000 5/5 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── assets
│   └── claude_integration.png
├── cmr-search.py
├── pyproject.toml
├── README.md
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.10

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Model Context Protocol (MCP) for NASA Earthdata Search (CMR)

This module is a [model context protocol](https://modelcontextprotocol.io/introduction) (MCP) for NASA's earthdata common metedata repository (CMR). The goal of this MCP server is to integrate AI retrievals with NASA Catalog of datasets by way of Earthaccess.

## Dependencies
uv -  a rust based python package manager
a LLM client, such as Claude desktop or chatGPT desktop (for consuming the MCP)

## Install and Run

Clone the repository to your local environment, or where your LLM client is running.

```
git clone https://github.com/podaac/cmr-mcp.git
cd cmr-mcp
```


### Install uv 

```
curl -LsSf https://astral.sh/uv/install.sh | sh
```


```
uv venv
source .venv/bin/activate
```

###  Install packages with uv
```
uv sync
```

use the outputs of `which uv` (UV_LIB) and `PWD` (CMR_MCP_INSTALL) to update the following configuration.


## Adding to AI Framework

In this example we'll use Claude desktop.

Update the `claude_desktop_config.json` file (sometimes this must be created). On a mac, this is often found in `~/Library/Application\ Support/Claude/claude_desktop_config.json`

Add the following configuration, filling in the values of UV_LIB and CMR_MCP_INSTALL - don't use environment variables here.

```
{
    "mcpServers": {
        "cmr": {
            "command": "$UV_LIB$",
            "args": [
                "--directory",
                "$CMR_MCP_INSTALL$",
                "run",
                "cmr-search.py"
            ]
        }
    }
}
```

## Use the MCP Server

Simply prompt your agent to `search cmr for...` data. Below is a simple example of this in action.

![Claude MCP usage](assets/claude_integration.png)

Other prompts that can work:

1. Search CMR for datasets from 2024 to 2025
2. Search CMR for PO.DAAC datasets from 2020 to 2024 with keyword Climate





```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "cmr-search-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "mcp[cli]>=1.6.0",
    "earthaccess>=0.14.0",
]

```

--------------------------------------------------------------------------------
/cmr-search.py:
--------------------------------------------------------------------------------

```python
import traceback
from typing import Any,  Optional

from mcp.server.fastmcp import FastMCP
import logging
import earthaccess

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

# Initialize FastMCP server
mcp = FastMCP("cmr-search")

def format_dataset(feature: dict) -> str:
    """Format an alert feature into a readable string."""
    props = feature

    logger.debug(props.concept_id())

    try:
        return f"""
ConceptID: {props.concept_id()}
Description: {props.abstract()}
Shortname: {props.summary()['short-name']}
"""
    except Exception as e:
        logging.error(traceback.format_exc())
        #Currently an error in earthaccess that relies on `FileDistributionInformation` to exist will be caught here from the 'summary()' method. 
        # Returning empty string.
        return ""


@mcp.tool()
async def get_datasets(
    startdate: str = None,
    stopdate: str = None,
    daac: Optional[str] = None,
    keyword: str= None) -> str:
    """Get a list of datasets form CMR based on keywords.

    Args:
        startdate: (Optional) Start date of search request (like "2002" or "2022-03-22")
        stopdate: (Optional) Stop date of search request (like "2002" or "2022-03-22")
        daac: the daac to search, e.g. NSIDC or PODAAC
        keywords: A list of keyword arguments to search collections for.
    """
    args = {}
    if keyword is not None:
         args['keyword'] = keyword
    if daac is not None:
         args['daac'] = daac
    if startdate is not None or stopdate is not None:
         args['temporal'] = (startdate, stopdate)

    collections = earthaccess.search_datasets(count=5,  **args )
    logger.debug(len(collections))
    
    #alerts = [format_dataset(feature) for feature in data["features"]]
    return "\n---\n".join([format_dataset(ds) for ds in collections])


if __name__ == "__main__":
    # Initialize and run the server
    mcp.run(transport='stdio')

```