Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions Sources/MCP/Base/PendingRequest.swift
Original file line number Diff line number Diff line change
@@ -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<T> {
Expand All @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions Tests/MCPTests/PendingRequestTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}