はじめに

Gemini Enterprise(以下 GE)に独自エージェントを登録し、DocBase の公式 MCP サーバーを接続しようとしましたが、DocBase MCPはOAuth 2.1 認証が必要になり、ClaudeやChatGPT などのAI クライアントでは動作確認済みと公式記載されてましたが、Gemini Enterprise からの接続は動作確認済み一覧になかったため、今回実践いたしました。

DocBase公式リモートMCP

全体アーキテクチャ

Gemini Enterprise App
│ ①ユーザーごとにOAuth同意 → GEがトークンを取得・保管
▼
root_agent(オーケストレーター)← GEへ登録
└─ docbase_agent(Docbase探索のサブエージェント)
└─ McpToolset ─→ Docbase MCP (https://mcp.docbase.io/mcp)
②実行時にGEが注入したトークンをBearerヘッダーで付与

下記設計の要点です。

  1. GEに登録するエージェントは1つとし、配下にサブエージェントをぶら下げる構成にしたい。
  2. トークンはGEがユーザーごとにサーバー側で管理
  3. 各ユーザーは自分のDocBase権限の範囲でしか検索できない形

エージェント実装(ADK)

エージェントはADK(Agent Development Kit)で実装し、開発には agents-cli を利用しました。
GEがセッションstate に注入するトークン(temp:)を実行時に取り出し、Bearer ヘッダーとしてMCP接続に付与する形です。

AUTH_ID = os.environ.get("DOCBASE_AUTH_ID", "docbase-oauth")

def _docbase_headers(context: ReadonlyContext) -> dict[str, str]:
token = context.state.get(f"temp:{AUTH_ID}") or context.state.get(AUTH_ID)
if not token:
token = os.environ.get("DOCBASE_ACCESS_TOKEN") # ローカル検証用
return {"Authorization": f"Bearer {token}"} if token else {}

McpToolset(
connection_params=StreamableHTTPConnectionParams(url="https://mcp.docbase.io/mcp"),
header_provider=_docbase_headers,
tool_filter=_readonly_tools_only, # search/get系のみ公開(後述)
)

制限として下記2つを入れてます。

1つ目はDocBase MCP経由の操作は読み取り専用としました。

ツール名のプレフィックスで search / get 系だけを利用するようにします。

_READONLY_PREFIXES = ("search", "get", "list", "read", "fetch", "find")

def _readonly_tools_only(tool: BaseTool, context: ReadonlyContext | None) -> bool:
return tool.name.lower().replace("-", "_").startswith(_READONLY_PREFIXES)

2つ目はレスポンス捏造の制御です。

トークン未取得のままLLMが動くと、存在しないメモとURLを生成してしまう事故があり、before_agent_callbackでトークンの有無をチェックし、なければ「認証未完了」の定型文を返すようにしています。

def require_docbase_auth(callback_context: CallbackContext) -> types.Content | None:
state = callback_context.state
token = (
state.get(f"temp:{AUTH_ID}") # 本番: GEが注入
or state.get(AUTH_ID) # ローカル: キャッシュ
or os.environ.get("DOCBASE_ACCESS_TOKEN") # ローカル検証用
)
if token:
return None # 認証OK → 通常どおりエージェントを実行

# トークンなし → LLMを実行せず定型文を返す(Contentを返すとエージェント本体はスキップされる)
return types.Content(
role="model",
parts=[types.Part(text=(
"Docbase への接続認証が完了していないため、探索を実行できません。"
"Docbase 連携の OAuth 認証を完了してから再度お試しください。"
))],
)

これらをサブエージェントに組み込みます。ADK ではbefore_agent_callbackが types.Content を返すと、エージェント本体(LLM呼び出し)はスキップされ、その内容がそのまま応答になります。

docbase_agent = Agent(
name="docbase_agent",
model=Gemini(model="gemini-flash-latest"),
instruction="...",
tools=[build_docbase_toolset()],
before_agent_callback=require_docbase_auth, # ← 捏造ガード
)

DocBase の DCR で scope を絞ると 500 エラー

DocBase の OAuth は Dynamic Client Registration(DCR)に対応しており、API でクライアント登録ができます。

読み取り専用にしたかったので "scope": "read" を指定して登録したところ、同意画面で承認をすると 500 Internal Server Errorになりました。

DCR登録時にscopeを指定して絞ることで起こる問題でした。
scope を指定せずに登録するとデフォルトの全スコープが割り当てられ、同意が正常に通りました。

# NG: "scope": "read" を入れると承認時に500
# OK: scopeを指定しない
curl -X POST https://docbase.io/oauth/register -H 'Content-Type: application/json' -d '{
"client_name": "Gemini Enterprise - my-agent",
"redirect_uris": ["https://vertexaisearch.cloud.google.com/oauth-redirect"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_post"
}'

また、同意画面にはスコープのチェックボックスがあり、利用時に促される承認の際にスコープにて書き込みや削除スコープを選ぶとその内容がそのままトークンに反映されるので、読み取り専用制御はエージェント側のツールフィルタで行いました。

動作確認

今回はDocBase内の社内の事例を検索するエージェントとして構築しました。

GEでエージェントを選び、最初のメッセージを送ると DocBaseアクセスの承認ボタンが表示されます。

その後、ブラウザより承認許可を促されます。

承認すると、以降は普通にチャットするだけでDocBaseの探索が可能になります。

まとめ

今回でGEでのOAuth認証でのMCP接続が可能であることの理解が進みました。

他のツール接続でも活用できる知見となりました。