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