Skip to main content
Glama
README.md
# NASA MCP Server

A small but complete **Model Context Protocol (MCP)** server you can deploy to
**Google Cloud Run**. It wraps three of NASA's free public APIs as tools an AI
assistant can call.

This is built as a learning project, so the code and these notes explain the
*why*, not just the *how*.

---

## What is an MCP server, briefly

MCP is a standard way to expose **tools** (callable functions), **resources**
(readable data), and **prompts** (templates) to an AI model. Think of it as a
web API designed specifically for LLMs. This project uses **tools** only.

There are two transports:

- **stdio** — the server runs as a local subprocess. Used by desktop apps
  (Claude Desktop, Cursor, IDE plugins). Not what we want here.
- **streamable HTTP** — the server runs as a normal web service reachable over
  a URL. **This is the one you use to host on Cloud Run.** All MCP traffic flows
  through a single endpoint at `/mcp`.

---

## Project structure

```
nasa-mcp-server/
├── server.py          # the MCP server + 3 NASA tools
├── requirements.txt   # mcp + httpx
├── Dockerfile         # container image for Cloud Run
└── .dockerignore
```

The three tools:

| Tool | What it does |
|------|--------------|
| `get_astronomy_picture` | NASA's Astronomy Picture of the Day (optionally for a past date) |
| `get_mars_photos` | Photos from a Mars rover on a given Earth date |
| `get_near_earth_objects` | Asteroids passing near Earth in a date range |

---

## Step 1 — Get a NASA API key

1. Go to <https://api.nasa.gov/> and fill in the short form (name + email).
2. The key is emailed to you within a few minutes. No credit card. Registered
   keys allow ~1,000 requests/hour.

You can skip this at first: the code falls back to NASA's shared `DEMO_KEY`,
which works instantly but is heavily rate-limited (you'll see `429` errors fast).

> **Do not paste your key into source code or share it in chat.** The server
> reads it from the `NASA_API_KEY` environment variable. Locally that's an
> `export`; on Cloud Run it's a Secret Manager secret (Step 4).

---

## Step 2 — Run it locally

```bash
cd nasa-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

export NASA_API_KEY=your_key_here     # omit to use DEMO_KEY
python server.py
```

The server starts on `http://localhost:8080/mcp`.

---

## Step 3 — Test with the MCP Inspector

The Inspector is the official tool for poking at an MCP server by hand. With the
server running, in another terminal:

```bash
npx @modelcontextprotocol/inspector
```

In the Inspector UI: set transport to **Streamable HTTP**, URL to
`http://localhost:8080/mcp`, connect, then open the **Tools** tab and run
`get_astronomy_picture` with no arguments. You should get back today's picture.

---

## Step 4 — Deploy to Google Cloud Run

Prerequisites: a GCP project with billing enabled and the `gcloud` CLI
installed and authenticated (`gcloud auth login`).

```bash
# Set your project and a region close to you
gcloud config set project YOUR_PROJECT_ID
gcloud config set run/region australia-southeast1   # Sydney; pick your own

# Enable the services we need
gcloud services enable run.googleapis.com \
                       cloudbuild.googleapis.com \
                       secretmanager.googleapis.com
```

### 4a. Store your NASA key in Secret Manager

Never bake the key into the image. Create a secret instead:

```bash
printf '%s' 'YOUR_NASA_KEY' | gcloud secrets create nasa-api-key --data-file=-
```

### 4b. Deploy from source

`gcloud` builds the container from the `Dockerfile` and deploys it in one step:

```bash
gcloud run deploy nasa-mcp \
  --source . \
  --allow-unauthenticated \
  --set-secrets=NASA_API_KEY=nasa-api-key:latest
```

When prompted to grant the runtime service account access to the secret, say
yes (or run `gcloud secrets add-iam-policy-binding nasa-api-key
--member="serviceAccount:$(gcloud run services describe nasa-mcp
--format='value(spec.template.spec.serviceAccountName)')"
--role="roles/secretmanager.secretAccessor"`).

Notes:
- You do **not** set `PORT` — Cloud Run injects it and `server.py` reads it.
- On success you get a URL like `https://nasa-mcp-xxxxxxxx.run.app`. Your MCP
  endpoint is that URL **plus `/mcp`**.

Quick check it's alive:

```bash
curl -X POST https://nasa-mcp-xxxxxxxx.run.app/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
```

A JSON response with `"serverInfo":{"name":"nasa",...}` means you're live.

---

## Step 5 — Connect it to Claude

Point the MCP Inspector at your live `https://.../mcp` URL first to confirm the
tools work remotely. To use it in Claude, add it as a **custom connector** in
Claude's settings (Settings → Connectors), pasting your `/mcp` URL. The exact
menu wording changes over time — see the current connector docs at
<https://support.claude.com>. Then ask Claude something like *"what's NASA's
astronomy picture of the day?"* and it will call your tool.

---

## ⚠️ About `--allow-unauthenticated`

The deploy command above makes your server **publicly reachable by anyone with
the URL**. For a learning project that's fine, but be aware: strangers could
call it and burn through your NASA rate limit. Before relying on it, lock it
down. Reasonable next steps, easiest first:

1. **Require a bearer token** — add a check in the server for a shared secret in
   the `Authorization` header, and supply that header from the client.
2. **Keep the service private** (drop `--allow-unauthenticated`) and require
   Google IAM auth, putting an authenticating proxy in front for Claude.
3. **Full OAuth 2.1**, which the MCP spec defines for remote servers. FastMCP
   has helpers for this — it's the "proper" answer but the most work.

---

## How the code maps to MCP concepts

- `FastMCP(name="nasa", ...)` creates the server. `stateless_http=True` and
  `json_response=True` are the recommended settings for a scalable HTTP
  deployment — Cloud Run may run several container instances, and stateless mode
  means none of them rely on in-memory session state.
- Each `@mcp.tool()` function becomes a callable tool. **The docstring is the
  description the model sees, and the type hints become the input schema** — so
  writing clear docstrings and precise types is the single highest-leverage
  thing you can do for tool quality.
- `mcp.run(transport="streamable-http")` serves everything at `/mcp`.

## Ideas to extend it

- Add a tool for NASA's EPIC (full-disc Earth imagery) or EONET (natural-event
  tracking) endpoints — same `_nasa_get` helper, new path.
- Swap NASA for a different free API (weather, exchange rates, etc.) — only the
  tool bodies change; the server scaffolding and deployment stay identical.
- Add an MCP **resource** (read-only data) alongside the tools.