diff --git a/Sources/MCP/Base/Messages.swift b/Sources/MCP/Base/Messages.swift index c5858ebe..91ecbd7b 100644 --- a/Sources/MCP/Base/Messages.swift +++ b/Sources/MCP/Base/Messages.swift @@ -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")) } } } diff --git a/Tests/MCPTests/RequestTests.swift b/Tests/MCPTests/RequestTests.swift index 7d4e8dde..edceb352 100644 --- a/Tests/MCPTests/RequestTests.swift +++ b/Tests/MCPTests/RequestTests.swift @@ -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(id: id, method: TestMethod.name, params: params) + let id: ID = 1 + let params = CallTool.Parameters(name: "test-tool") + let request = Request(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.self, from: data) + let decoded = try decoder.decode(Request.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() @@ -66,14 +66,14 @@ 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.self, from: data) - #expect(decoded.id == "test-id") + #expect(decoded.id == 1) #expect(decoded.method == EmptyMethod.name) } @@ -81,14 +81,14 @@ struct RequestTests { 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.self, from: data) - #expect(decoded.id == "test-id") + #expect(decoded.id == 1) #expect(decoded.method == Ping.name) } @@ -96,14 +96,14 @@ struct RequestTests { 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.self, from: data) - #expect(decoded.id == "test-id") + #expect(decoded.id == 1) #expect(decoded.method == Ping.name) } @@ -111,14 +111,153 @@ struct RequestTests { 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.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.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.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.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.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.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.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.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.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.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.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.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.self, + from: withCursor.data(using: .utf8)!) + #expect(decodedWithCursor.params.cursor == "next-page") + } }