From b6caa41ea45e985cf52afcbb35deb4af04c990c9 Mon Sep 17 00:00:00 2001 From: Maria Tigina Date: Mon, 25 Aug 2025 17:16:18 +0200 Subject: [PATCH 1/2] Fix serializer for CreateElicitationResult --- .../kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt index 7925ddac..07a0bf4c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt @@ -225,6 +225,7 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio return when { jsonObject.contains("model") -> CreateMessageResult.serializer() jsonObject.contains("roots") -> ListRootsResult.serializer() + jsonObject.contains("action") -> CreateElicitationResult.serializer() else -> null } } From 8b2fb739393de2c6153b5c58ae155d0014412a7a Mon Sep 17 00:00:00 2001 From: Maria Tigina Date: Mon, 25 Aug 2025 17:53:27 +0200 Subject: [PATCH 2/2] Add tests --- .../kotlin/sdk/RequestSerializerTest.kt | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt new file mode 100644 index 00000000..14f67794 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/RequestSerializerTest.kt @@ -0,0 +1,324 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class RequestSerializerTest { + + // Client Result Tests + @Test + fun `should deserialize CreateMessageResult polymorphically`() { + val json = """{ + "model": "test-model", + "role": "assistant", + "content": { + "type": "text", + "text": "Hello" + }, + "stopReason": "endTurn" + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("test-model", decoded.model) + assertEquals(Role.assistant, decoded.role) + assertEquals(StopReason.EndTurn, decoded.stopReason) + } + + @Test + fun `should deserialize ListRootsResult polymorphically`() { + val json = """{ + "roots": [ + { + "uri": "file:///test", + "name": "Test Root" + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.roots.size) + assertEquals("file:///test", decoded.roots[0].uri) + assertEquals("Test Root", decoded.roots[0].name) + } + + @Test + fun `should deserialize CreateElicitationResult polymorphically`() { + val json = """{ + "action": "accept", + "content": { + "timezone": "Europe/Amsterdam" + } + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(CreateElicitationResult.Action.accept, decoded.action) + } + + // Server Result Tests + @Test + fun `should deserialize ListToolsResult polymorphically`() { + val json = """{ + "tools": [ + { + "name": "test-tool", + "description": "A test tool", + "inputSchema": { + "type": "object", + "properties": {} + } + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.tools.size) + assertEquals("test-tool", decoded.tools[0].name) + assertEquals("A test tool", decoded.tools[0].description) + } + + @Test + fun `should deserialize ListResourcesResult polymorphically`() { + val json = """{ + "resources": [ + { + "uri": "file:///test.txt", + "name": "test.txt", + "mimeType": "text/plain" + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.resources.size) + assertEquals("file:///test.txt", decoded.resources[0].uri) + assertEquals("test.txt", decoded.resources[0].name) + assertEquals("text/plain", decoded.resources[0].mimeType) + } + + @Test + fun `should deserialize ListResourceTemplatesResult polymorphically`() { + val json = """{ + "resourceTemplates": [ + { + "uriTemplate": "file:///templates/{name}", + "name": "template", + "mimeType": "text/plain" + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.resourceTemplates.size) + assertEquals("file:///templates/{name}", decoded.resourceTemplates[0].uriTemplate) + assertEquals("template", decoded.resourceTemplates[0].name) + assertEquals("text/plain", decoded.resourceTemplates[0].mimeType) + } + + @Test + fun `should deserialize ListPromptsResult polymorphically`() { + val json = """{ + "prompts": [ + { + "name": "test-prompt", + "description": "A test prompt" + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.prompts.size) + assertEquals("test-prompt", decoded.prompts[0].name) + assertEquals("A test prompt", decoded.prompts[0].description) + } + + @Test + fun `should deserialize InitializeResult polymorphically`() { + val json = """{ + "capabilities": { + "logging": {}, + "prompts": { + "listChanged": true + }, + "resources": { + "subscribe": true, + "listChanged": true + }, + "tools": { + "listChanged": true + } + }, + "protocolVersion": "2024-11-05", + "serverInfo": { + "name": "Test Server", + "version": "1.0.0" + } + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("2024-11-05", decoded.protocolVersion) + assertEquals("Test Server", decoded.serverInfo.name) + assertEquals("1.0.0", decoded.serverInfo.version) + } + + @Test + fun `should deserialize GetPromptResult polymorphically`() { + val json = """{ + "description": "A test prompt", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Hello" + } + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("A test prompt", decoded.description) + assertEquals(1, decoded.messages.size) + assertEquals(Role.user, decoded.messages[0].role) + } + + @Test + fun `should deserialize CompleteResult polymorphically`() { + val json = """{ + "completion": { + "values": ["option1", "option2"], + "total": 2, + "hasMore": false + } + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(2, decoded.completion.values.size) + assertEquals("option1", decoded.completion.values[0]) + assertEquals("option2", decoded.completion.values[1]) + assertEquals(2, decoded.completion.total) + assertEquals(false, decoded.completion.hasMore) + } + + @Test + fun `should deserialize ReadResourceResult polymorphically`() { + val json = """{ + "contents": [ + { + "uri": "file:///test.txt", + "mimeType": "text/plain", + "text": "Hello World" + } + ] + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.contents.size) + assertIs(decoded.contents[0]) + val textContent = decoded.contents[0] as TextResourceContents + assertEquals("file:///test.txt", textContent.uri) + assertEquals("text/plain", textContent.mimeType) + assertEquals("Hello World", textContent.text) + } + + @Test + fun `should deserialize CallToolResult polymorphically`() { + val json = """{ + "content": [ + { + "type": "text", + "text": "Tool result" + } + ], + "isError": false + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1, decoded.content.size) + assertIs(decoded.content[0]) + assertEquals("Tool result", (decoded.content[0] as TextContent).text) + assertEquals(false, decoded.isError) + } + + @Test + fun `should deserialize CompatibilityCallToolResult polymorphically`() { + val json = """{ + "toolResult": {"result": "Legacy tool result"}, + "content": [], + "isError": false + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(buildJsonObject { put("result", "Legacy tool result") }, decoded.toolResult) + } + + // Fallback Test + @Test + fun `should deserialize EmptyRequestResult for unknown result type`() { + val json = """{"unknownField": "value"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(EmptyJsonObject, decoded._meta) + } + + @Test + fun `should handle empty JSON object`() { + val json = """{}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + } + + // Priority Test - Client results should take precedence over server results + @Test + fun `should prioritize client results over server results when both match`() { + // This JSON could potentially match both CreateMessageResult (client) and CallToolResult (server) + // but CreateMessageResult should be selected first due to the order + val json = """{ + "model": "test-model", + "role": "assistant", + "content": { + "type": "text", + "text": "Test message" + }, + "stopReason": "endTurn" + }""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("test-model", decoded.model) + } +}