McpClient
Connect your agent to external Model Context Protocol (MCP) 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
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.
Use addMcpServer() to connect to an MCP server:
// Simple connectionawait this.addMcpServer("notion", "https://mcp.notion.so/mcp");
// With explicit callback hostawait this.addMcpServer("github", "https://mcp.github.com/mcp", { callbackHost: "https://my-worker.workers.dev",});// Simple connectionawait this.addMcpServer("notion", "https://mcp.notion.so/mcp");
// With explicit callback hostawait 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: { type: "streamable-http", },});await this.addMcpServer("server", "https://mcp.example.com/mcp", { transport: { 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": "...", }, },});await this.addMcpServer("internal", "https://internal-mcp.example.com/mcp", { transport: { headers: { Authorization: "Bearer my-token", "CF-Access-Client-Id": "...", "CF-Access-Client-Secret": "...", }, },});addMcpServer() returns the connection state:
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)
class MyAgent extends Agent { async onRequest(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 }); }}class MyAgent extends Agent { 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}/callbackFor example: https://my-worker.workers.dev/agents/my-agent/default/callback
OAuth tokens are securely stored in SQLite, and persist across agent restarts.
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 agentexport 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 agentexport 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 }) ); },};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" }, }); }, }); }}Once connected, access the server's capabilities:
const state = this.getMcpServers();
// All tools from all connected serversfor (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();
// All tools from all connected serversfor (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 resourcesfor (const resource of state.resources) { console.log(`Resource: ${resource.name} (${resource.uri})`);}
// Available promptsfor (const prompt of state.prompts) { console.log(`Prompt: ${prompt.name}`);}const state = this.getMcpServers();
// Available resourcesfor (const resource of state.resources) { console.log(`Resource: ${resource.name} (${resource.uri})`);}
// Available promptsfor (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"}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";import { openai } from "@ai-sdk/openai";
export class MyAgent extends Agent { async onRequest(request) { const response = await generateText({ model: openai("gpt-4"), prompt: "What's the weather in San Francisco?", tools: this.mcp.getAITools(), // Converts MCP tools to AI SDK format });
return new Response(response.text); }}import { generateText } from "ai";import { openai } from "@ai-sdk/openai";
export class MyAgent extends Agent { async onRequest(request: Request) { const response = await generateText({ model: openai("gpt-4"), prompt: "What's the weather in San Francisco?", tools: this.mcp.getAITools(), // Converts MCP tools to AI SDK format });
return new Response(response.text); }}await this.removeMcpServer(serverId);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})`);}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";import { useState } from "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.state} </div> ))}
<h2>Available Tools ({tools.length})</h2> {tools.map((tool) => ( <div key={`${tool.serverId}-${tool.name}`}>{tool.name}</div> ))} </div> );}import { useAgent } from "agents/react";import { useState } from "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.state} </div> ))}
<h2>Available Tools ({tools.length})</h2> {tools.map((tool) => ( <div key={`${tool.serverId}-${tool.name}`}>{tool.name}</div> ))} </div> );}Add a connection to an MCP server and make its tools available to your agent.
async addMcpServer( serverName: string, url: string, options?: { callbackHost?: string; callbackPath?: string; agentsPrefix?: string; client?: ClientOptions; transport?: { headers?: HeadersInit; type?: "sse" | "streamable-http" | "auto"; }; }): Promise< | { id: string; state: "authenticating"; authUrl: string } | { id: string; state: "ready" }>serverName(string, required) — Display name for the MCP serverurl(string, required) — URL of the MCP server endpointoptions(object, optional) — Connection configuration:callbackHost— Host for OAuth callback URL. If omitted, automatically derived from the incoming requestcallbackPath— Custom callback URL path that bypasses the default/agents/{class}/{name}/callbackconstruction. Required whensendIdentityOnConnectisfalseto prevent leaking the instance name. When set, the callback URL becomes{callbackHost}/{callbackPath}. You must route this path to the agent instance viagetAgentByNameagentsPrefix— URL prefix for OAuth callback path. Default:"agents". Ignored whencallbackPathis providedclient— MCP client configuration options (passed to@modelcontextprotocol/sdkClient constructor). By default, includesCfWorkerJsonSchemaValidatorfor validating tool parameters against JSON schemastransport— Transport layer configuration:headers— Custom HTTP headers for authenticationtype— Transport type:"streamable-http"(default),"sse", or"auto"
A Promise that resolves to a discriminated union based on connection state:
-
When
stateis"authenticating":id(string) — Unique identifier for this server connectionstate("authenticating") — Server is waiting for OAuth authorizationauthUrl(string) — OAuth authorization URL for user authentication
-
When
stateis"ready":id(string) — Unique identifier for this server connectionstate("ready") — Server is fully connected and operational
Disconnect from an MCP server and clean up its resources.
async removeMcpServer(id: string): Promise<void>id(string, required) — Server connection ID returned fromaddMcpServer()
Get the current state of all MCP server connections.
getMcpServers(): MCPServersStatetype MCPServersState = { servers: Record< string, { name: string; server_url: string; auth_url: string | null; state: | "authenticating" | "connecting" | "connected" | "discovering" | "ready" | "failed"; capabilities: ServerCapabilities | null; instructions: string | null; error: string | null; } >; tools: Array<Tool & { serverId: string }>; prompts: Array<Prompt & { serverId: string }>; resources: Array<Resource & { serverId: string }>; resourceTemplates: Array<ResourceTemplate & { serverId: string }>;};The state field indicates the connection lifecycle:
authenticating— Waiting for OAuth authorization to completeconnecting— Establishing transport connectionconnected— Transport connection establisheddiscovering— Discovering server capabilities (tools, resources, prompts)ready— Fully connected and operationalfailed— Connection failed (seeerrorfield 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.
Configure OAuth callback behavior for MCP servers requiring authentication. This method allows you to customize what happens after a user completes OAuth authorization.
this.mcp.configureOAuthCallback(options: { successRedirect?: string; errorRedirect?: string; customHandler?: () => Response | Promise<Response>;}): voidoptions(object, required) — OAuth callback configuration:successRedirect(string, optional) — URL to redirect to after successful authenticationerrorRedirect(string, optional) — URL to redirect to after failed authentication. Error message is appended as?error=<message>query parametercustomHandler(function, optional) — Custom handler for complete control over the callback response. Must return a Response
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.
Configure in onStart() before any OAuth flows begin:
export class MyAgent extends Agent { onStart() { // Option 1: Simple redirects this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", errorRedirect: "/auth-error", });
// Option 2: Custom handler (e.g., for popup windows) this.mcp.configureOAuthCallback({ customHandler: () => { return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); }, }); }}export class MyAgent extends Agent { onStart() { // Option 1: Simple redirects this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", errorRedirect: "/auth-error", });
// Option 2: Custom handler (e.g., for popup windows) this.mcp.configureOAuthCallback({ customHandler: () => { return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); }, }); }}For fine-grained control, use this.mcp directly:
// 1. Register the serverawait 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. Connectconst connectResult = await this.mcp.connectToServer(id);
if (connectResult.state === "failed") { console.error("Connection failed:", connectResult.error); return;}
if (connectResult.state === "authenticating") { // Handle OAuth... return;}
// 3. Discover capabilitiesconst discoverResult = await this.mcp.discoverIfConnected(id);
if (!discoverResult?.success) { console.error("Discovery failed:", discoverResult?.error);}// 1. Register the serverawait 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. Connectconst connectResult = await this.mcp.connectToServer(id);
if (connectResult.state === "failed") { console.error("Connection failed:", connectResult.error); return;}
if (connectResult.state === "authenticating") { // Handle OAuth... return;}
// 3. Discover capabilitiesconst discoverResult = await this.mcp.discoverIfConnected(id);
if (!discoverResult?.success) { console.error("Discovery failed:", discoverResult?.error);}// Listen for state changesthis.mcp.onServerStateChanged(() => { console.log("MCP server state changed"); this.broadcastMcpServers(); // Notify connected clients});// Listen for state changesthis.mcp.onServerStateChanged(() => { console.log("MCP server state changed"); this.broadcastMcpServers(); // Notify connected clients});Register a server without immediately connecting.
async registerServer( id: string, options: { url: string; name: string; callbackUrl: string; clientOptions?: ClientOptions; transportOptions?: TransportOptions; }): Promise<string>Establish a connection to a previously registered server.
async connectToServer(id: string): Promise<MCPConnectionResult>
type MCPConnectionResult = | { state: "failed"; error: string } | { state: "authenticating"; authUrl: string } | { state: "connected" }Check server capabilities if a connection is active.
async discoverIfConnected( serverId: string, options?: { timeoutMs?: number }): Promise<MCPDiscoverResult | undefined>
type MCPDiscoverResult = { success: boolean; state: MCPConnectionState; error?: string;}Close the connection to a specific server while keeping it registered.
async closeConnection(id: string): Promise<void>Close all active server connections while preserving registrations.
async closeAllConnections(): Promise<void>Get all discovered MCP tools in a format compatible with the AI SDK.
getAITools(): ToolSetTools are automatically namespaced by server ID to prevent conflicts when multiple MCP servers expose tools with the same name.
Use error detection utilities to handle connection errors:
import { isUnauthorized, isTransportNotImplemented } from "agents/mcp";
export class MyAgent extends Agent { async onRequest(request) { try { await this.addMcpServer("Server", "https://mcp.example.com/mcp"); } catch (error) { if (isUnauthorized(error)) { return new Response("Authentication required", { status: 401 }); } else if (isTransportNotImplemented(error)) { return new Response("Transport not supported", { status: 400 }); } throw error; } }}import { isUnauthorized, isTransportNotImplemented } from "agents/mcp";
export class MyAgent extends Agent { async onRequest(request: Request) { try { await this.addMcpServer("Server", "https://mcp.example.com/mcp"); } catch (error) { if (isUnauthorized(error)) { return new Response("Authentication required", { status: 401 }); } else if (isTransportNotImplemented(error)) { return new Response("Transport not supported", { status: 400 }); } throw error; } }}