diff --git a/src/content/changelog/agents/2025-11-26-mcp-connection-state-improvements.mdx b/src/content/changelog/agents/2025-11-26-mcp-connection-state-improvements.mdx new file mode 100644 index 000000000000000..adbe00d01118143 --- /dev/null +++ b/src/content/changelog/agents/2025-11-26-mcp-connection-state-improvements.mdx @@ -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/) diff --git a/src/content/docs/agents/model-context-protocol/mcp-client-api.mdx b/src/content/docs/agents/model-context-protocol/mcp-client-api.mdx index ef2b250663594e2..a1756cd7e66ddbc 100644 --- a/src/content/docs/agents/model-context-protocol/mcp-client-api.mdx +++ b/src/content/docs/agents/model-context-protocol/mcp-client-api.mdx @@ -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( @@ -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 @@ -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 @@ -94,26 +117,32 @@ A Promise that resolves to an object containing: ```ts title="src/index.ts" export class MyAgent extends Agent { async onRequest(request: Request): Promise { - 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 }); + } } } ``` -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] @@ -193,8 +222,9 @@ An `MCPServersState` object containing: state: | "authenticating" | "connecting" - | "ready" + | "connected" | "discovering" + | "ready" | "failed"; capabilities: ServerCapabilities | null; instructions: string | null; @@ -228,7 +258,16 @@ export class MyAgent extends Agent { -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 @@ -251,6 +290,51 @@ export class MyAgent extends Agent { 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: + + + +```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" +``` + + + +### 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: