McpClient

Last updated Oct 21, 2026 Connect your agent to external Model Context Protocol (MCP) servers to use their tools, resources, and prompts. Agents SDK v0.20.0 uses @modelcontextprotocol/client and negotiates stateless or legacy behavior automatically.

Refer to Migrate to MCP SDK v2 for package, type, OAuth provider, and rollout changes.

Overview

The MCP client capability lets your agent:

  • Connect to external MCP servers - GitHub, Slack, databases, AI services
  • Use their tools - Call functions exposed by MCP servers
  • Access resources - Read data from MCP servers
  • Use prompts - Leverage pre-built prompt templates

Note

This page covers connecting to MCP servers as a client. To create your own MCP server, refer to Creating MCP servers.

Quick start

import { Agent } from "agents";

export class MyAgent extends Agent {
    async onRequest(request) {
        // Add an MCP server
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
        );

if (result.state === "authenticating") {
            // Server requires OAuth - redirect user to authorize
            return Response.redirect(result.authUrl);
        }

// Server is ready - tools are now available
        const state = this.getMcpServers();
        console.log(`Connected! ${state.tools.length} tools available`);

return new Response("MCP server connected");
    }
}
import { Agent } from "agents";

export class MyAgent extends Agent {
    async onRequest(request: Request) {
        // Add an MCP server
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
        );

if (result.state === "authenticating") {
            // Server requires OAuth - redirect user to authorize
            return Response.redirect(result.authUrl);
        }

// Server is ready - tools are now available
        const state = this.getMcpServers();
        console.log(`Connected! ${state.tools.length} tools available`);

return new Response("MCP server connected");
    }
}

Connections persist in the agent's SQL storage, and when an agent connects to an MCP server, all tools from that server become available automatically.

Adding MCP servers

Use addMcpServer() to connect to an MCP server. For non-OAuth servers, no options are needed:

// Non-OAuth server — no options required
await this.addMcpServer("notion", "https://mcp.notion.so/mcp");

// OAuth server — callbackHost is auto-derived from the incoming request,
// but you can set it explicitly if needed (e.g. custom domains)
await this.addMcpServer("github", "https://mcp.github.com/mcp", {
    callbackHost: "https://my-worker.workers.dev",
});
// Non-OAuth server — no options required
await this.addMcpServer("notion", "https://mcp.notion.so/mcp");

// OAuth server — callbackHost is auto-derived from the incoming request,
// but you can set it explicitly if needed (e.g. custom domains)
await this.addMcpServer("github", "https://mcp.github.com/mcp", {
    callbackHost: "https://my-worker.workers.dev",
});

Stable server IDs

By default, each connection is assigned a generated nanoid(8) ID. Pass id for connector-style integrations so tools surface as readable keys instead of opaque connection IDs.

await this.addMcpServer("GitHub", env.MCP_SESSION, {
    id: "github",
    props: { token: "..." },
});
// tools surface as `tool_github_<name>`
await this.addMcpServer("GitHub", env.MCP_SESSION, {
    id: "github",
    props: { token: "..." },
});
// tools surface as `tool_github_<name>`

When provided, this id replaces the generated value as the server's ID in storage, restore, listServers(), listTools(), getAITools(), and OAuth state. The supplied ID is normalized via the exported normalizeServerId helper, so values like "GitHub MCP!" become "github-mcp" — guaranteeing the ID is safe to embed in AI SDK tool names and storage keys.

Stable IDs are fully additive — no existing code breaks. If you add { id: "github" } to an addMcpServer call for a server already registered under an auto-generated ID, the SDK transparently migrates the existing storage row, in-memory connection, and OAuth-related storage keys to the new stable ID. No removeMcpServer step is required. addMcpServer only throws on a genuinely ambiguous collision: the same stable ID already belongs to a different (name, url) server.

Transport options

MCP supports multiple transport types:

await this.addMcpServer("server", "https://mcp.example.com/mcp", {
    transport: {
        type: "streamable-http",
    },
});
await this.addMcpServer("server", "https://mcp.example.com/mcp", {
    transport: {
        type: "streamable-http",
    },
});
Transport Description
auto Auto-detect based on server response (default)
streamable-http HTTP with streaming
sse Server-Sent Events - legacy/compatibility transport

Custom headers

For servers behind authentication (like Cloudflare Access) or using bearer tokens:

await this.addMcpServer("internal", "https://internal-mcp.example.com/mcp", {
    transport: {
        headers: {
            Authorization: "Bearer my-token",
            "CF-Access-Client-Id": "...",
            "CF-Access-Client-Secret": "...",
        },
    },
});
await this.addMcpServer("internal", "https://internal-mcp.example.com/mcp", {
    transport: {
        headers: {
            Authorization: "Bearer my-token",
            "CF-Access-Client-Id": "...",
            "CF-Access-Client-Secret": "...",
        },
    },
});

URL security

MCP server URLs are validated before connection to prevent Server-Side Request Forgery (SSRF). The following URL targets are blocked:

  • Private/internal IP ranges (RFC 1918: 10.x, 172.16-31.x, 192.168.x)
  • Unspecified addresses ( 0.0.0.0, [::])
  • Link-local addresses ( 169.254.x, fe80::)
  • IPv6 unique-local addresses ( fc00::/7)
  • IPv4-mapped IPv6 addresses that resolve to private ranges (for example, [::ffff:10.0.0.1])
  • Cloud metadata endpoints ( metadata.google.internal)

Loopback addresses (localhost, 127.x.x.x, [::1]) are allowed for local development.

For production connections to internal services, use the RPC transport with a Durable Object binding instead of HTTP.

Return value

addMcpServer() returns the connection state:

  • ready - Server connected and tools discovered
  • authenticating - Server requires OAuth; redirect user to authUrl

OAuth authentication

Many MCP servers require OAuth authentication. The agent handles the OAuth flow automatically.

How it works

sequenceDiagram
    participant Client
    participant Agent
    participant MCPServer

Client->>Agent: addMcpServer(name, url)
    Agent->>MCPServer: Connect
    MCPServer-->>Agent: Requires OAuth
    Agent-->>Client: state: authenticating, authUrl
    Client->>MCPServer: User authorizes
    MCPServer->>Agent: Callback with code
    Agent->>MCPServer: Exchange for token
    Agent-->>Client: onMcpUpdate (ready)

Handling OAuth in your agent

class MyAgent extends Agent {
    async onRequest(request) {
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
        );

if (result.state === "authenticating") {
            // Redirect the user to the OAuth authorization page
            return Response.redirect(result.authUrl);
        }

return Response.json({ status: "connected", id: result.id });
    }
}
class MyAgent extends Agent {
    async onRequest(request: Request) {
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
        );

if (result.state === "authenticating") {
            // Redirect the user to the OAuth authorization page
            return Response.redirect(result.authUrl);
        }

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

OAuth callback

The callback URL is automatically constructed:

https://{host}/{agentsPrefix}/{agent-name}/{instance-name}/callback

For example: https://my-worker.workers.dev/agents/my-agent/default/callback

OAuth tokens are securely stored in SQLite, and persist across agent restarts.

Protecting instance names in OAuth callbacks

When using sendIdentityOnConnect: false to hide sensitive instance names (like session IDs or user IDs), the default OAuth callback URL would expose the instance name. To prevent this security issue, you must provide a custom callbackPath.

import { Agent, routeAgentRequest, getAgentByName } from "agents";

export class SecureAgent extends Agent {
    static options = { sendIdentityOnConnect: false };

async onRequest(request) {
        // callbackPath is required when sendIdentityOnConnect is false
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
            {
                callbackPath: "mcp-oauth-callback", // Custom path without instance name
            },
        );

if (result.state === "authenticating") {
            return Response.redirect(result.authUrl);
        }

return new Response("Connected!");
    }
}

// Route the custom callback path to the agent
export default {
    async fetch(request, env) {
        const url = new URL(request.url);

// Route custom MCP OAuth callback to agent instance
        if (url.pathname.startsWith("/mcp-oauth-callback")) {
            // Implement this to extract the instance name from your session/auth mechanism
            const instanceName = await getInstanceNameFromSession(request);

const agent = await getAgentByName(env.SecureAgent, instanceName);
            return agent.fetch(request);
        }

// Standard agent routing
        return (
            (await routeAgentRequest(request, env)) ??
            new Response("Not found", { status: 404 })
        );
    },
};
import { Agent, routeAgentRequest, getAgentByName } from "agents";

export class SecureAgent extends Agent {
    static options = { sendIdentityOnConnect: false };

async onRequest(request: Request) {
        // callbackPath is required when sendIdentityOnConnect is false
        const result = await this.addMcpServer(
            "github",
            "https://mcp.github.com/mcp",
            {
                callbackPath: "mcp-oauth-callback", // Custom path without instance name
            },
        );

if (result.state === "authenticating") {
            return Response.redirect(result.authUrl);
        }

return new Response("Connected!");
    }
}

// Route the custom callback path to the agent
export default {
    async fetch(request: Request, env: Env) {
        const url = new URL(request.url);

// Route custom MCP OAuth callback to agent instance
        if (url.pathname.startsWith("/mcp-oauth-callback")) {
            // Implement this to extract the instance name from your session/auth mechanism
            const instanceName = await getInstanceNameFromSession(request);

const agent = await getAgentByName(env.SecureAgent, instanceName);
            return agent.fetch(request);
        }

// Standard agent routing
        return (
            (await routeAgentRequest(request, env)) ??
            new Response("Not found", { status: 404 })
        );
    },
} satisfies ExportedHandler<Env>;

How callback matching works

OAuth callbacks are matched by the state query parameter (format: {serverId}:{stateValue}), not by URL path. This means your custom callbackPath can be any path you choose, as long as requests to that path are routed to the correct agent instance.

Custom OAuth callback handling

Configure how OAuth completion is handled. By default, successful authentication redirects to your application origin, while failed authentication displays an HTML error page.

export class MyAgent extends Agent {
    onStart() {
        this.mcp.configureOAuthCallback({
            // Redirect after successful auth
            successRedirect: "https://myapp.com/success",

// Redirect on error with error message in query string
            errorRedirect: "https://myapp.com/error",

// Or use a custom handler
            customHandler: () => {
                // Close popup window after auth completes
                return new Response("<script>window.close();</script>", {
                    headers: { "content-type": "text/html" },
                });
            },
        });
    }
}
export class MyAgent extends Agent {
    onStart() {
        this.mcp.configureOAuthCallback({
            // Redirect after successful auth
            successRedirect: "https://myapp.com/success",

// Redirect on error with error message in query string
            errorRedirect: "https://myapp.com/error",

// Or use a custom handler
            customHandler: () => {
                // Close popup window after auth completes
                return new Response("<script>window.close();</script>", {
                    headers: { "content-type": "text/html" },
                });
            },
        });
    }
}

Using MCP capabilities

Once connected, access the server's capabilities:

Get available tools

Use listTools() to inspect the raw MCP catalog without preparing tools for an AI SDK model call:

const tools = this.mcp.listTools();

for (const tool of tools) {
    console.log(`Tool: ${tool.name}`);
    console.log(`  From server: ${tool.serverId}`);
    console.log(`  Title: ${tool.title ?? tool.annotations?.title ?? tool.name}`);
    console.log(`  Description: ${tool.description}`);
}

```ts const tools = this.mcp.listTools();