🔌

自前のMCP ServerをClaude.ai / ChatGPTのカスタムコネクタとして公開する

に公開

Claude.aiとChatGPTがMCPのカスタムコネクタに対応した。自分で運用しているMCP Serverを登録すると、チャット画面からコーディング不要で接続できる。

EDINET DB(edinetdb.jp)でこの対応をやったので、実装の全体像と判断ポイントを書く。

前提: Claude.ai / ChatGPT の MCP Connector は何をしているか

ユーザーがSettings → ConnectorsにURLを入力すると、クライアント(Claude.ai / ChatGPT)は以下の流れを踏む。

1. GET /.well-known/oauth-protected-resource
   → MCP Serverが保護リソースであることを確認、AS(Authorization Server)のURLを取得

2. GET /.well-known/oauth-authorization-server
   → authorize / token / registration エンドポイントを取得

3. POST /oauth/register (DCR)
   → client_id と client_secret を動的に取得

4. GET /oauth/authorize?response_type=code&client_id=...&code_challenge=...&code_challenge_method=S256
   → ユーザーを認証画面にリダイレクト

5. POST /oauth/token
   → 認可コード + PKCE verifier → access_token取得

6. POST /mcp (Authorization: Bearer <access_token>)
   → MCP Streamable HTTPでツール呼び出し

OAuth 2.1 + PKCE (S256) が必須。plain は許容されない。

実装すべきエンドポイント

1. Protected Resource Metadata

RFC 9728。MCP Serverが「認証が必要」であることをクライアントに伝える最初の接点。

# /.well-known/oauth-protected-resource
{
    "resource": "https://edinetdb.jp/mcp",
    "authorization_servers": ["https://edinetdb.jp"],
    "bearer_methods_supported": ["header"],
    "resource_documentation": "https://edinetdb.jp/docs/mcp-guide"
}

MCP Server自体が401を返すとき、WWW-AuthenticateヘッダにこのメタデータのURLを含める。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://edinetdb.jp/.well-known/oauth-protected-resource"

Claude.aiはこの401をトリガーにOAuthフローを開始する。

2. Authorization Server Metadata

RFC 8414。クライアントがOAuthの各エンドポイントを発見するための標準メタデータ。

# /.well-known/oauth-authorization-server
{
    "issuer": "https://edinetdb.jp",
    "authorization_endpoint": "https://edinetdb.jp/oauth/authorize",
    "token_endpoint": "https://edinetdb.jp/oauth/token",
    "registration_endpoint": "https://edinetdb.jp/oauth/register",
    "scopes_supported": ["mcp"],
    "response_types_supported": ["code"],
    "grant_types_supported": ["authorization_code"],
    "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
    "code_challenge_methods_supported": ["S256"],
    "service_documentation": "https://edinetdb.jp/docs/mcp-guide"
}

token_endpoint_auth_methods_supported"none"を含める。Claude.aiはpublic client(client_secretなし)として接続してくるケースがある。

3. Dynamic Client Registration (DCR)

RFC 7591。クライアントが自分自身を登録するエンドポイント。

# POST /oauth/register
# Request:
{
    "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
    "client_name": "Claude"
}

# Response:
{
    "client_id": "60154f73-d646-47e0-b170-e698e7ef18d2",
    "client_secret": "a1b2c3d4...",
    "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "client_secret_post",
    "client_id_issued_at": 1740328897,
    "client_secret_expires_at": 0
}

client_idはUUID v4、client_secretは64文字のhex。secretはSHA-256でハッシュ化してからDBに保存する。

注意点として、Claude.aiはユーザーごとに(あるいはセッションごとに)DCRを叩く場合がある。同じredirect_uriで複数のclient_idが登録されるのは正常な挙動。

4. Authorize エンドポイント

ここが自前のMCP Serverの設計判断が一番分かれるところ。

EDINET DBの場合、Google OAuth(OpenID Connect)をIdPとして使っている。/oauth/authorizeに来たリクエストを、そのままGoogle OAuth認証画面にリダイレクトする。

Claude.ai → /oauth/authorize?client_id=...&code_challenge=...&redirect_uri=...
         → Google OAuth認証画面
         → /oauth/callback (Googleからのリダイレクト)
         → redirect_uri?code=<signed_blob> (Claude.aiに認可コードを返す)

認可コードはHMACで署名したJSONブロブにした。DBに認可コードを保存しない。

payload = {
    "sub": google_sub,      # Googleユーザーの安定ID
    "email": email,
    "cid": client_id,
    "ru": redirect_uri,
    "cc": code_challenge,    # PKCE
    "ccm": "S256",
    "ts": int(time.time()),  # タイムスタンプ
}
blob = json.dumps(payload)
signature = hmac.new(SECRET_KEY, blob.encode(), hashlib.sha256).hexdigest()[:32]
auth_code = base64url_encode(json.dumps({"p": blob, "s": signature}))

Cloud Runのような水平スケーリング環境では、インスタンス間で状態を共有する仕組み(Redis等)を持たないことが多い。HMACブロブなら任意のインスタンスで検証できる。

TTLは300秒。タイムスタンプを検証して期限切れの認可コードを拒否する。

5. Token エンドポイント

認可コードをaccess_tokenに交換する。

# POST /oauth/token
# Request:
{
    "grant_type": "authorization_code",
    "code": "<signed_blob>",
    "client_id": "<uuid>",
    "code_verifier": "<pkce_verifier>",
    "redirect_uri": "https://claude.ai/api/mcp/auth_callback"
}

# Response:
{
    "access_token": "edb_...",
    "token_type": "Bearer",
    "scope": "mcp",
    "expires_in": 315360000
}

EDINET DBの設計判断として、access_tokenそのものがAPIキーになっている。別途token管理テーブルを持たない。

理由は3つ。

  1. APIキー認証は既に運用中で、認証・認可・レート制限のインフラが整っている
  2. refresh_tokenを発行する必要がない(APIキーは長期有効)
  3. OAuthフロー以外の接続方法(Claude Code、Cursor等)と同じAPIキーを使える

PKCE検証はここで行う。

verifier_hash = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip("=")

if not hmac.compare_digest(verifier_hash, stored_code_challenge):
    return error("invalid_grant")

hmac.compare_digestで定時間比較する。==で比較するとタイミング攻撃の余地が生まれる。

Claude.ai 固有の注意点

redirect_uri

https://claude.ai/api/mcp/auth_callback
https://claude.com/api/mcp/auth_callback

両方のドメインがある。事前登録する場合は両方をカバーする。DCR経由なら都度登録されるので問題ない。

public client

Claude.aiはclient_secretなしで/oauth/tokenを叩いてくることがある。token_endpoint_auth_methodsに"none"を入れておく必要がある。

client_secretがない場合でもclient_idの存在確認は行い、PKCEの検証は必須のまま。セキュリティはPKCEで担保する設計。

セッション

Claude.aiはMCPのセッションをConnectorごとに保持している。一度接続が確立すると、access_tokenがConnector設定に保存される。ユーザーがConnectorを削除するまで有効。

ChatGPT 固有の注意点

redirect_uri

https://chatgpt.com/connector_platform_oauth_redirect
https://platform.openai.com/apps-manage/oauth

Claude.aiと異なるパターン。

DCR

ChatGPTはDCRを使ってclient_idを自動登録する。事前にoauth_clientsテーブルに登録しておくことも可能だが、DCR対応している方が確実。

Streamable HTTP Transport

MCP Serverのtransport。/mcpエンドポイントにPOSTでJSON-RPCリクエストを送る。

POST /mcp HTTP/1.1
Authorization: Bearer edb_...
Content-Type: application/json

{"jsonrpc":"2.0","method":"tools/list","id":1}

レスポンスはJSON-RPCのレスポンスをそのまま返す。SSE(Server-Sent Events)でストリーミングすることも可能だが、EDINET DBはJSONレスポンスのみ(財務データは一発で返せるサイズなので、ストリーミングの利点が薄い)。

MCP Server を公開するときのチェックリスト

実装を終えてから振り返ったチェックリスト。

  • /.well-known/oauth-protected-resource が正しいJSONを返す
  • /.well-known/oauth-authorization-server が全エンドポイントURLを返す
  • code_challenge_methods_supported"S256"が含まれる
  • token_endpoint_auth_methods_supported"none"が含まれる(public client対応)
  • DCR(/oauth/register)が任意のredirect_uriを受け入れて、client_id/secretを返す
  • client_secretはハッシュ化して保存
  • 認可コードのTTLが5分以内
  • PKCE S256の検証に定時間比較を使っている
  • /oauth/tokenがform-encodedとJSON両方を受け入れる
  • access_tokenがMCPエンドポイントで使える
  • /mcpの401レスポンスにWWW-Authenticateヘッダがある
  • HTTPSが有効(OAuth 2.1の必須要件)

まとめ

MCP Serverのカスタムコネクタ対応は、要素技術としてはOAuth 2.1の標準的な実装。ただし、Claude.ai / ChatGPTそれぞれのクライアント挙動の違い(redirect_uriのパターン、public client対応の要否、DCRの使い方)を押さえる必要がある。

HMACブロブによるstateless設計はCloud Run環境で効果的に機能した。認可コード・セッション・stateパラメータの全てをDBなしで管理できるので、水平スケーリングとの相性がいい。

EDINET DB: https://edinetdb.jp
接続手順(Claude.ai): https://edinetdb.jp/docs/connect-claude
接続手順(ChatGPT): https://edinetdb.jp/docs/connect-chatgpt
MCP接続ガイド: https://edinetdb.jp/docs/mcp-guide

Discussion