|
| 1 | +--- |
| 2 | +title: MCP Storage Adapter |
| 3 | +pcx_content_type: concept |
| 4 | +tags: |
| 5 | + - MCP |
| 6 | +sidebar: |
| 7 | + order: 8 |
| 8 | +--- |
| 9 | + |
| 10 | +import { TypeScriptExample } from "~/components"; |
| 11 | + |
| 12 | +The MCP storage adapter pattern provides a flexible way to persist MCP server configurations and connection state. By default, Agents use SQL storage, but you can implement custom storage backends for specialized use cases. |
| 13 | + |
| 14 | +## Overview |
| 15 | + |
| 16 | +MCP server configurations are stored persistently so that connections can be restored after Agent restarts or during OAuth flows. The storage adapter interface abstracts these storage operations, making it possible to: |
| 17 | + |
| 18 | +- Use different storage backends (SQL, KV, external databases) |
| 19 | +- Test MCP functionality with mock storage |
| 20 | +- Implement custom persistence strategies |
| 21 | + |
| 22 | +## Default Behavior |
| 23 | + |
| 24 | +When you use `this.addMcpServer()` in an Agent class, storage is handled automatically using the Agent's built-in SQL storage. No additional configuration is required: |
| 25 | + |
| 26 | +<TypeScriptExample> |
| 27 | + |
| 28 | +```ts title="src/index.ts" |
| 29 | +export class MyAgent extends Agent<Env, never> { |
| 30 | + async onRequest(request: Request): Promise<Response> { |
| 31 | + // Storage is handled automatically |
| 32 | + const { id } = await this.addMcpServer( |
| 33 | + "Weather API", |
| 34 | + "https://weather-mcp.example.com/mcp", |
| 35 | + ); |
| 36 | + |
| 37 | + return new Response(`Connected: ${id}`); |
| 38 | + } |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +</TypeScriptExample> |
| 43 | + |
| 44 | +## Storage Adapter Interface |
| 45 | + |
| 46 | +For advanced use cases, you can implement the `MCPStorageAdapter` interface: |
| 47 | + |
| 48 | +```ts |
| 49 | +interface MCPStorageAdapter { |
| 50 | + create(): void | Promise<void>; |
| 51 | + destroy(): void | Promise<void>; |
| 52 | + saveServer(server: MCPServerRow): void | Promise<void>; |
| 53 | + removeServer(serverId: string): void | Promise<void>; |
| 54 | + listServers(): MCPServerRow[] | Promise<MCPServerRow[]>; |
| 55 | + getServerByCallbackUrl( |
| 56 | + callbackUrl: string, |
| 57 | + ): MCPServerRow | null | Promise<MCPServerRow | null>; |
| 58 | + clearOAuthCredentials(serverId: string): void | Promise<void>; |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### MCPServerRow Type |
| 63 | + |
| 64 | +The storage adapter works with server configuration objects: |
| 65 | + |
| 66 | +```ts |
| 67 | +type MCPServerRow = { |
| 68 | + id: string; |
| 69 | + name: string; |
| 70 | + server_url: string; |
| 71 | + client_id: string | null; |
| 72 | + auth_url: string | null; |
| 73 | + callback_url: string; |
| 74 | + server_options: string | null; |
| 75 | +}; |
| 76 | +``` |
| 77 | + |
| 78 | +## Built-in Storage Adapter |
| 79 | + |
| 80 | +The `AgentMCPStorageAdapter` class wraps Agent SQL storage and is used automatically by the Agent class: |
| 81 | + |
| 82 | +<TypeScriptExample> |
| 83 | + |
| 84 | +```ts title="src/index.ts" |
| 85 | +import { AgentMCPStorageAdapter } from "agents/mcp"; |
| 86 | + |
| 87 | +// This is done automatically inside the Agent class |
| 88 | +const storage = new AgentMCPStorageAdapter(this.sql.bind(this)); |
| 89 | +``` |
| 90 | + |
| 91 | +</TypeScriptExample> |
| 92 | + |
| 93 | +## Custom Storage Implementation |
| 94 | + |
| 95 | +If you need to use `MCPClientManager` directly (outside of an Agent), you must provide a storage adapter: |
| 96 | + |
| 97 | +<TypeScriptExample> |
| 98 | + |
| 99 | +```ts title="src/index.ts" |
| 100 | +import { MCPClientManager, type MCPStorageAdapter, type MCPServerRow } from "agents/mcp"; |
| 101 | + |
| 102 | +class CustomStorageAdapter implements MCPStorageAdapter { |
| 103 | + private servers = new Map<string, MCPServerRow>(); |
| 104 | + |
| 105 | + create(): void { |
| 106 | + // Initialize storage |
| 107 | + } |
| 108 | + |
| 109 | + destroy(): void { |
| 110 | + // Clean up storage |
| 111 | + this.servers.clear(); |
| 112 | + } |
| 113 | + |
| 114 | + saveServer(server: MCPServerRow): void { |
| 115 | + this.servers.set(server.id, server); |
| 116 | + } |
| 117 | + |
| 118 | + removeServer(serverId: string): void { |
| 119 | + this.servers.delete(serverId); |
| 120 | + } |
| 121 | + |
| 122 | + listServers(): MCPServerRow[] { |
| 123 | + return Array.from(this.servers.values()); |
| 124 | + } |
| 125 | + |
| 126 | + getServerByCallbackUrl(callbackUrl: string): MCPServerRow | null { |
| 127 | + for (const server of this.servers.values()) { |
| 128 | + if (server.callback_url === callbackUrl) { |
| 129 | + return server; |
| 130 | + } |
| 131 | + } |
| 132 | + return null; |
| 133 | + } |
| 134 | + |
| 135 | + clearOAuthCredentials(serverId: string): void { |
| 136 | + const server = this.servers.get(serverId); |
| 137 | + if (server) { |
| 138 | + server.callback_url = ""; |
| 139 | + server.auth_url = null; |
| 140 | + this.servers.set(serverId, server); |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +// Use custom storage with MCPClientManager |
| 146 | +const manager = new MCPClientManager("my-client", "1.0.0", { |
| 147 | + storage: new CustomStorageAdapter(), |
| 148 | +}); |
| 149 | +``` |
| 150 | + |
| 151 | +</TypeScriptExample> |
| 152 | + |
| 153 | +## Storage Operations |
| 154 | + |
| 155 | +### Server Lifecycle |
| 156 | + |
| 157 | +The storage adapter manages server configuration throughout its lifecycle: |
| 158 | + |
| 159 | +1. **Creation**: `saveServer()` is called when connecting to a new server via `addMcpServer()` |
| 160 | +2. **OAuth Flow**: `clearOAuthCredentials()` is called after successful authentication to prevent replay attacks |
| 161 | +3. **Restoration**: `listServers()` is called on Agent initialization to restore connections |
| 162 | +4. **Removal**: `removeServer()` is called when disconnecting via `removeMcpServer()` |
| 163 | + |
| 164 | +### Security Considerations |
| 165 | + |
| 166 | +The `clearOAuthCredentials()` method is called after successful OAuth authentication to clear both `callback_url` and `auth_url`. This prevents: |
| 167 | + |
| 168 | +- Malicious second callback attempts from being processed |
| 169 | +- Unnecessary re-authentication prompts on reconnection |
| 170 | +- OAuth tokens from being exposed in storage |
| 171 | + |
| 172 | +## Migration Notes |
| 173 | + |
| 174 | +If you are using `MCPClientManager` directly (not through the Agent class), you must update your code to provide a storage adapter: |
| 175 | + |
| 176 | +**Before:** |
| 177 | + |
| 178 | +```ts |
| 179 | +const manager = new MCPClientManager("client-name", "1.0.0"); |
| 180 | +``` |
| 181 | + |
| 182 | +**After:** |
| 183 | + |
| 184 | +```ts |
| 185 | +import { MCPClientManager, AgentMCPStorageAdapter } from "agents/mcp"; |
| 186 | + |
| 187 | +const manager = new MCPClientManager("client-name", "1.0.0", { |
| 188 | + storage: new AgentMCPStorageAdapter(sqlFunction), |
| 189 | +}); |
| 190 | +``` |
| 191 | + |
| 192 | +This change does not affect normal Agent usage - storage is handled automatically when using `this.addMcpServer()`. |
| 193 | + |
| 194 | +## Next Steps |
| 195 | + |
| 196 | +- [MCP Client API reference](/agents/model-context-protocol/mcp-client-api/) — Full API documentation |
| 197 | +- [OAuth handling guide](/agents/guides/oauth-mcp-client/) — Complete OAuth integration guide |
| 198 | +- [Connect your first MCP server](/agents/guides/connect-mcp-client/) — Tutorial to get started |
0 commit comments