Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

MCP サーバーで OAuth を処理する

最終更新 Markdown で表示Agent セットアップ

OAuth で保護された MCP サーバー(Slack や Notion など)に接続する場合、エージェントがデータへアクセスする前にユーザー認証が必要です。このガイドでは、途切れのない認可のための OAuth フローの実装を説明します。

仕組み

  1. サーバー URL を指定して addMcpServer() を呼びます
  2. OAuth が必要な場合、すぐには接続せず authUrl が返ります
  3. ユーザーに authUrl を提示します(リダイレクト、ポップアップ、またはリンク)
  4. ユーザーはプロバイダーのサイトで認証します
  5. プロバイダーはエージェントのコールバック URL へ戻します
  6. エージェントが接続を自動で完了します

MCP クライアントは組み込みの DurableObjectOAuthClientProvider で OAuth 状態を安全に管理します。nonce とサーバー ID を保存し、コールバック時に検証し、使用後または期限切れ後にクリーンアップします。

OAuth を開始する

OAuth 保護サーバーへ接続するときは、authUrl が返ったかを確認します。あれば、ユーザーをリダイレクトして認可を完了させます。

export class MyAgent extends Agent {
	async onRequest(request) {
		const url = new URL(request.url);

		if (url.pathname.endsWith("/connect") && request.method === "POST") {
			const { id, authUrl } = await this.addMcpServer(
				"Cloudflare Observability",
				"https://observability.mcp.cloudflare.com/mcp",
			);

			if (authUrl) {
				// OAuth required - redirect user to authorize
				return Response.redirect(authUrl, 302);
			}

			// Already authenticated - connection complete
			return Response.json({ serverId: id, status: "connected" });
		}

		return new Response("Not found", { status: 404 });
	}
}
src/index.tsts
export class MyAgent extends Agent<Env> {
	async onRequest(request: Request): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname.endsWith("/connect") && request.method === "POST") {
			const { id, authUrl } = await this.addMcpServer(
				"Cloudflare Observability",
				"https://observability.mcp.cloudflare.com/mcp",
			);

			if (authUrl) {
				// OAuth required - redirect user to authorize
				return Response.redirect(authUrl, 302);
			}

			// Already authenticated - connection complete
			return Response.json({ serverId: id, status: "connected" });
		}

		return new Response("Not found", { status: 404 });
	}
}

別の提示方法

自動リダイレクトの代わりに、authUrl を次の形で提示できます。

  • ポップアップウィンドウ: ダッシュボード型アプリでは window.open(authUrl, '_blank', 'width=600,height=700')
  • クリック可能なリンク: 複数ステップのフローではボタンまたはリンクとして表示します
  • ディープリンク: モバイルアプリではカスタム URL スキームを使います

コールバックの動作を設定する

OAuth が完了すると、プロバイダーはエージェントのコールバック URL へ戻します。デフォルトでは、認証成功時はアプリケーションのオリジンへリダイレクトし、失敗時はエラーメッセージ付きの HTML エラーページを表示します。

アプリケーションへリダイレクトする

OAuth 完了後、ユーザーをアプリケーションへ戻します。

export class MyAgent extends Agent {
	onStart() {
		this.mcp.configureOAuthCallback({
			successRedirect: "/dashboard",
			errorRedirect: "/auth-error",
		});
	}
}
src/index.tsts
export class MyAgent extends Agent<Env> {
	onStart() {
		this.mcp.configureOAuthCallback({
			successRedirect: "/dashboard",
			errorRedirect: "/auth-error",
		});
	}
}

成功時は /dashboard、失敗時は /auth-error?error=<message> へ戻ります。

ポップアップウィンドウを閉じる

OAuth をポップアップで開いた場合は、完了時に自動で閉じます。

import { Agent } from "agents";

export class MyAgent extends Agent {
	onStart() {
		this.mcp.configureOAuthCallback({
			customHandler: () => {
				// Close the popup after OAuth completes
				return new Response("<script>window.close();</script>", {
					headers: { "content-type": "text/html" },
				});
			},
		});
	}
}
src/index.tsts
import { Agent } from "agents";

export class MyAgent extends Agent<Env> {
	onStart() {
		this.mcp.configureOAuthCallback({
			customHandler: () => {
				// Close the popup after OAuth completes
				return new Response("<script>window.close();</script>", {
					headers: { "content-type": "text/html" },
				});
			},
		});
	}
}

メインアプリケーションは、ポップアップが閉じたことを検知して接続状態を更新できます。OAuth が失敗すると接続状態は "failed" になり、エラーメッセージは UI 表示用に server.error へ保存されます。

接続状態を監視する

React アプリケーション

useAgent フックで、WebSocket 経由のリアルタイム更新を受け取ります。

import { useAgent } from "agents/react";
import { useState } from "react";

function App() {
	const [mcpState, setMcpState] = useState({
		prompts: [],
		resources: [],
		servers: {},
		tools: [],
	});

	const agent = useAgent({
		agent: "my-agent",
		name: "session-id",
		onMcpUpdate: (mcpServers) => {
			// Automatically called when MCP state changes!
			setMcpState(mcpServers);
		},
	});

	return (
		<div>
			{Object.entries(mcpState.servers).map(([id, server]) => (
				<div key={id}>
					<strong>{server.name}</strong>: {server.state}
					{server.state === "authenticating" && server.auth_url && (
						<button onClick={() => window.open(server.auth_url, "_blank")}>
							Authorize
						</button>
					)}
					{server.state === "failed" && server.error && (
						<p className="error">{server.error}</p>
					)}
				</div>
			))}
		</div>
	);
}
src/App.tsxtsx
import { useAgent } from "agents/react";
import { useState } from "react";
import type { MCPServersState } from "agents";

function App() {
	const [mcpState, setMcpState] = useState<MCPServersState>({
		prompts: [],
		resources: [],
		servers: {},
		tools: [],
	});

	const agent = useAgent({
		agent: "my-agent",
		name: "session-id",
		onMcpUpdate: (mcpServers: MCPServersState) => {
			// Automatically called when MCP state changes!
			setMcpState(mcpServers);
		},
	});

	return (
		<div>
			{Object.entries(mcpState.servers).map(([id, server]) => (
				<div key={id}>
					<strong>{server.name}</strong>: {server.state}
					{server.state === "authenticating" && server.auth_url && (
						<button onClick={() => window.open(server.auth_url, "_blank")}>
							Authorize
						</button>
					)}
					{server.state === "failed" && server.error && (
						<p className="error">{server.error}</p>
					)}
				</div>
			))}
		</div>
	);
}

onMcpUpdate コールバックは MCP 状態の変化時に自動で発火します。ポーリングは不要です。

その他のフレームワーク

エンドポイントで接続状態をポーリングします。

export class MyAgent extends Agent {
	async onRequest(request) {
		const url = new URL(request.url);

		if (
			url.pathname.endsWith("connection-status") &&
			request.method === "GET"
		) {
			const mcpState = this.getMcpServers();

			const connections = Object.entries(mcpState.servers).map(
				([id, server]) => ({
					serverId: id,
					name: server.name,
					state: server.state,
					isReady: server.state === "ready",
					needsAuth: server.state === "authenticating",
					authUrl: server.auth_url,
				}),
			);

			return Response.json(connections);
		}

		return new Response("Not found", { status: 404 });
	}
}
src/index.tsts
export class MyAgent extends Agent<Env> {
	async onRequest(request: Request): Promise<Response> {
		const url = new URL(request.url);

		if (
			url.pathname.endsWith("connection-status") &&
			request.method === "GET"
		) {
			const mcpState = this.getMcpServers();

			const connections = Object.entries(mcpState.servers).map(
				([id, server]) => ({
					serverId: id,
					name: server.name,
					state: server.state,
					isReady: server.state === "ready",
					needsAuth: server.state === "authenticating",
					authUrl: server.auth_url,
				}),
			);

			return Response.json(connections);
		}

		return new Response("Not found", { status: 404 });
	}
}

接続状態の流れは次のとおりです。authenticating(OAuth が必要)→ connecting(セットアップ中)→ ready(利用可能)

失敗を処理する

OAuth が失敗すると接続状態は "failed" になり、エラーメッセージは server.error フィールドに保存されます。このエラーを UI に表示し、再試行できるようにします。

import { useAgent } from "agents/react";
import { useState } from "react";

function App() {
	const [mcpState, setMcpState] = useState({
		prompts: [],
		resources: [],
		servers: {},
		tools: [],
	});

	const agent = useAgent({
		agent: "my-agent",
		name: "session-id",
		onMcpUpdate: setMcpState,
	});

	const handleRetry = async (serverId, serverUrl, name) => {
		// Remove failed connection
		await fetch(`/agents/my-agent/session-id/disconnect`, {
			method: "POST",
			body: JSON.stringify({ serverId }),
		});

		// Retry connection
		const response = await fetch(`/agents/my-agent/session-id/connect`, {
			method: "POST",
			body: JSON.stringify({ serverUrl, name }),
		});
		const { authUrl } = await response.json();
		if (authUrl) window.open(authUrl, "_blank");
	};

	return (
		<div>
			{Object.entries(mcpState.servers).map(([id, server]) => (
				<div key={id}>
					<strong>{server.name}</strong>: {server.state}
					{server.state === "failed" && (
						<div>
							{server.error && <p className="error">{server.error}</p>}
							<button
								onClick={() => handleRetry(id, server.server_url, server.name)}
							>
								Retry Connection
							</button>
						</div>
					)}
				</div>
			))}
		</div>
	);
}
src/App.tsxtsx
import { useAgent } from "agents/react";
import { useState } from "react";
import type { MCPServersState } from "agents";

function App() {
	const [mcpState, setMcpState] = useState<MCPServersState>({
		prompts: [],
		resources: [],
		servers: {},
		tools: [],
	});

	const agent = useAgent({
		agent: "my-agent",
		name: "session-id",
		onMcpUpdate: setMcpState,
	});

	const handleRetry = async (
		serverId: string,
		serverUrl: string,
		name: string,
	) => {
		// Remove failed connection
		await fetch(`/agents/my-agent/session-id/disconnect`, {
			method: "POST",
			body: JSON.stringify({ serverId }),
		});

		// Retry connection
		const response = await fetch(`/agents/my-agent/session-id/connect`, {
			method: "POST",
			body: JSON.stringify({ serverUrl, name }),
		});
		const { authUrl } = await response.json();
		if (authUrl) window.open(authUrl, "_blank");
	};

	return (
		<div>
			{Object.entries(mcpState.servers).map(([id, server]) => (
				<div key={id}>
					<strong>{server.name}</strong>: {server.state}
					{server.state === "failed" && (
						<div>
							{server.error && <p className="error">{server.error}</p>}
							<button
								onClick={() => handleRetry(id, server.server_url, server.name)}
							>
								Retry Connection
							</button>
						</div>
					)}
				</div>
			))}
		</div>
	);
}

よくある失敗理由:

  • ユーザーがキャンセルした: 認可完了前に OAuth ウィンドウを閉じた
  • 資格情報が無効: プロバイダーの資格情報が誤っていた
  • 権限が拒否された: ユーザーに必要な権限がない
  • セッション期限切れ: OAuth セッションがタイムアウトした

失敗した接続は、removeMcpServer(serverId) で削除するまで状態に残ります。エラーメッセージは XSS 攻撃を防ぐために自動でエスケープされるため、UI にそのまま表示して問題ありません。

完全な例

この例は、Cloudflare Observability との一連の OAuth 連携です。ユーザーが接続し、ポップアップで認可すると、接続が利用可能になります。エラーは接続状態に自動保存され、UI に表示できます。

import { Agent, routeAgentRequest } from "agents";

export class MyAgent extends Agent {
	onStart() {
		this.mcp.configureOAuthCallback({
			customHandler: () => {
				// Close popup after OAuth completes (success or failure)
				return new Response("<script>window.close();</script>", {
					headers: { "content-type": "text/html" },
				});
			},
		});
	}

	async onRequest(request) {
		const url = new URL(request.url);

		// Connect to MCP server
		if (url.pathname.endsWith("/connect") && request.method === "POST") {
			const { id, authUrl } = await this.addMcpServer(
				"Cloudflare Observability",
				"https://observability.mcp.cloudflare.com/mcp",
			);

			if (authUrl) {
				return Response.json({
					serverId: id,
					authUrl: authUrl,
					message: "Please authorize access",
				});
			}

			return Response.json({ serverId: id, status: "connected" });
		}

		// Check connection status
		if (url.pathname.endsWith("/status") && request.method === "GET") {
			const mcpState = this.getMcpServers();
			const connections = Object.entries(mcpState.servers).map(
				([id, server]) => ({
					serverId: id,
					name: server.name,
					state: server.state,
					authUrl: server.auth_url,
				}),
			);
			return Response.json(connections);
		}

		// Disconnect
		if (url.pathname.endsWith("/disconnect") && request.method === "POST") {
			const { serverId } = await request.json();
			await this.removeMcpServer(serverId);
			return Response.json({ message: "Disconnected" });
		}

		return new Response("Not found", { status: 404 });
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env, { cors: true })) ||
			new Response("Not found", { status: 404 })
		);
	},
};
src/index.tsts
import { Agent, routeAgentRequest } from "agents";

type Env = {
	MyAgent: DurableObjectNamespace<MyAgent>;
};

export class MyAgent extends Agent<Env> {
	onStart() {
		this.mcp.configureOAuthCallback({
			customHandler: () => {
				// Close popup after OAuth completes (success or failure)
				return new Response("<script>window.close();</script>", {
					headers: { "content-type": "text/html" },
				});
			},
		});
	}

	async onRequest(request: Request): Promise<Response> {
		const url = new URL(request.url);

		// Connect to MCP server
		if (url.pathname.endsWith("/connect") && request.method === "POST") {
			const { id, authUrl } = await this.addMcpServer(
				"Cloudflare Observability",
				"https://observability.mcp.cloudflare.com/mcp",
			);

			if (authUrl) {
				return Response.json({
					serverId: id,
					authUrl: authUrl,
					message: "Please authorize access",
				});
			}

			return Response.json({ serverId: id, status: "connected" });
		}

		// Check connection status
		if (url.pathname.endsWith("/status") && request.method === "GET") {
			const mcpState = this.getMcpServers();
			const connections = Object.entries(mcpState.servers).map(
				([id, server]) => ({
					serverId: id,
					name: server.name,
					state: server.state,
					authUrl: server.auth_url,
				}),
			);
			return Response.json(connections);
		}

		// Disconnect
		if (url.pathname.endsWith("/disconnect") && request.method === "POST") {
			const { serverId } = (await request.json()) as { serverId: string };
			await this.removeMcpServer(serverId);
			return Response.json({ message: "Disconnected" });
		}

		return new Response("Not found", { status: 404 });
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env, { cors: true })) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

関連情報

MCP Client API

MCP クライアントの API ドキュメントです。

役に立ちましたか?