> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Client Example

> Build an agent that connects to remote MCP servers, handles OAuth authentication, and aggregates tools, prompts, and resources

An Agent that acts as an MCP **client** - dynamically connects to remote MCP servers, handles OAuth authentication, and aggregates tools, prompts, and resources from all connected servers.

## What it demonstrates

* **`addMcpServer` / `removeMcpServer`** - managing MCP server connections from an Agent
* **`onMcpUpdate`** - real-time state updates pushed to the React frontend via WebSocket
* **OAuth popup flow** - `configureOAuthCallback` with a custom handler that closes the popup after auth
* **`agentFetch`** - making HTTP requests to the Agent's custom endpoints from the client

## Server Implementation

```typescript src/server.ts theme={null}
import { Agent, callable, routeAgentRequest } from "agents";

export class MyAgent extends Agent {
  onStart() {
    this.mcp.configureOAuthCallback({
      customHandler: (result) => {
        if (result.authSuccess) {
          return new Response("<script>window.close();</script>", {
            headers: { "content-type": "text/html" },
            status: 200
          });
        }
        const error = result.authError || "Unknown error";
        return new Response(`Authentication Failed: ${error}`, {
          headers: { "content-type": "text/plain" },
          status: 400
        });
      }
    });
  }

  @callable()
  async addServer(name: string, url: string) {
    await this.addMcpServer(name, url, {
      callbackHost: this.env.HOST
    });
  }

  @callable()
  async disconnectServer(serverId: string) {
    await this.removeMcpServer(serverId);
  }
}

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

## Client Implementation

The React frontend uses `useAgent` with `onMcpUpdate` to receive real-time server state:

```typescript src/client.tsx theme={null}
import { useAgent } from "agents/react";
import { useState } from "react";
import type { MyAgent } from "./server";

function McpClientApp() {
  const [mcpServers, setMcpServers] = useState<McpServerInfo[]>([]);
  const [connected, setConnected] = useState(false);

  const agent = useAgent<MyAgent>({
    agent: "MyAgent",
    name: sessionId,
    onMcpUpdate: (servers) => {
      // Real-time updates when servers are added/removed/changed
      setMcpServers(servers);
    },
    onOpen: () => setConnected(true),
    onClose: () => setConnected(false)
  });

  const addServer = async (name: string, url: string) => {
    await agent.call("addServer", [name, url]);
  };

  const removeServer = async (serverId: string) => {
    await agent.call("disconnectServer", [serverId]);
  };

  return (
    <div>
      <h1>MCP Servers</h1>
      {mcpServers.map((server) => (
        <div key={server.id}>
          <h2>{server.name}</h2>
          <p>Status: {server.status}</p>
          <h3>Tools</h3>
          <ul>
            {server.tools.map((tool) => (
              <li key={tool.name}>{tool.name}: {tool.description}</li>
            ))}
          </ul>
          <button onClick={() => removeServer(server.id)}>
            Disconnect
          </button>
        </div>
      ))}
      <button onClick={() => addServer("Demo", "http://localhost:5173/mcp")}>
        Add Server
      </button>
    </div>
  );
}
```

## How It Works

<Steps>
  <Step title="Agent manages connections">
    The Agent manages MCP server connections via the built-in `mcp` property. Each connection maintains state about available tools, prompts, and resources.
  </Step>

  <Step title="OAuth configuration">
    `configureOAuthCallback` sets up the OAuth flow. When an MCP server requires authentication, a popup window opens. After auth completes, the custom handler closes the popup automatically.
  </Step>

  <Step title="Real-time updates">
    When servers are added, removed, or change state, the Agent broadcasts updates via WebSocket. The client's `onMcpUpdate` callback receives the new state.
  </Step>

  <Step title="Call server methods">
    The client uses `agent.call()` to invoke callable methods on the Agent, like adding or removing servers.
  </Step>
</Steps>

## OAuth Flow

For MCP servers that require authentication:

```typescript theme={null}
// Server: configure OAuth callback
this.mcp.configureOAuthCallback({
  customHandler: (result) => {
    if (result.authSuccess) {
      // Close the popup window
      return new Response("<script>window.close();</script>", {
        headers: { "content-type": "text/html" }
      });
    }
    // Show error in popup
    return new Response(`Auth failed: ${result.authError}`, {
      status: 400
    });
  }
});

// Client: add server that requires auth
await agent.call("addServer", [
  "GitHub",
  "https://mcp-server.example.com/mcp"
]);
// If auth is required, a popup automatically opens
// After user authorizes, popup closes and connection completes
```

## MCP Server State

The `onMcpUpdate` callback receives an array of server info:

```typescript theme={null}
type McpServerInfo = {
  id: string;
  name: string;
  url: string;
  status: "connecting" | "connected" | "disconnected" | "error";
  error?: string;
  tools: {
    name: string;
    description: string;
    inputSchema: Record<string, unknown>;
  }[];
  prompts: {
    name: string;
    description: string;
  }[];
  resources: {
    uri: string;
    name: string;
    description: string;
  }[];
};
```

## Using MCP Tools in AI Chat

Combine MCP client with AI chat:

```typescript theme={null}
import { AIChatAgent } from "@cloudflare/ai-chat";
import { streamText } from "ai";

export class ChatAgent extends AIChatAgent {
  waitForMcpConnections = true;

  async onChatMessage() {
    // Get tools from all connected MCP servers
    const mcpTools = this.mcp.getAITools();

    const result = streamText({
      model,
      tools: {
        // MCP tools are now available to the LLM
        ...mcpTools,
        // Plus any local tools
        myLocalTool: tool({ ... })
      },
      messages: this.messages
    });

    return result.toUIMessageStreamResponse();
  }
}
```

## Running the Example

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    npm install
    ```
  </Step>

  <Step title="Configure environment">
    Copy `.env.example` to `.env`:

    ```bash theme={null}
    cp .env.example .env
    ```

    Set `HOST` to your callback host (usually `http://localhost:5173` for local dev).
  </Step>

  <Step title="Start the server">
    ```bash theme={null}
    npm run dev
    ```
  </Step>

  <Step title="Connect to a server">
    Open [http://localhost:5173](http://localhost:5173), enter an MCP server URL, and click Connect.

    To test with authentication, run the [`mcp-worker-authenticated`](/examples/mcp-worker-authenticated) example alongside this one.
  </Step>
</Steps>

## Testing with Another Example

Run the [MCP Server example](/examples/mcp-server) in another terminal:

```bash theme={null}
cd examples/mcp
npm install && npm run dev
```

Then in this example, add the server:

* Name: `Demo`
* URL: `http://localhost:5174/mcp` (note the different port)

You'll see the server's tools and resources appear in the UI.

## Related Examples

<CardGroup cols={2}>
  <Card title="MCP Server" icon="server" href="/examples/mcp-server">
    Build a stateful MCP server
  </Card>

  <Card title="MCP Authenticated" icon="lock" href="/examples/mcp-worker-authenticated">
    MCP server with OAuth authentication
  </Card>

  <Card title="AI Chat" icon="comments" href="/examples/ai-chat">
    Use MCP tools in AI chat (includes MCP client)
  </Card>

  <Card title="MCP Guide" icon="book" href="/guides/mcp">
    In-depth guide to Model Context Protocol
  </Card>
</CardGroup>
