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.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues