Skip to main content
Glama
rc-kazu

mcp-oauth-dcr

by rc-kazu
README.md
# mcp-oauth-dcr

ローカル MCP サーバを OAuth(DCR)で保護するハンズオン用リポジトリです。

- **認可サーバ (AS)**: `node-oidc-provider` @ `http://localhost:4000`
- **MCP サーバ (RS)**: `@modelcontextprotocol/sdk` @ `http://localhost:3000/mcp`

## デモのセットアップ

### 1. 依存関係のインストール

```bash
npm ci
```

### 2. サーバを起動(ターミナルを 2 つ使う)

```bash
# ターミナル 1: 認可サーバ
npm run auth

# ターミナル 2: MCP サーバ
npm run mcp
```

起動確認:

| サービス | URL |
|---|---|
| 認可サーバ | http://localhost:4000 |
| MCP サーバ | http://localhost:3000/mcp |
| 保護リソースメタデータ (PRM) | http://localhost:3000/.well-known/oauth-protected-resource/mcp |

### 3. 自動検証(任意)

両方起動した状態で:

```bash
npx tsx src/verify.ts
```

`401 → メタデータ発見 → DCR → トークン検証 → 接続確立` が成功すれば、サーバ側は問題ありません。

### 4. MCP Inspector で手動デモ

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

ターミナルに表示される **トークン付き URL**(例: `http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=...`)をブラウザで開きます。

Inspector の設定:

| 項目 | 値 |
|---|---|
| Transport Type | **Streamable HTTP** |
| URL | `http://localhost:3000/mcp` |
| Connection Type | **Via Proxy**(Direct は CORS で失敗しやすい) |

**Connect** を押すと OAuth フローが始まります。

1. 最初に 401(`Missing Authorization header`)が出る → **正常**
2. DCR でクライアント登録
3. ブラウザで `http://localhost:4000` のログイン画面が開く
4. **Sign-in** → 任意のユーザー名・パスワード(例: `test` / `password`)
5. **Continue**(同意)
6. Inspector に戻り、接続完了
7. `whoami` / `add` ツールを呼び出せる

### 5. よくあるトラブル

**`invalid_client` / `client is invalid`**

認可サーバ(`npm run auth`)を再起動すると、DCR で登録したクライアント情報は **メモリ上から消えます**。Inspector の sessionStorage には古い `client_id` が残っていると、このエラーになります。

対処:

1. Inspector で **Connect をやり直す**(新規 DCR)
2. 直らなければブラウザの DevTools → Application → Session Storage → `localhost:6274` をクリアしてから再接続

**Continue 後に `Failed to fetch`**

トークン交換(`POST /token`)の CORS 問題です。最新の `auth-server.ts` では `clientBasedCORS` を設定済みです。`npm run auth` が最新コードで動いているか確認してください。

**Direct 接続で `Failed to fetch`**

Connection Type は **Via Proxy** を使ってください。

## jwks.json について

**誰が**: 認可サーバ(`src/auth-server.ts`)  
**いつ**: **`npm run auth` の初回起動時**(リポジトリ直下に `jwks.json` が無い場合)

処理の流れ:

1. `loadOrCreateJwks()` が `jwks.json` の有無を確認
2. **無ければ** `jose` で RS256 の鍵ペアを生成し、`jwks.json` に書き込む
3. **あれば** 既存ファイルを読み込んで使う(再起動しても `kid` が変わらない)

用途:

- AS が JWT アクセストークンに署名する **秘密鍵**(ファイル内に含まれる)
- AS が `http://localhost:4000/jwks` で公開する **公開鍵**
- MCP サーバ(RS)がその JWKS を取りに行き、Bearer トークンの署名を検証する

`verify.ts` も同じ `jwks.json` を読んで、デモ用にトークンを自前署名します(本番フローでは AS が `/token` で発行します)。

`.gitignore` に入っているので Git にはコミットしません。削除して `npm run auth` し直すと新しい鍵が生成されます(その間に発行済みトークンは検証できなくなります)。

## 構成

```
src/
├── config.ts        共有設定(ポート、issuer、resource URL)
├── auth-server.ts   認可サーバ :4000
├── mcp-server.ts    MCP サーバ(リソースサーバ) :3000
└── verify.ts        自動検証スクリプト
jwks.json            AS 署名鍵(初回起動時に自動生成・gitignore)
```

## スクリプト

| コマンド | 説明 |
|---|---|
| `npm run auth` | 認可サーバ起動 |
| `npm run mcp` | MCP サーバ起動 |
| `npm run check` | TypeScript 型チェック |
| `npx tsx src/verify.ts` | OAuth フロー自動検証 |

## ライセンス

[MIT](LICENSE)