Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 22 additions & 74 deletions src/content/docs/agents/guides/oauth-mcp-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,49 +93,23 @@ export class MyAgent extends Agent<Env, never> {

Users return to `/dashboard` on success or `/auth-error?error=<message>` on failure.

### Custom handler for popup windows
### Close popup window

If you opened OAuth in a popup, use a custom handler to close it automatically. The handler receives an `MCPClientOAuthResult` object and must check `authSuccess` to handle both success and error cases:
If you opened OAuth in a popup, close it automatically when complete:

<TypeScriptExample>

```ts title="src/index.ts"
import { Agent } from "agents";
import type { MCPClientOAuthResult } from "agents/mcp";

export class MyAgent extends Agent<Env, never> {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: (result: MCPClientOAuthResult) => {
if (result.authSuccess) {
// Success - close the popup
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
}
// Error - show message briefly, then close
const errorJson = JSON.stringify(
result.authError ?? "Unknown error",
);
return new Response(
`<!DOCTYPE html>
<html>
<head><title>Authentication Failed</title></head>
<body style="font-family:system-ui,sans-serif;max-width:400px;margin:80px auto;padding:0 20px;text-align:center;">
<h2 style="color:#c00;">Authentication Failed</h2>
<p id="err" style="color:#666;"></p>
<p style="color:#999;font-size:0.85em;">This window will close automatically...</p>
<script>
document.getElementById("err").textContent = ${errorJson};
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>`,
{
headers: { "content-type": "text/html" },
status: 400,
},
);
customHandler: () => {
// Close the popup after OAuth completes
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
Expand All @@ -144,13 +118,7 @@ export class MyAgent extends Agent<Env, never> {

</TypeScriptExample>

:::note[Important]

Always check `result.authSuccess` in your custom handler. The handler is called for both successful and failed authentication attempts. Without this check, authentication errors will be ignored and users will not know why the connection failed.

:::

Your main application can detect the popup closing and refresh the connection status.
Your main application can detect the popup closing and refresh the connection status. If OAuth fails, the connection state becomes `"failed"` and the error message is stored in `server.error` for display in your UI.

## Monitor connection status

Expand All @@ -162,6 +130,7 @@ Use the `useAgent` hook for real-time updates via WebSocket:

```tsx title="src/App.tsx"
import { useAgent } from "agents/react";
import { useState } from "react";
import type { MCPServersState } from "agents";

function App() {
Expand Down Expand Up @@ -191,6 +160,9 @@ function App() {
Authorize
</button>
)}
{server.state === "failed" && server.error && (
<p className="error">{server.error}</p>
)}
</div>
))}
</div>
Expand Down Expand Up @@ -244,12 +216,13 @@ Connection states flow: `authenticating` (needs OAuth) → `connecting` (complet

## Handle failures

When OAuth fails, the connection state becomes `"failed"` and the `error` field contains the error message. Display the error to your users and allow them to retry:
When OAuth fails, the connection state becomes `"failed"` and the error message is stored in the `server.error` field. Display this error in your UI and allow users to retry:

<TypeScriptExample>

```tsx title="src/App.tsx"
import { useAgent } from "agents/react";
import { useState } from "react";
import type { MCPServersState } from "agents";

function App() {
Expand Down Expand Up @@ -293,7 +266,7 @@ function App() {
<strong>{server.name}</strong>: {server.state}
{server.state === "failed" && (
<div>
{server.error && <p style={{ color: "red" }}>Error: {server.error}</p>}
{server.error && <p className="error">{server.error}</p>}
<button
onClick={() => handleRetry(id, server.server_url, server.name)}
>
Expand All @@ -317,17 +290,16 @@ Common failure reasons:
- **Permission denied**: User lacks required permissions
- **Expired session**: OAuth session timed out

Failed connections remain in state until removed with `removeMcpServer(serverId)`.
Failed connections remain in state until removed with `removeMcpServer(serverId)`. The error message is automatically escaped to prevent XSS attacks, so it is safe to display directly in your UI.

## Complete example

This example demonstrates a complete OAuth integration with Cloudflare Observability. Users connect, authorize in a popup window, and the connection becomes available.
This example demonstrates a complete OAuth integration with Cloudflare Observability. Users connect, authorize in a popup window, and the connection becomes available. Errors are automatically stored in the connection state for display in your UI.

<TypeScriptExample>

```ts title="src/index.ts"
import { Agent, routeAgentRequest } from "agents";
import type { MCPClientOAuthResult } from "agents/mcp";

type Env = {
MyAgent: DurableObjectNamespace<MyAgent>;
Expand All @@ -336,35 +308,11 @@ type Env = {
export class MyAgent extends Agent<Env, never> {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: (result: MCPClientOAuthResult) => {
if (result.authSuccess) {
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
}
// Error - show message briefly, then close
const errorJson = JSON.stringify(
result.authError ?? "Unknown error",
);
return new Response(
`<!DOCTYPE html>
<html>
<head><title>Authentication Failed</title></head>
<body style="font-family:system-ui,sans-serif;max-width:400px;margin:80px auto;padding:0 20px;text-align:center;">
<h2 style="color:#c00;">Authentication Failed</h2>
<p id="err" style="color:#666;"></p>
<p style="color:#999;font-size:0.85em;">This window will close automatically...</p>
<script>
document.getElementById("err").textContent = ${errorJson};
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>`,
{
headers: { "content-type": "text/html" },
status: 400,
},
);
customHandler: () => {
// Close popup after OAuth completes (success or failure)
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
Expand Down
86 changes: 20 additions & 66 deletions src/content/docs/agents/model-context-protocol/mcp-client-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,6 @@ Configure how OAuth completion is handled. By default, successful authentication
<TypeScriptExample>

```ts
import type { MCPClientOAuthResult } from "agents/mcp";

export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
Expand All @@ -269,26 +267,12 @@ export class MyAgent extends Agent {
// Redirect on error with error message in query string
errorRedirect: "https://myapp.com/error",

// Or use a custom handler for complete control
customHandler: (result: MCPClientOAuthResult) => {
// result.authSuccess - boolean indicating if auth succeeded
// result.authError - string with error message (if failed)
// result.serverId - ID of the MCP server

if (result.authSuccess) {
// Close popup window after successful auth
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
}
// Handle error - must check authSuccess to avoid ignoring failures
return new Response(
`Auth failed: ${result.authError ?? "Unknown error"}`,
{
status: 400,
headers: { "content-type": "text/plain" },
},
);
// Or use a custom handler
customHandler: () => {
// Close popup window after auth completes
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
Expand All @@ -297,12 +281,6 @@ export class MyAgent extends Agent {

</TypeScriptExample>

:::note[Important]

When using `customHandler`, always check `result.authSuccess` to handle both success and failure cases. The handler is called for all OAuth callback requests, including failures.

:::

## Using MCP capabilities

Once connected, access the server's capabilities:
Expand Down Expand Up @@ -453,7 +431,7 @@ function Dashboard() {
<h2>Connected Servers</h2>
{Object.entries(servers).map(([id, server]) => (
<div key={id}>
{server.name}: {server.connectionState}
{server.name}: {server.state}
</div>
))}

Expand Down Expand Up @@ -559,7 +537,7 @@ type MCPServersState = {
| "failed";
capabilities: ServerCapabilities | null;
instructions: string | null;
error?: string; // Present when state is "failed"
error: string | null;
}
>;
tools: Array<Tool & { serverId: string }>;
Expand All @@ -576,7 +554,9 @@ The `state` field indicates the connection lifecycle:
- `connected` — Transport connection established
- `discovering` — Discovering server capabilities (tools, resources, prompts)
- `ready` — Fully connected and operational
- `failed` — Connection failed (check the `error` field for details)
- `failed` — Connection failed (see `error` field for details)

The `error` field contains an error message when `state` is `"failed"`. Error messages from external OAuth providers are automatically escaped to prevent XSS attacks, making them safe to display directly in your UI.

### `configureOAuthCallback()`

Expand All @@ -586,7 +566,7 @@ Configure OAuth callback behavior for MCP servers requiring authentication. This
this.mcp.configureOAuthCallback(options: {
successRedirect?: string;
errorRedirect?: string;
customHandler?: (result: MCPClientOAuthResult) => Response | Promise<Response>;
customHandler?: () => Response | Promise<Response>;
}): void
```

Expand All @@ -595,35 +575,24 @@ this.mcp.configureOAuthCallback(options: {
- `options` (object, required) — OAuth callback configuration:
- `successRedirect` (string, optional) — URL to redirect to after successful authentication
- `errorRedirect` (string, optional) — URL to redirect to after failed authentication. Error message is appended as `?error=<message>` query parameter
- `customHandler` (function, optional) — Custom handler for complete control over the callback response. Receives an `MCPClientOAuthResult` object and must return a Response

#### MCPClientOAuthResult type

The `customHandler` receives an object with the following properties:

```ts
type MCPClientOAuthResult = {
serverId: string; // ID of the MCP server
authSuccess: boolean; // true if authentication succeeded
authError?: string; // Error message if authentication failed
};
```
- `customHandler` (function, optional) — Custom handler for complete control over the callback response. Must return a Response

#### Default behavior

When no configuration is provided:

- **Success**: Redirects to your application origin
- **Failure**: Displays an HTML error page with the error message

If OAuth fails, the connection state becomes `"failed"` and the error message is stored in the `server.error` field for display in your UI.

#### Usage

Configure in `onStart()` before any OAuth flows begin:

<TypeScriptExample>

```ts
import type { MCPClientOAuthResult } from "agents/mcp";

export class MyAgent extends Agent {
onStart() {
// Option 1: Simple redirects
Expand All @@ -634,19 +603,10 @@ export class MyAgent extends Agent {

// Option 2: Custom handler (e.g., for popup windows)
this.mcp.configureOAuthCallback({
customHandler: (result: MCPClientOAuthResult) => {
if (result.authSuccess) {
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
}
return new Response(
`Auth failed: ${result.authError ?? "Unknown error"}`,
{
status: 400,
headers: { "content-type": "text/plain" },
},
);
customHandler: () => {
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
Expand All @@ -655,12 +615,6 @@ export class MyAgent extends Agent {

</TypeScriptExample>

:::caution

Always check `result.authSuccess` in your `customHandler`. The handler is called for both successful and failed authentication. Ignoring this check will cause authentication errors to be silently swallowed.

:::

## Advanced: MCPClientManager

For fine-grained control, use `this.mcp` directly:
Expand Down