# 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.

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')
```