Skip to main content
Glama
uirooong

yahoo-finance-mcp

by uirooong
README.md
# yahoo-finance-mcp

Yahoo!ファイナンス日本版の株価を提供する **Streamable HTTP 型 MCP サーバー**。
Bun で動き、Docker コンテナとして起動する(npm は使わない)。

- 調査の一次資料: [`research/FINDINGS.md`](research/FINDINGS.md) — 実通信で確認した API 仕様
- 設計の根拠: [`DESIGN.md`](DESIGN.md) — なぜこの構成なのか

> **利用条件を先に読むこと。** このサーバーが取得するデータは再配布・商用利用が
> 規約で禁止されています。詳しくは [利用条件](#利用条件) を参照。

---

## できること

| ツール | 内容 |
|---|---|
| `get_quote` | 現在値・前日比・騰落率・始値・高値・安値・出来高・売買代金・PER・PBR・配当利回り・時価総額・年初来高安など。最大 50 銘柄 |
| `get_price_history` | 日足 OHLCV(最大 3000 本)/分足(当日・直近5営業日) |
| `get_pts_quote` | PTS(夜間取引)の四本値・出来高 |
| `search_symbols` | 会社名から銘柄コードを検索 |

国内株式・ETF・REIT・国内指数・米国株に対応。
リソース `yahoo-finance://usage-notes` にデータの鮮度と制約をまとめてある。

## 動かす

```bash
bun install
bun run up        # .env を生成 → イメージをビルド → 起動
```

`bun run up` は `.env` が無ければ作り、`MCP_BEARER_TOKEN` を 32 バイトの乱数から生成する。
**既にトークンがあれば黙って上書きしない。**

```bash
bun run setup            # .env を用意(トークンが無ければ生成、あればそのまま)
bun run setup --rotate   # トークンを差し替える
bun run setup --quiet    # トークンを画面に出さない
```

`bun run setup` は MCP クライアントに貼る設定 JSON をそのまま出力する:

```json
{
  "mcpServers": {
    "yahoo-finance": {
      "type": "http",
      "url": "http://127.0.0.1:3000/mcp",
      "headers": { "Authorization": "Bearer 62a5e0…(生成された値)" }
    }
  }
}
```

確認と停止:

```bash
curl -s localhost:3000/healthz
MCP_BEARER_TOKEN=$(grep -oP '(?<=MCP_BEARER_TOKEN=).*' .env) bun run smoke
bun run logs
bun run down
```

`compose.yaml` は **127.0.0.1 にのみ公開**する。公開ネットワークに出さないこと。

### `.env` を変えたあと

**`docker compose restart` では反映されない。** 環境変数はコンテナ生成時に焼き込まれるため、
作り直しが要る。

```bash
docker compose up -d      # コンテナを作り直して .env を反映(bun run up でも可)
```

実測(`.env` にトークンを入れて認証を有効化した直後):

| 操作 | 認証なしアクセス |
|---|---|
| `docker compose restart` | `200` ← 反映されていない |
| `docker compose up -d` | `401` ← 反映された |

反映されたかは起動ログで確認できる。

```bash
docker compose logs --tail=5 | grep -E 'auth|warning'
# "auth":"bearer"   -> 認証あり
# "auth":"disabled" -> 認証なし
```

### 認証を無効化する

信頼できる環境だけで使うこと。`MCP_BEARER_TOKEN` を空にして `MCP_ALLOW_NO_AUTH=1` を立てる。

```bash
sed -i 's/^MCP_BEARER_TOKEN=.*/MCP_BEARER_TOKEN=/;s/^MCP_ALLOW_NO_AUTH=.*/MCP_ALLOW_NO_AUTH=1/' .env
docker compose up -d
```

無効化しても **127.0.0.1 バインドと Origin 検証は残る**ので、ブラウザのページからは届かない
(`Origin` 付きのリクエストは `403`)。一方で**同じマシンで動く任意のプロセスからは素通し**になる。
読み取り専用とはいえ、あなたの上流セッションと IP で Yahoo を叩けてしまう点に注意。

戻すとき:

```bash
bun run setup --rotate    # トークンを生成(トークンがあれば認証が有効になる)
docker compose up -d
```

`MCP_ALLOW_NO_AUTH=1` が立っていても、`MCP_BEARER_TOKEN` に値があれば**認証は有効**になる。
フラグが効くのはトークンが空のときだけ。`bun run setup` は無効化中の `.env` に黙って
トークンを生やさない(`--rotate` を明示したときだけ生成する)。

### `.env` とコンテナの関係

`bun run setup` が生成する `.env` は `.env.example` を土台にしており、全設定項目がコメント付きで入る。
`MCP_PORT` / `MCP_ALLOWED_ORIGINS` / `YF_MAX_CONCURRENCY` / `YF_MIN_INTERVAL_MS` / `LOG_LEVEL` / `TZ` は
`.env` の値がそのままコンテナに渡る。`MCP_HOST` と `YF_SESSION_FILE` だけはコンテナ固有の値
(`0.0.0.0` と `/data/session.json`)に固定され、`.env` では変えられない。

`.env` は `.gitignore` 済み。コミットされるのは値が空の `.env.example` だけ。

### ローカル開発(Docker なし)

```bash
bun run dev           # .env を用意して --hot で起動
bun test              # 単体テスト (48 件)
bun run test:live     # 実ネットワークを叩く統合テスト (6 件)
bun run typecheck
```

## 設定

| 環境変数 | 既定 | 説明 |
|---|---|---|
| `MCP_HOST` / `MCP_PORT` | `127.0.0.1` / `3000` | 待ち受け(コンテナ内は `0.0.0.0`) |
| `MCP_BEARER_TOKEN` | — | **必須**。未設定だと起動を拒否する |
| `MCP_ALLOW_NO_AUTH` | — | `1` で認証を無効化(非推奨) |
| `MCP_ALLOWED_ORIGINS` | 空 | 許可する `Origin`。空 = Origin 付きリクエストを全て拒否 |
| `MCP_MAX_BODY_BYTES` | `1048576` | リクエストボディ上限 |
| `YF_MAX_CONCURRENCY` | `5` | 上流への同時接続数(**上限 10 でクランプ**) |
| `YF_MIN_INTERVAL_MS` | `50` | 上流リクエストの最小間隔(**下限 20ms**) |
| `YF_SESSION_FILE` | 空 | Cookie B の保存先。空なら永続化しない |
| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` |

## 仕組み

```
MCP Client ──POST /mcp──▶ Origin検証 → Bearer認証
                          └▶ StreamableHTTPServerTransport (ステートレス)
                             └▶ tools → TtlCache(single-flight) → Limiter(5並列)
                                        └▶ YahooClient ─┬─ x-jwt-token
                                           YahooSession ─┴─ Cookie B
                                                             │
                                                    finance.yahoo.co.jp
```

要点は 3 つ。

**1. WebSocket も SSE も無い。** 本家は 60 秒間隔のポーリングだけで動いている
(JS チャンク全 24 本を全文検索して `WebSocket` / `EventSource` / `text/event-stream` の
出現数が 0、`refreshInterval:6e4` が 3 件)。サーバー発の push が要らないので
**ステートレス Streamable HTTP**(`sessionIdGenerator: undefined`)で構成している。

**2. 認証は Cookie `B` にバインドされた JWT。** 上流の JWT はクレームが `exp` のみで
ユーザー識別情報を持たない。したがって **Yahoo セッションはプロセスに 1 つ**で足り、
MCP クライアントが何本つながっても上流から見たセッションは 1 つになる。
Cookie B は volume に永続化し、コンテナ再起動後も同じセッションを引き継ぐ。

**3. single-flight が生命線。** 同一銘柄への同時リクエストは 1 本に畳まれる。
実測(Docker 上で同一銘柄を 20 並列):

```
上流リクエスト: 2 本 (合流 38)      ← priceBoard + detailData の 2 本だけ
bootstrap 回数: 1
```

## データの鮮度(重要)

| 項目 | 実態 |
|---|---|
| 取引値 | 東証・福証・札証は**リアルタイム**(`delayMinutes: 0`)。日経平均・米国株は 15 分遅れ |
| **出来高・売買代金** | **全市場で最低 15 分遅れ**。`volume.realtime: false` として返る |
| 日足 | **当日分を含まない**(引け後に追加される) |
| 分足 | ほぼ遅延なし。ただし国内株式・ETF のみ |

出力の `volume.realtime` / `volume.asOf` / `delayMinutes` / `quoteAsOf` を必ず見ること。
取引値が 14:57 の時に出来高が 14:41 ということが普通に起きる。

数値が `null` のときは **0 ではなく「算出不能・未提供」**を意味する
(上流が `"---"` を返している。赤字予想銘柄の PER など)。

## 取得できないもの

- **板情報(気配値)** — 無料枠では全て `999` のダミー値が返るため、意図的にツール化していない
- **国内指数・米国株の分足** — 上流が 0 件を返す(日足は取得できる)
- **ポートフォリオ** — ログインが必要

## セキュリティ

Streamable HTTP はブラウザから到達しうる HTTP エンドポイントなので、stdio と違い明示的な防御が要る。

- `Origin` 検証(DNS リバインディング対策、MCP 仕様の MUST)
- Bearer 認証(`timingSafeEqual` で比較)
- 127.0.0.1 バインド、ボディサイズ上限
- コンテナは `read_only` / `cap_drop: ALL` / 非 root(`USER bun`)/ `no-new-privileges`
- ログは Cookie B と JWT を必ずマスクする(`424a...q6(len=58)` の形)

## 壊れたときに見る場所

| 症状 | ログ | 原因と対処 |
|---|---|---|
| 全ツールが失敗 | `yahoo.jwt_not_found` | 上流のページ構造変更。`src/yahoo/session.ts` の `JWT_PATTERNS` を更新 |
| 全ツールが失敗 | `yahoo.session_rejected_retrying` が連続 | Cookie B と JWT のバインド方式が変わった。FINDINGS.md の切り分け手順で再調査 |
| 一部の項目だけ null | `yahoo.schema_mismatch` | 上流のレスポンス形式変更。`research/samples/` の固定入力テストで差分を特定 |
| `get_price_history` だけ 404 | — | `/chart/ex/v1/...` の二重パスが廃止された。最も壊れやすい箇所 |
| 突然の 403 / 429 連発 | `upstream.errors` が急増 | WAF による遮断。`YF_MAX_CONCURRENCY` を下げる |

`/healthz` が運用メトリクスを返す。**`session.bootstrapCount` が跳ねたら single-flight が壊れている**
(正常なら 1 日十数回に収まる)。

```json
{
  "status": "ok",
  "session": { "present": true, "expiresInSec": 7180, "bootstrapCount": 1 },
  "upstream": { "requests": 42, "errors": 0, "sessionRetries": 0 },
  "cache": { "entries": 12, "inflight": 0, "hits": 30, "misses": 12, "coalesced": 38 },
  "limiter": { "active": 0, "queued": 0 }
}
```

## 利用条件

取得元の Yahoo!ファイナンスには以下の定めがある。

- [注記事項](https://finance.yahoo.co.jp/feature/promotion/caution)
  「**株式情報の転用、販売は固く禁じます。**」
  「当社は、この情報を用いて行う判断の一切について責任を負うものではありません。」
- [LINEヤフー共通利用規約](https://www.lycorp.co.jp/ja/company/terms/) 第14条(当社サービス等の再利用の禁止)
  「お客様は…当社サービスやそれらを構成するデータを、**その提供目的を超えて利用することができません**。
  この場合、当社は、それらの行為を差し止める権利ならびにそれらの行為によってお客様が得た利益相当額を請求する権利を有します。」
- 同 第15条(5) 「当社のサーバーまたはネットワークの機能を破壊したり、妨害したりする行為、**BOT**…を利用して
  当社サービスを不正に操作する行為」を禁止

したがって本サーバーは:

1. **127.0.0.1 にのみバインド**し、公開ネットワークに出さない構成にしてある
2. 同時実行数を**環境変数では緩められない**(上限 10 でクランプ)
3. 全ツールの出力に出典と免責を付ける
4. データを**再配布・販売・第三者提供しない**こと

**商用利用・再配布が必要な場合は、この方法を使わないこと。**
[J-Quants API](https://jpx-jquants.com/)(JPX 公式、遅延データの無料枠あり)、
QUICK、LSEG 等の正規のデータベンダー契約を利用する。

データ提供元は東京証券取引所・大阪取引所・名古屋証券取引所・野村総合研究所・東洋経済新報社・
ウエルスアドバイザー・リフィニティブ・ジャパン・LINE FX。日経平均株価の著作権は日本経済新聞社に帰属する。