ChatGPTのMCPサーバーをOAuth認証で作成する方法

Higtyのシステムの作り方

概要

この記事ではOAuth認証付きのMCPサーバーの作成方法について簡単に説明します。

ChatGPTを参考に解説していきますが多分Claudeでも動くでしょう。


必要な手順は以下になります。

・oauth-protected-resource.json

・oauth-authorization-server.jsonとopenid-configuration.json

・/registerエンドポイント(クライアントの自動登録)

・/authorizeページ(ログインページ)と/api/authorizeエンドポイント

・/tokenとrefresh_token

それでは解説していきます。



oauth-protected-resource.json

ChatGPTはMCPサーバーにどのようなOAuth認証のAPIのエンドポイントがあるのかわかりません。それを知らせるためのファイルがこのoauth-protected-resource.jsonファイルになります。自分のMCPサーバーのドメインがhttps://xxx.your-service.com だとして、このファイルをhttps://xxx.your-service.com/.well-known/oauth-protected-resource というエンドポイントに置いておくとMCPクライアント(ChatGPTやClaude)がこのファイルにアクセスし必要な情報を読み取ってくれます。MCPのリソースサーバーのドメインがxxx.your-service.com、認証サーバーのドメインがauth.your-domain.comだとするとファイルの中身はこんな感じです。

{
  "resource": "https://xxx.your-domain.com",
  "authorization_servers": [ "https://auth.your-domain.com" ],
  "bearer_methods_supported": [ "header" ],
  "scopes_supported": [
    "openid",
    "profile",
    "offline_access",
    "project"
  ],
  "resource_documentation": "https://xxx.your-domain.com/oauth_document"
}


oauth-authorization-server.jsonとopenid-configuration.json

次はOAuth認証を行う認証サーバーにファイルを置きます。

(※もしかしたらどちらか片方だけでもいけるかも?未確認)

認証サーバーのドメインがauth.your-domain.com だとするとそれぞれのファイルは以下のようになります。


https://auth.your-domain.com/.well-known/oauth-authorization-server


{
  "issuer": "https://auth.your-domain.com/",
  "authorization_endpoint": "https://auth.your-domain.com/authorize",
  "token_endpoint": "https://auth.your-domain.com/token",
  "registration_endpoint": "https://auth.your-domain.com/register",
  "jwks_uri": "https://auth.your-domain.com/jwks.json",
  "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ],
  "token_endpoint_auth_signing_alg_values_supported": [ "HS256", "RS256" ],
  "userinfo_endpoint": "https://auth.your-domain.com/user/info",
  "scopes_supported": [
    "openid",
    "profile",
    "offline_access",
    "project"
  ],
  "response_types_supported": [
    "code"
  ],
  "service_documentation": "http://xxx.your-domain.com/oauth_document",
  "ui_locales_supported": [ "en-US", "ja-JP" ]
}


https://auth.your-domain.com/.well-known/openid-configuration


{
  "issuer": "https://auth.your-domain.com/",
  "authorization_endpoint": "https://auth.your-domain.com/authorize",
  "token_endpoint": "https://auth.your-domain.com/token",
  "registration_endpoint": "https://auth.your-domain.com/register",
  "jwks_uri": "https://auth.your-domain.com/jwks.json",
  "scopes_supported": [
    "openid",
    "profile",
    "offline_access",
    "project"
  ],
  "response_types_supported": [
    "code"
  ],
  "code_challenge_methods_supported": [ "S256" ],
  "response_modes_supported": [ "query", "fragment", "form_post" ],
  "subject_types_supported": [ "public" ],
  "id_token_signing_alg_values_supported": [ "HS256", "RS256" ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "aud",
    "auth_time",
    "created_at",
    "email",
    "email_verified",
    "exp",
    "family_name",
    "given_name",
    "iat",
    "identities",
    "iss",
    "name",
    "nickname",
    "phone_number",
    "picture",
    "sub"
  ],
  "request_uri_parameter_supported": false
}


/registerエンドポイント

今までのOAuth認証のクライアントの作成ではOAuthサーバー(Googleカレンダー、Microsoft365など)のWEBページでアプリの登録が事前に必要でした。OAuthサーバーはプラットフォーム企業(GoogleやMicrosoftなど)でアプリ開発者は小さい企業や個人という関係でした。この関係であればアプリ開発者がGoogleのページでアプリ登録をしてそこで得たclient_idとclient_secretをアプリに設定して作業するという流れは自然です。


しかし今回のMCPの仕組みではプラットフォームとアプリ開発者の関係が逆になっています。MCPサーバーが多数作られていったとして、OpenAIの社員がFigmaなどの大きな企業のみならず、小さな企業のMCPサーバーのWEBページでChatGPTのアプリを登録しそのclient_idなどをChatGPTで使うようにする形は非現実的です。


そこでDynamic Client Registrationという仕組みが新たにOAuth認証に導入されました。

RFC7591

https://datatracker.ietf.org/doc/html/rfc7591

これによってChatGPTは/registerエンドポイントにPOSTリクエストを送るだけでclient_idなどの情報を取得できるようになります。

ChatGPTの場合、以下のようなリクエストのBodyが/registerエンドポイントに送信されてきます。

{
   "client_name":"ChatGPT",
   "redirect_uris":[
      "https://chatgpt.com/connector_platform_oauth_redirect"
   ],
   "grant_types":[
      "authorization_code",
      "refresh_token"
   ],
   "response_types":[
      "code"
   ],
   "token_endpoint_auth_method":"client_secret_post"
}


これに対してMCPサーバーからは以下のようなレスポンスを返します。

{
  "client_id": "xxx",
  "client_secret": "xxx",
  "client_id_issued_at": 12345, --UnixTime
  "client_secret_expires_at": 123456, --UnixTime
  "redirect_uris": [
    "https://chatgpt.com/connector_platform_oauth_redirect"
  ],
  "grant_types": ["authorization_code", "refresh_token"],
  "client_name": "ChatGPT",
  "token_endpoint_auth_method": "client_secret_basic",
  "logo_uri": "https://xxx.your-domain.com/logo.png",
  "jwks_uri": "https://xxx.your-domain.com/my_public_keys.jwks",
  "example_extension_parameter": "value1"
}


MCPサーバー側は動的に生成したclient_idなどの情報をDBに保存する処理を作り、上記のレスポンスを返す感じのコードを作ることになります。

コードのイメージ

private static async ValueTask OAuthRegister(HigLaboAppHttpContext context)
{
    var r = await context.CreateFromBody<DynamicClientRegistration>();

    var now = DateTimeOffset.Now;
    var sp = new OAuthClient_Add();
    sp.ClientId = Guid.NewGuid();
    sp.DisplayName = r.client_name;
    sp.CreateTime = now;
    sp.RequestBody = await context.Request.GetRequestBodyTextAsync();
    sp.ClientSecret = HigLaboAppSecurity.CreateRandomString(128);
    await sp.ExecuteNonQueryAsync();

    r.client_id = sp.ClientId.ToString()!;
    r.client_secret = sp.ClientSecret;
    r.client_id_issued_at = now.ToUnixTimeSeconds();
    r.client_secret_expires_at = now.AddDays(100).ToUnixTimeSeconds();
    r.grant_types.Clear();
    r.grant_types.Add("authorization_code");
    r.grant_types.Add("refresh_token");
    await context.Response.WriteAsJsonAsync(r);
}


/authorizeページ(ログインページ)と/api/authorizeエンドポイント

次はログインのページを作ります。上記の各種設定ファイルの構成と/registerエンドポイントまで正しく作れている場合、ChatGPT上でMCPサーバーの設定をするとこの/authorizeページに遷移します。


まずはChatGPTでMCPサーバーの設定を行います。まだベータ版扱いなので開発者モードにしてから設定する必要があります。(※2025/10/18現在)


作成するボタンを押すとブラウザで/authorizeページが表示されます。以下のようなURLになります。

/authorize?response_type=code&client_id=xxx&redirect_uri=https%3A%2F%2Fchatgpt.com%2Fconnector_platform_oauth_redirect&state=oauth_s_68f014b79c6881919164d1a86bbca75a&resource=https%3A%2F%2Fxxx.your-domain.com


このページは基本的にはログインID(メールアドレス)とパスワードとログインボタンがある認証画面になると思います。ログインボタンを押したときに/api/authorizeエンドポイントが呼び出され、メールアドレスとパスワードの認証が合っていればChatGPTから指定されたURLへリダイレクトをする形になります。

リダイレクトのURLは以下のようになります。

Redirect URL: https://chatgpt.com/connector_platform_oauth_redirect?code=xxx&state=yyy

認証が成功したときにcodeの値はこちらで生成してUserIdと紐づけてDBなどに保存しておく必要があります。リダイレクトしてすぐに、ChatGPTは/tokenエンドポイントにこのcodeの値付きでPOSTリクエストを送ってきます。この時のリクエストはJSON形式ではなく、application/x-www-form-urlencoded形式で送られてきます。

/tokenエンドポイントに送られてくるBodyは以下のような形になります。

grant_type=authorization_code&code=xxx&redirect_uri=https://chatgpt.com/connector_platform_oauth_redirect&client_id=some_client_id&client_secret=some_client_secret_value&resource=https://xxx.your-domain.com

Bodyからgrant_typeの値がauthorization_codeであることを確認します。その後にclient_id、client_secret、codeの値を取り出し、DBに保存しておいたclient_id、client_secret、codeの値からUserIdを特定しアクセストークンをレスポンスとして返します。

{
   access_token = "some_access_token",
   token_type ="Bearer",
   expires_in = 3600,
   refresh_token = "some_refresh_token",
   scope = "offline_access profile"
}

これで認証が完了しMCPサーバーとして無事に登録が完了するはずです。



OAuth認証の仕様上、しばらく経つとaccess_tokenの期限が切れます。その時は/tokenエンドポイントにgrant_type=refresh_tokenのリクエストが飛んできます。フォーマットはapplication/x-www-form-urlencodedです。

grant_type=refresh_token&refresh_token=xxx&client_id=some_client_id&client_secret=some_client_secret_value&resource=https://xxx.your-domain.com

パラメーターにrefresh_tokenが含まれているのでそれを使って新しいaccess_tokenを発行しレスポンスとして返します。


ASP.NET Coreの場合、サーバーのコードはこんな感じになるでしょう。

private static async ValueTask OAuthToken(HigLaboAppHttpContext context)
{
    var ff = context.Request.CreateDictionaryFromRequestFormAsync();
    var grantType = ff.GetValueOrDefault("grant_type");
    var code = ff.GetValueOrDefault("code");
    var client_id = ff.GetValueOrDefault("client_id");
    var client_secret = ff.GetValueOrDefault("client_secret");

    if (grantType == "authorization_code")
    {
        var rToken = await DB.OAuthToken_Get_By_Code_OrNull(code);
        if (rToken == null)
        {
            throw new HigLaboAppException("Invalid code");
        }
        if (client_secret != null)
        {
            var rClient = await DB.OAuthClient_Get_By_ClientId_ClientSecret_OrNull(rToken.ClientId, client_secret);
            if (rClient == null)
            {
                throw new HigLaboAppException("Invalid client_secret");
            }
        }
        else
        {
            throw new HigLaboAppException("client_secret required.");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            access_token = rToken.AccessToken,
            token_type = "Bearer",
            expires_in = 3600,
            refresh_token = rToken.RefreshToken,
            scope = string.Join(' ', rToken.ScopeList),
        });
    }
    else if (grantType == "refresh_token")
    {
        var refresh_token = ff.GetValueOrDefault("refresh_token");
        var rToken = await DB.OAuthToken_Get_By_RefreshToken_OrNull(refresh_token);
        if (rToken == null)
        {
            throw new HigLaboAppException("Invalid refresh_token");
        }
        await context.Response.WriteAsJsonAsync(new
        {
            access_token = rToken.AccessToken,
            token_type = "Bearer",
            expires_in = 3600,
            refresh_token = rToken.RefreshToken,
            scope = string.Join(' ', rToken.ScopeList),
        });
    }
    else
    {
        throw new HigLaboAppException("Invalid grant_type." + grantType);
    }
}



これで認証は完了です。ChatGPTで会話していて自分の作ったツールが使われた場合、以下のようなリクエストが送られてきます。

BodyはJSON形式で以下のようになります。

{
   "method":"tools/call",
   "params":{
      "_meta":{
         "openai/userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
         "openai/locale":"ja-JP",
         "openai/userLocation":{
            "city":"Kodaira",
            "region":"Tokyo",
            "country":"JP",
            "timezone":"Asia/Tokyo",
            "latitude":"35.72603",
            "longitude":"139.48508"
         },
         "openai/subject":"v1/oHBmOflqrwxZrURnT9JVlM1mNecpeWJXMEbXoUUtJmHs"
      },
      "name":"get_todays_weather",
      "arguments":{
         "city":"福岡"
      }
   },
   "jsonrpc":"2.0",
   "id":1
}

リクエストのヘッダーにはアクセストークンがセットされています。

Authorization: Bearer access_token_value_xxxxxxxxxx

この値を使ってユーザーを特定できます。

MCPサーバーの処理を実行して結果をChatGPTにレスポンスとして返すという感じで実装すれば様々な機能をChatGPTから呼べるようになります。



下記の記事でASP.NET CoreでMCPサーバーの各処理の作り方を解説しています。

https://www.higlabo.ai/blog/higty-tech/aspnet-mcp-server-from-openai





参考記事

https://platform.openai.com/docs/guides/tools-connectors-mcp

https://qiita.com/yokawasa/items/74717789465ef2aeedfe#3-%E5%8B%95%E7%9A%84%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E7%99%BB%E9%8C%B2dynamic-client-registration