atama plus techblog
🐾

FastMCP + FastAPIで複数認証方式を実装する - OAuth & SAキー認証を両立したMCPサーバの実践例

に公開

はじめに

atama plusにてデータエンジニアをしているTackungです。
この記事はatama plus Advent Calendar 2025の12月3日の記事です!

我々のチームでは現在、AI-readyなデータ基盤構築の一環として、社内データをAPI/MCPで利用可能にするサーバを開発しています。この記事では、FastMCP + FastAPIを使った複数の認証方式のインタフェースを1つのサーバ上で実現する実装プラクティスを紹介します。

特に、「GoogleアカウントによるOAuth認証」と「サービスアカウントキー認証」を同時にサポートする方法について、実装時の工夫や実装例を紹介します。
FastMCPは現在、Googleアカウントのほか、Auth0、AWS Cognito、Azure EntraID、GitHubなど多数の認証コネクタを提供しているため、他の認証プロバイダーを利用する場合でも本記事の内容は参考になるかと思います。

こんな方向けの記事

  • 認証付きMCPサーバを構築しようと考えている方
  • 複数の認証方式をサポートする必要がある方
  • FastMCP + FastAPIでの実装例を探している方

前提知識

本記事は下記については既にざっくりと理解していることを想定しています。

  • FastAPIの基本的な使い方
  • MCP(Model Context Protocol)の基本概念
  • OAuth2系の基礎知識
  • Cloud Run等のコンテナ環境へのデプロイ方法

なお、本記事ではMCPサーバの実装部分に焦点を当てており、デプロイ部分については触れていません。
ご参考までに我々がGoogle Cloud上にMCPサーバをデプロイする際に参考にしたドキュメント、記事をリンクしておきます!

Cloud RunへのMCPサーバーのデプロイ方法:

https://codelabs.developers.google.com/codelabs/cloud-run/how-to-deploy-a-secure-mcp-server-on-cloud-run?hl=ja#0

Google Cloud側でのOAuthクライアント設定方法:

https://zenn.dev/kimitsu/articles/gemini-cli-cloud-run-mcp

MCPの基本概念や実装例については過去に弊社テックブログでも紹介しています。合わせてご参照ください:
https://zenn.dev/atamaplus/articles/5bf2bc1acef5b1

背景

なぜこのサーバが必要だったのか

我々のチームでは、社内のデータ基盤上にあるコンテンツデータを、さまざまな形で活用する取り組みを進めています。その一環として、これを実現するためのインタフェースの開発を現在進めています。具体的には以下のような要件がありました。

  • API経由でのアクセス: アプリケーションからプログラマティックにデータを取得したい
  • MCP経由でのアクセス: VSCodeやClaude Desktopなどの個人の環境から利用できるツールから対話的にデータを検索したい。また、将来的にはアプリケーション内に組み込まれたAIエージェント等からの利用も見据えている
  • 1つのサーバで両方を提供: 保守性とコスト効率の観点から、1つのサーバ上でAPIとMCPの両方を提供したい

上記の要件を踏まえ、APIとMCPを統合しやすい点(FastAPI統合をサポート)複数の認証プロバイダライブラリが利用可能な点からFastMCPを採用しました。

認証方式を使い分ける必要性

実装を進めていくにあたって利用方法によって適切な認証方式が異なるという課題が見えてきました。
(余談:もともとCloud Run Services Proxyを経由することでVSCodeでもSAキーでの認証によるMCPクライアント利用ができていたのですが、2025年8月ごろのVSCodeアップデートにてMCPサーバの認証に関する仕様変更が加わったようで、何もしていないのに壊れた現象に直面してこの課題を認識しました...)

判明した時の社内Slackでのつぶやき

利用ケース別で考えると、下記のような認証の使い分けをしたいと考えました。

  • アプリケーション(API利用)・AIエージェント(MCP利用): サービスアカウント(SA)キーを使った認証
    • 自動化しやすい
    • 毎回の利用時に人の介在が不要
    • 適切な場所でキー登録・管理する必要がある
  • 個人利用(VSCodeやClaude Desktop等経由のMCP利用): 個別のGoogle WorkspaceアカウントによるOAuth認証
    • ユーザーごとの権限管理が可能
    • ブラウザベースの認証フロー
    • 個人のアカウント(Googleアカウント等)で認証
    • ユーザ間でSAキーファイルの共有やりとりをする必要がない

そのため、今回サーバを提供するにあたって2つの異なる認証方式をサポートするという仕様が必要であることが明らかになりました。

MCPにおける認証の現状

MCPの認証について世の中の現状を調査しました(2025年10月時点)。

MCPサーバの認証[1]OAuth2.1準拠が必須とされています。OAuth2.1では、認可コードフロー(Authorization Code Flow with PKCE)を使ったセキュアな認証フローが定められており、ユーザーの同意を得た上でアクセストークンを発行する仕組みです。

また、FastMCPはGoogleProvider(OAuth認証)やJWTVerifier(トークンベース認証)といった各種認証プロバイダー向けの認証機能が提供されています。

一方で社内利用のような中規模での複数認証方式の実装例はまだ確立していない状況でした。個人向け(小規模)や一般公開向け(大規模)の事例はいくつか見つかるものの、限られた組織内で複数の認証方式を提供している実装事例は自分の探す限りでは見つかりませんでした。

https://modelcontextprotocol.io/specification/draft/basic/authorization
https://gofastmcp.com/servers/auth/authentication
https://hi120ki.github.io/ja/blog/posts/20250728/

調査を進めるのと並行して、FastMCPのGitHub repositoryにて我々と似たような課題に関するissueが起票→対応したドキュメントが追加されるという状況も観測しており、MCPにおいて認証部分に関してもまだ過渡期フェーズであることが伺えます。
https://github.com/jlowin/fastmcp/issues/1579
https://gofastmcp.com/deployment/http#mounting-authenticated-servers

※あくまでも個人の見解です。

実装アプローチ

基本設計とアーキテクチャ全体像

上記の調査の内容を踏まえて、FastAPI上にマウントすることで認証方式別のFastMCPの2つのインスタンスを1つのFastAPIアプリケーション上で動かすという設計を採用しました。

インタフェース部分に関する最終的な構成は下記のようになっています。

インフラ部分の細かな説明は省きますが、WAFにて不正なリクエストを拒否+アプリケーションレイヤーにてSAキートークン/OAuth認証という構成となっています。

エンドポイント設計

2つの認証方式を異なるエンドポイントで提供するようにしています。

  • OAuth認証エンドポイント: /mcp/
    • FastMCPのGoogleProviderを使用
    • ブラウザベースの認可コードフローで認証
  • JWT(SAキートークン)認証エンドポイント: /api/mcp/
    • FastMCPのJWTVerifierを使用
    • SAキーから生成されたトークンで認証
    • HTTPリクエストのAuthorizationヘッダーに含まれるJWTトークンを検証

実装例

上記の設計を実現するミニマムな実装例を紹介します。

mcp_server.py(FastMCPサーバ定義)
import os
from typing import List
from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
from fastmcp.server.auth.providers.jwt import JWTVerifier

# Google OAuth認証設定
oauth_auth = GoogleProvider(
    client_id=os.getenv("GOOGLE_CLIENT_ID"),
    client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
    base_url=os.getenv("OAUTH_BASE_URL"),  # OAuth認証用に登録したURL
    redirect_path="/auth/callback",
)

# JWT認証設定(Google Cloud サービスアカウント用)
jwt_auth = JWTVerifier(
    jwks_uri="https://www.googleapis.com/oauth2/v3/certs"
)

# FastMCPインスタンスの作成
mcp_oauth = FastMCP(name="mcp-oauth", auth=oauth_auth)
mcp_jwt = FastMCP(name="mcp-jwt", auth=jwt_auth)


# 複数MCPインスタンスにツールを一括登録するデコレータ
def multi_mcp_tool(instances: List[FastMCP]):
    def decorator(func):
        for instance in instances:
            instance.tool()(func)
        return func
    return decorator


# ツール定義(両方のインスタンスに登録)
@multi_mcp_tool([mcp_oauth, mcp_jwt])
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@multi_mcp_tool([mcp_oauth, mcp_jwt])
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"
main.py(FastAPIアプリケーション)
from contextlib import asynccontextmanager
from fastapi import FastAPI
from mcp_server import mcp_oauth, mcp_jwt

# FastMCPアプリの作成
mcp_oauth_app = mcp_oauth.http_app(path="/mcp")
mcp_jwt_app = mcp_jwt.http_app(path="/mcp")


# 両方のMCPアプリのlifespanを統合
@asynccontextmanager
async def combined_lifespan(app: FastAPI):
    async with mcp_oauth_app.lifespan(app):
        async with mcp_jwt_app.lifespan(app):
            yield


# FastAPIアプリを作成
app = FastAPI(title="Dual Auth MCP Server", lifespan=combined_lifespan)

# MCPアプリをマウント
app.mount("/api", mcp_jwt_app)   # JWT認証: /api/mcp/
app.mount("/", mcp_oauth_app)    # OAuth認証: /mcp/

実装における工夫ポイント

この実装を進める中で、いくつかの技術的な課題に直面したのでその内容とともに工夫点を紹介します。

工夫1: Lifespanの統合

認証方法別でFastMCPのインスタンスを2つ作成してFastAPIに1つずつ順にマウントしたところ、以下のエラーが発生しました。

RuntimeError: Task group is not initialized. Make sure to use run().
FastMCP's StreamableHTTPSessionManager task group was not initialized.

原因: FastMCPの各インスタンスは独立したtask groupを持っており、それぞれのlifespanを初期化する必要があります。しかし、FastAPIのlifespanパラメータには1つしか渡せません。

アプローチ: asynccontextmanagerを使って両方のlifespanを統合しました。

from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastmcp import FastMCP

# インスタンス作成
mcp_oauth = FastMCP(name="mcp-oauth", auth=GoogleProvider(...))
mcp_jwt = FastMCP(name="mcp-jwt", auth=JWTVerifier(...))

# FastMCPアプリ作成
mcp_oauth_app = mcp_oauth.http_app(path="/mcp")
mcp_jwt_app = mcp_jwt.http_app(path="/mcp")

# 両方のMCPアプリのlifespanを統合
@asynccontextmanager
async def combined_lifespan(app: FastAPI):
    async with mcp_oauth_app.lifespan(app):
        async with mcp_jwt_app.lifespan(app):
            yield

# FastAPIアプリに統合したlifespanを渡す
app = FastAPI(
    title="Dual Auth MCP Server",
    lifespan=combined_lifespan
)

ポイント: 多層のasync withでネストさせることで、両方のtask groupを初期化できます。なお、今回のケースではネストの順番に特に意味はありません。

工夫2: パスルーティングの設計

2つのMCPインスタンスを異なるパスにマウントする必要があるため、FastMCPのhttp_app()とFastAPIのmount()を組み合わせてパスを切るようにしました。

# OAuth認証用
mcp_oauth_app = mcp_oauth.http_app(path="/mcp")
app.mount("/", mcp_oauth_app)
# 最終的なエンドポイント: / + /mcp = /mcp/

# JWT認証用
mcp_jwt_app = mcp_jwt.http_app(path="/mcp")
app.mount("/api", mcp_jwt_app)
# 最終的なエンドポイント: /api + /mcp = /api/mcp/

注意点: FastAPIのmount()登録順にマッチングを試みるため、より具体的なパスを先にマウントする必要があります。

# 正しい例:正しい順序(具体的なパスを先に記述)
app.mount("/api", mcp_jwt_app)
app.mount("/", mcp_oauth_app)

# 間違った例:逆順だと /api/mcp/ が / にマッチしてしまう
app.mount("/", mcp_oauth_app)
app.mount("/api", mcp_jwt_app)  # 到達不可能

工夫3: ツール実装の共通化

2つのMCPインスタンスで同じ処理のツールを提供する場合、コードの重複を避けたいという課題がありました(DRY原則)。
何も考えずに実装すると、下記のように各MCPインスタンスに対して同じツール定義を2回書く必要があります。

@mcp_oauth.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp_jwt.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

アプローチ: 複数のMCPインスタンスに一括でツールを登録するカスタムデコレータを作成しました。

from typing import List
from fastmcp import FastMCP

def multi_mcp_tool(instances: List[FastMCP]):
    """複数のMCPインスタンスにツールを登録するデコレータ"""
    def decorator(func):
        for instance in instances:
            instance.tool()(func)
        return func
    return decorator

このデコレータを使うことで、1回の定義で複数のMCPインスタンスにツールを登録できるようにしています。

mcp_oauth = FastMCP(name="mcp-oauth", auth=GoogleProvider(...))
mcp_jwt = FastMCP(name="mcp-jwt", auth=JWTVerifier(...))
...

@multi_mcp_tool([mcp_oauth, mcp_jwt])
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

メリット:

  • ツール定義が1箇所で済み、実装コスト削減
  • ロジックのユニットテストがしやすい
  • 両方の認証方式で完全に同じ動作を保証
  • 新しいツールを追加する際も、デコレータを付けるだけで両方のインスタンスに登録される

工夫4: 認証ミドルウェアのスキップパス設計

FastAPIにはMiddlewareという仕組みがあり、すべてのリクエストに対して認証などの共通の処理を挟むことができます。我々のサーバでは、MCP以外のAPIエンドポイント(/api/v1/*など)に対してGoogle IDトークンを検証する認証ミドルウェアを導入しています。

一方で、MCPクライアント利用にてOAuth認証を利用する場合、初回の認証フロー自体は認証なしでアクセスできる必要があります

具体的には、ユーザーが初めてMCPクライアントからアクセスする際、以下のようなフローが発生します。

  1. クライアントが/.well-known/oauth-protected-resourceにアクセスしてOAuth設定を取得
  2. /authorizeで認証を開始
  3. Googleの認証画面でログイン
  4. /auth/callbackにリダイレクトされてトークンを受け取る

これらのエンドポイントが認証ミドルウェアでブロックされてしまうと、認証フロー自体が開始できないという問題が発生します。

アプローチ: OAuth認証フローに必要なパスを認証スキップ対象として設定することで、OAuthにおける初回認証フローを実現しています。

# 認証をスキップするパス(OAuth認証フローに必要)
SKIP_PATHS = [
    "/.well-known/",  # OAuthメタデータ(認証設定の取得)
    "/authorize",     # OAuth認証開始
    "/auth/",         # OAuthコールバック
    "/token",         # トークン取得
    "/consent",       # OAuth同意画面
    "/mcp/",          # MCP OAuth認証(FastMCP側で認証)
    "/api/mcp/",      # MCP JWT認証(FastMCP側で認証)
]

def _is_skip_authentication(path: str) -> bool:
    """認証スキップの判断"""
    return any(path.startswith(prefix) for prefix in SKIP_PATHS)

ポイント: MCPエンドポイント(/mcp/, /api/mcp/)自体もスキップ対象にしていますが、これはFastMCP側の認証プロバイダー(GoogleProvider, JWTVerifier)が認証を担当するためです。エンドポイントにより各リクエスト方式での適切な認証を切り替えています。

おわりに

今回はFastMCP + FastAPIで複数の認証方式を1つのサーバで実現するための実装例と工夫ポイントを紹介しました。

MCPの認証に関する実装方法はまだ確立途上と認識しており、本記事の内容も自分たちのケースに最適化した1つの実装例ではありますが、この記事が同じような課題に直面している方の参考になれば幸いです。
最後までお読みいただきありがとうございました!

脚注
  1. なお、本記事執筆中の2025年11月25日にMCP仕様アップデートがあり、OpenID Connect Discovery統合やセキュリティベストプラクティス(Confused Deputy問題への対策、トークンパススルーの禁止など)が明確化されました。詳細は公式ドキュメントをご参照ください。 ↩︎

atama plus techblog
atama plus techblog

Discussion