Initial commit

This commit is contained in:
Mattt Zmuda 2025-02-05 11:13:57 -08:00
commit cc603d35fe
No known key found for this signature in database
8 changed files with 970 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

31
Package.swift Normal file
View file

@ -0,0 +1,31 @@
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "mcp-swift-sdk",
platforms: [
.macOS(.v12),
.iOS(.v15),
.tvOS(.v15),
.watchOS(.v8)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MCP",
targets: ["MCP"]),
],
dependencies: [],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MCP",
dependencies: []),
.testTarget(
name: "MCPTests",
dependencies: ["MCP"]),
]
)

115
README.md Normal file
View file

@ -0,0 +1,115 @@
# MCP Swift SDK
A Swift implementation of the Model Context Protocol (MCP) client. This SDK provides a modern, Swift-native way to interact with MCP servers.
## Features
- Full async/await support
- Type-safe API
- Resource management
- Tool integration
- Configurable transport layer
- Swift concurrency with actor-based design
## Requirements
- Swift 5.7+
- macOS 12.0+
- iOS 15.0+
- tvOS 15.0+
- watchOS 8.0+
## Installation
### Swift Package Manager
Add the following to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/yourusername/mcp-swift-sdk.git", from: "1.0.0")
]
```
## Usage
### Basic Setup
```swift
import MCP
// Create a transport (e.g., StdioTransport)
let transport = StdioTransport(command: "/path/to/server")
// Create client info
let clientInfo = Implementation(name: "MyApp", version: "1.0.0")
// Initialize the client
let client = MCPClient(transport: transport, clientInfo: clientInfo)
// Connect and initialize
try await client.connect()
let result = try await client.initialize()
```
### Working with Resources
```swift
// List available resources
let resources = try await client.listResources()
// Read a resource
let resource = try await client.readResource("resource://example")
// Subscribe to resource updates
try await client.subscribeResource("resource://example")
// Unsubscribe from resource updates
try await client.unsubscribeResource("resource://example")
```
### Working with Tools
```swift
// List available tools
let tools = try await client.listTools()
// Call a tool
let result = try await client.callTool(name: "example-tool", arguments: ["key": "value"])
```
### Custom Transport
You can create your own transport by implementing the `MCPTransport` protocol:
```swift
public protocol MCPTransport {
func connect() async throws
func disconnect() async
func sendRequest(_ data: Data) async throws -> Data
func sendNotification(_ data: Data) async throws
}
```
## Error Handling
The SDK uses the `MCPError` type for error handling:
```swift
public enum MCPError: LocalizedError {
case connectionClosed
case invalidResponse
case invalidParams(String)
case serverError(String)
case protocolError(String)
case transportError(Error)
}
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.

364
Sources/MCP/MCP.swift Normal file
View file

@ -0,0 +1,364 @@
import Foundation
/// Protocol version constants
public enum MCPVersion {
public static let latest = "1.0.0"
public static let supported = ["1.0.0"]
}
/// MCP error types
public enum MCPError: LocalizedError {
case connectionClosed
case invalidResponse
case invalidParams(String)
case serverError(String)
case protocolError(String)
case transportError(Error)
public var errorDescription: String? {
switch self {
case .connectionClosed:
return "Connection closed"
case .invalidResponse:
return "Invalid response from server"
case .invalidParams(let message):
return "Invalid parameters: \(message)"
case .serverError(let message):
return "Server error: \(message)"
case .protocolError(let message):
return "Protocol error: \(message)"
case .transportError(let error):
return "Transport error: \(error.localizedDescription)"
}
}
}
/// Base protocol for MCP messages
public protocol MCPMessage: Codable {
var jsonrpc: String { get }
}
/// Protocol for MCP requests
public protocol MCPRequest: MCPMessage {
var id: String { get }
var method: String { get }
}
/// Protocol for MCP notifications
public protocol MCPNotification: MCPMessage {
var method: String { get }
}
/// Protocol for MCP responses
public protocol MCPResponse: MCPMessage {
var id: String { get }
}
/// Implementation information
public struct Implementation: Codable {
public let name: String
public let version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// Client capabilities
public struct ClientCapabilities: Codable {
public var sampling: Bool?
public var experimental: [String: [String: Any]]?
public var roots: RootsCapability?
public init(sampling: Bool? = nil,
experimental: [String: [String: Any]]? = nil,
roots: RootsCapability? = nil) {
self.sampling = sampling
self.experimental = experimental
self.roots = roots
}
enum CodingKeys: String, CodingKey {
case sampling
case experimental
case roots
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
sampling = try container.decodeIfPresent(Bool.self, forKey: .sampling)
experimental = try container.decodeIfPresent([String: [String: Any]].self, forKey: .experimental)
roots = try container.decodeIfPresent(RootsCapability.self, forKey: .roots)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(sampling, forKey: .sampling)
try container.encodeIfPresent(experimental, forKey: .experimental)
try container.encodeIfPresent(roots, forKey: .roots)
}
}
/// Roots capability configuration
public struct RootsCapability: Codable {
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Server capabilities
public struct ServerCapabilities: Codable {
public var logging: Bool?
public var prompts: Bool?
public var resources: ResourcesCapability?
public var tools: Bool?
public init(logging: Bool? = nil,
prompts: Bool? = nil,
resources: ResourcesCapability? = nil,
tools: Bool? = nil) {
self.logging = logging
self.prompts = prompts
self.resources = resources
self.tools = tools
}
}
/// Resources capability configuration
public struct ResourcesCapability: Codable {
public var subscribe: Bool?
public init(subscribe: Bool? = nil) {
self.subscribe = subscribe
}
}
/// Initialize request parameters
public struct InitializeRequestParams: Codable {
public let protocolVersion: String
public let capabilities: ClientCapabilities
public let clientInfo: Implementation
public init(protocolVersion: String = MCPVersion.latest,
capabilities: ClientCapabilities,
clientInfo: Implementation) {
self.protocolVersion = protocolVersion
self.capabilities = capabilities
self.clientInfo = clientInfo
}
}
/// Initialize request
public struct InitializeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "initialize"
public let params: InitializeRequestParams
public init(id: String = UUID().uuidString, params: InitializeRequestParams) {
self.id = id
self.params = params
}
}
/// Initialize result
public struct InitializeResult: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: InitializeResultData
public struct InitializeResultData: Codable {
public let protocolVersion: String
public let capabilities: ServerCapabilities
public let serverInfo: Implementation
public let instructions: String?
}
}
/// MCP Client protocol
public protocol MCPClientProtocol {
func connect() async throws
func disconnect() async
func initialize() async throws -> InitializeResult
func ping() async throws
// Resource methods
func listResources() async throws -> ListResourcesResponse
func readResource(_ uri: String) async throws -> ReadResourceResponse
func subscribeResource(_ uri: String) async throws
func unsubscribeResource(_ uri: String) async throws
// Tool methods
func listTools() async throws -> ListToolsResponse
func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse
}
/// MCP Client implementation
public actor MCPClient: MCPClientProtocol {
private let transport: MCPTransport
private let clientInfo: Implementation
private var capabilities: ClientCapabilities
private var serverCapabilities: ServerCapabilities?
private var serverVersion: Implementation?
private var instructions: String?
public init(transport: MCPTransport,
clientInfo: Implementation,
capabilities: ClientCapabilities = ClientCapabilities()) {
self.transport = transport
self.clientInfo = clientInfo
self.capabilities = capabilities
}
public func connect() async throws {
try await transport.connect()
}
public func disconnect() async {
await transport.disconnect()
}
public func initialize() async throws -> InitializeResult {
let request = InitializeRequest(params: InitializeRequestParams(
capabilities: capabilities,
clientInfo: clientInfo
))
let response: InitializeResult = try await sendRequest(request)
// Validate protocol version
guard MCPVersion.supported.contains(response.result.protocolVersion) else {
throw MCPError.protocolError("Unsupported protocol version: \(response.result.protocolVersion)")
}
// Store server information
serverCapabilities = response.result.capabilities
serverVersion = response.result.serverInfo
instructions = response.result.instructions
// Send initialized notification
try await sendNotification(InitializedNotification())
return response
}
public func ping() async throws {
let request = PingRequest()
try await sendRequest(request)
}
// MARK: - Resource Methods
public func listResources() async throws -> ListResourcesResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ListResourcesRequest()
return try await sendRequest(request)
}
public func readResource(_ uri: String) async throws -> ReadResourceResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ReadResourceRequest(uri: uri)
return try await sendRequest(request)
}
public func subscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = SubscribeRequest(uri: uri)
try await sendRequest(request)
}
public func unsubscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = UnsubscribeRequest(uri: uri)
try await sendRequest(request)
}
// MARK: - Tool Methods
public func listTools() async throws -> ListToolsResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = ListToolsRequest()
return try await sendRequest(request)
}
public func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = CallToolRequest(name: name, arguments: arguments)
return try await sendRequest(request)
}
private func sendRequest<T: MCPRequest, U: MCPResponse>(_ request: T) async throws -> U {
let data = try JSONEncoder().encode(request)
let responseData = try await transport.sendRequest(data)
return try JSONDecoder().decode(U.self, from: responseData)
}
private func sendNotification<T: MCPNotification>(_ notification: T) async throws {
let data = try JSONEncoder().encode(notification)
try await transport.sendNotification(data)
}
// MARK: - Public Properties
/// Get the server's capabilities after initialization
public var serverCapabilitiesAfterInit: ServerCapabilities? {
serverCapabilities
}
/// Get the server's version information after initialization
public var serverVersionAfterInit: Implementation? {
serverVersion
}
/// Get the server's instructions after initialization
public var serverInstructions: String? {
instructions
}
}
/// MCP Transport protocol
public protocol MCPTransport {
func connect() async throws
func disconnect() async
func sendRequest(_ data: Data) async throws -> Data
func sendNotification(_ data: Data) async throws
}
/// Basic notification types
public struct InitializedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/initialized"
}
public struct PingRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "ping"
public init(id: String = UUID().uuidString) {
self.id = id
}
}

166
Sources/MCP/Resources.swift Normal file
View file

@ -0,0 +1,166 @@
import Foundation
/// Resource metadata
public struct ResourceMetadata: Codable {
public var name: String
public var uri: String
public var metadata: [String: String]?
public init(name: String, uri: String, metadata: [String: String]? = nil) {
self.name = name
self.uri = uri
self.metadata = metadata
}
}
/// Resource content types
public enum ResourceContent: Codable {
case text(String)
case binary(Data)
case embedded([EmbeddedResource])
private enum CodingKeys: String, CodingKey {
case type
case content
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let content = try container.decode(String.self, forKey: .content)
self = .text(content)
case "binary":
let content = try container.decode(Data.self, forKey: .content)
self = .binary(content)
case "embedded":
let content = try container.decode([EmbeddedResource].self, forKey: .content)
self = .embedded(content)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown resource type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let content):
try container.encode("text", forKey: .type)
try container.encode(content, forKey: .content)
case .binary(let content):
try container.encode("binary", forKey: .type)
try container.encode(content, forKey: .content)
case .embedded(let content):
try container.encode("embedded", forKey: .type)
try container.encode(content, forKey: .content)
}
}
}
/// Embedded resource type
public struct EmbeddedResource: Codable {
public var type: String
public var data: String
public var metadata: [String: String]?
public init(type: String, data: String, metadata: [String: String]? = nil) {
self.type = type
self.data = data
self.metadata = metadata
}
}
/// Resource request types
public struct ListResourcesRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/list"
public init(id: String = UUID().uuidString) {
self.id = id
}
}
public struct ReadResourceRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/read"
public let params: ReadResourceParams
public struct ReadResourceParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = ReadResourceParams(uri: uri)
}
}
public struct SubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/subscribe"
public let params: SubscribeParams
public struct SubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = SubscribeParams(uri: uri)
}
}
public struct UnsubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/unsubscribe"
public let params: UnsubscribeParams
public struct UnsubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = UnsubscribeParams(uri: uri)
}
}
/// Resource response types
public struct ListResourcesResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ListResourcesResult
public struct ListResourcesResult: Codable {
public let resources: [ResourceMetadata]
}
}
public struct ReadResourceResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ReadResourceResult
public struct ReadResourceResult: Codable {
public let content: ResourceContent
}
}
/// Resource notification types
public struct ResourceUpdatedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/resource/updated"
public let params: ResourceUpdatedParams
public struct ResourceUpdatedParams: Codable {
public let uri: String
public let content: ResourceContent
}
}

View file

@ -0,0 +1,105 @@
import Foundation
/// A transport implementation that communicates over standard input/output
public actor StdioTransport: MCPTransport {
private var isConnected = false
private let processQueue = DispatchQueue(label: "com.mcp.stdio", qos: .userInitiated)
private let process: Process
private let inputPipe = Pipe()
private let outputPipe = Pipe()
private let errorPipe = Pipe()
public init(command: String, arguments: [String] = [], environment: [String: String]? = nil) {
self.process = Process()
self.process.executableURL = URL(fileURLWithPath: command)
self.process.arguments = arguments
if let env = environment {
self.process.environment = env
}
self.process.standardInput = inputPipe
self.process.standardOutput = outputPipe
self.process.standardError = errorPipe
}
public func connect() async throws {
guard !isConnected else { return }
try process.run()
isConnected = true
// Start reading output asynchronously
Task {
await readOutput()
}
}
public func disconnect() async {
guard isConnected else { return }
process.terminate()
isConnected = false
}
public func sendRequest(_ data: Data) async throws -> Data {
guard isConnected else {
throw MCPError.connectionClosed
}
// Write request
try await withCheckedThrowingContinuation { continuation in
processQueue.async {
do {
let message = data + "\n".data(using: .utf8)!
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
// Read response
if let response = try self.outputPipe.fileHandleForReading.readToEnd() {
continuation.resume(returning: response)
} else {
continuation.resume(throwing: MCPError.invalidResponse)
}
} catch {
continuation.resume(throwing: MCPError.transportError(error))
}
}
}
}
public func sendNotification(_ data: Data) async throws {
guard isConnected else {
throw MCPError.connectionClosed
}
try await withCheckedThrowingContinuation { continuation in
processQueue.async {
do {
let message = data + "\n".data(using: .utf8)!
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
continuation.resume()
} catch {
continuation.resume(throwing: MCPError.transportError(error))
}
}
}
}
private func readOutput() async {
let handle = outputPipe.fileHandleForReading
while isConnected {
do {
let data = try handle.read(upToCount: 4096)
if let data = data, !data.isEmpty {
// Process received data
// In a real implementation, you would parse the JSON-RPC messages
// and handle them appropriately
print("Received data: \(String(data: data, encoding: .utf8) ?? "")")
}
} catch {
print("Error reading output: \(error)")
break
}
}
}
}

175
Sources/MCP/Tools.swift Normal file
View file

@ -0,0 +1,175 @@
import Foundation
/// Tool metadata
public struct Tool: Codable {
public let name: String
public let description: String?
public let inputSchema: [String: Any]?
enum CodingKeys: String, CodingKey {
case name
case description
case inputSchema
}
public init(name: String, description: String? = nil, inputSchema: [String: Any]? = nil) {
self.name = name
self.description = description
self.inputSchema = inputSchema
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decodeIfPresent(String.self, forKey: .description)
inputSchema = try container.decodeIfPresent([String: Any].self, forKey: .inputSchema)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encodeIfPresent(description, forKey: .description)
if let schema = inputSchema {
try container.encode(schema, forKey: .inputSchema)
}
}
}
/// Tool request types
public struct ListToolsRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "tools/list"
public init(id: String = UUID().uuidString) {
self.id = id
}
}
public struct CallToolRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "tools/call"
public let params: CallToolParams
public struct CallToolParams: Codable {
public let name: String
public let arguments: [String: Any]?
enum CodingKeys: String, CodingKey {
case name
case arguments
}
public init(name: String, arguments: [String: Any]? = nil) {
self.name = name
self.arguments = arguments
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
arguments = try container.decodeIfPresent([String: Any].self, forKey: .arguments)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
if let args = arguments {
try container.encode(args, forKey: .arguments)
}
}
}
public init(id: String = UUID().uuidString, name: String, arguments: [String: Any]? = nil) {
self.id = id
self.params = CallToolParams(name: name, arguments: arguments)
}
}
/// Tool response types
public struct ListToolsResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ListToolsResult
public struct ListToolsResult: Codable {
public let tools: [Tool]
}
}
public struct CallToolResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: CallToolResult
public struct CallToolResult: Codable {
public let content: [ToolContent]
public let isError: Bool?
public init(content: [ToolContent], isError: Bool? = nil) {
self.content = content
self.isError = isError
}
}
}
/// Tool content types
public enum ToolContent: Codable {
case text(String)
case image(ImageContent)
private enum CodingKeys: String, CodingKey {
case type
case text
case image
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text)
case "image":
let image = try container.decode(ImageContent.self, forKey: .image)
self = .image(image)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown tool content type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let image):
try container.encode("image", forKey: .type)
try container.encode(image, forKey: .image)
}
}
}
/// Image content type
public struct ImageContent: Codable {
public let data: String
public let mimeType: String
public let metadata: [String: String]?
public init(data: String, mimeType: String, metadata: [String: String]? = nil) {
self.data = data
self.mimeType = mimeType
self.metadata = metadata
}
}
/// Tool notification types
public struct ToolListChangedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/tools/list_changed"
}

View file

@ -0,0 +1,6 @@
import Testing
@testable import MCP
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}