MCP client¶
DeepAgents ships a built-in Model Context Protocol client that connects to any number of stdio or HTTP MCP servers, exposes their tools as native AgentTool instances, and isolates per-server failures so one bad server does not break the rest.
MultiServerMCPClient¶
public actor MultiServerMCPClient {
public init(
configs: [MCPServerConfig],
openURL: @escaping (URL) -> Void = { _ in }
)
public func tools() async -> [any AgentTool]
public func load() async -> (tools: [any AgentTool], statuses: [MCPServerStatus])
public func disconnectAll() async
}
| Method | Meaning |
|---|---|
init(configs:openURL:) |
Create the client with a list of server configs. openURL is called when an HTTP/OAuth flow needs to open a browser URL. |
tools() |
Connect to all enabled servers (if not already connected) and return the merged tool list. Failures are logged and skipped. |
load() |
Like tools(), but also returns a [MCPServerStatus] array so callers can inspect which servers connected successfully and which failed. |
disconnectAll() |
Gracefully disconnect all active sessions. Call this when your agent is done to avoid leaking subprocess or network connections. |
MCPServerConfig¶
public struct MCPServerConfig: Codable, Sendable, Identifiable {
public enum Kind: String { case stdio, http }
public enum Auth: String { case none, oauth }
public var id: UUID
public var name: String
public var kind: Kind
public var isEnabled: Bool
// stdio fields
public var command: String
public var args: [String]
public var env: [String: String]
// http fields
public var url: String
public var headers: [String: String]
public var auth: Auth
// shared
public var approvalMode: ToolApprovalMode
}
Kind.stdio - local subprocess¶
The client launches command with args and communicates over stdin/stdout using the MCP stdio transport. env is merged into the subprocess environment.
MCPServerConfig(
id: UUID(),
name: "filesystem",
kind: .stdio,
isEnabled: true,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
env: [:],
url: "", headers: [:], auth: .none,
approvalMode: .ask
)
Kind.http - remote server¶
The client connects to url over HTTP using the MCP Streamable HTTP transport (or SSE, depending on the server). headers are sent with every request; auth: .oauth triggers an OAuth 2.0 flow that calls openURL when a browser redirect is needed.
MCPServerConfig(
id: UUID(),
name: "remote-search",
kind: .http,
isEnabled: true,
command: "", args: [], env: [:],
url: "https://my-mcp-server.example.com/mcp",
headers: ["Authorization": "Bearer \(token)"],
auth: .none,
approvalMode: .approve
)
approvalMode¶
Each server config carries an approvalMode: ToolApprovalMode that is used by AgentToolPolicy when the host manages a policy-driven approval flow. See Tools & policy for details on ToolApprovalMode.
Tool namespacing¶
Tools from an MCP server are namespaced by prepending the server name and a double underscore:
For example, a server named filesystem with a tool named read_file becomes filesystem__read_file in the agent's tool set. This prevents name collisions across servers and makes it clear in the conversation history which server handled each call.
Per-server failure isolation¶
If a server fails to connect or crashes mid-run, that server's tools become unavailable but all other servers continue operating normally. The failure is logged; the agent does not see an error unless it actually tries to call one of the unavailable tools.
This means you can list experimental or optional servers in your config without risking the whole agent run if one is offline.
Lifecycle¶
The recommended pattern:
import DeepAgents
// 1. Build the client
let mcpClient = MultiServerMCPClient(configs: [
MCPServerConfig(
id: UUID(), name: "search", kind: .stdio, isEnabled: true,
command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"],
env: ["BRAVE_API_KEY": ProcessInfo.processInfo.environment["BRAVE_API_KEY"] ?? ""],
url: "", headers: [:], auth: .none, approvalMode: .ask
)
])
// 2. Connect and get tools (with status)
let (mcpTools, statuses) = await mcpClient.load()
for status in statuses {
print("\(status.name): \(status.isConnected ? "ok" : "failed")")
}
// 3. Wrap tools in MCPMiddleware and pass to the agent
let agent = createDeepAgent(
model: model,
middleware: [MCPMiddleware(tools: mcpTools)]
)
// 4. Run
let ok = await agent.run([.human("Search for Swift 6 concurrency improvements")]) { event in
// handle events
}
// 5. Disconnect
await mcpClient.disconnectAll()
Use load() over tools() in production
load() returns connection statuses alongside the tools, letting you log or surface server failures before the agent run starts. tools() is a convenience shortcut when you don't need the status array.
MCPMiddleware¶
public struct MCPMiddleware: AgentMiddleware {
public init(tools: [any AgentTool])
public var tools: [any AgentTool]
}
MCPMiddleware is a thin wrapper that registers the MCP tools into the agent's tool set. It carries no hooks (no beforeModel, no wrapToolCall); it exists purely to deliver tools through the standard middleware pipeline. Tool execution calls the underlying MCP session transparently.
MCPMiddleware vs. catalog middleware
Built-in toolsets like WebToolsMiddleware and FilesystemMiddleware are in the middleware catalog and can be referenced by string ID in AgentToolPolicy. MCPMiddleware is not in the catalog - it is an ad-hoc middleware you construct at runtime with whatever tools MultiServerMCPClient returned.
Complete example with createDeepAgent¶
import DeepAgents
import DeepAgentsAnthropic
let mcpClient = MultiServerMCPClient(configs: [
MCPServerConfig(
id: UUID(), name: "notes", kind: .stdio, isEnabled: true,
command: "/usr/local/bin/my-notes-server", args: [],
env: [:], url: "", headers: [:], auth: .none, approvalMode: .ask
)
])
let mcpTools = await mcpClient.tools()
let model = AnthropicChatModel(
baseURL: URL(string: "https://api.anthropic.com")!,
model: "claude-opus-4-8",
apiKey: ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"]
)
let agent = createDeepAgent(
model: model,
middleware: [MCPMiddleware(tools: mcpTools)],
includeGeneralPurpose: true
)
_ = await agent.run([.human("List my recent notes")]) { _ in }
await mcpClient.disconnectAll()
The notes__list_notes, notes__read_note, etc. tools are now available to the agent exactly like built-in tools.
See Connect MCP servers for a step-by-step setup guide including OAuth configuration and policy-driven approval modes.