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
30 changes: 24 additions & 6 deletions Sources/MCP/Base/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public enum MCPError: Swift.Error, Sendable {

// Server errors (-32000 to -32099)
case serverError(code: Int, message: String)
/// A server error with structured JSON-RPC error data.
case serverErrorWithData(code: Int, message: String, data: Value)

// MCP specific errors
case urlElicitationRequired(message: String, elicitations: [URLElicitationInfo]) // -32042
Expand All @@ -53,6 +55,7 @@ public enum MCPError: Swift.Error, Sendable {
case .invalidParams: return -32602
case .internalError: return -32603
case .serverError(let code, _): return code
case .serverErrorWithData(let code, _, _): return code
case .urlElicitationRequired: return -32042
case .connectionClosed: return -32000
case .transportError: return -32001
Expand Down Expand Up @@ -91,6 +94,8 @@ extension MCPError: LocalizedError {
return "Internal error" + (detail.map { ": \($0)" } ?? "")
case .serverError(_, let message):
return "Server error: \(message)"
case .serverErrorWithData(_, let message, _):
return message
case .urlElicitationRequired(let message, _):
return "URL elicitation required: \(message)"
case .connectionClosed:
Expand All @@ -112,7 +117,7 @@ extension MCPError: LocalizedError {
return "Invalid method parameter(s)"
case .internalError:
return "Internal JSON-RPC error"
case .serverError:
case .serverError, .serverErrorWithData:
return "Server-defined error occurred"
case .urlElicitationRequired:
return "The server requires user authentication or input via external URL"
Expand Down Expand Up @@ -187,6 +192,9 @@ extension MCPError: Codable {
// No additional data for server errors
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
break
case .serverErrorWithData(_, let message, let data):
try container.encode(message, forKey: .message)
try container.encode(data, forKey: .data)
case .urlElicitationRequired(let message, let elicitations):
// Encode the raw message so decode can round-trip without prefix doubling
try container.encode(message, forKey: .message)
Expand Down Expand Up @@ -218,11 +226,12 @@ extension MCPError: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)
let code = try container.decode(Int.self, forKey: .code)
let message = try container.decode(String.self, forKey: .message)
let data = try container.decodeIfPresent([String: Value].self, forKey: .data)
let data = try container.decodeIfPresent(Value.self, forKey: .data)
let dataObject = data?.objectValue

// Helper to extract detail from data, falling back to message if needed
let unwrapDetail: (String?) -> String? = { fallback in
guard let detailValue = data?["detail"] else { return fallback }
guard let detailValue = dataObject?["detail"] else { return fallback }
if case .string(let str) = detailValue { return str }
return fallback
}
Expand All @@ -241,7 +250,7 @@ extension MCPError: Codable {
case -32042:
// Extract elicitations array from data
var elicitations: [URLElicitationInfo] = []
if case .array(let items) = data?["elicitations"] {
if case .array(let items) = dataObject?["elicitations"] {
for item in items {
if case .object(let dict) = item,
case .string(let mode) = dict["mode"],
Expand All @@ -262,7 +271,7 @@ extension MCPError: Codable {
case -32001:
// Extract underlying error string if present
let underlyingErrorString =
data?["error"].flatMap { val -> String? in
dataObject?["error"].flatMap { val -> String? in
if case .string(let str) = val { return str }
return nil
} ?? message
Expand All @@ -274,7 +283,11 @@ extension MCPError: Codable {
)
)
default:
self = .serverError(code: code, message: message)
if let data {
self = .serverErrorWithData(code: code, message: message, data: data)
} else {
self = .serverError(code: code, message: message)
}
}
}
}
Expand All @@ -291,6 +304,8 @@ extension MCPError: Equatable {
case (.internalError(let a), .internalError(let b)): return a == b
case (.serverError(let c1, let m1), .serverError(let c2, let m2)):
return c1 == c2 && m1 == m2
case (.serverErrorWithData(let c1, let m1, let d1), .serverErrorWithData(let c2, let m2, let d2)):
return c1 == c2 && m1 == m2 && d1 == d2
case (.urlElicitationRequired(let m1, let e1), .urlElicitationRequired(let m2, let e2)):
return m1 == m2 && e1 == e2
case (.connectionClosed, .connectionClosed): return true
Expand Down Expand Up @@ -319,6 +334,9 @@ extension MCPError: Hashable {
hasher.combine(detail)
case .serverError(_, let message):
hasher.combine(message)
case .serverErrorWithData(_, let message, let data):
hasher.combine(message)
hasher.combine(data)
case .urlElicitationRequired(let message, let elicitations):
hasher.combine(message)
hasher.combine(elicitations)
Expand Down
41 changes: 41 additions & 0 deletions Tests/MCPTests/ResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import Testing

import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
import class Foundation.JSONSerialization
import class Foundation.NSDictionary
import struct Foundation.Data

@testable import MCP

Expand Down Expand Up @@ -116,4 +119,42 @@ struct ResponseTests {
#expect(Bool(false), "Expected success result")
}
}

@Test("Unsupported protocol version error keeps its structured data")
func testUnsupportedVersionErrorData() throws {
let json = #"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"Unsupported protocol version","data":{"supported":["2026-07-28","2025-11-25"],"requested":"1900-01-01"}}}"#
let response = try JSONDecoder().decode(Response<EmptyMethod>.self, from: Data(json.utf8))

guard case .failure(.serverErrorWithData(let code, let message, let data)) = response.result else {
Issue.record("Expected a structured server error")
return
}
#expect(code == -32022)
#expect(message == "Unsupported protocol version")
#expect(data.objectValue?["requested"] == .string("1900-01-01"))
#expect(data.objectValue?["supported"] == .array([.string("2026-07-28"), .string("2025-11-25")]))

let encoded = try JSONEncoder().encode(response)
let actual = try JSONSerialization.jsonObject(with: encoded) as? NSDictionary
let expected = try JSONSerialization.jsonObject(with: Data(json.utf8)) as? NSDictionary
#expect(actual == expected)
}

@Test("Missing capability error retains nested capability object")
func testMissingCapabilityErrorData() throws {
let json = #"{"jsonrpc":"2.0","id":1,"error":{"code":-32021,"message":"Server requires elicitation","data":{"requiredCapabilities":{"elicitation":{}}}}}"#
let response = try JSONDecoder().decode(Response<EmptyMethod>.self, from: Data(json.utf8))

guard case .failure(.serverErrorWithData(let code, _, let data)) = response.result else {
Issue.record("Expected a structured server error")
return
}
#expect(code == -32021)
#expect(data.objectValue?["requiredCapabilities"] == .object(["elicitation": .object([:])]))

let encoded = try JSONEncoder().encode(response)
let actual = try JSONSerialization.jsonObject(with: encoded) as? NSDictionary
let expected = try JSONSerialization.jsonObject(with: Data(json.utf8)) as? NSDictionary
#expect(actual == expected)
}
}