Connect your agent to external MCP (Model Context Protocol) servers to use their tools, resources, and prompts. This enables your agent to interact with GitHub, Slack, databases, and other services through a standardized protocol.
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, see Creating MCP Servers.
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");
}
}Use addMcpServer() to connect to an MCP server:
const result = await this.addMcpServer(name, url, options?);// Simple connection
await this.addMcpServer("notion", "https://mcp.notion.so/mcp");
// With explicit callback host (rarely needed — auto-derived from request or WebSocket URI)
await this.addMcpServer("github", "https://mcp.github.com/mcp", {
callbackHost: "https://my-worker.workers.dev"
});MCP supports multiple transport types:
await this.addMcpServer("server", "https://mcp.example.com/mcp", {
transport: {
// Transport type: "streamable-http" (default), "sse", or "auto"
type: "streamable-http"
}
});| Transport | Description |
|---|---|
"streamable-http" |
HTTP with streaming - recommended default |
"sse" |
Server-Sent Events - legacy / compatibility transport |
"auto" |
Auto-detect based on server response |
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": "..."
}
}
});Configure retry behavior for connection and reconnection attempts:
await this.addMcpServer("github", "https://mcp.github.com/mcp", {
retry: {
maxAttempts: 5,
baseDelayMs: 1000,
maxDelayMs: 10000
}
});These options are persisted and used when reconnecting after hibernation or after OAuth completion. Default: 3 attempts, 500ms base delay, 5s max delay. See Retries for more details.
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::) - Cloud metadata endpoints (
169.254.169.254) - IPv6 unique-local addresses (
fc00::/7)
Loopback development URLs such as localhost, 127.0.0.1, and ::1 are allowed.
If you need to connect to another internal MCP server, use the RPC transport with a Durable Object binding instead of HTTP.
addMcpServer() returns the connection state:
type AddMcpServerResult =
| { id: string; state: "ready" }
| { id: string; state: "authenticating"; authUrl: string };ready- Server connected and tools discoveredauthenticating- Server requires OAuth; redirect user toauthUrl
Many MCP servers require OAuth authentication. The agent handles the OAuth flow automatically.
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)
async onRequest(request: Request) {
const result = await this.addMcpServer("github", "https://mcp.github.com/mcp");
if (result.state === "authenticating") {
// Option 1: Redirect the user
return Response.redirect(result.authUrl);
// Option 2: Return the URL for client-side redirect
return Response.json({
status: "needs_auth",
authUrl: result.authUrl
});
}
return Response.json({ status: "connected", id: result.id });
}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.
For custom OAuth completion behavior:
// In your agent constructor or onStart
this.mcp.configureOAuthCallback({
// Redirect after successful auth
successRedirect: "https://myapp.com/success",
// Redirect on error
errorRedirect: "https://myapp.com/error",
// Or use a custom handler
customHandler: (result) => {
return new Response(
JSON.stringify({
success: result.authSuccess,
serverId: result.serverId,
error: result.authError
}),
{
headers: { "Content-Type": "application/json" }
}
);
}
});By default, agents use dynamic client registration to authenticate with MCP servers. If you need to use a different OAuth strategy — such as pre-registered client credentials, mTLS-based authentication, or other mechanisms — override the createMcpOAuthProvider method in your agent subclass:
import { Agent } from "agents";
import type { AgentMcpOAuthProvider } from "agents";
class MyAgent extends Agent {
createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider {
return new MyCustomOAuthProvider(this.ctx.storage, this.name, callbackUrl);
}
}Your custom class must implement the AgentMcpOAuthProvider interface, which extends the MCP SDK's OAuthClientProvider with additional properties (authUrl, clientId, serverId) and methods (checkState, consumeState, deleteCodeVerifier) used by the agent's MCP connection lifecycle.
The override is used for both new connections (addMcpServer) and restored connections after a Durable Object restart, so your custom provider is always used consistently.
The most common customization is using a different storage backend while keeping the built-in OAuth logic (CSRF state, PKCE, nonce generation, token management). Import DurableObjectOAuthClientProvider and pass your own storage adapter:
import { Agent, DurableObjectOAuthClientProvider } from "agents";
import type { AgentMcpOAuthProvider } from "agents";
class MyAgent extends Agent {
createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider {
return new DurableObjectOAuthClientProvider(
myCustomStorage, // any DurableObjectStorage-compatible adapter
this.name,
callbackUrl
);
}
}Once connected, access the server's capabilities:
const state = this.getMcpServers();
// All tools from all connected servers
for (const tool of state.tools) {
console.log(`Tool: ${tool.name}`);
console.log(` From server: ${tool.serverId}`);
console.log(` Description: ${tool.description}`);
}const state = this.getMcpServers();
// Available resources
for (const resource of state.resources) {
console.log(`Resource: ${resource.name} (${resource.uri})`);
}
// Available prompts
for (const prompt of state.prompts) {
console.log(`Prompt: ${prompt.name}`);
}const state = this.getMcpServers();
for (const [id, server] of Object.entries(state.servers)) {
console.log(`${server.name}: ${server.state}`);
// state: "ready" | "authenticating" | "connecting" | "connected" | "discovering" | "failed"
}To use MCP tools with the Vercel AI SDK, use this.mcp.getAITools() which converts MCP tools to AI SDK format:
import { generateText } from "ai";
async function chat(prompt: string) {
const response = await generateText({
model: openai("gpt-4"),
prompt,
tools: this.mcp.getAITools() // Converts MCP tools to AI SDK format
});
return response;
}Note:
getMcpServers().toolsreturns raw MCPToolobjects for inspection. Usethis.mcp.getAITools()when passing tools to the AI SDK.
await this.removeMcpServer(serverId);This disconnects from the server and removes it from storage.
MCP servers persist across agent restarts:
- Server configuration stored in SQLite
- OAuth tokens stored securely
- Connections restored automatically when agent wakes
const state = this.getMcpServers();
for (const [id, server] of Object.entries(state.servers)) {
console.log(`${id}: ${server.name} (${server.server_url})`);
}Connected clients receive real-time MCP updates via WebSocket:
import { useAgent } from "agents/react";
function Dashboard() {
const [tools, setTools] = useState([]);
const [servers, setServers] = useState({});
const agent = useAgent({
agent: "MyAgent",
onMcpUpdate: (mcpState) => {
setTools(mcpState.tools);
setServers(mcpState.servers);
}
});
return (
<div>
<h2>Connected Servers</h2>
{Object.entries(servers).map(([id, server]) => (
<div key={id}>
{server.name}: {server.connectionState}
</div>
))}
<h2>Available Tools ({tools.length})</h2>
{tools.map(tool => (
<div key={`${tool.serverId}-${tool.name}`}>
{tool.name}
</div>
))}
</div>
);
}For fine-grained control, use this.mcp directly:
// 1. Register the server (saves to storage and creates in-memory connection)
const id = "my-server";
await this.mcp.registerServer(id, {
url: "https://mcp.example.com/mcp",
name: "My Server",
callbackUrl: "https://my-worker.workers.dev/agents/my-agent/default/callback",
transport: { type: "auto" }
});
// 2. Connect (initializes transport, handles OAuth if needed)
const connectResult = await this.mcp.connectToServer(id);
if (connectResult.state === "failed") {
console.error("Connection failed:", connectResult.error);
return;
}
if (connectResult.state === "authenticating") {
console.log("OAuth required:", connectResult.authUrl);
return;
}
// 3. Discover capabilities (transitions from "connected" to "ready")
if (connectResult.state === "connected") {
const discoverResult = await this.mcp.discoverIfConnected(id);
if (!discoverResult?.success) {
console.error("Discovery failed:", discoverResult?.error);
}
}// Listen for state changes (onServerStateChanged is an Event<void>)
const disposable = this.mcp.onServerStateChanged(() => {
console.log("MCP server state changed");
this.broadcastMcpServers(); // Notify connected clients
});
// Clean up the subscription when no longer needed
// disposable.dispose();After hibernation or when connections are being restored in the background, MCP tools may not be immediately available. Use waitForConnections() to wait until all in-flight connection and discovery operations have settled:
// Wait indefinitely for all connections to be ready
await this.mcp.waitForConnections();
// Wait with a timeout (in milliseconds)
await this.mcp.waitForConnections({ timeout: 10_000 });This is useful when you need to call this.mcp.getAITools() immediately after the agent wakes from hibernation. Without waiting, tools from servers that are still reconnecting will be missing.
Note:
AIChatAgenthandles this automatically via thewaitForMcpConnectionsproperty (defaults to{ timeout: 10_000 }). You only needwaitForConnections()directly when usingAgentwith MCP, or when you want finer control insideonChatMessage.
async retryConnection(serverId: string) {
const result = await this.mcp.connectToServer(serverId);
if (result.state === "connected") {
await this.mcp.discoverIfConnected(serverId);
} else if (result.state === "failed") {
console.error("Reconnection failed:", result.error);
}
}The examples/mcp-client example demonstrates:
- Adding and removing MCP servers dynamically
- Custom OAuth callback handling (popup-closing behavior)
- Listing tools from connected servers
- Real-time state updates to the frontend
// From examples/mcp-client/src/server.ts
export class MyAgent extends Agent {
onStart() {
// Custom OAuth callback that closes the popup window
this.mcp.configureOAuthCallback({
customHandler: (result) => {
if (result.authSuccess) {
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" }
});
}
// Handle error...
}
});
}
async onRequest(request: Request) {
const url = new URL(request.url);
if (url.pathname.endsWith("add-mcp")) {
const { name, url } = await request.json();
await this.addMcpServer(name, url);
return new Response("Ok");
}
// ...
}
}// HTTP transport (Streamable HTTP, SSE)
async addMcpServer(
name: string,
url: string,
options?: {
callbackHost?: string; // auto-derived from request or WebSocket connection URI; only set to override
callbackPath?: string; // custom callback URL path (bypasses default /agents/{class}/{name}/callback)
agentsPrefix?: string;
client?: ClientOptions;
transport?: {
headers?: HeadersInit;
type?: "sse" | "streamable-http" | "auto"; // default: "auto"
};
retry?: RetryOptions; // retry options for connection/reconnection
}
): Promise<
| { id: string; state: "ready" }
| { id: string; state: "authenticating"; authUrl: string }
>
// RPC transport (Durable Object binding — no HTTP overhead)
async addMcpServer(
name: string,
binding: DurableObjectNamespace,
options?: {
props?: Record<string, unknown>; // passed to the McpAgent's onStart(props)
client?: ClientOptions;
retry?: RetryOptions;
}
): Promise<{ id: string; state: "ready" }>
// Legacy signature (still supported)
async addMcpServer(
name: string,
url: string,
callbackHost?: string,
agentsPrefix?: string,
options?: { ... }
): Promise<...>Add and connect to an MCP server. Throws if connection or discovery fails.
callbackHost is automatically derived from the incoming HTTP request or WebSocket connection URI — you almost never need to set it explicitly. It is only needed when the auto-detected host does not match your desired OAuth callback origin (for example, behind a reverse proxy). For RPC transport, pass a DurableObjectNamespace binding instead of a URL. See MCP Transports for details.
Calling addMcpServer is idempotent when both the server name and URL match an existing active connection — the existing connection is returned without creating a duplicate. This makes it safe to call in onStart() without worrying about duplicate connections on restart.
If you call addMcpServer with the same name but a different URL, a new connection is created. Both connections remain active and their tools are merged in getAITools(). To replace a server, call removeMcpServer(oldId) first.
Note: URLs are normalized before comparison (trailing slashes, default ports, and hostname case are handled), so
https://MCP.Example.comandhttps://mcp.example.com/are treated as the same URL.
async removeMcpServer(id: string): Promise<void>Disconnect from and remove an MCP server.
getMcpServers(): MCPServersStateGet the current state of all MCP servers and their capabilities.
type MCPServersState = {
servers: {
[id: string]: MCPServer;
};
tools: (Tool & { serverId: string })[];
prompts: (Prompt & { serverId: string })[];
resources: (Resource & { serverId: string })[];
};type MCPServer = {
name: string;
server_url: string;
auth_url: string | null;
state:
| "ready"
| "authenticating"
| "connecting"
| "connected"
| "discovering"
| "failed";
error: string | null;
instructions: string | null;
capabilities: ServerCapabilities | null;
};