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 discoveredauthenticating- Server requires OAuth; redirect user toauthUrl
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();
