INTEGRITY Dokumentace

Zpracování OAuth se servery MCP

Při připojování k serverům MCP chráněným pomocí OAuth (například Slack nebo Notion) se musí vaši uživatelé ověřit, než bude moci váš Agent přistupovat k jejich datům. Tato příručka popisuje, jak implementovat OAuth toky, aby autorizace proběhla pro uživatele co nejjednodušeji.

Jak to funguje

  1. Zavolejte addMcpServer() s URL serveru
  2. Pokud je vyžadováno OAuth, authUrl se vrátí namísto okamžitého připojení
  3. Zobrazte authUrl vašemu uživateli (přesměrování, vyskakovací okno nebo odkaz)
  4. Uživatel se ověří na webu poskytovatele
  5. Poskytovatel přesměruje zpět na callback URL vašeho Agenta
  6. Váš agent automaticky dokončí připojení

Klient MCP používá vestavěný DurableObjectOAuthClientProvider k bezpečné správě stavu OAuth: ukládá nonce a ID serveru, ověřuje je při zpětném volání a po použití nebo vypršení platnosti je odstraní.

Zahájení OAuth

Při připojování k serveru chráněnému pomocí OAuth zkontrolujte, zda authUrl se vrátí. Pokud je přítomen, přesměrujte uživatele k dokončení autorizace:

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.ts
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 });
	}
}

Alternativní přístupy

Místo automatického přesměrování můžete zobrazit authUrl vašemu uživateli jako:

Konfigurace chování callbacku

Po dokončení OAuth vás poskytovatel přesměruje zpět na callback URL vašeho Agenta. Ve výchozím nastavení úspěšné ověření přesměruje na origin vaší aplikace, zatímco neúspěšné ověření zobrazí chybovou stránku HTML s textem chyby.

Přesměrování do vaší aplikace

Po dokončení OAuth přesměrujte uživatele zpět do své aplikace:

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

Uživatelé se vracejí na /dashboard při úspěchu nebo /auth-error?error=<message> při selhání.

Zavřít vyskakovací okno

Pokud jste OAuth otevřeli ve vyskakovacím okně, po dokončení ho automaticky zavřete:

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.ts
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" },
				});
			},
		});
	}
}

Vaše hlavní aplikace může detekovat zavření vyskakovacího okna a obnovit stav připojení. Pokud OAuth selže, stav připojení se změní na "failed" a chybová zpráva se uloží do server.error pro zobrazení ve vašem UI.

Sledování stavu připojení

Aplikace React

Použijte useAgent hook pro aktualizace v reálném čase přes 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.tsx
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 callback se spustí automaticky při změně stavu MCP, není potřeba žádné dotazování (polling).

Ostatní frameworky

Stav připojení zjišťujte dotazováním na endpoint:

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.ts
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 });
	}
}

Průběh stavů připojení: authenticating (vyžaduje OAuth) → connecting (dokončení nastavení) → ready (k dispozici pro použití)

Zpracování selhání

Když OAuth selže, stav připojení se změní na "failed" a chybová zpráva se uloží do server.error pole. Zobrazte tuto chybu ve svém uživatelském rozhraní a umožněte uživatelům opakovat pokus:

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.tsx
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>
	);
}

Běžné důvody selhání:

Neúspěšná připojení zůstávají ve stavu, dokud nejsou odebrána pomocí removeMcpServer(serverId). Chybová zpráva se automaticky escapuje, aby se zabránilo útokům XSS, takže ji lze bezpečně zobrazit přímo v uživatelském rozhraní.

Kompletní příklad

Tento příklad ukazuje kompletní integraci OAuth s Cloudflare Observability. Uživatelé se připojí, autorizují se ve vyskakovacím okně a připojení se poté zpřístupní. Chyby se automaticky ukládají do stavu připojení, aby se daly zobrazit ve vašem 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.ts
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>;

Připojení k serveru MCP

Začínáme bez OAuth.

API klienta MCP

Kompletní dokumentace API pro klienty MCP.