A Swift 6 client library for the Model Context Protocol (MCP), enabling Swift applications to connect to MCP servers over HTTP/SSE, WebSocket, or stdio.
- Three transports — HTTP/SSE (production), WebSocket, and stdio (local dev)
- Full MCP spec coverage — tools, resources, prompts, sampling, roots, notifications, progress, autocompletion
- Swift 6 strict concurrency — actor-based client, all types
Sendable - Cross-platform — macOS, Linux (via AsyncHTTPClient + NIO)
- Testable —
MCPClientProtocolfor mocking in tests - Bi-directional — handles server-to-client requests (
roots/list,sampling/createMessage) - Auto-reconnect — HTTP/SSE transport reconnects with exponential backoff
- MCPExplorer — included SwiftUI macOS app for interactive server exploration
- Swift 6.0+
- macOS 14+ / iOS 17+ / tvOS 17+ / watchOS 10+
- Linux (Ubuntu 22.04+ tested)
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/jpurnell/SwiftMCPClient.git", branch: "main")
]Then add MCPClient to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "MCPClient", package: "SwiftMCPClient")
]
)import MCPClient
let transport = HTTPSSETransport(url: URL(string: "https://mcp.example.com/sse")!)
let client = MCPClientConnection(transport: transport)
// Initialize the connection
let serverInfo = try await client.initialize(
clientName: "MyApp",
clientVersion: "1.0.0"
)
// Discover available tools
let tools = try await client.listTools()
for tool in tools {
print("\(tool.name): \(tool.description ?? "")")
}
// Call a tool
let result = try await client.callTool(
name: "analyze_content",
arguments: ["url": .string("https://example.com")]
)
for content in result.content {
if case .text(let text, _) = content {
print(text)
}
}
// Disconnect when done
try await client.disconnect()let transport = WebSocketTransport(url: URL(string: "wss://mcp.example.com/ws")!)
let client = MCPClientConnection(transport: transport)
let _ = try await client.initialize(clientName: "MyApp", clientVersion: "1.0.0")let transport = StdioTransport(
command: "/usr/local/bin/my-mcp-server",
arguments: ["--mode", "stdio"]
)
let client = MCPClientConnection(transport: transport)
let _ = try await client.initialize(clientName: "MyApp", clientVersion: "1.0.0")| Transport | Use Case | Protocol | Auto-Reconnect |
|---|---|---|---|
HTTPSSETransport |
Remote servers (production) | GET for SSE stream, POST for requests | Yes |
WebSocketTransport |
Remote servers (bidirectional) | WebSocket text frames | No |
StdioTransport |
Local development | Subprocess stdin/stdout | No |
let transport = HTTPSSETransport(
url: URL(string: "https://mcp.example.com/sse")!,
headers: ["Authorization": "Bearer token123"],
connectionTimeout: 30,
maxReconnectAttempts: 3,
reconnectBaseDelay: 1.0,
trustSelfSignedCertificates: false // true only for dev/testing
)let tools = try await client.listTools()
let result = try await client.callTool(name: "score_page", arguments: [
"url": .string("https://example.com"),
"depth": .integer(2)
])let resources = try await client.listResources()
let contents = try await client.readResource(uri: "file:///config.json")
// Subscribe to resource changes
try await client.subscribeResource(uri: "file:///data.json")
for await uri in client.resourceUpdates {
print("Resource updated: \(uri)")
}let prompts = try await client.listPrompts()
let result = try await client.getPrompt(
name: "analyze",
arguments: ["topic": "performance"]
)
for message in result.messages {
print("\(message.role): \(message.content)")
}let completions = try await client.complete(
ref: .prompt(name: "analyze"),
argumentName: "topic",
argumentValue: "perf"
)
// completions.values: ["performance", "permissions", ...]// All notifications
for await notification in client.notifications {
switch notification {
case .progress(let p): print("Progress: \(p.progress)/\(p.total ?? 0)")
case .logMessage(let m): print("Log [\(m.level)]: \(m.data)")
case .toolsListChanged: print("Tools changed")
default: break
}
}
// Filtered streams
for await progress in client.progressUpdates { ... }
for await log in client.logMessages { ... }// Handle roots/list requests from the server
client.setRootsHandler {
return [MCPRoot(uri: "file:///project", name: "My Project")]
}
// Handle sampling requests from the server
client.setSamplingHandler { request in
// Forward to your LLM, return the response
return MCPSamplingResponse(
model: "claude-3",
role: .assistant,
content: .text("Response text")
)
}Use MCPClientProtocol to mock the client in your tests:
final class MockMCPClient: MCPClientProtocol {
func callTool(name: String, arguments: [String: AnyCodableValue]?) async throws -> MCPToolResult {
return MCPToolResult(content: [.text("mocked result")])
}
// ... implement other protocol methods
}do {
let result = try await client.callTool(name: "analyze", arguments: [:])
} catch let error as MCPError {
switch error {
case .connectionFailed(let reason): print("Connection failed: \(reason)")
case .requestFailed(let code, let message, _): print("RPC error \(code): \(message)")
case .timeout: print("Request timed out")
case .invalidResponse: print("Invalid server response")
case .transportClosed: print("Connection closed unexpectedly")
case .processSpawnFailed(let reason): print("Subprocess failed: \(reason)")
}
}The package includes MCPExplorer, a SwiftUI macOS app for interactively connecting to MCP servers, browsing tools/resources/prompts, and watching notifications in real time.
Build and run:
swift build --product MCPExplorer
.build/debug/MCPExplorerMCPClient/
├── MCPClientConnection # Actor — manages connection lifecycle and JSON-RPC
├── MCPClientProtocol # Protocol — testable interface
├── MCPMessageDispatcher # Request/response correlation
├── Transport/
│ ├── MCPTransport # Protocol — pluggable transport layer
│ ├── HTTPSSETransport # HTTP POST + Server-Sent Events
│ ├── WebSocketTransport # WebSocket text frames
│ ├── StdioTransport # Subprocess stdin/stdout
│ └── SSEParser # SSE event stream parser
├── AnyCodableValue # Type-safe JSON value wrapper
├── JSONRPCTypes # JSON-RPC 2.0 envelope types
├── MCPTypes # Capabilities, initialization
├── MCPError # Error enum
└── *Types.swift # Resources, prompts, sampling, roots, notifications, completion
MIT