Skip to main content
Glama
lukegohwb-ctrl

market-data-mcp

README.md
# market-data-mcp

A tiny **remote MCP server** that serves **live US stock quotes**, for use as a
custom connector in **Claude / Cowork (claude.ai)**.

Cowork only accepts *remote* MCP connectors — a public `https://` URL. The local
`claude_desktop_config.json` method does **not** work in Cowork. This server uses
**streamable-HTTP** transport so it can be deployed to a public URL and added as a
custom connector.

- **Tool name:** `get_quotes` (keep this exact — your dashboard calls it)
- **Endpoint path:** `/mcp` → the connector URL is `<public-url>/mcp`

---

## Tool contract

### `get_quotes`
**Input**
```json
{ "symbols": "AMD,MSFT,MU" }
```
Comma-separated tickers, case-insensitive.

**Output** — JSON keyed by UPPERCASE ticker:
```json
{
  "AMD":  {"price": 552.05, "currency": "USD", "asof": "2026-07-06T20:00:00Z", "source": "Yahoo Finance"},
  "MSFT": {"price": 498.10, "currency": "USD", "asof": "2026-07-06T20:00:00Z", "source": "Yahoo Finance"}
}
```
A per-ticker failure returns `{"TICKER": {"error": "..."}}` and the rest still succeed.

### `get_quote_history` (optional, for charts)
```json
{ "symbol": "AMD", "range": "1mo", "interval": "1d" }
```
Returns parallel arrays:
```json
{ "symbol": "AMD", "dates": ["2026-06-09", "..."], "closes": [548.2, "..."] }
```
`range`: `1d,5d,1mo,3mo,6mo,1y,2y,5y,max` · `interval`: `1m,5m,15m,30m,1h,1d,1wk,1mo`.
History is always sourced from Yahoo Finance (keyless).

### Calling it from the dashboard
```js
const quotes = await window.cowork.callMcpTool("get_quotes", { symbols: "AMD,MSFT" });
// quotes.AMD.price, quotes.AMD.asof, ...
```

---

## Configuration (environment variables)

| Var             | Default | Purpose |
|-----------------|---------|---------|
| `PORT`          | `8000`  | Port to bind. Most hosts inject this. |
| `DATA_PROVIDER` | `yahoo` | `yahoo` (no key) · `finnhub` · `alphavantage`. |
| `API_KEY`       | —       | Key for `finnhub` / `alphavantage`. |
| `AUTH_TOKEN`    | —       | If set, requests must send `Authorization: Bearer <token>`. |

---

## 1. Run locally

```bash
pip install -r requirements.txt
python server.py
# -> Serving MCP at http://0.0.0.0:8000/mcp
```

Quick health check:
```bash
curl http://localhost:8000/health          # -> ok
```

### Prove the tool returns the contract shape (curl)
MCP is a JSON-RPC handshake, so a raw quote call is two steps. Easiest proof is the
test suite (below). To exercise the transport directly, initialize then call the tool
(streamable-HTTP requires the `text/event-stream` accept header):

```bash
curl -sN http://localhost:8000/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":"0"}}}'
```
(The tests call the tools directly and assert the exact shape — that's the fastest check.)

---

## 2. Quick public test (Cloudflare tunnel, ~2 min, no account)

With `python server.py` running, in another terminal:

```bash
# install cloudflared first (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
cloudflared tunnel --url http://localhost:8000
```

It prints a public URL like `https://random-words.trycloudflare.com`.
Your connector URL is that **+ `/mcp`**:

```
https://random-words.trycloudflare.com/mcp
```

> The tunnel URL dies when you close the terminal — great for testing, not permanent.
> For an always-on URL, deploy (next section).

---

## 3. Deploy to a public https URL (always-on, free tier)

You need a public URL Claude can reach. Any of these free tiers work; each gives you
`https://<something>` — your connector URL is that **+ `/mcp`**.

### Render (simplest — uses `render.yaml`)
1. Push this repo to GitHub.
2. Render → **New + → Blueprint** → pick the repo. It reads `render.yaml`.
3. (Optional) In the service's **Environment**, set `AUTH_TOKEN` and, if using a paid
   provider, `DATA_PROVIDER` + `API_KEY`.
4. Deploy. Your URL is `https://<name>.onrender.com` → connector = `.../mcp`.
   *(Free instances sleep when idle; the first request after a nap wakes it in ~30s.)*

### Railway
1. Push to GitHub → Railway → **New Project → Deploy from repo**.
2. It detects the `Dockerfile`. Set env vars as needed. Railway sets `PORT` for you.
3. **Settings → Networking → Generate Domain** → connector = `https://<domain>/mcp`.

### Fly.io (uses `fly.toml` + `Dockerfile`)
```bash
fly launch --no-deploy      # accept the included fly.toml
fly secrets set AUTH_TOKEN=your-long-random-token   # optional but recommended
fly deploy
```
Connector = `https://<app>.fly.dev/mcp`.

**Start command everywhere:** `python server.py` (the Docker image already runs it).

---

## 4. Add it in Claude → Settings → Connectors

1. Claude → **Settings → Connectors → Add custom connector**.
2. **Name:** `Market Data` (anything).
3. **URL:** paste your endpoint — the public URL **plus `/mcp`**, e.g.
   `https://your-app.onrender.com/mcp`.
4. If you set `AUTH_TOKEN`: open **Advanced settings** and put the token where it asks
   for the bearer token / `Authorization` header (send header `Authorization: Bearer <token>`).
   Leave OAuth Client ID/Secret blank — this server uses a static bearer token, not OAuth.
5. **Save.** Then ask Claude to *"refresh live quotes"* — it will call `get_quotes`.

---

## 5. Tests

```bash
pip install -r requirements.txt pytest
pytest -q
```
The suite asserts the exact `get_quotes` contract (uppercase keys; `price/currency/asof/source`;
per-ticker error isolation) and that history returns equal-length `dates`/`closes`.
Tests skip automatically if there's no network.

---

## Security

- **Read-only public quotes.** No account, no brokerage credentials, ever. Do not extend
  this server to touch trading or account APIs.
- **Lock it down** by setting `AUTH_TOKEN` — every request then needs
  `Authorization: Bearer <token>`, which you configure once in the connector's Advanced
  settings. `/health` stays open for platform health checks.
- Store `API_KEY` / `AUTH_TOKEN` as host **secrets/env vars**, never in the repo.
- Optionally restrict inbound traffic to Anthropic's IP ranges at your host/proxy for
  defense in depth.