Skip to content

Commit 7c9f8b0

Browse files
authored
feat: enums connection state and standardise tool discovery (#672)
* use enums * move broadcast out of the finally block * use .then * wip client connection refactor * feat: discoverIfConnected to refresh mcp tools. * changeset * fix types on discover result * allow discovery on ready servers also * fix: allow for duplicate callbacks in connecting state * don't block if failed to get tools * better error message here * discover on restore * discover result types * use remove rather than closeConnections * fix: discovery states * move discovery to client connection * fix: boot loop on slow mcp servers restored in on start * fix: merge artifacts * lil test fix. * fix: discover tests * update changeset
1 parent 293b546 commit 7c9f8b0

File tree

12 files changed

+1188
-542
lines changed

12 files changed

+1188
-542
lines changed

.changeset/smooth-rats-sink.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"agents": patch
3+
---
4+
5+
- `MCPClientConnection.init()` no longer triggers discovery automatically. Discovery should be done via `discover()` or through `MCPClientManager.discoverIfConnected()`
6+
7+
### Features
8+
9+
- New `discover()` method on `MCPClientConnection` with full lifecycle management:
10+
- Handles state transitions (CONNECTED → DISCOVERING → READY on success, CONNECTED on failure)
11+
- Supports cancellation via AbortController (cancels previous in-flight discovery)
12+
- Configurable timeout (default 15s)
13+
- New `cancelDiscovery()` method to abort in-flight discoveries
14+
- New `discoverIfConnected()` on `MCPClientManager` for simpler capability discovery per server
15+
- `createConnection()` now returns the connection object for immediate use
16+
- Created `MCPConnectionState` enum to formalize possible states: `idle`, `connecting`, `authenticating`, `connected`, `discovering`, `ready`, `failed`
17+
18+
### Fixes
19+
20+
- **Fixed discovery hanging on repeated requests** - New discoveries now cancel previous in-flight ones via AbortController
21+
- **Fixed Durable Object crash-looping** - `restoreConnectionsFromStorage()` now starts connections in background (fire-and-forget) to avoid blocking `onStart` and causing `blockConcurrencyWhile` timeouts
22+
- **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
23+
- **Set discovery timeout to 15s**
24+
- MCP Client Discovery failures now throw errors immediately instead of continuing with empty arrays
25+
- Added "connected" state to represent a connected server with no tools loaded yet

packages/agents/src/index.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from "partyserver";
2424
import { camelCaseToKebabCase } from "./client";
2525
import { MCPClientManager, type MCPClientOAuthResult } from "./mcp/client";
26-
import type { MCPConnectionState } from "./mcp/client-connection";
26+
import { MCPConnectionState } from "./mcp/client-connection";
2727
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
2828
import type { TransportType } from "./mcp/types";
2929
import { genericObservability, type Observability } from "./observability";
@@ -1367,7 +1367,8 @@ export class Agent<
13671367
* @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
13681368
* @param agentsPrefix agents routing prefix if not using `agents`
13691369
* @param options MCP client and transport options
1370-
* @returns authUrl
1370+
* @returns Server id and state - either "authenticating" with authUrl, or "ready"
1371+
* @throws If connection or discovery fails
13711372
*/
13721373
async addMcpServer(
13731374
serverName: string,
@@ -1381,7 +1382,18 @@ export class Agent<
13811382
type?: TransportType;
13821383
};
13831384
}
1384-
): Promise<{ id: string; authUrl: string | undefined }> {
1385+
): Promise<
1386+
| {
1387+
id: string;
1388+
state: typeof MCPConnectionState.AUTHENTICATING;
1389+
authUrl: string;
1390+
}
1391+
| {
1392+
id: string;
1393+
state: typeof MCPConnectionState.READY;
1394+
authUrl?: undefined;
1395+
}
1396+
> {
13851397
// If callbackHost is not provided, derive it from the current request
13861398
let resolvedCallbackHost = callbackHost;
13871399
if (!resolvedCallbackHost) {
@@ -1446,21 +1458,34 @@ export class Agent<
14461458
}
14471459
});
14481460

1449-
// Connect to server (updates storage with auth URL if OAuth)
1450-
// This fires onServerStateChanged event which triggers broadcast
14511461
const result = await this.mcp.connectToServer(id);
14521462

1453-
return {
1454-
id,
1455-
authUrl: result.state === "authenticating" ? result.authUrl : undefined
1456-
};
1463+
if (result.state === MCPConnectionState.FAILED) {
1464+
// Server stays in storage so user can retry via connectToServer(id)
1465+
throw new Error(
1466+
`Failed to connect to MCP server at ${url}: ${result.error}`
1467+
);
1468+
}
1469+
1470+
if (result.state === MCPConnectionState.AUTHENTICATING) {
1471+
return { id, state: result.state, authUrl: result.authUrl };
1472+
}
1473+
1474+
// State is CONNECTED - discover capabilities
1475+
const discoverResult = await this.mcp.discoverIfConnected(id);
1476+
1477+
if (discoverResult && !discoverResult.success) {
1478+
// Server stays in storage - connection is still valid, user can retry discovery
1479+
throw new Error(
1480+
`Failed to discover MCP server capabilities: ${discoverResult.error}`
1481+
);
1482+
}
1483+
1484+
return { id, state: MCPConnectionState.READY };
14571485
}
14581486

14591487
async removeMcpServer(id: string) {
1460-
if (this.mcp.mcpConnections[id]) {
1461-
await this.mcp.closeConnection(id);
1462-
}
1463-
this.mcp.removeServer(id);
1488+
await this.mcp.removeServer(id);
14641489
}
14651490

14661491
getMcpServers(): MCPServersState {
@@ -1535,21 +1560,16 @@ export class Agent<
15351560

15361561
// If auth was successful, establish the connection in the background
15371562
if (result.authSuccess) {
1538-
this.broadcastMcpServers();
1539-
1540-
this.mcp
1541-
.establishConnection(result.serverId)
1542-
.catch((error) => {
1543-
console.error(
1544-
"[Agent handleMcpOAuthCallback] Connection establishment failed:",
1545-
error
1546-
);
1547-
})
1548-
.finally(() => {
1549-
this.broadcastMcpServers();
1550-
});
1563+
this.mcp.establishConnection(result.serverId).catch((error) => {
1564+
console.error(
1565+
"[Agent handleMcpOAuthCallback] Connection establishment failed:",
1566+
error
1567+
);
1568+
});
15511569
}
15521570

1571+
this.broadcastMcpServers();
1572+
15531573
// Return the HTTP response for the OAuth callback
15541574
return this.handleOAuthCallbackResponse(result, request);
15551575
}

0 commit comments

Comments
 (0)