diff --git a/Sources/MCP/Base/Error.swift b/Sources/MCP/Base/Error.swift index bba1543f..9830fca0 100644 --- a/Sources/MCP/Base/Error.swift +++ b/Sources/MCP/Base/Error.swift @@ -6,8 +6,24 @@ import Foundation @preconcurrency import SystemPackage #endif +/// Top-level namespace for backward compatibility +/// +/// This is provided to allow existing code that uses `MCP.Error` to continue +/// to work without modification. +/// +/// The MCPError type is now the recommended way to handle errors in MCP. +/// +/// - Warning: This namespace is deprecated and will be removed in a future version. +public enum MCP { + /// Deprecated type alias for MCPError + @available(*, deprecated, renamed: "MCPError", message: "Use MCPError instead of MCP.Error") + public typealias Error = MCPError +} + +// MARK: - + /// A model context protocol error. -public enum Error: Sendable { +public enum MCPError: Error, Sendable { // Standard JSON-RPC 2.0 errors (-32700 to -32603) case parseError(String?) // -32700 case invalidRequest(String?) // -32600 @@ -20,7 +36,7 @@ public enum Error: Sendable { // Transport specific errors case connectionClosed - case transportError(Swift.Error) + case transportError(Error) /// The JSON-RPC 2.0 error code public var code: Int { @@ -37,7 +53,7 @@ public enum Error: Sendable { } /// Check if an error represents a "resource temporarily unavailable" condition - public static func isResourceTemporarilyUnavailable(_ error: Swift.Error) -> Bool { + public static func isResourceTemporarilyUnavailable(_ error: Error) -> Bool { #if canImport(System) if let errno = error as? System.Errno, errno == .resourceTemporarilyUnavailable { return true @@ -53,7 +69,7 @@ public enum Error: Sendable { // MARK: LocalizedError -extension Error: LocalizedError { +extension MCPError: LocalizedError { public var errorDescription: String? { switch self { case .parseError(let detail): @@ -116,7 +132,7 @@ extension Error: LocalizedError { // MARK: CustomDebugStringConvertible -extension Error: CustomDebugStringConvertible { +extension MCPError: CustomDebugStringConvertible { public var debugDescription: String { switch self { case .transportError(let error): @@ -131,7 +147,7 @@ extension Error: CustomDebugStringConvertible { // MARK: Codable -extension Error: Codable { +extension MCPError: Codable { private enum CodingKeys: String, CodingKey { case code, message, data } @@ -199,15 +215,15 @@ extension Error: Codable { // MARK: Equatable -extension Error: Equatable { - public static func == (lhs: Error, rhs: Error) -> Bool { +extension MCPError: Equatable { + public static func == (lhs: MCPError, rhs: MCPError) -> Bool { lhs.code == rhs.code } } // MARK: Hashable -extension Error: Hashable { +extension MCPError: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(code) switch self { diff --git a/Sources/MCP/Base/Messages.swift b/Sources/MCP/Base/Messages.swift index 0c6caf2f..e5effeb5 100644 --- a/Sources/MCP/Base/Messages.swift +++ b/Sources/MCP/Base/Messages.swift @@ -60,7 +60,7 @@ extension Method { } /// Create a response with the given error. - public static func response(id: ID, error: Error) -> Response { + public static func response(id: ID, error: MCPError) -> Response { Response(id: id, error: error) } } @@ -186,14 +186,14 @@ public struct Response: Hashable, Identifiable, Codable, Sendable { /// The response ID. public let id: ID /// The response result. - public let result: Swift.Result + public let result: Swift.Result public init(id: ID, result: M.Result) { self.id = id self.result = .success(result) } - public init(id: ID, error: Error) { + public init(id: ID, error: MCPError) { self.id = id self.result = .failure(error) } @@ -224,7 +224,7 @@ public struct Response: Hashable, Identifiable, Codable, Sendable { id = try container.decode(ID.self, forKey: .id) if let result = try? container.decode(M.Result.self, forKey: .result) { self.result = .success(result) - } else if let error = try? container.decode(Error.self, forKey: .error) { + } else if let error = try? container.decode(MCPError.self, forKey: .error) { self.result = .failure(error) } else { throw DecodingError.dataCorrupted( diff --git a/Sources/MCP/Base/Transports.swift b/Sources/MCP/Base/Transports.swift index bd439321..28eec380 100644 --- a/Sources/MCP/Base/Transports.swift +++ b/Sources/MCP/Base/Transports.swift @@ -23,7 +23,7 @@ public protocol Transport: Actor { func send(_ data: Data) async throws /// Receives data in an async sequence - func receive() -> AsyncThrowingStream + func receive() -> AsyncThrowingStream } /// Standard input/output transport implementation @@ -74,11 +74,11 @@ public actor StdioTransport: Transport { private func setNonBlocking(fileDescriptor: FileDescriptor) throws { let flags = fcntl(fileDescriptor.rawValue, F_GETFL) guard flags >= 0 else { - throw Error.transportError(Errno.badFileDescriptor) + throw MCPError.transportError(Errno.badFileDescriptor) } let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK) guard result >= 0 else { - throw Error.transportError(Errno.badFileDescriptor) + throw MCPError.transportError(Errno.badFileDescriptor) } } @@ -110,7 +110,7 @@ public actor StdioTransport: Transport { messageContinuation.yield(Data(messageData)) } } - } catch let error where Error.isResourceTemporarilyUnavailable(error) { + } catch let error where MCPError.isResourceTemporarilyUnavailable(error) { try? await Task.sleep(for: .milliseconds(10)) continue } catch { @@ -133,7 +133,7 @@ public actor StdioTransport: Transport { public func send(_ message: Data) async throws { guard isConnected else { - throw Error.transportError(Errno.socketNotConnected) + throw MCPError.transportError(Errno.socketNotConnected) } // Add newline as delimiter @@ -149,16 +149,16 @@ public actor StdioTransport: Transport { if written > 0 { remaining = remaining.dropFirst(written) } - } catch let error where Error.isResourceTemporarilyUnavailable(error) { + } catch let error where MCPError.isResourceTemporarilyUnavailable(error) { try await Task.sleep(for: .milliseconds(10)) continue } catch { - throw Error.transportError(error) + throw MCPError.transportError(error) } } } - public func receive() -> AsyncThrowingStream { + public func receive() -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { for await message in messageStream { @@ -179,8 +179,8 @@ public actor StdioTransport: Transport { public nonisolated let logger: Logger private var isConnected = false - private let messageStream: AsyncThrowingStream - private let messageContinuation: AsyncThrowingStream.Continuation + private let messageStream: AsyncThrowingStream + private let messageContinuation: AsyncThrowingStream.Continuation // Track connection state for continuations private var connectionContinuationResumed = false @@ -195,7 +195,7 @@ public actor StdioTransport: Transport { ) // Create message stream - var continuation: AsyncThrowingStream.Continuation! + var continuation: AsyncThrowingStream.Continuation! self.messageStream = AsyncThrowingStream { continuation = $0 } self.messageContinuation = continuation } @@ -209,9 +209,9 @@ public actor StdioTransport: Transport { // Wait for connection to be ready try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in + [weak self] (continuation: CheckedContinuation) in guard let self = self else { - continuation.resume(throwing: MCP.Error.internalError("Transport deallocated")) + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) return } @@ -245,7 +245,7 @@ public actor StdioTransport: Transport { } } - private func handleConnectionReady(continuation: CheckedContinuation) + private func handleConnectionReady(continuation: CheckedContinuation) async { if !connectionContinuationResumed { @@ -259,7 +259,7 @@ public actor StdioTransport: Transport { } private func handleConnectionFailed( - error: Swift.Error, continuation: CheckedContinuation + error: Error, continuation: CheckedContinuation ) async { if !connectionContinuationResumed { connectionContinuationResumed = true @@ -268,13 +268,13 @@ public actor StdioTransport: Transport { } } - private func handleConnectionCancelled(continuation: CheckedContinuation) + private func handleConnectionCancelled(continuation: CheckedContinuation) async { if !connectionContinuationResumed { connectionContinuationResumed = true logger.warning("Connection cancelled") - continuation.resume(throwing: MCP.Error.internalError("Connection cancelled")) + continuation.resume(throwing: MCPError.internalError("Connection cancelled")) } } @@ -288,7 +288,7 @@ public actor StdioTransport: Transport { public func send(_ message: Data) async throws { guard isConnected else { - throw MCP.Error.internalError("Transport not connected") + throw MCPError.internalError("Transport not connected") } // Add newline as delimiter @@ -299,9 +299,9 @@ public actor StdioTransport: Transport { var sendContinuationResumed = false try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in + [weak self] (continuation: CheckedContinuation) in guard let self = self else { - continuation.resume(throwing: MCP.Error.internalError("Transport deallocated")) + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) return } @@ -316,7 +316,7 @@ public actor StdioTransport: Transport { if let error = error { self.logger.error("Send error: \(error)") continuation.resume( - throwing: MCP.Error.internalError("Send error: \(error)")) + throwing: MCPError.internalError("Send error: \(error)")) } else { continuation.resume() } @@ -326,7 +326,7 @@ public actor StdioTransport: Transport { } } - public func receive() -> AsyncThrowingStream { + public func receive() -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { do { @@ -363,7 +363,7 @@ public actor StdioTransport: Transport { } catch let error as NWError { if !Task.isCancelled { logger.error("Network error occurred", metadata: ["error": "\(error)"]) - messageContinuation.finish(throwing: MCP.Error.transportError(error)) + messageContinuation.finish(throwing: MCPError.transportError(error)) } break } catch { @@ -382,9 +382,9 @@ public actor StdioTransport: Transport { var receiveContinuationResumed = false return try await withCheckedThrowingContinuation { - [weak self] (continuation: CheckedContinuation) in + [weak self] (continuation: CheckedContinuation) in guard let self = self else { - continuation.resume(throwing: MCP.Error.internalError("Transport deallocated")) + continuation.resume(throwing: MCPError.internalError("Transport deallocated")) return } @@ -394,12 +394,12 @@ public actor StdioTransport: Transport { if !receiveContinuationResumed { receiveContinuationResumed = true if let error = error { - continuation.resume(throwing: MCP.Error.transportError(error)) + continuation.resume(throwing: MCPError.transportError(error)) } else if let content = content { continuation.resume(returning: content) } else { continuation.resume( - throwing: MCP.Error.internalError("No data received")) + throwing: MCPError.internalError("No data received")) } } } diff --git a/Sources/MCP/Client/Client.swift b/Sources/MCP/Client/Client.swift index e597c44e..7b4dd31a 100644 --- a/Sources/MCP/Client/Client.swift +++ b/Sources/MCP/Client/Client.swift @@ -112,16 +112,16 @@ public actor Client { private var task: Task? /// An error indicating a type mismatch when decoding a pending request - private struct TypeMismatchError: Swift.Error {} + private struct TypeMismatchError: Error {} /// A pending request with a continuation for the result private struct PendingRequest { - let continuation: CheckedContinuation + let continuation: CheckedContinuation } /// A type-erased pending request private struct AnyPendingRequest { - private let _resume: (Result) -> Void + private let _resume: (Result) -> Void init(_ request: PendingRequest) { _resume = { result in @@ -146,7 +146,7 @@ public actor Client { _resume(.success(value)) } - func resume(throwing error: Swift.Error) { + func resume(throwing error: Error) { _resume(.failure(error)) } } @@ -201,7 +201,7 @@ public actor Client { "Unexpected message received by client", metadata: metadata) } } - } catch let error where Error.isResourceTemporarilyUnavailable(error) { + } catch let error where MCPError.isResourceTemporarilyUnavailable(error) { try? await Task.sleep(for: .milliseconds(10)) continue } catch { @@ -217,7 +217,7 @@ public actor Client { public func disconnect() async { // Cancel all pending requests for (id, request) in pendingRequests { - request.resume(throwing: Error.internalError("Client disconnected")) + request.resume(throwing: MCPError.internalError("Client disconnected")) pendingRequests.removeValue(forKey: id) } @@ -247,7 +247,7 @@ public actor Client { /// Send a request and receive its response public func send(_ request: Request) async throws -> M.Result { guard let connection = connection else { - throw Error.internalError("Client connection not initialized") + throw MCPError.internalError("Client connection not initialized") } let requestData = try JSONEncoder().encode(request) @@ -274,7 +274,7 @@ public actor Client { private func addPendingRequest( id: ID, - continuation: CheckedContinuation, + continuation: CheckedContinuation, type: T.Type ) { pendingRequests[id] = AnyPendingRequest(PendingRequest(continuation: continuation)) @@ -439,10 +439,10 @@ public actor Client { { if configuration.strict { guard let capabilities = serverCapabilities else { - throw Error.methodNotFound("Server capabilities not initialized") + throw MCPError.methodNotFound("Server capabilities not initialized") } guard capabilities[keyPath: keyPath] != nil else { - throw Error.methodNotFound("\(name) is not supported by the server") + throw MCPError.methodNotFound("\(name) is not supported by the server") } } } diff --git a/Sources/MCP/Server/Server.swift b/Sources/MCP/Server/Server.swift index 4868f293..ef50e2c5 100644 --- a/Sources/MCP/Server/Server.swift +++ b/Sources/MCP/Server/Server.swift @@ -195,9 +195,9 @@ public actor Server { requestID = .number(intValue) } } - throw Error.parseError("Invalid message format") + throw MCPError.parseError("Invalid message format") } - } catch let error where Error.isResourceTemporarilyUnavailable(error) { + } catch let error where MCPError.isResourceTemporarilyUnavailable(error) { // Resource temporarily unavailable, retry after a short delay try? await Task.sleep(for: .milliseconds(10)) continue @@ -206,8 +206,8 @@ public actor Server { "Error processing message", metadata: ["error": "\(error)"]) let response = AnyMethod.response( id: requestID ?? .random, - error: error as? Error - ?? Error.internalError(error.localizedDescription) + error: error as? MCPError + ?? MCPError.internalError(error.localizedDescription) ) try? await send(response) } @@ -265,7 +265,7 @@ public actor Server { /// Send a response to a request public func send(_ response: Response) async throws { guard let connection = connection else { - throw Error.internalError("Server connection not initialized") + throw MCPError.internalError("Server connection not initialized") } let encoder = JSONEncoder() @@ -278,7 +278,7 @@ public actor Server { /// Send a notification to connected clients public func notify(_ notification: Message) async throws { guard let connection = connection else { - throw Error.internalError("Server connection not initialized") + throw MCPError.internalError("Server connection not initialized") } let encoder = JSONEncoder() @@ -311,7 +311,7 @@ public actor Server { // Find handler for method name guard let handler = methodHandlers[request.method] else { - let error = Error.methodNotFound("Unknown method: \(request.method)") + let error = MCPError.methodNotFound("Unknown method: \(request.method)") let response = AnyMethod.response(id: request.id, error: error) try await send(response) throw error @@ -322,7 +322,7 @@ public actor Server { let response = try await handler(request) try await send(response) } catch { - let mcpError = error as? Error ?? Error.internalError(error.localizedDescription) + let mcpError = error as? MCPError ?? MCPError.internalError(error.localizedDescription) let response = AnyMethod.response(id: request.id, error: mcpError) try await send(response) throw error @@ -361,7 +361,7 @@ public actor Server { private func checkInitialized() throws { guard isInitialized else { - throw Error.invalidRequest("Server is not initialized") + throw MCPError.invalidRequest("Server is not initialized") } } @@ -371,16 +371,16 @@ public actor Server { // Initialize withMethodHandler(Initialize.self) { [weak self] params in guard let self = self else { - throw Error.internalError("Server was deallocated") + throw MCPError.internalError("Server was deallocated") } guard await !self.isInitialized else { - throw Error.invalidRequest("Server is already initialized") + throw MCPError.invalidRequest("Server is already initialized") } // Validate protocol version guard Version.latest == params.protocolVersion else { - throw Error.invalidRequest( + throw MCPError.invalidRequest( "Unsupported protocol version: \(params.protocolVersion)") } diff --git a/Tests/MCPTests/ClientTests.swift b/Tests/MCPTests/ClientTests.swift index ebbf0261..4eaf1361 100644 --- a/Tests/MCPTests/ClientTests.swift +++ b/Tests/MCPTests/ClientTests.swift @@ -90,8 +90,8 @@ struct ClientTests { do { try await client.connect(transport: transport) #expect(Bool(false), "Expected connection to fail") - } catch let error as Error { - if case Error.transportError = error { + } catch let error as MCPError { + if case MCPError.transportError = error { #expect(Bool(true)) } else { #expect(Bool(false), "Expected transport error") @@ -112,8 +112,8 @@ struct ClientTests { do { try await client.ping() #expect(Bool(false), "Expected ping to fail") - } catch let error as Error { - if case Error.transportError = error { + } catch let error as MCPError { + if case MCPError.transportError = error { #expect(Bool(true)) } else { #expect(Bool(false), "Expected transport error") @@ -132,12 +132,12 @@ struct ClientTests { try await client.connect(transport: transport) // Create a task for listPrompts - let promptsTask = Task { + let promptsTask = Task { do { _ = try await client.listPrompts() #expect(Bool(false), "Expected listPrompts to fail in strict mode") - } catch let error as Error { - if case Error.methodNotFound = error { + } catch let error as MCPError { + if case MCPError.methodNotFound = error { #expect(Bool(true)) } else { #expect(Bool(false), "Expected methodNotFound error, got \(error)") @@ -185,7 +185,7 @@ struct ClientTests { // Create an error response with the same ID let errorResponse = Response( id: decodedRequest.id, - error: Error.methodNotFound("Test: Prompts capability not available") + error: MCPError.methodNotFound("Test: Prompts capability not available") ) try await transport.queue(response: errorResponse) @@ -193,8 +193,8 @@ struct ClientTests { do { _ = try await client.listPrompts() #expect(Bool(false), "Expected listPrompts to fail in non-strict mode") - } catch let error as Error { - if case Error.methodNotFound = error { + } catch let error as MCPError { + if case MCPError.methodNotFound = error { #expect(Bool(true)) } else { #expect(Bool(false), "Expected methodNotFound error, got \(error)") diff --git a/Tests/MCPTests/Helpers/MockTransport.swift b/Tests/MCPTests/Helpers/MockTransport.swift index 571715ef..1ebdce5c 100644 --- a/Tests/MCPTests/Helpers/MockTransport.swift +++ b/Tests/MCPTests/Helpers/MockTransport.swift @@ -1,4 +1,8 @@ -import Foundation +import class Foundation.JSONEncoder +import class Foundation.JSONDecoder +import struct Foundation.Data +import struct Foundation.POSIXError + import Logging @testable import MCP @@ -26,7 +30,7 @@ actor MockTransport: Transport { private var dataToReceive: [Data] = [] private(set) var receivedMessages: [String] = [] - private var dataStreamContinuation: AsyncThrowingStream.Continuation? + private var dataStreamContinuation: AsyncThrowingStream.Continuation? var shouldFailConnect = false var shouldFailSend = false @@ -37,7 +41,7 @@ actor MockTransport: Transport { public func connect() async throws { if shouldFailConnect { - throw Error.transportError(POSIXError(.ECONNREFUSED)) + throw MCPError.transportError(POSIXError(.ECONNREFUSED)) } isConnected = true } @@ -50,13 +54,13 @@ actor MockTransport: Transport { public func send(_ message: Data) async throws { if shouldFailSend { - throw Error.transportError(POSIXError(.EIO)) + throw MCPError.transportError(POSIXError(.EIO)) } sentData.append(message) } - public func receive() -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in + public func receive() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in dataStreamContinuation = continuation for message in dataToReceive { continuation.yield(message) @@ -76,7 +80,7 @@ actor MockTransport: Transport { shouldFailSend = shouldFail } - func queue(request: Request) throws { + func queue(request: Request) throws { let data = try encoder.encode(request) if let continuation = dataStreamContinuation { continuation.yield(data) @@ -85,12 +89,12 @@ actor MockTransport: Transport { } } - func queue(response: Response) throws { + func queue(response: Response) throws { let data = try encoder.encode(response) dataToReceive.append(data) } - func queue(notification: Message) throws { + func queue(notification: Message) throws { let data = try encoder.encode(notification) dataToReceive.append(data) } diff --git a/Tests/MCPTests/ResponseTests.swift b/Tests/MCPTests/ResponseTests.swift index f09a916e..ff82e64b 100644 --- a/Tests/MCPTests/ResponseTests.swift +++ b/Tests/MCPTests/ResponseTests.swift @@ -43,7 +43,7 @@ struct ResponseTests { @Test("Error response initialization and encoding") func testErrorResponse() throws { let id: ID = "test-id" - let error = MCP.Error.parseError(nil) + let error = MCPError.parseError(nil) let response = Response(id: id, error: error) let encoder = JSONEncoder() @@ -65,7 +65,7 @@ struct ResponseTests { @Test("Error response with detail") func testErrorResponseWithDetail() throws { let id: ID = "test-id" - let error = MCP.Error.parseError("Invalid syntax") + let error = MCPError.parseError("Invalid syntax") let response = Response(id: id, error: error) let encoder = JSONEncoder() diff --git a/Tests/MCPTests/ServerTests.swift b/Tests/MCPTests/ServerTests.swift index f55706db..adf3ee62 100644 --- a/Tests/MCPTests/ServerTests.swift +++ b/Tests/MCPTests/ServerTests.swift @@ -109,7 +109,7 @@ struct ServerTests { try await server.start(transport: transport) { clientInfo, _ in if clientInfo.name == "BlockedClient" { - throw Error.invalidRequest("Client not allowed") + throw MCPError.invalidRequest("Client not allowed") } } diff --git a/Tests/MCPTests/TransportTests.swift b/Tests/MCPTests/TransportTests.swift index c0823da3..b4871bee 100644 --- a/Tests/MCPTests/TransportTests.swift +++ b/Tests/MCPTests/TransportTests.swift @@ -57,7 +57,7 @@ struct StdioTransportTests { try writer.close() // Start receiving messages - let stream: AsyncThrowingStream = await transport.receive() + let stream: AsyncThrowingStream = await transport.receive() var iterator = stream.makeAsyncIterator() // Get first message @@ -79,7 +79,7 @@ struct StdioTransportTests { try writer.writeAll(invalidJSON.data(using: .utf8)!) try writer.close() - let stream: AsyncThrowingStream = await transport.receive() + let stream: AsyncThrowingStream = await transport.receive() var iterator = stream.makeAsyncIterator() _ = try await iterator.next() @@ -100,7 +100,7 @@ struct StdioTransportTests { try await transport.connect() #expect(Bool(false), "Expected connect to throw an error") } catch { - #expect(error is MCP.Error) + #expect(error is MCPError) } await transport.disconnect() @@ -119,7 +119,7 @@ struct StdioTransportTests { try await transport.connect() #expect(Bool(false), "Expected connect to throw an error") } catch { - #expect(error is MCP.Error) + #expect(error is MCPError) } await transport.disconnect()