#
tokens: 1604/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

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

# Files

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

```
1 | 3.10
2 | 
```

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

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

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

```markdown
 1 | # Model Context Protocol (MCP) for NASA Earthdata Search (CMR)
 2 | 
 3 | 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.
 4 | 
 5 | ## Dependencies
 6 | uv -  a rust based python package manager
 7 | a LLM client, such as Claude desktop or chatGPT desktop (for consuming the MCP)
 8 | 
 9 | ## Install and Run
10 | 
11 | Clone the repository to your local environment, or where your LLM client is running.
12 | 
13 | ```
14 | git clone https://github.com/podaac/cmr-mcp.git
15 | cd cmr-mcp
16 | ```
17 | 
18 | 
19 | ### Install uv 
20 | 
21 | ```
22 | curl -LsSf https://astral.sh/uv/install.sh | sh
23 | ```
24 | 
25 | 
26 | ```
27 | uv venv
28 | source .venv/bin/activate
29 | ```
30 | 
31 | ###  Install packages with uv
32 | ```
33 | uv sync
34 | ```
35 | 
36 | use the outputs of `which uv` (UV_LIB) and `PWD` (CMR_MCP_INSTALL) to update the following configuration.
37 | 
38 | 
39 | ## Adding to AI Framework
40 | 
41 | In this example we'll use Claude desktop.
42 | 
43 | 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`
44 | 
45 | Add the following configuration, filling in the values of UV_LIB and CMR_MCP_INSTALL - don't use environment variables here.
46 | 
47 | ```
48 | {
49 |     "mcpServers": {
50 |         "cmr": {
51 |             "command": "$UV_LIB$",
52 |             "args": [
53 |                 "--directory",
54 |                 "$CMR_MCP_INSTALL$",
55 |                 "run",
56 |                 "cmr-search.py"
57 |             ]
58 |         }
59 |     }
60 | }
61 | ```
62 | 
63 | ## Use the MCP Server
64 | 
65 | Simply prompt your agent to `search cmr for...` data. Below is a simple example of this in action.
66 | 
67 | ![Claude MCP usage](assets/claude_integration.png)
68 | 
69 | Other prompts that can work:
70 | 
71 | 1. Search CMR for datasets from 2024 to 2025
72 | 2. Search CMR for PO.DAAC datasets from 2020 to 2024 with keyword Climate
73 | 
74 | 
75 | 
76 | 
77 | 
```

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

```toml
 1 | [project]
 2 | name = "cmr-search-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | dependencies = [
 8 |     "mcp[cli]>=1.6.0",
 9 |     "earthaccess>=0.14.0",
10 | ]
11 | 
```

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

```python
 1 | import traceback
 2 | from typing import Any,  Optional
 3 | 
 4 | from mcp.server.fastmcp import FastMCP
 5 | import logging
 6 | import earthaccess
 7 | 
 8 | logger = logging.getLogger(__name__)
 9 | logging.basicConfig(level=logging.DEBUG)
10 | 
11 | # Initialize FastMCP server
12 | mcp = FastMCP("cmr-search")
13 | 
14 | def format_dataset(feature: dict) -> str:
15 |     """Format an alert feature into a readable string."""
16 |     props = feature
17 | 
18 |     logger.debug(props.concept_id())
19 | 
20 |     try:
21 |         return f"""
22 | ConceptID: {props.concept_id()}
23 | Description: {props.abstract()}
24 | Shortname: {props.summary()['short-name']}
25 | """
26 |     except Exception as e:
27 |         logging.error(traceback.format_exc())
28 |         #Currently an error in earthaccess that relies on `FileDistributionInformation` to exist will be caught here from the 'summary()' method. 
29 |         # Returning empty string.
30 |         return ""
31 | 
32 | 
33 | @mcp.tool()
34 | async def get_datasets(
35 |     startdate: str = None,
36 |     stopdate: str = None,
37 |     daac: Optional[str] = None,
38 |     keyword: str= None) -> str:
39 |     """Get a list of datasets form CMR based on keywords.
40 | 
41 |     Args:
42 |         startdate: (Optional) Start date of search request (like "2002" or "2022-03-22")
43 |         stopdate: (Optional) Stop date of search request (like "2002" or "2022-03-22")
44 |         daac: the daac to search, e.g. NSIDC or PODAAC
45 |         keywords: A list of keyword arguments to search collections for.
46 |     """
47 |     args = {}
48 |     if keyword is not None:
49 |          args['keyword'] = keyword
50 |     if daac is not None:
51 |          args['daac'] = daac
52 |     if startdate is not None or stopdate is not None:
53 |          args['temporal'] = (startdate, stopdate)
54 | 
55 |     collections = earthaccess.search_datasets(count=5,  **args )
56 |     logger.debug(len(collections))
57 |     
58 |     #alerts = [format_dataset(feature) for feature in data["features"]]
59 |     return "\n---\n".join([format_dataset(ds) for ds in collections])
60 | 
61 | 
62 | if __name__ == "__main__":
63 |     # Initialize and run the server
64 |     mcp.run(transport='stdio')
65 | 
```