From 32433ad102734d3436b23bf79c4c980b508f3e53 Mon Sep 17 00:00:00 2001 From: Ehsan Azish Date: Thu, 24 Sep 2026 19:17:01 +0300 Subject: [PATCH] Keep raw response when pending result decoding fails --- Sources/MCP/Base/PendingRequest.swift | 37 +++++++++++++---- Tests/MCPTests/PendingRequestTests.swift | 52 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 Tests/MCPTests/PendingRequestTests.swift diff --git a/Sources/MCP/Base/PendingRequest.swift b/Sources/MCP/Base/PendingRequest.swift index b6e0a83b..56917b7b 100644 --- a/Sources/MCP/Base/PendingRequest.swift +++ b/Sources/MCP/Base/PendingRequest.swift @@ -1,8 +1,18 @@ import class Foundation.JSONDecoder import class Foundation.JSONEncoder -/// An error indicating a type mismatch when decoding a pending request response. -struct TypeMismatchError: Swift.Error {} +/// A response could not be decoded as the result type expected by the request. +public struct TypeMismatchError: Swift.Error { + public let expectedType: String + public let rawValue: Value? + public let underlyingError: (any Swift.Error)? + + init(expectedType: String, rawValue: Value?, underlyingError: (any Swift.Error)?) { + self.expectedType = expectedType + self.rawValue = rawValue + self.underlyingError = underlyingError + } +} /// A pending request with a continuation for the result. struct PendingRequest { @@ -19,13 +29,24 @@ struct AnyPendingRequest: Sendable { case .success(let value): if let typedValue = value as? T { request.continuation.resume(returning: typedValue) - } else if let value = value as? Value, - let data = try? JSONEncoder().encode(value), - let decoded = try? JSONDecoder().decode(T.self, from: data) - { - request.continuation.resume(returning: decoded) + } else if let value = value as? Value { + do { + let data = try JSONEncoder().encode(value) + let decoded = try JSONDecoder().decode(T.self, from: data) + request.continuation.resume(returning: decoded) + } catch { + request.continuation.resume(throwing: TypeMismatchError( + expectedType: String(reflecting: T.self), + rawValue: value, + underlyingError: error + )) + } } else { - request.continuation.resume(throwing: TypeMismatchError()) + request.continuation.resume(throwing: TypeMismatchError( + expectedType: String(reflecting: T.self), + rawValue: nil, + underlyingError: nil + )) } case .failure(let error): request.continuation.resume(throwing: error) diff --git a/Tests/MCPTests/PendingRequestTests.swift b/Tests/MCPTests/PendingRequestTests.swift new file mode 100644 index 00000000..2541087f --- /dev/null +++ b/Tests/MCPTests/PendingRequestTests.swift @@ -0,0 +1,52 @@ +import Testing + +@testable import MCP + +@Suite("Pending request tests") +struct PendingRequestTests { + @Test("Failed tool result decoding exposes the raw value and decoding error") + func failedDecodingKeepsResponse() async throws { + let raw = Value.object(["unexpected": .string("response")]) + + do { + let _: CallTool.Result = try await withCheckedThrowingContinuation { continuation in + let pending = AnyPendingRequest(PendingRequest(continuation: continuation)) + pending.resume(returning: raw) + } + Issue.record("Expected decoding to fail") + } catch let error as TypeMismatchError { + #expect(error.expectedType.contains("CallTool.Result")) + #expect(error.rawValue == raw) + guard case .keyNotFound(let key, _) = error.underlyingError as? DecodingError else { + Issue.record("Expected the missing content key to be reported") + return + } + #expect(key.stringValue == "content") + } + } + + @Test("Valid tool result still decodes") + func validResultStillDecodes() async throws { + let raw = Value.object(["content": .array([])]) + let result: CallTool.Result = try await withCheckedThrowingContinuation { continuation in + let pending = AnyPendingRequest(PendingRequest(continuation: continuation)) + pending.resume(returning: raw) + } + #expect(result.content.isEmpty) + } + + @Test("Non-JSON response reports the expected type") + func mismatchedResponseType() async throws { + do { + let _: CallTool.Result = try await withCheckedThrowingContinuation { continuation in + let pending = AnyPendingRequest(PendingRequest(continuation: continuation)) + pending.resume(returning: 42) + } + Issue.record("Expected a type mismatch") + } catch let error as TypeMismatchError { + #expect(error.expectedType.contains("CallTool.Result")) + #expect(error.rawValue == nil) + #expect(error.underlyingError == nil) + } + } +}