Skip to content

Commit 293b546

Browse files
authored
remove breaking change with async storage apis. (#676)
* refactor: simplify MCPClientManager storage implementation and fix initialization order - Changed MCPClientManager to use DurableObjectStorage directly instead of abstraction layer - Removed MCPClientStorage and OAuthClientStorage interfaces - Removed AgentMCPClientStorage adapter class - Fixed initialization order: MCPClientManager now created AFTER database tables to prevent table-not-found errors during DO restart - Changed getMcpServers() from async to sync method - Added defensive checks for storage * add return types to oauth client provider
1 parent cccbd0f commit 293b546

File tree

7 files changed

+268
-398
lines changed

7 files changed

+268
-398
lines changed

.changeset/plenty-friends-hope.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
---
2-
"agents": minor
2+
"agents": patch
33
---
44

5-
### Breaking Changes
6-
7-
- **`getMcpServers()` is now async**: Changed from synchronous to asynchronous method to support storage operations
8-
- **`DurableObjectOAuthClientProvider` constructor**: Now accepts `OAuthClientStorage` interface instead of `DurableObjectStorage`
9-
105
### New Features
116

127
- **`MCPClientManager` API changes**:
138
- New `registerServer()` method to register servers (replaces part of `connect()`)
149
- New `connectToServer()` method to establish connection (replaces part of `connect()`)
1510
- `connect()` method deprecated (still works for backward compatibility)
16-
- Requires `MCPClientStorage` interface implementation (provided via `AgentMCPClientStorage`)
17-
- **Storage abstraction layer**: New `MCPClientStorage` and `OAuthClientStorage` interfaces enable custom storage implementations beyond Durable Objects
1811
- **Connection state observability**: New `onServerStateChanged()` event for tracking all server state changes
1912
- **Improved reconnect logic**: `restoreConnectionsFromStorage()` handles failed connections
2013

2114
### Bug Fixes
2215

2316
- Fixed failed connections not being recreated on restore
2417
- Fixed redundant storage operations during connection restoration
18+
- Fixed potential OAuth storage initialization issue by excluding non-serializable authProvider from stored server options
19+
- Added defensive checks for storage initialization in MCPClientManager and DurableObjectOAuthClientProvider
20+
- Fixed initialization order: MCPClientManager is now created AFTER database tables are created to prevent possible table-not-found errors during DO restart

packages/agents/src/index.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { MCPClientManager, type MCPClientOAuthResult } from "./mcp/client";
2626
import type { MCPConnectionState } from "./mcp/client-connection";
2727
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
2828
import type { TransportType } from "./mcp/types";
29-
import { AgentMCPClientStorage } from "./mcp/client-storage";
3029
import { genericObservability, type Observability } from "./observability";
3130
import { DisposableStore } from "./core/events";
3231
import { MessageType } from "./ai-types";
@@ -404,33 +403,12 @@ export class Agent<
404403
constructor(ctx: AgentContext, env: Env) {
405404
super(ctx, env);
406405

407-
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", {
408-
storage: new AgentMCPClientStorage(
409-
this.sql.bind(this),
410-
this.ctx.storage.kv
411-
)
412-
});
413-
414406
if (!wrappedClasses.has(this.constructor)) {
415407
// Auto-wrap custom methods with agent context
416408
this._autoWrapCustomMethods();
417409
wrappedClasses.add(this.constructor);
418410
}
419411

420-
// Broadcast server state whenever MCP state changes (register, connect, OAuth, remove, etc.)
421-
this._disposables.add(
422-
this.mcp.onServerStateChanged(async () => {
423-
await this.broadcastMcpServers();
424-
})
425-
);
426-
427-
// Emit MCP observability events
428-
this._disposables.add(
429-
this.mcp.onObservabilityEvent((event) => {
430-
this.observability?.emit(event);
431-
})
432-
);
433-
434412
this.sql`
435413
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
436414
id TEXT PRIMARY KEY NOT NULL,
@@ -472,6 +450,25 @@ export class Agent<
472450
)
473451
`;
474452

453+
// Initialize MCPClientManager AFTER tables are created
454+
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", {
455+
storage: this.ctx.storage
456+
});
457+
458+
// Broadcast server state whenever MCP state changes (register, connect, OAuth, remove, etc.)
459+
this._disposables.add(
460+
this.mcp.onServerStateChanged(async () => {
461+
this.broadcastMcpServers();
462+
})
463+
);
464+
465+
// Emit MCP observability events
466+
this._disposables.add(
467+
this.mcp.onObservabilityEvent((event) => {
468+
this.observability?.emit(event);
469+
})
470+
);
471+
475472
const _onRequest = this.onRequest.bind(this);
476473
this.onRequest = (request: Request) => {
477474
return agentContext.run(
@@ -603,7 +600,7 @@ export class Agent<
603600

604601
connection.send(
605602
JSON.stringify({
606-
mcp: await this.getMcpServers(),
603+
mcp: this.getMcpServers(),
607604
type: MessageType.CF_AGENT_MCP_SERVERS
608605
})
609606
);
@@ -637,7 +634,7 @@ export class Agent<
637634
async () => {
638635
await this._tryCatch(async () => {
639636
await this.mcp.restoreConnectionsFromStorage(this.name);
640-
await this.broadcastMcpServers();
637+
this.broadcastMcpServers();
641638
return _onStart(props);
642639
});
643640
}
@@ -1409,7 +1406,7 @@ export class Agent<
14091406
const id = nanoid(8);
14101407

14111408
const authProvider = new DurableObjectOAuthClientProvider(
1412-
this.ctx.storage.kv,
1409+
this.ctx.storage,
14131410
this.name,
14141411
callbackUrl
14151412
);
@@ -1463,18 +1460,18 @@ export class Agent<
14631460
if (this.mcp.mcpConnections[id]) {
14641461
await this.mcp.closeConnection(id);
14651462
}
1466-
await this.mcp.removeServer(id);
1463+
this.mcp.removeServer(id);
14671464
}
14681465

1469-
async getMcpServers(): Promise<MCPServersState> {
1466+
getMcpServers(): MCPServersState {
14701467
const mcpState: MCPServersState = {
14711468
prompts: this.mcp.listPrompts(),
14721469
resources: this.mcp.listResources(),
14731470
servers: {},
14741471
tools: this.mcp.listTools()
14751472
};
14761473

1477-
const servers = await this.mcp.listServers();
1474+
const servers = this.mcp.listServers();
14781475

14791476
if (servers && Array.isArray(servers) && servers.length > 0) {
14801477
for (const server of servers) {
@@ -1501,10 +1498,10 @@ export class Agent<
15011498
return mcpState;
15021499
}
15031500

1504-
private async broadcastMcpServers() {
1501+
private broadcastMcpServers() {
15051502
this.broadcast(
15061503
JSON.stringify({
1507-
mcp: await this.getMcpServers(),
1504+
mcp: this.getMcpServers(),
15081505
type: MessageType.CF_AGENT_MCP_SERVERS
15091506
})
15101507
);
@@ -1527,7 +1524,7 @@ export class Agent<
15271524
request: Request
15281525
): Promise<Response | null> {
15291526
// Check if this is an OAuth callback request
1530-
const isCallback = await this.mcp.isCallbackRequest(request);
1527+
const isCallback = this.mcp.isCallbackRequest(request);
15311528
if (!isCallback) {
15321529
return null;
15331530
}

packages/agents/src/mcp/client-storage.ts

Lines changed: 0 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -10,130 +10,3 @@ export type MCPServerRow = {
1010
callback_url: string;
1111
server_options: string | null;
1212
};
13-
14-
/**
15-
* KV storage interface for OAuth-related data
16-
* Used by OAuth providers to store tokens, client info, etc.
17-
*/
18-
export interface OAuthClientStorage {
19-
/**
20-
* Get a value from key-value storage (for OAuth data like tokens, client info, etc.)
21-
*/
22-
get<T>(key: string): Promise<T | undefined> | undefined;
23-
24-
/**
25-
* Put a value into key-value storage (for OAuth data like tokens, client info, etc.)
26-
*/
27-
put(key: string, value: unknown): Promise<void> | void;
28-
}
29-
30-
/**
31-
* Storage interface for MCP client manager
32-
* Abstracts storage operations to decouple from specific storage implementations
33-
*/
34-
export interface MCPClientStorage extends OAuthClientStorage {
35-
/**
36-
* Save or update an MCP server configuration
37-
*/
38-
saveServer(server: MCPServerRow): Promise<void>;
39-
40-
/**
41-
* Remove an MCP server from storage
42-
*/
43-
removeServer(serverId: string): Promise<void>;
44-
45-
/**
46-
* List all MCP servers from storage
47-
*/
48-
listServers(): Promise<MCPServerRow[]>;
49-
50-
/**
51-
* Get an MCP server by its callback URL
52-
* Used during OAuth callback to identify which server is being authenticated
53-
*/
54-
getServerByCallbackUrl(callbackUrl: string): Promise<MCPServerRow | null>;
55-
56-
/**
57-
* Clear auth_url after successful OAuth authentication
58-
* This prevents the agent from continuously asking for OAuth on reconnect
59-
* when stored tokens are still valid.
60-
*/
61-
clearAuthUrl(serverId: string): Promise<void>;
62-
}
63-
64-
/**
65-
* SQL-based storage adapter that wraps SQL operations
66-
* Used by Agent class to provide SQL access to MCPClientManager
67-
*/
68-
export class AgentMCPClientStorage implements MCPClientStorage {
69-
constructor(
70-
private sql: <T extends Record<string, unknown>>(
71-
strings: TemplateStringsArray,
72-
...values: (string | number | boolean | null)[]
73-
) => T[],
74-
private kv: SyncKvStorage
75-
) {}
76-
77-
async saveServer(server: MCPServerRow) {
78-
this.sql`
79-
INSERT OR REPLACE INTO cf_agents_mcp_servers (
80-
id,
81-
name,
82-
server_url,
83-
client_id,
84-
auth_url,
85-
callback_url,
86-
server_options
87-
)
88-
VALUES (
89-
${server.id},
90-
${server.name},
91-
${server.server_url},
92-
${server.client_id ?? null},
93-
${server.auth_url ?? null},
94-
${server.callback_url},
95-
${server.server_options ?? null}
96-
)
97-
`;
98-
}
99-
100-
async removeServer(serverId: string) {
101-
this.sql`
102-
DELETE FROM cf_agents_mcp_servers WHERE id = ${serverId}
103-
`;
104-
}
105-
106-
async listServers() {
107-
const servers = this.sql<MCPServerRow>`
108-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
109-
FROM cf_agents_mcp_servers
110-
`;
111-
return servers;
112-
}
113-
114-
async getServerByCallbackUrl(callbackUrl: string) {
115-
const results = this.sql<MCPServerRow>`
116-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
117-
FROM cf_agents_mcp_servers
118-
WHERE callback_url = ${callbackUrl}
119-
LIMIT 1
120-
`;
121-
return results.length > 0 ? results[0] : null;
122-
}
123-
124-
async clearAuthUrl(serverId: string) {
125-
this.sql`
126-
UPDATE cf_agents_mcp_servers
127-
SET auth_url = NULL
128-
WHERE id = ${serverId}
129-
`;
130-
}
131-
132-
async get<T>(key: string) {
133-
return this.kv.get<T>(key);
134-
}
135-
136-
async put(key: string, value: unknown) {
137-
return this.kv.put(key, value);
138-
}
139-
}

0 commit comments

Comments
 (0)