Skip to content
Merged
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
22 changes: 21 additions & 1 deletion Sources/MCP/Base/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,31 @@ extension Request {
method = try container.decode(String.self, forKey: .method)

if M.Parameters.self is NotRequired.Type {
// For NotRequired parameters, use decodeIfPresent or init()
params =
(try container.decodeIfPresent(M.Parameters.self, forKey: .params)
?? (M.Parameters.self as! NotRequired.Type).init() as! M.Parameters)
} else if let value = try? container.decode(M.Parameters.self, forKey: .params) {
// If params exists and can be decoded, use it
params = value
} else if !container.contains(.params)
|| (try? container.decodeNil(forKey: .params)) == true
{
// If params is missing or explicitly null, use Empty for Empty parameters
// or throw for non-Empty parameters
if M.Parameters.self == Empty.self {
params = Empty() as! M.Parameters
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Missing required params field"))
}
} else {
params = try container.decode(M.Parameters.self, forKey: .params)
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid params field"))
}
}
}
Expand Down
173 changes: 156 additions & 17 deletions Tests/MCPTests/RequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,33 @@ struct RequestTests {

@Test("Request initialization with parameters")
func testRequestInitialization() throws {
let id: ID = "test-id"
let params = TestMethod.Parameters(value: "test")
let request = Request<TestMethod>(id: id, method: TestMethod.name, params: params)
let id: ID = 1
let params = CallTool.Parameters(name: "test-tool")
let request = Request<CallTool>(id: id, method: CallTool.name, params: params)

#expect(request.id == id)
#expect(request.method == TestMethod.name)
#expect(request.params.value == "test")
#expect(request.method == CallTool.name)
#expect(request.params.name == "test-tool")
}

@Test("Request encoding and decoding")
func testRequestEncodingDecoding() throws {
let request = TestMethod.request(id: "test-id", TestMethod.Parameters(value: "test"))
let request = CallTool.request(id: 1, CallTool.Parameters(name: "test-tool"))

let encoder = JSONEncoder()
let decoder = JSONDecoder()

let data = try encoder.encode(request)
let decoded = try decoder.decode(Request<TestMethod>.self, from: data)
let decoded = try decoder.decode(Request<CallTool>.self, from: data)

#expect(decoded.id == request.id)
#expect(decoded.method == request.method)
#expect(decoded.params.value == request.params.value)
#expect(decoded.params.name == request.params.name)
}

@Test("Empty parameters request encoding")
func testEmptyParametersRequestEncoding() throws {
let request = EmptyMethod.request(id: "test-id")
let request = EmptyMethod.request(id: 1)

let encoder = JSONEncoder()
let decoder = JSONDecoder()
Expand All @@ -66,59 +66,198 @@ struct RequestTests {
func testEmptyParametersRequestDecoding() throws {
// Create a minimal JSON string
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"empty.method"}
{"jsonrpc":"2.0","id":1,"method":"empty.method"}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<EmptyMethod>.self, from: data)

#expect(decoded.id == "test-id")
#expect(decoded.id == 1)
#expect(decoded.method == EmptyMethod.name)
}

@Test("NotRequired parameters request decoding - with params")
func testNotRequiredParametersRequestDecodingWithParams() throws {
// Test decoding when params field is present
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"ping","params":{}}
{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<Ping>.self, from: data)

#expect(decoded.id == "test-id")
#expect(decoded.id == 1)
#expect(decoded.method == Ping.name)
}

@Test("NotRequired parameters request decoding - without params")
func testNotRequiredParametersRequestDecodingWithoutParams() throws {
// Test decoding when params field is missing
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"ping"}
{"jsonrpc":"2.0","id":1,"method":"ping"}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<Ping>.self, from: data)

#expect(decoded.id == "test-id")
#expect(decoded.id == 1)
#expect(decoded.method == Ping.name)
}

@Test("NotRequired parameters request decoding - with null params")
func testNotRequiredParametersRequestDecodingWithNullParams() throws {
// Test decoding when params field is null
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"ping","params":null}
{"jsonrpc":"2.0","id":1,"method":"ping","params":null}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<Ping>.self, from: data)

#expect(decoded.id == "test-id")
#expect(decoded.id == 1)
#expect(decoded.method == Ping.name)
}

@Test("Required parameters request decoding - missing params")
func testRequiredParametersRequestDecodingMissingParams() throws {
let jsonString = """
{"jsonrpc":"2.0","id":1,"method":"tools/call"}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
#expect(throws: DecodingError.self) {
_ = try decoder.decode(Request<CallTool>.self, from: data)
}
}

@Test("Required parameters request decoding - null params")
func testRequiredParametersRequestDecodingNullParams() throws {
let jsonString = """
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":null}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
#expect(throws: DecodingError.self) {
_ = try decoder.decode(Request<CallTool>.self, from: data)
}
}

@Test("Empty parameters request decoding - with null params")
func testEmptyParametersRequestDecodingNullParams() throws {
let jsonString = """
{"jsonrpc":"2.0","id":1,"method":"empty.method","params":null}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<EmptyMethod>.self, from: data)

#expect(decoded.id == 1)
#expect(decoded.method == EmptyMethod.name)
}

@Test("Empty parameters request decoding - with empty object params")
func testEmptyParametersRequestDecodingEmptyParams() throws {
let jsonString = """
{"jsonrpc":"2.0","id":1,"method":"empty.method","params":{}}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<EmptyMethod>.self, from: data)

#expect(decoded.id == 1)
#expect(decoded.method == EmptyMethod.name)
}

@Test("Initialize request decoding - requires params")
func testInitializeRequestDecodingRequiresParams() throws {
// Test missing params field
let missingParams = """
{"jsonrpc":"2.0","id":"test-id","method":"initialize"}
"""
let decoder = JSONDecoder()
#expect(throws: DecodingError.self) {
_ = try decoder.decode(
Request<Initialize>.self, from: missingParams.data(using: .utf8)!)
}

// Test null params
let nullParams = """
{"jsonrpc":"2.0","id":"test-id","method":"initialize","params":null}
"""
#expect(throws: DecodingError.self) {
_ = try decoder.decode(Request<Initialize>.self, from: nullParams.data(using: .utf8)!)
}

// Verify that empty object params works (since fields have defaults)
let emptyParams = """
{"jsonrpc":"2.0","id":"test-id","method":"initialize","params":{}}
"""
let decoded = try decoder.decode(
Request<Initialize>.self, from: emptyParams.data(using: .utf8)!)
#expect(decoded.params.protocolVersion == Version.latest)
#expect(decoded.params.clientInfo.name == "unknown")
}

@Test("Invalid parameters request decoding")
func testInvalidParametersRequestDecoding() throws {
let jsonString = """
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"invalid":"value"}}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
#expect(throws: DecodingError.self) {
_ = try decoder.decode(Request<CallTool>.self, from: data)
}
}

@Test("NotRequired parameters request decoding")
func testNotRequiredParametersRequestDecoding() throws {
// Test with missing params
let missingParams = """
{"jsonrpc":"2.0","id":1,"method":"tools/list"}
"""
let decoder = JSONDecoder()
let decodedMissing = try decoder.decode(
Request<ListTools>.self,
from: missingParams.data(using: .utf8)!)
#expect(decodedMissing.id == 1)
#expect(decodedMissing.method == ListTools.name)
#expect(decodedMissing.params.cursor == nil)

// Test with null params
let nullParams = """
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":null}
"""
let decodedNull = try decoder.decode(
Request<ListTools>.self,
from: nullParams.data(using: .utf8)!)
#expect(decodedNull.params.cursor == nil)

// Test with empty object params
let emptyParams = """
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
"""
let decodedEmpty = try decoder.decode(
Request<ListTools>.self,
from: emptyParams.data(using: .utf8)!)
#expect(decodedEmpty.params.cursor == nil)

// Test with provided cursor
let withCursor = """
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"cursor":"next-page"}}
"""
let decodedWithCursor = try decoder.decode(
Request<ListTools>.self,
from: withCursor.data(using: .utf8)!)
#expect(decodedWithCursor.params.cursor == "next-page")
}
}