Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
title: MCP connection state improvements and standardized tool discovery
description: The Agents SDK now includes a formalized MCPConnectionState enum, improved discovery lifecycle with timeouts and cancellation, and better error handling for MCP server connections.
products:
- agents
- workers
date: 2025-11-26
---

We have improved the MCP (Model Context Protocol) client connection lifecycle in the [Agents SDK](https://github.com/cloudflare/agents) with better state management, explicit discovery control, and enhanced reliability.

## New Features

### MCPConnectionState Enum

A new `MCPConnectionState` enum formalizes the connection lifecycle with six distinct states:

- `AUTHENTICATING` — Waiting for OAuth authorization
- `CONNECTING` — Establishing transport connection
- `CONNECTED` — Transport connected, ready for discovery
- `DISCOVERING` — Fetching server capabilities
- `READY` — Fully connected with all tools available
- `FAILED` — Connection or discovery error

```ts
import { MCPConnectionState } from "agents";

if (result.state === MCPConnectionState.READY) {
// Server is connected and tools are available
}
```

### Enhanced Discovery Control

Discovery is now a separate, explicit phase with full lifecycle management:

- **Configurable timeout** — Default 15-second timeout prevents hanging connections
- **Cancellation support** — New discoveries automatically cancel previous in-flight requests
- **Better error handling** — Discovery failures throw errors immediately instead of continuing with empty tool arrays
- **New `discover()` method** — On `MCPClientConnection` for manual discovery control
- **New `discoverIfConnected()` method** — On `MCPClientManager` for simpler per-server discovery

### Improved `addMcpServer()` Return Type

The return type is now a discriminated union that makes OAuth vs. ready states explicit:

```ts
const result = await this.addMcpServer("Weather", "https://weather.example.com/mcp");

if (result.state === "authenticating") {
// Redirect user to result.authUrl
} else {
// result.state === "ready", tools are available
}
```

### Better `createConnection()` Return Value

The `createConnection()` method now returns the connection object for immediate use, eliminating the need for separate lookup calls.

## Bug Fixes

- **Fixed discovery hanging on repeated requests** — New discoveries now cancel previous in-flight ones via AbortController
- **Fixed Durable Object crash-looping** — `restoreConnectionsFromStorage()` now starts connections in background to avoid blocking initialization and causing timeouts
- **Fixed OAuth callback race condition** — When `auth_url` exists in storage during restoration, state is set to `AUTHENTICATING` directly instead of calling `connectToServer()` which was overwriting the state

## Migration Guide

### Breaking Change: `MCPClientConnection.init()` No Longer Auto-Discovers

Previously, `init()` would automatically trigger discovery. Now connection and discovery are separate phases.

**For most use cases, no changes are needed** — `addMcpServer()` handles both connection and discovery automatically.

If you are using lower-level APIs like `MCPClientConnection` directly, you must now call `discover()` explicitly after connection:

```ts
// Before
await connection.init();
// init() would automatically discover

// After
await connection.init();
await connection.discover();
// Or use the MCPClientManager method
await clientManager.discoverIfConnected(serverId);
```

### Updated Return Type for `addMcpServer()`

The return type changed from:

```ts
{ id: string; authUrl: string | undefined }
```

To a discriminated union:

```ts
| { id: string; state: "authenticating"; authUrl: string }
| { id: string; state: "ready"; authUrl?: undefined }
```

Update your code to check the `state` field:

```ts
// Before
const { id, authUrl } = await this.addMcpServer(...);
if (authUrl) {
// Handle OAuth
}

// After
const result = await this.addMcpServer(...);
if (result.state === "authenticating") {
// Handle OAuth with result.authUrl
}
```

### New Connection State: `connected`

A new `connected` state represents a connected transport with no tools loaded yet. This state occurs between `CONNECTING` and `DISCOVERING`, allowing you to distinguish between:

- **`connected`** — Transport is connected, but capabilities are not yet discovered
- **`ready`** — Transport is connected and all capabilities have been discovered

Update any code that checks connection states to account for this new intermediate state.

## Learn More

- [MCP Client API reference](/agents/model-context-protocol/mcp-client-api/)
- [Connect to MCP servers guide](/agents/guides/connect-mcp-client/)
- [OAuth handling for MCP clients](/agents/guides/oauth-mcp-client/)
124 changes: 104 additions & 20 deletions src/content/docs/agents/model-context-protocol/mcp-client-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Connections persist in the Agent's [SQL storage](/agents/api-reference/store-and

### `addMcpServer()`

Add a connection to an MCP server and make its tools available to your Agent.
Add a connection to an MCP server and make its tools available to your Agent. This method establishes the connection and automatically discovers server capabilities (tools, resources, prompts).

```ts
async addMcpServer(
Expand All @@ -65,7 +65,18 @@ async addMcpServer(
type?: "sse" | "streamable-http" | "auto";
};
}
): Promise<{ id: string; authUrl: string | undefined }>
): Promise<
| {
id: string;
state: "authenticating";
authUrl: string;
}
| {
id: string;
state: "ready";
authUrl?: undefined;
}
>
```

#### Parameters
Expand All @@ -82,10 +93,22 @@ async addMcpServer(

#### Returns

A Promise that resolves to an object containing:
A Promise that resolves to a discriminated union based on connection state:

- **`id`** (string) — Unique identifier for this server connection
- **`authUrl`** (string | undefined) — OAuth authorization URL if authentication is required, otherwise `undefined`
- If **OAuth authentication is required**:
- **`id`** (string) — Unique identifier for this server connection
- **`state`** (`"authenticating"`) — Indicates OAuth flow is needed
- **`authUrl`** (string) — OAuth authorization URL for user authentication

- If **connection and discovery succeeded**:
- **`id`** (string) — Unique identifier for this server connection
- **`state`** (`"ready"`) — Server is connected and tools are available
- **`authUrl`** (undefined) — Not present for non-OAuth connections

#### Throws

- Throws an error if connection fails
- Throws an error if discovery fails (with 15-second timeout)

#### Example

Expand All @@ -94,26 +117,32 @@ A Promise that resolves to an object containing:
```ts title="src/index.ts"
export class MyAgent extends Agent<Env, never> {
async onRequest(request: Request): Promise<Response> {
const { id, authUrl } = await this.addMcpServer(
"Weather API",
"https://weather-mcp.example.com/mcp",
);

if (authUrl) {
// User needs to complete OAuth flow
return new Response(JSON.stringify({ serverId: id, authUrl }), {
headers: { "Content-Type": "application/json" },
});
}
try {
const result = await this.addMcpServer(
"Weather API",
"https://weather-mcp.example.com/mcp",
);

return new Response("Connected", { status: 200 });
if (result.state === "authenticating") {
// User needs to complete OAuth flow
return new Response(JSON.stringify({ serverId: result.id, authUrl: result.authUrl }), {
headers: { "Content-Type": "application/json" },
});
}

// Server is ready - tools are available
return new Response(`Connected: ${result.id}`, { status: 200 });
} catch (error) {
// Connection or discovery failed
return new Response(`Failed to connect: ${error.message}`, { status: 500 });
}
}
}
```

</TypeScriptExample>

If the MCP server requires OAuth authentication, `authUrl` will be returned for user authentication. Connections persist across requests and the Agent will automatically reconnect if the connection is lost.
If the MCP server requires OAuth authentication, the result will include `state: "authenticating"` with an `authUrl` for user authentication. Otherwise, the server transitions to `"ready"` state with all tools discovered and available. Connections persist across requests and the Agent will automatically reconnect if the connection is lost.

:::note[Default JSON Schema Validation]

Expand Down Expand Up @@ -193,8 +222,9 @@ An `MCPServersState` object containing:
state:
| "authenticating"
| "connecting"
| "ready"
| "connected"
| "discovering"
| "ready"
| "failed";
capabilities: ServerCapabilities | null;
instructions: string | null;
Expand Down Expand Up @@ -228,7 +258,16 @@ export class MyAgent extends Agent<Env, never> {

</TypeScriptExample>

The `state` field can be: `authenticating`, `connecting`, `ready`, `discovering`, or `failed`. Use this method to monitor connection status, list available tools, or build UI for connected servers.
The `state` field represents the connection lifecycle:

- **`authenticating`** — Waiting for OAuth authorization
- **`connecting`** — Establishing transport connection
- **`connected`** — Transport connected, but capabilities not yet discovered
- **`discovering`** — Discovering server capabilities (tools, resources, prompts)
- **`ready`** — Fully connected with all capabilities discovered
- **`failed`** — Connection or discovery failed

Use this method to monitor connection status, list available tools, or build UI for connected servers.

## OAuth Configuration

Expand All @@ -251,6 +290,51 @@ export class MyAgent extends Agent<Env, never> {

You can also provide a `customHandler` function for full control over the callback response. Refer to the [OAuth handling guide](/agents/guides/oauth-mcp-client) for details.

## Connection State Lifecycle

The Agents SDK exports an `MCPConnectionState` enum that defines the connection lifecycle states:

<TypeScriptExample>

```ts title="src/index.ts"
import { MCPConnectionState } from "agents";

// Access state constants
console.log(MCPConnectionState.AUTHENTICATING); // "authenticating"
console.log(MCPConnectionState.CONNECTING); // "connecting"
console.log(MCPConnectionState.CONNECTED); // "connected"
console.log(MCPConnectionState.DISCOVERING); // "discovering"
console.log(MCPConnectionState.READY); // "ready"
console.log(MCPConnectionState.FAILED); // "failed"
```

</TypeScriptExample>

### State Transitions

Connections follow this lifecycle:

**Non-OAuth flow:**
```
CONNECTING → CONNECTED → DISCOVERING → READY
```

**OAuth flow:**
```
AUTHENTICATING → (user completes OAuth) → CONNECTING → CONNECTED → DISCOVERING → READY
```

Any state can transition to `FAILED` if an error occurs.

### Key Changes in Connection Behavior

Previously, `init()` would automatically trigger discovery. Now:

1. **Connection and discovery are separate phases** — After a successful connection (state `CONNECTED`), you must explicitly call discovery
2. **`addMcpServer()` handles both automatically** — For most use cases, use `addMcpServer()` which connects and discovers in one call
3. **Discovery has a 15-second timeout** — If a server does not respond within 15 seconds, discovery fails and the connection returns to `CONNECTED` state
4. **Discovery can be cancelled** — Repeated discovery requests automatically cancel previous in-flight discoveries

## Error Handling

Use error detection utilities to handle connection errors:
Expand Down
Loading