Skip to main content
Glama
waggle-sensor

Sage MCP Server

Official
README.md
# Sage MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io/) server for the
[Sage Grande Testbed](https://sagecontinuum.org/) — cyberinfrastructure for
AI@Edge. It exposes ~30 MCP tools, resources, and prompts for querying sensor
data, submitting edge-computing jobs, discovering plugins, and browsing Sage
documentation from any MCP-compatible client (Claude Desktop, Cursor,
custom agents, etc.).

Built on [FastMCP 2.10+](https://github.com/jlowin/fastmcp) and the
[MCP SDK 1.12+](https://github.com/modelcontextprotocol/python-sdk); supports
`stdio`, `sse`, and `streamable-http` transports.

---

## Choose your setup

There are three ways to run this — pick the one that fits.

| # | Path | Who it's for | You need |
|---|------|--------------|----------|
| 1 | [**Hosted (mcp.sagecontinuum.org)**](#1-hosted-mcpsagecontinuumorg) | Everyone. Zero install. | An MCP client + a Sage token. |
| 2 | [**Local — stdio**](#2-local--stdio-for-ide-clients) | IDE users who want to run the server themselves. | Python 3.11+ and this repo. |
| 3 | [**Local — HTTP**](#3-local--http-for-development-or-non-ide-clients) | Devs, custom agents, curl testing. | Python 3.11+ or Docker. |

All three expose the exact same tools. The only difference between hosted and
local is *how much data you can reach*: without Sage credentials, every tool
still runs but falls back to **public data only**. See
[Authentication & credentials](#authentication--credentials) for how to add
your token.

---

## 1. Hosted (mcp.sagecontinuum.org)

The server is already running at `https://mcp.sagecontinuum.org/mcp`. Point
any MCP client at it — no install needed.

**Cursor** (`~/.cursor/mcp.json`) or **Claude Desktop**
(`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
  "mcpServers": {
    "sage": {
      "url": "https://mcp.sagecontinuum.org/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_USERNAME:YOUR_ACCESS_TOKEN"
      }
    }
  }
}
```

Get `YOUR_ACCESS_TOKEN` from
<https://portal.sagecontinuum.org/account/access>. The `Bearer` value must be
in `username:token` form for anything beyond public data.

Restart your client, then ask natural questions:

> "Show me temperature readings from node W023 in the last hour."
> "Find nodes in Chicago with recent camera images."
> "What's the highest temperature recorded today across all nodes?"

---

## 2. Local — stdio (for IDE clients)

Run the server as a subprocess of your IDE. Nothing binds to a network port.
This is the most private mode — data and credentials never leave your machine.

**Install:**

```bash
git clone https://github.com/waggle-sensor/sage-mcp.git
cd sage-mcp
pip install -r requirements.txt
cp .env.example .env       # edit and add SAGE_USER / SAGE_PASS if you have them
```

**Wire it into Cursor/Claude Desktop:**

```json
{
  "mcpServers": {
    "sage-local": {
      "command": "/absolute/path/to/sage-mcp/scripts/run-local-stdio.sh",
      "env": {
        "SAGE_USER": "your-sage-username",
        "SAGE_PASS": "your-sage-access-token"
      }
    }
  }
}
```

`env:` values here override anything in `.env` — useful when you want
different tokens per IDE profile. Omit them entirely to run against public
data only.

---

## 3. Local — HTTP (for development or non-IDE clients)

Bind the server to a local port. Useful for `curl`, custom agents, browser
extensions, or anything that connects to a URL rather than spawning a
subprocess.

### Option A — bare Python

```bash
pip install -r requirements.txt
cp .env.example .env       # optional
./scripts/run-local-http.sh
```

Server is now on `http://127.0.0.1:8000/mcp`. Quick sanity check:

```bash
curl http://127.0.0.1:8000/health
# {"status":"ok"}
```

### Option B — Docker

```bash
cp .env.example .env       # optional
docker compose up          # http://127.0.0.1:8000/mcp — laptop-safe default
```

The default `docker compose up` publishes only to `127.0.0.1`. To expose it
on all interfaces (cloud deployment), use the `cloud` profile:

```bash
docker compose --profile cloud up -d
```

For older MCP clients that only speak SSE:

```bash
docker compose --profile sse up   # http://127.0.0.1:8001/sse
```

### Point a client at your local server

```json
{
  "mcpServers": {
    "sage-local-http": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}
```

---

## Authentication & credentials

The server never *requires* a Sage token — it just does less without one.

| Scenario | What works | What doesn't |
|----------|------------|--------------|
| No token | Public sensor data, node listings, docs, plugin discovery, image proxy for public images | Protected datasets, `/proxy/image` for restricted content, job submission (`sesctl` still needs a real token) |
| `username:token` provided | Everything the token's Sage account has access to | Anything not shared with your account |

**How to provide a token, in order of preference:**

1. **`.env` file** (`SAGE_USER=…` + `SAGE_PASS=…`) — picked up by both
   `./scripts/run-local-http.sh` and `./scripts/run-local-stdio.sh` and by
   `docker compose`.
2. **Per-request HTTP header** — for the hosted server or any HTTP transport:
   - `Authorization: Bearer username:token` (recommended)
   - `Authorization: Basic <base64(username:token)>`
   - `X-SAGE-Token: username:token`
3. **Query parameter** — `?token=username:token` as a last resort (shows up
   in server logs, so avoid for anything sensitive).

Get your token from
<https://portal.sagecontinuum.org/account/access>. Access to protected data
also requires signing the Sage Data Use Agreement.

---

## Configuration reference

Every option is an environment variable — see [`.env.example`](.env.example)
for the full list with comments.

| Variable | Default | Purpose |
|----------|---------|---------|
| `MCP_TRANSPORT` | `streamable-http` | `stdio` / `sse` / `streamable-http` / `http` (alias) |
| `MCP_HOST` | `0.0.0.0` | Bind address (network transports only) |
| `MCP_PORT` | `8000` | Bind port (network transports only) |
| `MCP_PATH` | `/mcp` | HTTP path (streamable-http only) |
| `LOG_LEVEL` | `INFO` | Python logging level |
| `SAGE_USER`, `SAGE_PASS` | — | Basic auth for `/proxy/image` |
| `SAGE_PROXY_BASE_URL` | `http://localhost:8000` | Public URL prefix in generated proxy links |
| `ADMIN_API_KEY` | — | Required to hit `/analytics/*` |
| `SAGE_MCP_SKIP_REGISTRY_REFRESH` | — | Skip ECR call on startup (offline/CI) |

---

## HTTP endpoints (non-MCP)

| Path | Auth | Purpose |
|------|------|---------|
| `GET /health` | none | Liveness / readiness probe |
| `GET /proxy/image?url=…` | `SAGE_USER`/`SAGE_PASS` or per-request token | Authenticated image proxy to `storage.sagecontinuum.org` |
| `GET /analytics/summary` | admin key | Aggregate usage counts |
| `GET /analytics/users` | admin key | Per-user stats |
| `GET /analytics/tools` | admin key | Per-tool stats |
| `GET /analytics/user/{id}` | admin key | One user + their tool usage |
| `GET /analytics/activity` | admin key | Recent activity feed (`?limit=`) |

Admin API key is accepted via `X-Admin-API-Key` header,
`Authorization: Bearer <key>`, or `?api_key=<key>`.

---

## Tools, resources & prompts

29 tools, 2 resources, 7 prompts. Highlights:

- **Sensor data:** `get_node_all_data`, `get_node_iio_data`,
  `get_environmental_summary`, `list_available_nodes`, `search_measurements`,
  `get_node_temperature`, `get_temperature_summary`
- **Node metadata:** `get_node_info`, `list_all_nodes`, `get_sensor_details`
- **Jobs:** `submit_sage_job`, `submit_plugin_job`, `submit_multi_plugin_job`,
  `check_job_status`, `query_job_data`, `force_remove_job`, `suspend_job`
- **Geography:** `get_nodes_by_location`, `get_measurement_stat_by_location`
- **Plugins:** `find_plugins_for_task`, `get_plugin_data`, `query_plugin_data_nl`,
  `create_plugin`
- **Images:** `get_cloud_images`, `get_image_data`, `get_image_proxy_url`
- **Docs:** `ask_sage_docs`, `sage_faq`, `search_sage_docs`
- **Resources:** `query://{plugin}`, `stats://temperature`
- **Prompts:** `getting_started_guide`, `plugin_development_guide`,
  `data_analysis_guide`, `troubleshooting_guide`, and three "suggest" prompts.

More docs in the `docs/` folder — [Getting Started](docs/GETTING_STARTED.md),
[Authentication](docs/AUTHENTICATION.md),
[Custom Functions](docs/CUSTOM_FUNCTIONS.md),
[Docker Deployment](docs/DOCKER_DEPLOY.md).

---

## Testing

```bash
pip install -r requirements.txt
python -m pytest tests/          # 61 tests, no network required
```

The test suite mocks `sage_data_client` and the ECR registry — nothing hits
Sage's servers during CI. Legacy interactive smoke scripts (`test_auth.py`,
`test_server.py`, `test_image_proxy.py`, ...) still work as manual tests;
they're excluded from `pytest` collection via `pytest.ini`.

---

## Extending

Add a tool by decorating a function in one of the modules under
`sage_mcp_server/tools/`:

```python
# sage_mcp_server/tools/sensor_tools.py

@mcp.tool
def my_custom_analysis(data_query: str, analysis_type: str = "basic") -> str:
    """Perform a custom analysis on Sage data."""
    df = data_service.query_data(...)
    return f"Analysis results: ..."
```

See [Custom Functions Guide](docs/CUSTOM_FUNCTIONS.md) for the full workflow
(fork → add → deploy).

---

## Project layout

```
sage_mcp.py                          # 30-line entrypoint; exposes `mcp`
sage_mcp_server/                     # FastMCP factory + services
├── server.py                        # build_server(), main()
├── auth.py                          # HTTP request auth extraction
├── analytics_service.py             # in-memory analytics
├── data_service.py                  # sage-data-client wrapper
├── docs_helper.py                   # docs search + FAQ
├── job_service.py                   # sesctl wrapper
├── job_templates.py                 # pre-baked plugin job templates
├── models.py                        # pydantic v2 domain models
├── plugin_generator.py              # cookiecutter-style plugin scaffolding
├── plugin_metadata.py               # ECR plugin registry
├── plugin_query_service.py          # NL plugin query
├── plugin_registry.py               # measurement/plugin catalog
├── utils.py                         # time parsing, timestamp formatting
└── tools/                           # MCP tools split by concern
    ├── sensor_tools.py
    ├── job_tools.py
    ├── geo_tools.py
    ├── plugin_tools.py
    ├── docs_tools.py
    ├── prompts.py
    └── http_routes.py               # /health, /analytics/*, /proxy/image
```

---

## License

MIT. See `LICENSE`.