主题
Swift SDK
Model Context Protocol (MCP) 的 Swift SDK 提供了构建 MCP 服务器和客户端的现代 Swift 解决方案,支持 async/await、类型安全和 Swift 并发特性。
安装
Swift Package Manager
在 Package.swift
中添加依赖:
swift
dependencies: [
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "1.0.0")
]
在目标中添加产品:
swift
.target(
name: "MyMcpServer",
dependencies: [
.product(name: "ModelContextProtocol", package: "swift-sdk")
]
)
Xcode 项目
- 在 Xcode 中打开项目
- 选择 File → Add Package Dependencies
- 输入 URL:
https://github.com/modelcontextprotocol/swift-sdk.git
- 选择版本并添加到项目
快速开始
创建 MCP 服务器
swift
import ModelContextProtocol
import Foundation
@main
struct MyMcpServer {
static func main() async throws {
// 创建服务器
let server = McpServer(
name: "my-swift-server",
version: "1.0.0",
description: "Swift MCP 服务器示例"
)
// 注册工具
server.registerTool(EchoTool())
// 启动服务器
try await server.start()
}
}
struct EchoRequest: Codable {
let message: String
}
struct EchoResponse: Codable {
let echo: String
}
struct EchoTool: Tool {
let name = "echo"
let description = "回显输入的消息"
func inputSchema() -> JSONSchema {
return JSONSchema.object([
"message": .string(description: "要回显的消息")
], required: ["message"])
}
func call(with arguments: [String: Any]) async throws -> ToolResult {
guard let message = arguments["message"] as? String else {
return .error("缺少 message 参数")
}
let response = EchoResponse(echo: message)
return .success(response)
}
}
创建 MCP 客户端
swift
import ModelContextProtocol
import Foundation
@main
struct MyMcpClient {
static func main() async throws {
// 创建传输层
let transport = StdioTransport()
// 创建客户端
let client = McpClient(
transport: transport,
connectTimeout: .seconds(10),
requestTimeout: .seconds(30)
)
do {
// 连接到服务器
try await client.connect()
// 列出可用工具
let tools = try await client.listTools()
print("可用工具: \(tools.map(\.name).joined(separator: ", "))")
// 调用工具
let arguments = ["message": "Hello from Swift!"]
let result = try await client.callTool(name: "echo", arguments: arguments)
print("工具调用结果: \(result)")
} catch {
print("错误: \(error)")
}
await client.close()
}
}
核心功能
服务器功能
工具注册
swift
// 计算器工具
struct CalculatorRequest: Codable {
let operation: Operation
let a: Double
let b: Double
enum Operation: String, Codable {
case add, subtract, multiply, divide
}
}
struct CalculatorResponse: Codable {
let result: Double
}
struct CalculatorTool: Tool {
let name = "calculator"
let description = "执行基本数学运算"
func inputSchema() -> JSONSchema {
return JSONSchema.object([
"operation": .string(enum: ["add", "subtract", "multiply", "divide"]),
"a": .number(description: "第一个数字"),
"b": .number(description: "第二个数字")
], required: ["operation", "a", "b"])
}
func call(with arguments: [String: Any]) async throws -> ToolResult {
guard let operationString = arguments["operation"] as? String,
let operation = CalculatorRequest.Operation(rawValue: operationString),
let a = arguments["a"] as? Double,
let b = arguments["b"] as? Double else {
return .error("无效的参数")
}
let result: Double
switch operation {
case .add:
result = a + b
case .subtract:
result = a - b
case .multiply:
result = a * b
case .divide:
guard b != 0 else {
return .error("除数不能为零")
}
result = a / b
}
let response = CalculatorResponse(result: result)
return .success(response)
}
}
// 注册工具
server.registerTool(CalculatorTool())
资源管理
swift
import Foundation
class FileResourceProvider: ResourceProvider {
private let basePath: URL
init(basePath: URL) {
self.basePath = basePath
}
func getResource(uri: ResourceURI) async throws -> Resource {
let filePath = basePath.appendingPathComponent(uri.path.dropFirst())
guard FileManager.default.fileExists(atPath: filePath.path) else {
throw ResourceError.notFound("资源不存在: \(uri)")
}
let content = try String(contentsOf: filePath)
return Resource(
uri: uri,
mimeType: "text/plain",
content: content
)
}
func listResources() async throws -> [ResourceURI] {
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(at: basePath, includingPropertiesForKeys: nil)
var resources: [ResourceURI] = []
while let fileURL = enumerator?.nextObject() as? URL {
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: fileURL.path, isDirectory: &isDirectory),
!isDirectory.boolValue {
let relativePath = fileURL.path.replacingOccurrences(
of: basePath.path,
with: ""
)
resources.append(ResourceURI("file://\(relativePath)"))
}
}
return resources
}
}
// 注册资源提供者
server.registerResourceProvider(FileResourceProvider(basePath: URL(fileURLWithPath: "/path/to/files")))
提示模板
swift
struct CodeReviewRequest: Codable {
let code: String
let language: String
}
class CodeReviewPromptProvider: PromptProvider {
func getPrompt(name: String, arguments: [String: Any]) async throws -> Prompt {
switch name {
case "code-review":
return try await createCodeReviewPrompt(arguments: arguments)
default:
throw PromptError.notFound("未知提示: \(name)")
}
}
func listPrompts() async throws -> [String] {
return ["code-review"]
}
private func createCodeReviewPrompt(arguments: [String: Any]) async throws -> Prompt {
guard let code = arguments["code"] as? String else {
throw PromptError.invalidArguments("缺少 code 参数")
}
let language = arguments["language"] as? String ?? "unknown"
let content = """
请审查以下 \(language) 代码并提供改进建议:
```\(language)
\(code)
```
"""
return Prompt(
name: "code-review",
description: "代码审查提示",
content: content
)
}
}
// 注册提示提供者
server.registerPromptProvider(CodeReviewPromptProvider())
客户端功能
连接管理
swift
// 自动重连客户端
let client = McpClient(
transport: transport,
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: .seconds(2)
)
// 连接状态监听
client.onConnected = {
print("已连接到服务器")
}
client.onDisconnected = {
print("与服务器断开连接")
}
client.onError = { error in
print("连接错误: \(error)")
}
并发操作
swift
// 并发调用多个工具
async let result1 = client.callTool(name: "tool1", arguments: args1)
async let result2 = client.callTool(name: "tool2", arguments: args2)
async let result3 = client.callTool(name: "tool3", arguments: args3)
let results = try await [result1, result2, result3]
for (index, result) in results.enumerated() {
print("结果 \(index + 1): \(result)")
}
流式操作
swift
// 流式工具调用
for try await chunk in client.callToolStream(name: "streaming-tool", arguments: args) {
print(chunk, terminator: "")
}
传输协议
Stdio 传输
swift
let transport = StdioTransport(
command: ["python", "server.py"],
workingDirectory: URL(fileURLWithPath: "/path/to/server"),
environment: ["ENV_VAR": "value"]
)
WebSocket 传输
swift
let transport = WebSocketTransport(
url: URL(string: "ws://localhost:8080/mcp")!,
headers: ["Authorization": "Bearer token"],
connectTimeout: .seconds(10)
)
HTTP SSE 传输
swift
let transport = SSETransport(
url: URL(string: "http://localhost:8080/mcp")!,
headers: ["Authorization": "Bearer token"],
readTimeout: .seconds(30)
)
高级特性
依赖注入支持
swift
import Swinject
// 创建容器
let container = Container()
// 注册服务
container.register(DatabaseService.self) { _ in
DatabaseService()
}.inObjectScope(.container)
container.register(EmailService.self) { _ in
EmailService()
}.inObjectScope(.container)
// 注册工具
container.register(DatabaseTool.self) { resolver in
DatabaseTool(databaseService: resolver.resolve(DatabaseService.self)!)
}
container.register(EmailTool.self) { resolver in
EmailTool(emailService: resolver.resolve(EmailService.self)!)
}
// 创建服务器
let server = McpServer(name: "my-server", version: "1.0.0")
// 注册工具
server.registerTool(container.resolve(DatabaseTool.self)!)
server.registerTool(container.resolve(EmailTool.self)!)
// 带依赖注入的工具
class DatabaseTool: Tool {
let name = "database-query"
let description = "执行数据库查询"
private let databaseService: DatabaseService
init(databaseService: DatabaseService) {
self.databaseService = databaseService
}
func inputSchema() -> JSONSchema {
return JSONSchema.object([
"query": .string(description: "SQL 查询语句")
], required: ["query"])
}
func call(with arguments: [String: Any]) async throws -> ToolResult {
guard let query = arguments["query"] as? String else {
return .error("缺少 query 参数")
}
do {
let result = try await databaseService.executeQuery(query)
return .success(result)
} catch {
return .error("查询执行失败: \(error.localizedDescription)")
}
}
}
中间件支持
swift
protocol Middleware {
func handle(request: Request, next: @escaping (Request) async throws -> Response) async throws -> Response
}
class LoggingMiddleware: Middleware {
func handle(request: Request, next: @escaping (Request) async throws -> Response) async throws -> Response {
print("处理请求: \(request.method)")
let startTime = Date()
do {
let response = try await next(request)
let duration = Date().timeIntervalSince(startTime)
print("请求完成,耗时: \(Int(duration * 1000))ms")
return response
} catch {
let duration = Date().timeIntervalSince(startTime)
print("请求处理失败,耗时: \(Int(duration * 1000))ms")
throw error
}
}
}
// 注册中间件
server.use(LoggingMiddleware())
server.use(AuthenticationMiddleware())
server.use(RateLimitingMiddleware())
配置管理
swift
struct McpConfig: Codable {
let server: ServerConfig
let logging: LoggingConfig
let transport: TransportConfig
struct ServerConfig: Codable {
let name: String
let version: String
let description: String
let maxConnections: Int
let requestTimeoutSeconds: Int
}
struct LoggingConfig: Codable {
let level: String
let file: String?
}
struct TransportConfig: Codable {
let type: String
let bufferSize: Int
}
}
// 加载配置
func loadConfig() throws -> McpConfig {
let configURL = URL(fileURLWithPath: "config.json")
let data = try Data(contentsOf: configURL)
return try JSONDecoder().decode(McpConfig.self, from: data)
}
// 使用配置创建服务器
let config = try loadConfig()
let server = McpServer(
name: config.server.name,
version: config.server.version,
description: config.server.description,
maxConnections: config.server.maxConnections,
requestTimeout: .seconds(config.server.requestTimeoutSeconds)
)
Actor 并发安全
swift
actor SafeCounter {
private var value = 0
func increment() -> Int {
value += 1
return value
}
func getValue() -> Int {
return value
}
}
class CounterTool: Tool {
let name = "counter"
let description = "线程安全的计数器"
private let counter = SafeCounter()
func inputSchema() -> JSONSchema {
return JSONSchema.object([
"action": .string(enum: ["increment", "get"])
], required: ["action"])
}
func call(with arguments: [String: Any]) async throws -> ToolResult {
guard let action = arguments["action"] as? String else {
return .error("缺少 action 参数")
}
switch action {
case "increment":
let newValue = await counter.increment()
return .success(["value": newValue])
case "get":
let currentValue = await counter.getValue()
return .success(["value": currentValue])
default:
return .error("不支持的操作: \(action)")
}
}
}
测试
单元测试
swift
import XCTest
@testable import ModelContextProtocol
final class EchoToolTests: XCTestCase {
func testCallWithValidMessage() async throws {
// Arrange
let tool = EchoTool()
let arguments = ["message": "test"]
// Act
let result = try await tool.call(with: arguments)
// Assert
switch result {
case .success(let data):
let response = try JSONDecoder().decode(EchoResponse.self, from: JSONSerialization.data(withJSONObject: data))
XCTAssertEqual(response.echo, "test")
case .error(let message):
XCTFail("期望成功,但得到错误: \(message)")
}
}
func testCallWithMissingMessage() async throws {
// Arrange
let tool = EchoTool()
let arguments: [String: Any] = [:]
// Act
let result = try await tool.call(with: arguments)
// Assert
switch result {
case .success:
XCTFail("期望错误,但得到成功")
case .error(let message):
XCTAssertTrue(message.contains("缺少 message 参数"))
}
}
}
集成测试
swift
import XCTest
@testable import ModelContextProtocol
final class McpIntegrationTests: XCTestCase {
var server: McpServer!
var client: McpClient!
override func setUp() async throws {
// 启动测试服务器
server = McpServer(name: "test-server", version: "1.0.0")
server.registerTool(EchoTool())
Task {
try await server.start()
}
// 等待服务器启动
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
// 创建客户端
let transport = StdioTransport()
client = McpClient(transport: transport)
try await client.connect()
}
override func tearDown() async throws {
await client?.close()
await server?.stop()
}
func testToolCall() async throws {
// Arrange
let arguments = ["message": "test"]
// Act
let result = try await client.callTool(name: "echo", arguments: arguments)
// Assert
XCTAssertNotNil(result)
// 验证结果内容
}
}
部署
iOS 应用集成
swift
import SwiftUI
import ModelContextProtocol
@main
struct McpApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject private var mcpManager = McpManager()
@State private var message = ""
@State private var response = ""
var body: some View {
VStack {
TextField("输入消息", text: $message)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("发送") {
Task {
response = await mcpManager.sendMessage(message)
}
}
.padding()
Text("响应: \(response)")
.padding()
Spacer()
}
.onAppear {
Task {
await mcpManager.connect()
}
}
}
}
@MainActor
class McpManager: ObservableObject {
private var client: McpClient?
func connect() async {
let transport = StdioTransport()
client = McpClient(transport: transport)
do {
try await client?.connect()
} catch {
print("连接失败: \(error)")
}
}
func sendMessage(_ message: String) async -> String {
guard let client = client else { return "未连接" }
do {
let result = try await client.callTool(
name: "echo",
arguments: ["message": message]
)
return "\(result)"
} catch {
return "错误: \(error)"
}
}
}
macOS 命令行工具
swift
import Foundation
import ArgumentParser
import ModelContextProtocol
@main
struct McpCLI: AsyncParsableCommand {
@Option(help: "服务器命令")
var serverCommand: String = "python server.py"
@Option(help: "工具名称")
var tool: String
@Option(help: "工具参数 (JSON 格式)")
var arguments: String = "{}"
func run() async throws {
let transport = StdioTransport(command: serverCommand.components(separatedBy: " "))
let client = McpClient(transport: transport)
do {
try await client.connect()
guard let data = arguments.data(using: .utf8),
let args = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw ValidationError("无效的 JSON 参数")
}
let result = try await client.callTool(name: tool, arguments: args)
let resultData = try JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
if let resultString = String(data: resultData, encoding: .utf8) {
print(resultString)
}
} catch {
print("错误: \(error)")
throw ExitCode.failure
}
await client.close()
}
}
Docker 部署
dockerfile
FROM swift:5.9-slim
WORKDIR /app
COPY Package.swift .
COPY Sources ./Sources
RUN swift build -c release
EXPOSE 8080
CMD [".build/release/McpServer"]