From cc603d35fe25f060a3350853968ff71bb08ca854 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 5 Feb 2025 11:13:57 -0800 Subject: [PATCH] Initial commit --- .gitignore | 8 + Package.swift | 31 +++ README.md | 115 ++++++++++ Sources/MCP/MCP.swift | 364 +++++++++++++++++++++++++++++++ Sources/MCP/Resources.swift | 166 ++++++++++++++ Sources/MCP/StdioTransport.swift | 105 +++++++++ Sources/MCP/Tools.swift | 175 +++++++++++++++ Tests/MCPTests/MCPTests.swift | 6 + 8 files changed, 970 insertions(+) create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/MCP/MCP.swift create mode 100644 Sources/MCP/Resources.swift create mode 100644 Sources/MCP/StdioTransport.swift create mode 100644 Sources/MCP/Tools.swift create mode 100644 Tests/MCPTests/MCPTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..7a3e270 --- /dev/null +++ b/Package.swift @@ -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"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccb738d --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/Sources/MCP/MCP.swift b/Sources/MCP/MCP.swift new file mode 100644 index 0000000..db6ddd6 --- /dev/null +++ b/Sources/MCP/MCP.swift @@ -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(_ 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(_ 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 + } +} diff --git a/Sources/MCP/Resources.swift b/Sources/MCP/Resources.swift new file mode 100644 index 0000000..3b7aced --- /dev/null +++ b/Sources/MCP/Resources.swift @@ -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 + } +} \ No newline at end of file diff --git a/Sources/MCP/StdioTransport.swift b/Sources/MCP/StdioTransport.swift new file mode 100644 index 0000000..b322e01 --- /dev/null +++ b/Sources/MCP/StdioTransport.swift @@ -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 + } + } + } +} \ No newline at end of file diff --git a/Sources/MCP/Tools.swift b/Sources/MCP/Tools.swift new file mode 100644 index 0000000..ec3adf9 --- /dev/null +++ b/Sources/MCP/Tools.swift @@ -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" +} \ No newline at end of file diff --git a/Tests/MCPTests/MCPTests.swift b/Tests/MCPTests/MCPTests.swift new file mode 100644 index 0000000..bd28e3a --- /dev/null +++ b/Tests/MCPTests/MCPTests.swift @@ -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. +}