From 32df4ddd365461a8ff9033638edba6c5fe53678d Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Tue, 22 Jul 2025 17:42:11 +0300 Subject: [PATCH 01/24] Introduce Kotlin integration tests --- .gitignore | 1 + .../kotlin/client/ClientIntegrationTest.kt | 45 -- .../integration/kotlin/KotlinTestBase.kt | 96 ++++ .../integration/kotlin/PromptEdgeCasesTest.kt | 402 ++++++++++++++++ .../kotlin/PromptIntegrationTest.kt | 417 +++++++++++++++++ .../kotlin/ResourceEdgeCasesTest.kt | 279 +++++++++++ .../kotlin/ResourceIntegrationTest.kt | 89 ++++ .../integration/kotlin/ToolEdgeCasesTest.kt | 439 ++++++++++++++++++ .../integration/kotlin/ToolIntegrationTest.kt | 402 ++++++++++++++++ ...tlinClientTypeScriptServerEdgeCasesTest.kt | 269 +++++++++++ .../KotlinClientTypeScriptServerTest.kt | 194 ++++++++ .../TypeScriptClientKotlinServerTest.kt | 202 ++++++++ .../typescript/TypeScriptEdgeCasesTest.kt | 215 +++++++++ .../typescript/TypeScriptTestBase.kt | 128 +++++ .../utils/KotlinServerForTypeScriptClient.kt | 394 ++++++++++++++++ .../kotlin/integration/utils/TestUtils.kt | 78 ++++ .../kotlin/integration/utils/myClient.ts | 93 ++++ 17 files changed, 3698 insertions(+), 45 deletions(-) delete mode 100644 src/jvmTest/kotlin/client/ClientIntegrationTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt create mode 100644 src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt create mode 100644 src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt create mode 100644 src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt create mode 100644 src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt create mode 100644 src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt create mode 100644 src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt create mode 100644 src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt create mode 100644 src/jvmTest/kotlin/integration/utils/TestUtils.kt create mode 100644 src/jvmTest/kotlin/integration/utils/myClient.ts diff --git a/.gitignore b/.gitignore index 389cec6fb..e650bb754 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +/src/jvmTest/resources/typescript-sdk/ ### IntelliJ IDEA ### .idea/modules.xml diff --git a/src/jvmTest/kotlin/client/ClientIntegrationTest.kt b/src/jvmTest/kotlin/client/ClientIntegrationTest.kt deleted file mode 100644 index 5eee8179a..000000000 --- a/src/jvmTest/kotlin/client/ClientIntegrationTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import kotlinx.coroutines.test.runTest -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import java.net.Socket - -class ClientIntegrationTest { - - fun createTransport(): StdioClientTransport { - val socket = Socket("localhost", 3000) - - return StdioClientTransport( - socket.inputStream.asSource().buffered(), - socket.outputStream.asSink().buffered() - ) - } - - @Disabled("This test requires a running server") - @Test - fun testRequestTools() = runTest { - val client = Client( - Implementation("test", "1.0"), - ) - - val transport = createTransport() - try { - client.connect(transport) - - val response: ListToolsResult? = client.listTools() - println(response?.tools) - - } finally { - transport.close() - } - } - -} diff --git a/src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt b/src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt new file mode 100644 index 000000000..3eaef18c6 --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt @@ -0,0 +1,96 @@ +package integration.kotlin + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.routing.* +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import kotlin.time.Duration.Companion.seconds +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +abstract class KotlinTestBase { + + protected val host = "localhost" + protected abstract val port: Int + + protected lateinit var server: Server + protected lateinit var client: Client + protected lateinit var serverEngine: EmbeddedServer<*, *> + + protected abstract fun configureServerCapabilities(): ServerCapabilities + protected abstract fun configureServer() + + @BeforeEach + fun setUp() { + setupServer() + runBlocking { + setupClient() + } + } + + protected suspend fun setupClient() { + val transport = SseClientTransport(HttpClient(CIO) { + install(SSE) + }, "http://$host:$port") + client = Client( + Implementation("test", "1.0"), + ) + client.connect(transport) + } + + protected fun setupServer() { + val capabilities = configureServerCapabilities() + + server = Server( + Implementation(name = "test-server", version = "1.0"), + ServerOptions(capabilities = capabilities) + ) + + configureServer() + + serverEngine = embeddedServer(ServerCIO, host = host, port = port) { + install(ServerSSE) + routing { + mcp { server } + } + }.start(wait = false) + } + + @AfterEach + fun tearDown() { + // close client + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + // stop server + if (::serverEngine.isInitialized) { + try { + serverEngine.stop(500, 1000) + } catch (e: Exception) { + println("Warning: Error during server stop: ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt b/src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt new file mode 100644 index 000000000..336d8c188 --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt @@ -0,0 +1,402 @@ +package integration.kotlin + +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PromptEdgeCasesTest : KotlinTestBase() { + + override val port = 3008 + + private val basicPromptName = "basic-prompt" + private val basicPromptDescription = "A basic prompt for testing" + + private val complexPromptName = "complex-prompt" + private val complexPromptDescription = "A complex prompt with many arguments" + + private val largePromptName = "large-prompt" + private val largePromptDescription = "A very large prompt for testing" + private val largePromptContent = "X".repeat(100_000) // 100KB of data + + private val specialCharsPromptName = "special-chars-prompt" + private val specialCharsPromptDescription = "A prompt with special characters" + private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + prompts = ServerCapabilities.Prompts( + listChanged = true + ) + ) + } + + override fun configureServer() { + server.addPrompt( + name = basicPromptName, + description = basicPromptDescription, + arguments = listOf( + PromptArgument( + name = "name", + description = "The name to greet", + required = true + ) + ) + ) { request -> + val name = request.arguments?.get("name") ?: "World" + + GetPromptResult( + description = basicPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Hello, $name!") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Greetings, $name! How can I assist you today?") + ) + ) + ) + } + + server.addPrompt( + name = complexPromptName, + description = complexPromptDescription, + arguments = listOf( + PromptArgument(name = "arg1", description = "Argument 1", required = true), + PromptArgument(name = "arg2", description = "Argument 2", required = true), + PromptArgument(name = "arg3", description = "Argument 3", required = true), + PromptArgument(name = "arg4", description = "Argument 4", required = false), + PromptArgument(name = "arg5", description = "Argument 5", required = false), + PromptArgument(name = "arg6", description = "Argument 6", required = false), + PromptArgument(name = "arg7", description = "Argument 7", required = false), + PromptArgument(name = "arg8", description = "Argument 8", required = false), + PromptArgument(name = "arg9", description = "Argument 9", required = false), + PromptArgument(name = "arg10", description = "Argument 10", required = false) + ) + ) { request -> + // validate required arguments + val requiredArgs = listOf("arg1", "arg2", "arg3") + for (argName in requiredArgs) { + if (request.arguments?.get(argName) == null) { + throw IllegalArgumentException("Missing required argument: $argName") + } + } + + val args = mutableMapOf() + for (i in 1..10) { + val argName = "arg$i" + val argValue = request.arguments?.get(argName) + if (argValue != null) { + args[argName] = argValue + } + } + + GetPromptResult( + description = complexPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Arguments: ${args.entries.joinToString { "${it.key}=${it.value}" }}") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Received ${args.size} arguments") + ) + ) + ) + } + + // Very large prompt + server.addPrompt( + name = largePromptName, + description = largePromptDescription, + arguments = listOf( + PromptArgument( + name = "size", + description = "Size multiplier", + required = false + ) + ) + ) { request -> + val size = request.arguments?.get("size")?.toIntOrNull() ?: 1 + val content = largePromptContent.repeat(size) + + GetPromptResult( + description = largePromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Generate a large response") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = content) + ) + ) + ) + } + + server.addPrompt( + name = specialCharsPromptName, + description = specialCharsPromptDescription, + arguments = listOf( + PromptArgument( + name = "special", + description = "Special characters to include", + required = false + ) + ) + ) { request -> + val special = request.arguments?.get("special") ?: specialCharsContent + + GetPromptResult( + description = specialCharsPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Special characters: $special") + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Received special characters: $special") + ) + ) + ) + } + } + + @Test + fun testBasicPrompt() { + runTest { + val testName = "Alice" + val result = client.getPrompt( + GetPromptRequest( + name = basicPromptName, + arguments = mapOf("name" to testName) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(basicPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertEquals("Hello, $testName!", userContent.text, "User message content should match") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertEquals( + "Greetings, $testName! How can I assist you today?", + assistantContent.text, + "Assistant message content should match" + ) + } + } + + @Test + fun testComplexPromptWithManyArguments() { + runTest { + val arguments = (1..10).associate { i -> "arg$i" to "value$i" } + + val result = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = arguments + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(complexPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + + // verify all arguments + val text = userContent.text ?: "" + for (i in 1..10) { + assertTrue(text.contains("arg$i=value$i"), "Message should contain arg$i=value$i") + } + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertEquals( + "Received 10 arguments", + assistantContent.text, + "Assistant message should indicate 10 arguments" + ) + } + } + + @Test + fun testLargePrompt() { + runTest { + val result = client.getPrompt( + GetPromptRequest( + name = largePromptName, + arguments = mapOf("size" to "1") + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(largePromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + val text = assistantContent.text ?: "" + assertEquals(100_000, text.length, "Assistant message should be 100KB in size") + } + } + + @Test + fun testSpecialCharacters() { + runTest { + val result = client.getPrompt( + GetPromptRequest( + name = specialCharsPromptName, + arguments = mapOf("special" to specialCharsContent) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(specialCharsPromptDescription, result.description, "Prompt description should match") + + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + val userText = userContent.text ?: "" + assertTrue(userText.contains(specialCharsContent), "User message should contain special characters") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + val assistantText = assistantContent.text ?: "" + assertTrue( + assistantText.contains(specialCharsContent), + "Assistant message should contain special characters" + ) + } + } + + @Test + fun testMissingRequiredArguments() { + runTest { + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf("arg4" to "value4", "arg5" to "value5") + ) + ) + } + } + + assertTrue( + exception.message?.contains("arg1") == true || + exception.message?.contains("arg2") == true || + exception.message?.contains("arg3") == true || + exception.message?.contains("required") == true, + "Exception should mention missing required arguments" + ) + } + } + + @Test + fun testConcurrentPromptRequests() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val promptName = when (index % 4) { + 0 -> basicPromptName + 1 -> complexPromptName + 2 -> largePromptName + else -> specialCharsPromptName + } + + val arguments = when (promptName) { + basicPromptName -> mapOf("name" to "User$index") + complexPromptName -> mapOf("arg1" to "v1", "arg2" to "v2", "arg3" to "v3") + largePromptName -> mapOf("size" to "1") + else -> mapOf("special" to "!@#$%^&*()") + } + + val result = client.getPrompt( + GetPromptRequest( + name = promptName, + arguments = arguments + ) + ) + + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.messages.isNotEmpty(), "Result messages should not be empty") + } + } + } + + @Test + fun testNonExistentPrompt() { + runTest { + val nonExistentPromptName = "non-existent-prompt" + + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = nonExistentPromptName, + arguments = mapOf("name" to "Test") + ) + ) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("does not exist") == true || + exception.message?.contains("unknown") == true || + exception.message?.contains("error") == true, + "Exception should indicate prompt not found" + ) + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt b/src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt new file mode 100644 index 000000000..8d96eb4e9 --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt @@ -0,0 +1,417 @@ +package integration.kotlin + +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PromptIntegrationTest : KotlinTestBase() { + + override val port = 3004 + private val testPromptName = "greeting" + private val testPromptDescription = "A simple greeting prompt" + private val complexPromptName = "multimodal-prompt" + private val complexPromptDescription = "A prompt with multiple content types" + private val conversationPromptName = "conversation" + private val conversationPromptDescription = "A prompt with multiple messages and roles" + private val strictPromptName = "strict-prompt" + private val strictPromptDescription = "A prompt with required arguments" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + prompts = ServerCapabilities.Prompts( + listChanged = true + ) + ) + } + + override fun configureServer() { + // simple prompt with a name parameter + server.addPrompt( + name = testPromptName, + description = testPromptDescription, + arguments = listOf( + PromptArgument( + name = "name", + description = "The name to greet", + required = true, + ) + ) + ) { request -> + val name = request.arguments?.get("name") ?: "World" + + GetPromptResult( + description = testPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Hello, $name!"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Greetings, $name! How can I assist you today?"), + ) + ) + ) + } + + // prompt with multiple content types + server.addPrompt( + name = complexPromptName, + description = complexPromptDescription, + arguments = listOf( + PromptArgument( + name = "topic", + description = "The topic to discuss", + required = false, + ), + PromptArgument( + name = "includeImage", + description = "Whether to include an image", + required = false, + ) + ) + ) { request -> + val topic = request.arguments?.get("topic") ?: "general knowledge" + val includeImage = request.arguments?.get("includeImage")?.toString()?.toBoolean() ?: true + + val messages = mutableListOf() + + messages.add( + PromptMessage( + role = Role.user, + content = TextContent(text = "I'd like to discuss $topic."), + ) + ) + + val assistantContents = mutableListOf() + assistantContents.add(TextContent(text = "I'd be happy to discuss $topic with you.")) + + if (includeImage) { + assistantContents.add( + ImageContent( + data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + mimeType = "image/png", + ) + ) + } + + messages.add( + PromptMessage( + role = Role.assistant, + content = assistantContents[0], + ) + ) + + GetPromptResult( + description = complexPromptDescription, + messages = messages, + ) + } + + // prompt with multiple messages and roles + server.addPrompt( + name = conversationPromptName, + description = conversationPromptDescription, + arguments = listOf( + PromptArgument( + name = "topic", + description = "The topic of the conversation", + required = false, + ) + ) + ) { request -> + val topic = request.arguments?.get("topic") ?: "weather" + + GetPromptResult( + description = conversationPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Let's talk about the $topic."), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "Sure, I'd love to discuss the $topic. What would you like to know?"), + ), + PromptMessage( + role = Role.user, + content = TextContent(text = "What's your opinion on the $topic?"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "As an AI, I don't have personal opinions, but I can provide information about $topic."), + ), + PromptMessage( + role = Role.user, + content = TextContent(text = "That's helpful, thank you!"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "You're welcome! Let me know if you have more questions about $topic."), + ) + ) + ) + } + + // prompt with strict required arguments + server.addPrompt( + name = strictPromptName, + description = strictPromptDescription, + arguments = listOf( + PromptArgument( + name = "requiredArg1", + description = "First required argument", + required = true, + ), + PromptArgument( + name = "requiredArg2", + description = "Second required argument", + required = true, + ), + PromptArgument( + name = "optionalArg", + description = "Optional argument", + required = false, + ) + ) + ) { request -> + val arg1 = request.arguments?.get("requiredArg1") + ?: throw IllegalArgumentException("Missing required argument: requiredArg1") + val arg2 = request.arguments["requiredArg2"] + ?: throw IllegalArgumentException("Missing required argument: requiredArg2") + val optArg = request.arguments["optionalArg"] ?: "default" + + GetPromptResult( + description = strictPromptDescription, + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent(text = "Required arguments: $arg1, $arg2. Optional: $optArg"), + ), + PromptMessage( + role = Role.assistant, + content = TextContent(text = "I received your arguments: $arg1, $arg2, and $optArg"), + ) + ) + ) + } + } + + @Test + fun testListPrompts() = runTest { + val result = client.listPrompts() + + assertNotNull(result, "List prompts result should not be null") + assertTrue(result.prompts.isNotEmpty(), "Prompts list should not be empty") + + val testPrompt = result.prompts.find { it.name == testPromptName } + assertNotNull(testPrompt, "Test prompt should be in the list") + assertEquals(testPromptDescription, testPrompt.description, "Prompt description should match") + + assertNotNull(testPrompt.arguments, "Prompt arguments should not be null") + assertTrue(testPrompt.arguments.isNotEmpty(), "Prompt arguments should not be empty") + + val nameArg = testPrompt.arguments.find { it.name == "name" } + assertNotNull(nameArg, "Name argument should be in the list") + assertEquals("The name to greet", nameArg.description, "Argument description should match") + assertEquals(true, nameArg.required, "Argument required flag should match") + } + + @Test + fun testGetPrompt() = runTest { + val testName = "Alice" + val result = client.getPrompt( + GetPromptRequest( + name = testPromptName, + arguments = mapOf("name" to testName) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(testPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertNotNull(userContent.text, "User message text should not be null") + assertEquals("Hello, $testName!", userContent.text, "User message content should match") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertNotNull(assistantContent.text, "Assistant message text should not be null") + assertEquals( + "Greetings, $testName! How can I assist you today?", + assistantContent.text, + "Assistant message content should match" + ) + } + + @Test + fun testMissingRequiredArguments() = runTest { + val promptsList = client.listPrompts() + assertNotNull(promptsList, "Prompts list should not be null") + val strictPrompt = promptsList.prompts.find { it.name == strictPromptName } + assertNotNull(strictPrompt, "Strict prompt should be in the list") + + assertNotNull(strictPrompt.arguments, "Prompt arguments should not be null") + val requiredArgs = strictPrompt.arguments.filter { it.required == true } + assertEquals(2, requiredArgs.size, "Strict prompt should have 2 required arguments") + + // test missing required arg + val exception = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = mapOf("requiredArg1" to "value1"), + ) + ) + } + } + + assertTrue( + exception.message?.contains("requiredArg2") == true, + "Exception should mention the missing argument" + ) + + // test with no args + val exception2 = assertThrows { + runBlocking { + client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = emptyMap(), + ) + ) + } + } + + assertTrue( + exception2.message?.contains("requiredArg") == true, + "Exception should mention a missing required argument" + ) + + // test with all required args + val result = client.getPrompt( + GetPromptRequest( + name = strictPromptName, + arguments = mapOf( + "requiredArg1" to "value1", + "requiredArg2" to "value2", + ) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertNotNull(userContent.text, "User message text should not be null") + assertTrue(userContent.text.contains("value1"), "Message should contain first argument") + assertTrue(userContent.text.contains("value2"), "Message should contain second argument") + } + + @Test + fun testComplexContentTypes() = runTest { + val topic = "artificial intelligence" + val result = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf( + "topic" to topic, + "includeImage" to "true", + ) + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(complexPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(2, result.messages.size, "Prompt should have 2 messages") + + val userMessage = result.messages.find { it.role == Role.user } + assertNotNull(userMessage, "User message should be in the list") + val userContent = userMessage.content as? TextContent + assertNotNull(userContent, "User message content should be TextContent") + assertNotNull(userContent.text, "User message text should not be null") + assertTrue(userContent.text.contains(topic), "User message should contain the topic") + + val assistantMessage = result.messages.find { it.role == Role.assistant } + assertNotNull(assistantMessage, "Assistant message should be in the list") + val assistantContent = assistantMessage.content as? TextContent + assertNotNull(assistantContent, "Assistant message content should be TextContent") + assertNotNull(assistantContent.text, "Assistant message text should not be null") + assertTrue(assistantContent.text.contains(topic), "Assistant message should contain the topic") + + val resultNoImage = client.getPrompt( + GetPromptRequest( + name = complexPromptName, + arguments = mapOf( + "topic" to topic, + "includeImage" to "false", + ) + ) + ) + + assertNotNull(resultNoImage, "Get prompt result (no image) should not be null") + assertEquals(2, resultNoImage.messages.size, "Prompt should have 2 messages") + } + + @Test + fun testMultipleMessagesAndRoles() = runTest { + val topic = "climate change" + val result = client.getPrompt( + GetPromptRequest( + name = conversationPromptName, + arguments = mapOf("topic" to topic), + ) + ) + + assertNotNull(result, "Get prompt result should not be null") + assertEquals(conversationPromptDescription, result.description, "Prompt description should match") + + assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty") + assertEquals(6, result.messages.size, "Prompt should have 6 messages") + + val userMessages = result.messages.filter { it.role == Role.user } + val assistantMessages = result.messages.filter { it.role == Role.assistant } + + assertEquals(3, userMessages.size, "Should have 3 user messages") + assertEquals(3, assistantMessages.size, "Should have 3 assistant messages") + + for (i in 0 until result.messages.size) { + val expectedRole = if (i % 2 == 0) Role.user else Role.assistant + assertEquals(expectedRole, result.messages[i].role, "Message $i should have role $expectedRole") + } + + for (message in result.messages) { + val content = message.content as? TextContent + assertNotNull(content, "Message content should be TextContent") + assertNotNull(content.text, "Message text should not be null") + + // Either the message contains the topic or it's a generic conversation message + val containsTopic = content.text.contains(topic) + val isGenericMessage = content.text.contains("thank you") || content.text.contains("welcome") + + assertTrue( + containsTopic || isGenericMessage, + "Message should either contain the topic or be a generic conversation message" + ) + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt b/src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt new file mode 100644 index 000000000..ec4995539 --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt @@ -0,0 +1,279 @@ +package integration.kotlin + +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ResourceEdgeCasesTest : KotlinTestBase() { + + override val port = 3007 + + private val testResourceUri = "test://example.txt" + private val testResourceName = "Test Resource" + private val testResourceDescription = "A test resource for integration testing" + private val testResourceContent = "This is the content of the test resource." + + private val binaryResourceUri = "test://image.png" + private val binaryResourceName = "Binary Resource" + private val binaryResourceDescription = "A binary resource for testing" + private val binaryResourceContent = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + + private val largeResourceUri = "test://large.txt" + private val largeResourceName = "Large Resource" + private val largeResourceDescription = "A large text resource for testing" + private val largeResourceContent = "X".repeat(100_000) // 100KB of data + + private val dynamicResourceUri = "test://dynamic.txt" + private val dynamicResourceName = "Dynamic Resource" + private val dynamicResourceContent = AtomicBoolean(false) + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + resources = ServerCapabilities.Resources( + subscribe = true, + listChanged = true, + ) + ) + } + + override fun configureServer() { + server.addResource( + uri = testResourceUri, + name = testResourceName, + description = testResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = testResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.addResource( + uri = binaryResourceUri, + name = binaryResourceName, + description = binaryResourceDescription, + mimeType = "image/png", + ) { request -> + ReadResourceResult( + contents = listOf( + BlobResourceContents( + blob = binaryResourceContent, + uri = request.uri, + mimeType = "image/png", + ) + ) + ) + } + + server.addResource( + uri = largeResourceUri, + name = largeResourceName, + description = largeResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = largeResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.addResource( + uri = dynamicResourceUri, + name = dynamicResourceName, + description = "A resource that can be updated", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = if (dynamicResourceContent.get()) "Updated content" else "Original content", + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { request, _ -> + EmptyRequestResult() + } + + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { request, _ -> + EmptyRequestResult() + } + } + + @Test + fun testBinaryResource() { + runTest { + val result = client.readResource(ReadResourceRequest(uri = binaryResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? BlobResourceContents + assertNotNull(content, "Resource content should be BlobResourceContents") + assertEquals(binaryResourceContent, content.blob, "Binary resource content should match") + assertEquals("image/png", content.mimeType, "MIME type should match") + } + } + + @Test + fun testLargeResource() { + runTest { + val result = client.readResource(ReadResourceRequest(uri = largeResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? TextResourceContents + assertNotNull(content, "Resource content should be TextResourceContents") + assertEquals(100_000, content.text.length, "Large resource content length should match") + assertEquals("X".repeat(100_000), content.text, "Large resource content should match") + } + } + + @Test + fun testInvalidResourceUri() { + runTest { + val invalidUri = "test://nonexistent.txt" + + val exception = assertThrows { + runBlocking { + client.readResource(ReadResourceRequest(uri = invalidUri)) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("invalid") == true || + exception.message?.contains("error") == true, + "Exception should indicate resource not found or invalid URI" + ) + } + } + + @Test + fun testDynamicResource() { + runTest { + val initialResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + assertNotNull(initialResult, "Initial read result should not be null") + val initialContent = (initialResult.contents.firstOrNull() as? TextResourceContents)?.text + assertEquals("Original content", initialContent, "Initial content should match") + + // update resource + dynamicResourceContent.set(true) + + val updatedResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri)) + assertNotNull(updatedResult, "Updated read result should not be null") + val updatedContent = (updatedResult.contents.firstOrNull() as? TextResourceContents)?.text + assertEquals("Updated content", updatedContent, "Updated content should match") + } + } + + @Test + fun testResourceAddAndRemove() { + runTest { + val initialList = client.listResources() + assertNotNull(initialList, "Initial list result should not be null") + val initialCount = initialList.resources.size + + val newResourceUri = "test://new-resource.txt" + server.addResource( + uri = newResourceUri, + name = "New Resource", + description = "A newly added resource", + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "New resource content", + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + val updatedList = client.listResources() + assertNotNull(updatedList, "Updated list result should not be null") + val updatedCount = updatedList.resources.size + + assertEquals(initialCount + 1, updatedCount, "Resource count should increase by 1") + val newResource = updatedList.resources.find { it.uri == newResourceUri } + assertNotNull(newResource, "New resource should be in the list") + + server.removeResource(newResourceUri) + + val finalList = client.listResources() + assertNotNull(finalList, "Final list result should not be null") + val finalCount = finalList.resources.size + + assertEquals(initialCount, finalCount, "Resource count should return to initial value") + val removedResource = finalList.resources.find { it.uri == newResourceUri } + assertEquals(null, removedResource, "Resource should be removed from the list") + } + } + + @Test + fun testConcurrentResourceOperations() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val uri = when (index % 3) { + 0 -> testResourceUri + 1 -> binaryResourceUri + else -> largeResourceUri + } + + val result = client.readResource(ReadResourceRequest(uri = uri)) + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty") + } + } + } + + @Test + fun testSubscribeAndUnsubscribe() { + runTest { + val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri)) + assertNotNull(subscribeResult, "Subscribe result should not be null") + + val unsubscribeResult = client.unsubscribeResource(UnsubscribeRequest(uri = testResourceUri)) + assertNotNull(unsubscribeResult, "Unsubscribe result should not be null") + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt b/src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt new file mode 100644 index 000000000..11ebedb9c --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt @@ -0,0 +1,89 @@ +package integration.kotlin + +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ResourceIntegrationTest : KotlinTestBase() { + + override val port = 3005 + private val testResourceUri = "test://example.txt" + private val testResourceName = "Test Resource" + private val testResourceDescription = "A test resource for integration testing" + private val testResourceContent = "This is the content of the test resource." + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + resources = ServerCapabilities.Resources( + subscribe = true, + listChanged = true, + ) + ) + } + + override fun configureServer() { + server.addResource( + uri = testResourceUri, + name = testResourceName, + description = testResourceDescription, + mimeType = "text/plain", + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = testResourceContent, + uri = request.uri, + mimeType = "text/plain", + ) + ) + ) + } + + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { request, _ -> + EmptyRequestResult() + } + + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { request, _ -> + EmptyRequestResult() + } + } + + @Test + fun testListResources() = runTest { + val result = client.listResources() + + assertNotNull(result, "List resources result should not be null") + assertTrue(result.resources.isNotEmpty(), "Resources list should not be empty") + + val testResource = result.resources.find { it.uri == testResourceUri } + assertNotNull(testResource, "Test resource should be in the list") + assertEquals(testResourceName, testResource.name, "Resource name should match") + assertEquals(testResourceDescription, testResource.description, "Resource description should match") + } + + @Test + fun testReadResource() = runTest { + val result = client.readResource(ReadResourceRequest(uri = testResourceUri)) + + assertNotNull(result, "Read resource result should not be null") + assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty") + + val content = result.contents.firstOrNull() as? TextResourceContents + assertNotNull(content, "Resource content should be TextResourceContents") + assertEquals(testResourceContent, content.text, "Resource content should match") + } + + @Test + fun testSubscribeAndUnsubscribe() { + runTest { + val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri)) + assertNotNull(subscribeResult, "Subscribe result should not be null") + + val unsubscribeResult = client.unsubscribeResource(UnsubscribeRequest(uri = testResourceUri)) + assertNotNull(unsubscribeResult, "Unsubscribe result should not be null") + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt b/src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt new file mode 100644 index 000000000..4d15c3acc --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt @@ -0,0 +1,439 @@ +package integration.kotlin + +import integration.utils.TestUtils.assertCallToolResult +import integration.utils.TestUtils.assertJsonProperty +import integration.utils.TestUtils.assertTextContent +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ToolEdgeCasesTest : KotlinTestBase() { + + override val port = 3009 + + private val basicToolName = "basic-tool" + private val basicToolDescription = "A basic tool for testing" + + private val complexToolName = "complex-tool" + private val complexToolDescription = "A complex tool with nested schema" + + private val largeToolName = "large-tool" + private val largeToolDescription = "A tool that returns a large response" + private val largeToolContent = "X".repeat(100_000) // 100KB of data + + private val slowToolName = "slow-tool" + private val slowToolDescription = "A tool that takes time to respond" + + private val specialCharsToolName = "special-chars-tool" + private val specialCharsToolDescription = "A tool that handles special characters" + private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + tools = ServerCapabilities.Tools( + listChanged = true + ) + ) + } + + override fun configureServer() { + server.addTool( + name = basicToolName, + description = basicToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "The text to echo back") + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + + CallToolResult( + content = listOf(TextContent(text = "Echo: $text")), + structuredContent = buildJsonObject { + put("result", text) + } + ) + } + + server.addTool( + name = complexToolName, + description = complexToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("user", buildJsonObject { + put("type", "object") + put("description", "User information") + put("properties", buildJsonObject { + put("name", buildJsonObject { + put("type", "string") + put("description", "User's name") + }) + put("age", buildJsonObject { + put("type", "integer") + put("description", "User's age") + }) + put("address", buildJsonObject { + put("type", "object") + put("description", "User's address") + put("properties", buildJsonObject { + put("street", buildJsonObject { + put("type", "string") + }) + put("city", buildJsonObject { + put("type", "string") + }) + put("country", buildJsonObject { + put("type", "string") + }) + }) + }) + }) + }) + put("options", buildJsonObject { + put("type", "array") + put("description", "Additional options") + put("items", buildJsonObject { + put("type", "string") + }) + }) + }, + required = listOf("user") + ) + ) { request -> + val user = request.arguments["user"] as? JsonObject + val name = (user?.get("name") as? JsonPrimitive)?.content ?: "Unknown" + val age = (user?.get("age") as? JsonPrimitive)?.content?.toIntOrNull() ?: 0 + + val address = user?.get("address") as? JsonObject + val street = (address?.get("street") as? JsonPrimitive)?.content ?: "Unknown" + val city = (address?.get("city") as? JsonPrimitive)?.content ?: "Unknown" + val country = (address?.get("country") as? JsonPrimitive)?.content ?: "Unknown" + + val options = (request.arguments["options"] as? JsonArray)?.mapNotNull { + (it as? JsonPrimitive)?.content + } ?: emptyList() + + val summary = + "User: $name, Age: $age, Address: $street, $city, $country, Options: ${options.joinToString(", ")}" + + CallToolResult( + content = listOf(TextContent(text = summary)), + structuredContent = buildJsonObject { + put("name", name) + put("age", age) + put("address", buildJsonObject { + put("street", street) + put("city", city) + put("country", country) + }) + put("options", buildJsonArray { + options.forEach { add(it) } + }) + } + ) + } + + server.addTool( + name = largeToolName, + description = largeToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("size", buildJsonObject { + put("type", "integer") + put("description", "Size multiplier") + }) + } + ) + ) { request -> + val size = (request.arguments["size"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1 + val content = largeToolContent.substring(0, largeToolContent.length.coerceAtMost(size * 1000)) + + CallToolResult( + content = listOf(TextContent(text = content)), + structuredContent = buildJsonObject { + put("size", content.length) + } + ) + } + + server.addTool( + name = slowToolName, + description = slowToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("delay", buildJsonObject { + put("type", "integer") + put("description", "Delay in milliseconds") + }) + } + ) + ) { request -> + val delay = (request.arguments["delay"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000 + + // simulate slow operation + runBlocking { + delay(delay.toLong()) + } + + CallToolResult( + content = listOf(TextContent(text = "Completed after ${delay}ms delay")), + structuredContent = buildJsonObject { + put("delay", delay) + } + ) + } + + server.addTool( + name = specialCharsToolName, + description = specialCharsToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("special", buildJsonObject { + put("type", "string") + put("description", "Special characters to process") + }) + } + ) + ) { request -> + val special = (request.arguments["special"] as? JsonPrimitive)?.content ?: specialCharsContent + + CallToolResult( + content = listOf(TextContent(text = "Received special characters: $special")), + structuredContent = buildJsonObject { + put("special", special) + put("length", special.length) + } + ) + } + } + + @Test + fun testBasicTool() { + runTest { + val testText = "Hello, world!" + val arguments = mapOf("text" to testText) + + val result = client.callTool(basicToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertTextContent(toolResult.content.firstOrNull(), "Echo: $testText") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "result", testText) + } + } + + @Test + fun testComplexNestedSchema() { + runTest { + val userJson = buildJsonObject { + put("name", JsonPrimitive("John Doe")) + put("age", JsonPrimitive(30)) + put("address", buildJsonObject { + put("street", JsonPrimitive("123 Main St")) + put("city", JsonPrimitive("New York")) + put("country", JsonPrimitive("USA")) + }) + } + + val optionsJson = buildJsonArray { + add(JsonPrimitive("option1")) + add(JsonPrimitive("option2")) + add(JsonPrimitive("option3")) + } + + val arguments = buildJsonObject { + put("user", userJson) + put("options", optionsJson) + } + + val result = client.callTool( + CallToolRequest( + name = complexToolName, + arguments = arguments + ) + ) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains("John Doe"), "Result should contain the name") + assertTrue(text.contains("30"), "Result should contain the age") + assertTrue(text.contains("123 Main St"), "Result should contain the street") + assertTrue(text.contains("New York"), "Result should contain the city") + assertTrue(text.contains("USA"), "Result should contain the country") + assertTrue(text.contains("option1"), "Result should contain option1") + assertTrue(text.contains("option2"), "Result should contain option2") + assertTrue(text.contains("option3"), "Result should contain option3") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "name", "John Doe") + assertJsonProperty(structuredContent, "age", 30) + + val address = structuredContent["address"] as? JsonObject + assertNotNull(address, "Address should be present in structured content") + assertJsonProperty(address, "street", "123 Main St") + assertJsonProperty(address, "city", "New York") + assertJsonProperty(address, "country", "USA") + + val options = structuredContent["options"] as? JsonArray + assertNotNull(options, "Options should be present in structured content") + assertEquals(3, options.size, "Options should have 3 items") + } + } + + @Test + fun testLargeResponse() { + runTest { + val size = 10 + val arguments = mapOf("size" to size) + + val result = client.callTool(largeToolName, arguments) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertEquals(10000, text.length, "Response should be 10KB in size") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "size", 10000) + } + } + + @Test + fun testSlowTool() { + runTest { + val delay = 500 + val arguments = mapOf("delay" to delay) + + val startTime = System.currentTimeMillis() + val result = client.callTool(slowToolName, arguments) + val endTime = System.currentTimeMillis() + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains("${delay}ms"), "Result should mention the delay") + assertTrue(endTime - startTime >= delay, "Tool should take at least the specified delay") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "delay", delay) + } + } + + @Test + fun testSpecialCharacters() { + runTest { + val arguments = mapOf("special" to specialCharsContent) + + val result = client.callTool(specialCharsToolName, arguments) + + val toolResult = assertCallToolResult(result) + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + val text = content.text ?: "" + + assertTrue(text.contains(specialCharsContent), "Result should contain the special characters") + + val structuredContent = toolResult.structuredContent as JsonObject + val special = structuredContent["special"]?.toString()?.trim('"') + + assertNotNull(special, "Special characters should be in structured content") + assertTrue(text.contains(specialCharsContent), "Special characters should be in the content") + } + } + + @Test + fun testConcurrentToolCalls() { + runTest { + val concurrentCount = 10 + val results = mutableListOf() + + runBlocking { + repeat(concurrentCount) { index -> + launch { + val toolName = when (index % 5) { + 0 -> basicToolName + 1 -> complexToolName + 2 -> largeToolName + 3 -> slowToolName + else -> specialCharsToolName + } + + val arguments = when (toolName) { + basicToolName -> mapOf("text" to "Concurrent call $index") + complexToolName -> mapOf( + "user" to mapOf( + "name" to "User $index", + "age" to 20 + index, + "address" to mapOf( + "street" to "Street $index", + "city" to "City $index", + "country" to "Country $index" + ) + ) + ) + + largeToolName -> mapOf("size" to 1) + slowToolName -> mapOf("delay" to 100) + else -> mapOf("special" to "!@#$%^&*()") + } + + val result = client.callTool(toolName, arguments) + + synchronized(results) { + results.add(result) + } + } + } + } + + assertEquals(concurrentCount, results.size, "All concurrent operations should complete") + results.forEach { result -> + assertNotNull(result, "Result should not be null") + assertTrue(result.content.isNotEmpty(), "Result content should not be empty") + } + } + } + + @Test + fun testNonExistentTool() { + runTest { + val nonExistentToolName = "non-existent-tool" + val arguments = mapOf("text" to "Test") + + val exception = assertThrows { + runBlocking { + client.callTool(nonExistentToolName, arguments) + } + } + + assertTrue( + exception.message?.contains("not found") == true || + exception.message?.contains("does not exist") == true || + exception.message?.contains("unknown") == true || + exception.message?.contains("error") == true, + "Exception should indicate tool not found" + ) + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt b/src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt new file mode 100644 index 000000000..dd971efa5 --- /dev/null +++ b/src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt @@ -0,0 +1,402 @@ +package integration.kotlin + +import integration.utils.TestUtils.assertCallToolResult +import integration.utils.TestUtils.assertJsonProperty +import integration.utils.TestUtils.assertTextContent +import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ToolIntegrationTest : KotlinTestBase() { + + override val port = 3006 + private val testToolName = "echo" + private val testToolDescription = "A simple echo tool that returns the input text" + private val complexToolName = "calculator" + private val complexToolDescription = "A calculator tool that performs operations on numbers" + private val errorToolName = "error-tool" + private val errorToolDescription = "A tool that demonstrates error handling" + private val multiContentToolName = "multi-content" + private val multiContentToolDescription = "A tool that returns multiple content types" + + override fun configureServerCapabilities(): ServerCapabilities { + return ServerCapabilities( + tools = ServerCapabilities.Tools( + listChanged = true + ) + ) + } + + override fun configureServer() { + setupEchoTool() + setupCalculatorTool() + setupErrorHandlingTool() + setupMultiContentTool() + } + + private fun setupEchoTool() { + server.addTool( + name = testToolName, + description = testToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "The text to echo back") + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "No text provided" + + CallToolResult( + content = listOf(TextContent(text = "Echo: $text")), + structuredContent = buildJsonObject { + put("result", text) + } + ) + } + } + + private fun setupCalculatorTool() { + server.addTool( + name = complexToolName, + description = complexToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("operation", buildJsonObject { + put("type", "string") + put("description", "The operation to perform (add, subtract, multiply, divide)") + put("enum", buildJsonArray { + add("add") + add("subtract") + add("multiply") + add("divide") + }) + }) + put("a", buildJsonObject { + put("type", "number") + put("description", "First operand") + }) + put("b", buildJsonObject { + put("type", "number") + put("description", "Second operand") + }) + put("precision", buildJsonObject { + put("type", "integer") + put("description", "Number of decimal places (optional)") + put("default", 2) + }) + put("showSteps", buildJsonObject { + put("type", "boolean") + put("description", "Whether to show calculation steps") + put("default", false) + }) + put("tags", buildJsonObject { + put("type", "array") + put("description", "Optional tags for the calculation") + put("items", buildJsonObject { + put("type", "string") + }) + }) + }, + required = listOf("operation", "a", "b") + ) + ) { request -> + val operation = (request.arguments["operation"] as? JsonPrimitive)?.content ?: "add" + val a = (request.arguments["a"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val b = (request.arguments["b"] as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0 + val precision = (request.arguments["precision"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 2 + val showSteps = (request.arguments["showSteps"] as? JsonPrimitive)?.content?.toBoolean() ?: false + val tags = (request.arguments["tags"] as? JsonArray)?.mapNotNull { + (it as? JsonPrimitive)?.content + } ?: emptyList() + + val result = when (operation) { + "add" -> a + b + "subtract" -> a - b + "multiply" -> a * b + "divide" -> if (b != 0.0) a / b else Double.POSITIVE_INFINITY + else -> 0.0 + } + + val formattedResult = "%.${precision}f".format(result) + + val textContent = if (showSteps) { + "Operation: $operation\nA: $a\nB: $b\nResult: $formattedResult\nTags: ${tags.joinToString(", ")}" + } else { + "Result: $formattedResult" + } + + CallToolResult( + content = listOf(TextContent(text = textContent)), + structuredContent = buildJsonObject { + put("operation", operation) + put("a", a) + put("b", b) + put("result", result) + put("formattedResult", formattedResult) + put("precision", precision) + put("tags", buildJsonArray { tags.forEach { add(it) } }) + } + ) + } + } + + private fun setupErrorHandlingTool() { + server.addTool( + name = errorToolName, + description = errorToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("errorType", buildJsonObject { + put("type", "string") + put("description", "Type of error to simulate (none, exception, error)") + put("enum", buildJsonArray { + add("none") + add("exception") + add("error") + }) + }) + put("message", buildJsonObject { + put("type", "string") + put("description", "Custom error message") + put("default", "An error occurred") + }) + }, + required = listOf("errorType") + ) + ) { request -> + val errorType = (request.arguments["errorType"] as? JsonPrimitive)?.content ?: "none" + val message = (request.arguments["message"] as? JsonPrimitive)?.content ?: "An error occurred" + + when (errorType) { + "exception" -> throw IllegalArgumentException(message) + "error" -> CallToolResult( + content = listOf(TextContent(text = "Error: $message")), + structuredContent = buildJsonObject { + put("error", true) + put("message", message) + } + ) + + else -> CallToolResult( + content = listOf(TextContent(text = "No error occurred")), + structuredContent = buildJsonObject { + put("error", false) + put("message", "Success") + } + ) + } + } + } + + private fun setupMultiContentTool() { + server.addTool( + name = multiContentToolName, + description = multiContentToolDescription, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("text", buildJsonObject { + put("type", "string") + put("description", "Text to include in the response") + }) + put("includeImage", buildJsonObject { + put("type", "boolean") + put("description", "Whether to include an image in the response") + put("default", true) + }) + }, + required = listOf("text") + ) + ) { request -> + val text = (request.arguments["text"] as? JsonPrimitive)?.content ?: "Default text" + val includeImage = (request.arguments["includeImage"] as? JsonPrimitive)?.content?.toBoolean() ?: true + + val content = mutableListOf( + TextContent(text = "Text content: $text") + ) + + if (includeImage) { + content.add( + ImageContent( + data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + mimeType = "image/png" + ) + ) + } + + CallToolResult( + content = content, + structuredContent = buildJsonObject { + put("text", text) + put("includeImage", includeImage) + } + ) + } + } + + + @Test + fun testListTools() = runTest { + val result = client.listTools() + + assertNotNull(result, "List utils result should not be null") + assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") + + val testTool = result.tools.find { it.name == testToolName } + assertNotNull(testTool, "Test tool should be in the list") + assertEquals(testToolDescription, testTool.description, "Tool description should match") + } + + @Test + fun testCallTool() = runTest { + val testText = "Hello, world!" + val arguments = mapOf("text" to testText) + + val result = client.callTool(testToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertTextContent(toolResult.content.firstOrNull(), "Echo: $testText") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "result", testText) + } + + @Test + fun testComplexInputSchemaTool() { + runTest { + val toolsList = client.listTools() + assertNotNull(toolsList, "Tools list should not be null") + val calculatorTool = toolsList.tools.find { it.name == complexToolName } + assertNotNull(calculatorTool, "Calculator tool should be in the list") + + val arguments = mapOf( + "operation" to "multiply", + "a" to 5.5, + "b" to 2.0, + "precision" to 3, + "showSteps" to true, + "tags" to listOf("test", "calculator", "integration"), + ) + + val result = client.callTool(complexToolName, arguments) + + val toolResult = assertCallToolResult(result) + + val content = toolResult.content.firstOrNull() as? TextContent + assertNotNull(content, "Tool result content should be TextContent") + assertNotNull(content.text, "Text content should not be null") + val contentText = content.text + + + assertTrue(contentText.contains("Operation"), "Result should contain operation") + assertTrue(contentText.contains("multiply"), "Result should contain multiply operation") + assertTrue(contentText.contains("5.5"), "Result should contain first operand") + assertTrue(contentText.contains("2.0"), "Result should contain second operand") + assertTrue(contentText.contains("11"), "Result should contain result value") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "operation", "multiply") + assertJsonProperty(structuredContent, "result", 11.0) + + val formattedResult = structuredContent["formattedResult"]?.toString()?.trim('"') ?: "" + assertTrue( + formattedResult == "11.000" || formattedResult == "11,000", + "Formatted result should be either '11.000' or '11,000', but was '$formattedResult'" + ) + assertJsonProperty(structuredContent, "precision", 3) + + val tags = structuredContent["tags"] as? JsonArray + assertNotNull(tags, "Tags should be present") + } + } + + @Test + fun testToolErrorHandling() = runTest { + val successArgs = mapOf("errorType" to "none") + val successResult = client.callTool(errorToolName, successArgs) + + val successToolResult = assertCallToolResult(successResult, "No error: ") + assertTextContent(successToolResult.content.firstOrNull(), "No error occurred") + + val noErrorStructured = successToolResult.structuredContent as JsonObject + assertJsonProperty(noErrorStructured, "error", false) + + val errorArgs = mapOf( + "errorType" to "error", + "message" to "Custom error message", + ) + val errorResult = client.callTool(errorToolName, errorArgs) + + val errorToolResult = assertCallToolResult(errorResult, "Error: ") + assertTextContent(errorToolResult.content.firstOrNull(), "Error: Custom error message") + + val errorStructured = errorToolResult.structuredContent as JsonObject + assertJsonProperty(errorStructured, "error", true) + assertJsonProperty(errorStructured, "message", "Custom error message") + + val exceptionArgs = mapOf( + "errorType" to "exception", + "message" to "Exception message", + ) + + val exception = assertThrows { + runBlocking { + client.callTool(errorToolName, exceptionArgs) + } + } + + assertTrue( + exception.message?.contains("Exception message") == true, + "Exception message should contain 'Exception message'" + ) + } + + @Test + fun testMultiContentTool() = runTest { + val testText = "Test multi-content" + val arguments = mapOf( + "text" to testText, + "includeImage" to true, + ) + + val result = client.callTool(multiContentToolName, arguments) + + val toolResult = assertCallToolResult(result) + assertEquals(2, toolResult.content.size, "Tool result should have 2 content items") + + val textContent = toolResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Result should contain TextContent") + assertNotNull(textContent.text, "Text content should not be null") + assertEquals("Text content: $testText", textContent.text, "Text content should match") + + val imageContent = toolResult.content.firstOrNull { it is ImageContent } as? ImageContent + assertNotNull(imageContent, "Result should contain ImageContent") + assertEquals("image/png", imageContent.mimeType, "Image MIME type should match") + assertTrue(imageContent.data.isNotEmpty(), "Image data should not be empty") + + val structuredContent = toolResult.structuredContent as JsonObject + assertJsonProperty(structuredContent, "text", testText) + assertJsonProperty(structuredContent, "includeImage", true) + + val textOnlyArgs = mapOf( + "text" to testText, + "includeImage" to false, + ) + + val textOnlyResult = client.callTool(multiContentToolName, textOnlyArgs) + + val textOnlyToolResult = assertCallToolResult(textOnlyResult, "Text-only: ") + assertEquals(1, textOnlyToolResult.content.size, "Text-only result should have 1 content item") + + assertTextContent(textOnlyToolResult.content.firstOrNull(), "Text content: $testText") + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt b/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt new file mode 100644 index 000000000..a2a2c9ec7 --- /dev/null +++ b/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt @@ -0,0 +1,269 @@ +package integration.typescript + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.* +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class KotlinClientTypeScriptServerEdgeCasesTest : TypeScriptTestBase() { + + private var port: Int = 0 + private val host = "localhost" + private lateinit var serverUrl: String + + private lateinit var client: Client + private lateinit var tsServerProcess: Process + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://$host:$port/mcp" + killProcessOnPort(port) + println("Starting TypeScript server on port $port") + val processBuilder = ProcessBuilder() + .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") + .directory(sdkDir) + .redirectErrorStream(true) + + tsServerProcess = processBuilder.start() + if (!waitForPort(host, port, 10)) { + throw IllegalStateException("TypeScript server did not become ready on $host:$port within timeout") + } + println("TypeScript server started on port $port") + + // print TypeScript server process output + val outputReader = createProcessOutputReader(tsServerProcess, "TS-SERVER") + outputReader.start() + } + + @AfterEach + fun tearDown() { + // close the client + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + // terminate TypeScript server + if (::tsServerProcess.isInitialized) { + try { + println("Stopping TypeScript server") + tsServerProcess.destroy() + if (waitForProcessTermination(tsServerProcess, 3)) { + println("TypeScript server stopped gracefully") + } else { + println("TypeScript server did not stop gracefully, forced termination") + } + } catch (e: Exception) { + println("Warning: Error during TypeScript server stop: ${e.message}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testNonExistentTool() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val nonExistentToolName = "non-existent-tool" + val arguments = mapOf("name" to "TestUser") + + val exception = assertThrows { + client.callTool(nonExistentToolName, arguments) + } + + val errorMessage = exception.message ?: "" + assertTrue( + errorMessage.contains("not found") || + errorMessage.contains("unknown") || + errorMessage.contains("error"), + "Exception should indicate tool not found: $errorMessage" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testSpecialCharactersInArguments() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" + val arguments = mapOf("name" to specialChars) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + + val text = textContent.text ?: "" + assertTrue( + text.contains(specialChars), + "Tool response should contain the special characters" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testLargePayload() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val largeName = "A".repeat(10 * 1024) + val arguments = mapOf("name" to largeName) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + + val text = textContent.text ?: "" + assertTrue( + text.contains("Hello,") && text.contains("A"), + "Tool response should contain the greeting with the large name" + ) + } + } + } + + @Test + @Timeout(60, unit = TimeUnit.SECONDS) + fun testConcurrentRequests() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val concurrentCount = 5 + val results = mutableListOf>() + + for (i in 1..concurrentCount) { + val deferred = async { + val name = "ConcurrentClient$i" + val arguments = mapOf("name" to name) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null for client $i") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present for client $i") + + textContent.text ?: "" + } + results.add(deferred) + } + + val responses = results.awaitAll() + + for (i in 1..concurrentCount) { + val expectedName = "ConcurrentClient$i" + val matchingResponses = responses.filter { it.contains("Hello, $expectedName!") } + assertEquals( + 1, + matchingResponses.size, + "Should have exactly one response for $expectedName" + ) + } + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testInvalidArguments() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val invalidArguments = mapOf( + "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))) + ) + + try { + val result = client.callTool("greet", invalidArguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + } catch (e: Exception) { + assertTrue( + e.message?.contains("invalid") == true || + e.message?.contains("error") == true, + "Exception should indicate invalid arguments: ${e.message}" + ) + } + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleToolCalls() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + repeat(10) { i -> + val name = "SequentialClient$i" + val arguments = mapOf("name" to name) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null for call $i") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present for call $i") + + assertEquals( + "Hello, $name!", + textContent.text, + "Tool response should contain the greeting with the provided name" + ) + } + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt b/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt new file mode 100644 index 000000000..1aaaeb81b --- /dev/null +++ b/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt @@ -0,0 +1,194 @@ +package integration.typescript + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.sse.* +import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class KotlinClientTypeScriptServerTest : TypeScriptTestBase() { + + private var port: Int = 0 + private val host = "localhost" + private lateinit var serverUrl: String + + private lateinit var client: Client + private lateinit var tsServerProcess: Process + + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://$host:$port/mcp" + killProcessOnPort(port) + println("Starting TypeScript server on port $port") + val processBuilder = ProcessBuilder() + .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") + .directory(sdkDir) + .redirectErrorStream(true) + + tsServerProcess = processBuilder.start() + if (!waitForPort(host, port, 10)) { + throw IllegalStateException("TypeScript server did not become ready on $host:$port within timeout") + } + println("TypeScript server started on port $port") + + // print TypeScript server process output + val outputReader = createProcessOutputReader(tsServerProcess, "TS-SERVER") + outputReader.start() + } + + @AfterEach + fun tearDown() { + // close the client + if (::client.isInitialized) { + try { + runBlocking { + withTimeout(3.seconds) { + client.close() + } + } + } catch (e: Exception) { + println("Warning: Error during client close: ${e.message}") + } + } + + // terminate TypeScript server + if (::tsServerProcess.isInitialized) { + try { + println("Stopping TypeScript server") + tsServerProcess.destroy() + if (waitForProcessTermination(tsServerProcess, 3)) { + println("TypeScript server stopped gracefully") + } else { + println("TypeScript server did not stop gracefully, forced termination") + } + } catch (e: Exception) { + println("Warning: Error during TypeScript server stop: ${e.message}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testKotlinClientConnectsToTypeScriptServer() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + assertNotNull(client, "Client should be initialized") + + val pingResult = client.ping() + assertNotNull(pingResult, "Ping result should not be null") + + val serverImpl = client.serverVersion + assertNotNull(serverImpl, "Server implementation should not be null") + println("Connected to TypeScript server: ${serverImpl.name} v${serverImpl.version}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testListTools() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val result = client.listTools() + assertNotNull(result, "Tools list should not be null") + assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") + + // Verify specific utils are available + val toolNames = result.tools.map { it.name } + assertTrue(toolNames.contains("greet"), "Greet tool should be available") + assertTrue(toolNames.contains("multi-greet"), "Multi-greet tool should be available") + assertTrue(toolNames.contains("collect-user-info"), "Collect-user-info tool should be available") + + println("Available utils: ${toolNames.joinToString()}") + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testToolCall() { + runBlocking { + withContext(Dispatchers.IO) { + client = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val testName = "TestUser" + val arguments = mapOf("name" to testName) + + val result = client.callTool("greet", arguments) + assertNotNull(result, "Tool call result should not be null") + + val callResult = result as CallToolResult + val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + assertNotNull(textContent, "Text content should be present in the result") + assertEquals( + "Hello, $testName!", + textContent.text, + "Tool response should contain the greeting with the provided name" + ) + } + } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleClients() { + runBlocking { + withContext(Dispatchers.IO) { + // First client connection + val client1 = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val tools1 = client1.listTools() + assertNotNull(tools1, "Tools list for first client should not be null") + assertTrue(tools1.tools.isNotEmpty(), "Tools list for first client should not be empty") + + val client2 = HttpClient(CIO) { + install(SSE) + }.mcpStreamableHttp(serverUrl) + + val tools2 = client2.listTools() + assertNotNull(tools2, "Tools list for second client should not be null") + assertTrue(tools2.tools.isNotEmpty(), "Tools list for second client should not be empty") + + val toolNames1 = tools1.tools.map { it.name } + val toolNames2 = tools2.tools.map { it.name } + + assertTrue(toolNames1.contains("greet"), "Greet tool should be available to first client") + assertTrue(toolNames1.contains("multi-greet"), "Multi-greet tool should be available to first client") + assertTrue(toolNames2.contains("greet"), "Greet tool should be available to second client") + assertTrue(toolNames2.contains("multi-greet"), "Multi-greet tool should be available to second client") + + client1.close() + client2.close() + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt b/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt new file mode 100644 index 000000000..f9c740cc0 --- /dev/null +++ b/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -0,0 +1,202 @@ +package integration.typescript + +import integration.utils.KotlinServerForTypeScriptClient +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.test.assertTrue + +class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { + + private var port: Int = 0 + private lateinit var serverUrl: String + private var httpServer: KotlinServerForTypeScriptClient? = null + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://localhost:$port/mcp" + killProcessOnPort(port) + httpServer = KotlinServerForTypeScriptClient() + httpServer?.start(port) + Thread.sleep(1000) + println("Kotlin server started on port $port") + } + + @AfterEach + fun tearDown() { + try { + httpServer?.stop() + println("HTTP server stopped") + } catch (e: Exception) { + println("Error during server shutdown: ${e.message}") + } + } + + @Test + @Timeout(20, unit = TimeUnit.SECONDS) + fun testToolCall() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + // call the "greet" tool + val testName = "TestUser" + val command = "npx tsx myClient.ts $serverUrl greet $testName" + val output = executeCommand(command, clientDir) + + assertTrue( + output.contains("Hello, $testName!"), + "Tool response should contain the greeting with the provided name" + ) + assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") + assertTrue(output.contains("Text content:"), "Output should contain the text content section") + assertTrue(output.contains("Structured content:"), "Output should contain the structured content section") + assertTrue( + output.contains("\"greeting\": \"Hello, $testName!\""), + "Structured content should contain the greeting" + ) + } + + @Test + @Timeout(20, unit = TimeUnit.SECONDS) + fun testToolCallWithSessionManagement() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val testName = "SessionTest" + val command = "npx tsx myClient.ts $serverUrl greet $testName" + val output = executeCommand(command, clientDir) + + assertTrue(output.contains("Connected to server"), "Client should connect to server") + assertTrue( + output.contains("Hello, $testName!"), + "Tool response should contain the greeting with the provided name" + ) + assertTrue(output.contains("Tool result:"), "Output should indicate a successful tool call") + assertTrue(output.contains("Disconnected from server"), "Client should disconnect cleanly") + + val multiGreetName = "NotificationTest" + val multiGreetCommand = "npx tsx myClient.ts $serverUrl multi-greet $multiGreetName" + val multiGreetOutput = executeCommand(multiGreetCommand, clientDir) + + assertTrue(multiGreetOutput.contains("Connected to server"), "Client should connect to server") + assertTrue( + multiGreetOutput.contains("Multiple greetings") || multiGreetOutput.contains("greeting"), + "Tool response should contain greeting message" + ) + assertTrue(multiGreetOutput.contains("Disconnected from server"), "Client should disconnect cleanly") + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleClientSequence() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val testName1 = "FirstClient" + val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" + val output1 = executeCommand(command1, clientDir) + + assertTrue(output1.contains("Connected to server"), "First client should connect to server") + assertTrue(output1.contains("Hello, $testName1!"), "Tool response should contain the greeting for first client") + assertTrue(output1.contains("Disconnected from server"), "First client should disconnect cleanly") + + val testName2 = "SecondClient" + val command2 = "npx tsx myClient.ts $serverUrl multi-greet $testName2" + val output2 = executeCommand(command2, clientDir) + + assertTrue(output2.contains("Connected to server"), "Second client should connect to server") + assertTrue( + output2.contains("Multiple greetings") || output2.contains("greeting"), + "Tool response should contain greeting message" + ) + assertTrue(output2.contains("Disconnected from server"), "Second client should disconnect cleanly") + + val command3 = "npx tsx myClient.ts $serverUrl" + val output3 = executeCommand(command3, clientDir) + + assertTrue(output3.contains("Connected to server"), "Third client should connect to server") + assertTrue(output3.contains("Available utils:"), "Third client should list available utils") + assertTrue(output3.contains("greet"), "Greet tool should be available to third client") + assertTrue(output3.contains("multi-greet"), "Multi-greet tool should be available to third client") + assertTrue(output3.contains("Disconnected from server"), "Third client should disconnect cleanly") + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testMultipleClientParallel() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val clientCount = 3 + val clients = listOf( + "FirstClient" to "greet", + "SecondClient" to "multi-greet", + "ThirdClient" to "" + ) + + val threads = mutableListOf() + val outputs = mutableListOf>() + val exceptions = mutableListOf() + + for (i in 0 until clientCount) { + val (clientName, toolName) = clients[i] + val thread = Thread { + try { + val command = if (toolName.isEmpty()) { + "npx tsx myClient.ts $serverUrl" + } else { + "npx tsx myClient.ts $serverUrl $toolName $clientName" + } + + val output = executeCommand(command, clientDir) + synchronized(outputs) { + outputs.add(i to output) + } + } catch (e: Exception) { + synchronized(exceptions) { + exceptions.add(e) + } + } + } + threads.add(thread) + thread.start() + Thread.sleep(100) + } + + threads.forEach { it.join() } + + assertTrue(exceptions.isEmpty(), "No exceptions should occur: ${exceptions.joinToString { it.message ?: "" }}") + + val sortedOutputs = outputs.sortedBy { it.first }.map { it.second } + + sortedOutputs.forEachIndexed { index, output -> + val clientName = clients[index].first + val toolName = clients[index].second + + when (toolName) { + "greet" -> { + val containsGreeting = output.contains("Hello, $clientName!") || + output.contains("\"greeting\": \"Hello, $clientName!\"") + assertTrue( + containsGreeting, + "Tool response should contain the greeting for $clientName" + ) + } + + "multi-greet" -> { + val containsGreeting = output.contains("Multiple greetings") || + output.contains("greeting") || + output.contains("greet") + assertTrue( + containsGreeting, + "Tool response should contain greeting message for $clientName" + ) + } + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt b/src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt new file mode 100644 index 000000000..81da42db9 --- /dev/null +++ b/src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -0,0 +1,215 @@ +package integration.typescript + +import integration.utils.KotlinServerForTypeScriptClient +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TypeScriptEdgeCasesTest : TypeScriptTestBase() { + + private var port: Int = 0 + private lateinit var serverUrl: String + private var httpServer: KotlinServerForTypeScriptClient? = null + + @BeforeEach + fun setUp() { + port = findFreePort() + serverUrl = "http://localhost:$port/mcp" + killProcessOnPort(port) + httpServer = KotlinServerForTypeScriptClient() + httpServer?.start(port) + if (!waitForPort("localhost", port, 10)) { + throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") + } + println("Kotlin server started on port $port") + } + + @AfterEach + fun tearDown() { + try { + httpServer?.stop() + println("HTTP server stopped") + } catch (e: Exception) { + println("Error during server shutdown: ${e.message}") + } + } + + @Test + @Timeout(20, unit = TimeUnit.SECONDS) + fun testErrorHandling() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" + val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, clientDir) + + assertTrue( + nonExistentToolOutput.contains("Tool \"non-existent-tool\" not found"), + "Client should handle non-existent tool gracefully" + ) + + val invalidUrlCommand = "npx tsx myClient.ts http://localhost:${port + 1000}/mcp greet TestUser" + val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, clientDir) + + assertTrue( + invalidUrlOutput.contains("Error:") || invalidUrlOutput.contains("ECONNREFUSED"), + "Client should handle connection errors gracefully" + ) + } + + @Test + @Timeout(20, unit = TimeUnit.SECONDS) + fun testSpecialCharacters() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val specialChars = "!@#$+-[].,?" + + val tempFile = File(clientDir, "special_chars.txt") + tempFile.writeText(specialChars) + + val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat special_chars.txt)\"" + val specialCharsOutput = executeCommand(specialCharsCommand, clientDir) + + tempFile.delete() + + assertTrue( + specialCharsOutput.contains("Hello, $specialChars!"), + "Tool should handle special characters in arguments" + ) + assertTrue( + specialCharsOutput.contains("Disconnected from server"), + "Client should disconnect cleanly after handling special characters" + ) + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testLargePayload() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val largeName = "A".repeat(10 * 1024) + + val tempFile = File(clientDir, "large_name.txt") + tempFile.writeText(largeName) + + val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat large_name.txt)\"" + val largePayloadOutput = executeCommand(largePayloadCommand, clientDir) + + tempFile.delete() + + assertTrue( + largePayloadOutput.contains("Hello,") && largePayloadOutput.contains("A".repeat(20)), + "Tool should handle large payloads" + ) + assertTrue( + largePayloadOutput.contains("Disconnected from server"), + "Client should disconnect cleanly after handling large payload" + ) + } + + @Test + @Timeout(60, unit = TimeUnit.SECONDS) + fun testComplexConcurrentRequests() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val commands = listOf( + "npx tsx myClient.ts $serverUrl greet \"Client1\"", + "npx tsx myClient.ts $serverUrl multi-greet \"Client2\"", + "npx tsx myClient.ts $serverUrl greet \"Client3\"", + "npx tsx myClient.ts $serverUrl", + "npx tsx myClient.ts $serverUrl multi-greet \"Client5\"" + ) + + val threads = commands.mapIndexed { index, command -> + Thread { + println("Starting client $index") + val output = executeCommand(command, clientDir) + println("Client $index completed") + + assertTrue( + output.contains("Connected to server"), + "Client $index should connect to server" + ) + assertTrue( + output.contains("Disconnected from server"), + "Client $index should disconnect cleanly" + ) + + when { + command.contains("greet \"Client1\"") -> + assertTrue(output.contains("Hello, Client1!"), "Client 1 should receive correct greeting") + + command.contains("multi-greet \"Client2\"") -> + assertTrue(output.contains("Multiple greetings"), "Client 2 should receive multiple greetings") + + command.contains("greet \"Client3\"") -> + assertTrue(output.contains("Hello, Client3!"), "Client 3 should receive correct greeting") + + !command.contains("greet") && !command.contains("multi-greet") -> + assertTrue(output.contains("Available utils:"), "Client 4 should list available tools") + + command.contains("multi-greet \"Client5\"") -> + assertTrue(output.contains("Multiple greetings"), "Client 5 should receive multiple greetings") + } + }.apply { start() } + } + + threads.forEach { it.join() } + } + + @Test + @Timeout(30, unit = TimeUnit.SECONDS) + fun testRapidSequentialRequests() { + val projectRoot = File(System.getProperty("user.dir")) + val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + + val outputs = (1..10).map { i -> + val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" + val output = executeCommand(command, clientDir) + + assertTrue( + output.contains("Connected to server"), + "Client $i should connect to server" + ) + assertTrue( + output.contains("Hello, RapidClient$i!"), + "Client $i should receive correct greeting" + ) + assertTrue( + output.contains("Disconnected from server"), + "Client $i should disconnect cleanly" + ) + + output + } + + assertEquals(10, outputs.size, "All 10 rapid requests should complete successfully") + } + + private fun executeCommandAllowingFailure(command: String, workingDir: File): String { + val process = ProcessBuilder() + .command("bash", "-c", command) + .directory(workingDir) + .redirectErrorStream(true) + .start() + + val output = StringBuilder() + process.inputStream.bufferedReader().useLines { lines -> + for (line in lines) { + println(line) + output.append(line).append("\n") + } + } + + process.waitFor(20, TimeUnit.SECONDS) + return output.toString() + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt b/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt new file mode 100644 index 000000000..1db55699e --- /dev/null +++ b/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt @@ -0,0 +1,128 @@ +package integration.typescript + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.net.ServerSocket +import java.util.concurrent.TimeUnit + +abstract class TypeScriptTestBase { + + companion object { + @JvmStatic + protected val sdkDir = File("src/jvmTest/resources/typescript-sdk") + + /** + * clone TypeScript SDK and install dependencies + */ + @JvmStatic + @BeforeAll + fun setupTypeScriptSdk() { + println("Cloning TypeScript SDK repository") + if (!sdkDir.exists()) { + val cloneCommand = + "git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk.git src/jvmTest/resources/typescript-sdk" + val process = ProcessBuilder() + .command("bash", "-c", cloneCommand) + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + if (exitCode != 0) { + throw RuntimeException("Failed to clone TypeScript SDK repository: exit code $exitCode") + } + } + + println("Installing TypeScript SDK dependencies") + executeCommand("npm install", sdkDir) + } + + @JvmStatic + @AfterAll + fun removeTypeScriptSDK() { + sdkDir.deleteRecursively() + } + + @JvmStatic + protected fun executeCommand(command: String, workingDir: File): String { + val process = ProcessBuilder() + .command("bash", "-c", command) + .directory(workingDir) + .redirectErrorStream(true) + .start() + + val output = StringBuilder() + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + println(line) + output.append(line).append("\n") + } + } + + val exitCode = process.waitFor() + if (exitCode != 0) { + throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") + } + + return output.toString() + } + + @JvmStatic + protected fun killProcessOnPort(port: Int) { + executeCommand("lsof -ti:$port | xargs kill -9 2>/dev/null || true", File(".")) + } + + @JvmStatic + protected fun findFreePort(minPort: Int = 3000, maxPort: Int = 4000): Int { + for (port in minPort..maxPort) { + try { + ServerSocket(port).use { + return port + } + } catch (_: Exception) { + // port is in use, try the next one + } + } + throw RuntimeException("No free port found between $minPort and $maxPort") + } + } + + protected fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { + if (process.isAlive && !process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { + process.destroyForcibly() + process.waitFor(2, TimeUnit.SECONDS) + return false + } + return true + } + + protected fun createProcessOutputReader(process: Process, prefix: String): Thread { + val outputReader = Thread { + try { + process.inputStream.bufferedReader().useLines { lines -> + for (line in lines) { + println("[$prefix] $line") + } + } + } catch (e: Exception) { + println("Warning: Error reading process output: ${e.message}") + } + } + outputReader.isDaemon = true + return outputReader + } + + protected fun waitForPort(host: String, port: Int, timeoutSeconds: Long = 10): Boolean { + val deadline = System.currentTimeMillis() + timeoutSeconds * 1000 + while (System.currentTimeMillis() < deadline) { + try { + java.net.Socket(host, port).use { return true } + } catch (_: Exception) { + Thread.sleep(100) + } + } + return false + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt b/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt new file mode 100644 index 000000000..891e9b42c --- /dev/null +++ b/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt @@ -0,0 +1,394 @@ +package integration.utils + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.* +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +private val logger = KotlinLogging.logger {} + +class KotlinServerForTypeScriptClient { + private val serverTransports = ConcurrentHashMap() + private val jsonFormat = Json { ignoreUnknownKeys = true } + private var server: EmbeddedServer<*, *>? = null + + fun start(port: Int = 3000) { + logger.info { "Starting HTTP server on port $port" } + + server = embeddedServer(CIO, port = port) { + routing { + post("/mcp") { + val sessionId = call.request.header("mcp-session-id") + val requestBody = call.receiveText() + + logger.debug { "Received request with sessionId: $sessionId" } + logger.trace { "Request body: $requestBody" } + + val jsonElement = try { + jsonFormat.parseToJsonElement(requestBody) + } catch (e: Exception) { + logger.error(e) { "Failed to parse request body as JSON" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + JsonObject( + mapOf( + "jsonrpc" to JsonPrimitive("2.0"), + "error" to JsonObject( + mapOf( + "code" to JsonPrimitive(-32700), + "message" to JsonPrimitive("Parse error: ${e.message}") + ) + ), + "id" to JsonNull + ) + ) + ) + ) + return@post + } + + if (sessionId != null && serverTransports.containsKey(sessionId)) { + logger.debug { "Using existing transport for session: $sessionId" } + val transport = serverTransports[sessionId]!! + transport.handleRequest(call, jsonElement) + } else { + if (isInitializeRequest(jsonElement)) { + val newSessionId = UUID.randomUUID().toString() + logger.info { "Creating new session with ID: $newSessionId" } + + val transport = HttpServerTransport(newSessionId) + + serverTransports[newSessionId] = transport + + val mcpServer = createMcpServer() + + call.response.header("mcp-session-id", newSessionId) + + val serverThread = Thread { + runBlocking { + mcpServer.connect(transport) + } + } + serverThread.start() + + Thread.sleep(500) + + transport.handleRequest(call, jsonElement) + } else { + logger.warn { "Invalid request: no session ID or not an initialization request" } + call.respond( + HttpStatusCode.BadRequest, + jsonFormat.encodeToString( + JsonObject.serializer(), + JsonObject( + mapOf( + "jsonrpc" to JsonPrimitive("2.0"), + "error" to JsonObject( + mapOf( + "code" to JsonPrimitive(-32000), + "message" to JsonPrimitive("Bad Request: No valid session ID provided") + ) + ), + "id" to JsonNull + ) + ) + ) + ) + } + } + } + + delete("/mcp") { + val sessionId = call.request.header("mcp-session-id") + if (sessionId != null && serverTransports.containsKey(sessionId)) { + logger.info { "Terminating session: $sessionId" } + val transport = serverTransports[sessionId]!! + serverTransports.remove(sessionId) + runBlocking { + transport.close() + } + call.respond(HttpStatusCode.OK) + } else { + logger.warn { "Invalid session termination request: $sessionId" } + call.respond(HttpStatusCode.BadRequest, "Invalid or missing session ID") + } + } + } + } + + server?.start(wait = false) + } + + fun stop() { + logger.info { "Stopping HTTP server" } + server?.stop(500, 1000) + server = null + } + + private fun createMcpServer(): Server { + val server = Server( + Implementation( + name = "kotlin-http-server", + version = "1.0.0" + ), + ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + tools = ServerCapabilities.Tools(listChanged = true), + ) + ) + ) + + server.addTool( + name = "greet", + description = "A simple greeting tool", + inputSchema = Tool.Input( + properties = buildJsonObject { + put("name", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Name to greet")) + }) + }, + required = listOf("name") + ) + ) { request -> + val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" + CallToolResult( + content = listOf(TextContent("Hello, $name!")), + structuredContent = buildJsonObject { + put("greeting", JsonPrimitive("Hello, $name!")) + } + ) + } + + server.addTool( + name = "multi-greet", + description = "A greeting tool that sends multiple notifications", + inputSchema = Tool.Input( + properties = buildJsonObject { + put("name", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Name to greet")) + }) + }, + required = listOf("name") + ) + ) { request -> + val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" + + repeat(3) { index -> + server.sendLoggingMessage( + LoggingMessageNotification( + level = LoggingLevel.info, + data = buildJsonObject { + put("message", JsonPrimitive("Greeting notification #${index + 1} for $name")) + } + ) + ) + } + + CallToolResult( + content = listOf(TextContent("Multiple greetings sent to $name!")), + structuredContent = buildJsonObject { + put("greeting", JsonPrimitive("Multiple greetings sent to $name!")) + put("notificationCount", JsonPrimitive(3)) + } + ) + } + + server.addPrompt( + name = "greeting-template", + description = "A simple greeting prompt template", + arguments = listOf( + PromptArgument( + name = "name", + description = "Name to include in greeting", + required = true + ) + ) + ) { request -> + GetPromptResult( + "Greeting for ${request.name}", + messages = listOf( + PromptMessage( + role = Role.user, + content = TextContent("Please greet ${(request.arguments?.get("name") as? JsonPrimitive)?.content ?: "someone"} in a friendly manner.") + ) + ) + ) + } + + server.addResource( + uri = "https://example.com/greetings/default", + name = "Default Greeting", + description = "A simple greeting resource", + mimeType = "text/plain" + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents("Hello, world!", request.uri, "text/plain") + ) + ) + } + + return server + } + + private fun isInitializeRequest(json: JsonElement): Boolean { + if (json !is JsonObject) return false + + val method = json["method"]?.jsonPrimitive?.contentOrNull + return method == "initialize" + } +} + +class HttpServerTransport(private val sessionId: String) : AbstractTransport() { + private val logger = KotlinLogging.logger {} + private val pendingResponses = ConcurrentHashMap>() + private val messageQueue = Channel(Channel.UNLIMITED) + + suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { + try { + logger.info { "Handling request body: $requestBody" } + val message = McpJson.decodeFromJsonElement(requestBody) + logger.info { "Decoded message: $message" } + + if (message is JSONRPCRequest) { + val id = message.id.toString() + logger.info { "Received request with ID: $id, method: ${message.method}" } + val responseDeferred = CompletableDeferred() + pendingResponses[id] = responseDeferred + logger.info { "Created deferred response for ID: $id" } + + logger.info { "Invoking onMessage handler" } + _onMessage.invoke(message) + logger.info { "onMessage handler completed" } + + try { + val response = withTimeoutOrNull(10000) { + responseDeferred.await() + } + + if (response != null) { + val jsonResponse = McpJson.encodeToString(response) + call.respondText(jsonResponse, ContentType.Application.Json) + } else { + logger.warn { "Timeout waiting for response to request ID: $id" } + call.respondText( + McpJson.encodeToString( + JSONRPCResponse( + id = message.id, + error = JSONRPCError( + code = ErrorCode.Defined.RequestTimeout, + message = "Request timed out" + ) + ) + ), + ContentType.Application.Json + ) + } + } catch (_: CancellationException) { + logger.warn { "Request cancelled for ID: $id" } + pendingResponses.remove(id) + if (!call.response.isCommitted) { + call.respondText( + McpJson.encodeToString( + JSONRPCResponse( + id = message.id, + error = JSONRPCError( + code = ErrorCode.Defined.ConnectionClosed, + message = "Request cancelled" + ) + ) + ), + ContentType.Application.Json, + HttpStatusCode.ServiceUnavailable + ) + } + } + } else { + call.respondText("", ContentType.Application.Json, HttpStatusCode.Accepted) + } + } catch (e: Exception) { + logger.error(e) { "Error handling request" } + if (!call.response.isCommitted) { + try { + val errorResponse = JSONRPCResponse( + id = RequestId.NumberId(0), + error = JSONRPCError( + code = ErrorCode.Defined.InternalError, + message = "Internal server error: ${e.message}" + ) + ) + + call.respondText( + McpJson.encodeToString(errorResponse), + ContentType.Application.Json, + HttpStatusCode.InternalServerError + ) + } catch (responseEx: Exception) { + logger.error(responseEx) { "Failed to send error response" } + } + } + } + } + + override suspend fun start() { + logger.debug { "Starting HTTP server transport for session: $sessionId" } + } + + override suspend fun send(message: JSONRPCMessage) { + logger.info { "Sending message: $message" } + + if (message is JSONRPCResponse) { + val id = message.id.toString() + logger.info { "Sending response for request ID: $id" } + val deferred = pendingResponses.remove(id) + if (deferred != null) { + logger.info { "Found pending response for ID: $id, completing deferred" } + deferred.complete(message) + return + } else { + logger.warn { "No pending response found for ID: $id" } + } + } else if (message is JSONRPCRequest) { + logger.info { "Sending request with ID: ${message.id}" } + } else if (message is JSONRPCNotification) { + logger.info { "Sending notification: ${message.method}" } + } + + logger.info { "Queueing message for next client request" } + messageQueue.send(message) + } + + override suspend fun close() { + logger.debug { "Closing HTTP server transport for session: $sessionId" } + messageQueue.close() + _onClose.invoke() + } +} + +fun main() { + val server = KotlinServerForTypeScriptClient() + server.start() +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/utils/TestUtils.kt b/src/jvmTest/kotlin/integration/utils/TestUtils.kt new file mode 100644 index 000000000..08458b766 --- /dev/null +++ b/src/jvmTest/kotlin/integration/utils/TestUtils.kt @@ -0,0 +1,78 @@ +package integration.utils + +import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase +import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent +import io.modelcontextprotocol.kotlin.sdk.TextContent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +object TestUtils { + fun runTest(block: suspend () -> T): T { + return runBlocking { + withContext(Dispatchers.IO) { + block() + } + } + } + + fun assertTextContent(content: PromptMessageContent?, expectedText: String) { + assertNotNull(content, "Content should not be null") + assertTrue(content is TextContent, "Content should be TextContent") + assertNotNull(content.text, "Text content should not be null") + assertEquals(expectedText, content.text, "Text content should match") + } + + fun assertCallToolResult(result: Any?, message: String = ""): CallToolResultBase { + assertNotNull(result, "${message}Call tool result should not be null") + assertTrue(result is CallToolResultBase, "${message}Result should be CallToolResultBase") + assertTrue(result.content.isNotEmpty(), "${message}Tool result content should not be empty") + assertNotNull(result.structuredContent, "${message}Tool result structured content should not be null") + + return result + } + + /** + * Asserts that a JSON property has the expected string value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: String, message: String = "") { + assertEquals(expectedValue, json[property]?.toString()?.trim('"'), "${message}${property} should match") + } + + /** + * Asserts that a JSON property has the expected numeric value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: Number, message: String = "") { + when (expectedValue) { + is Int -> assertEquals( + expectedValue, + (json[property] as? JsonPrimitive)?.content?.toIntOrNull(), + "${message}${property} should match" + ) + + is Double -> assertEquals( + expectedValue, + (json[property] as? JsonPrimitive)?.content?.toDoubleOrNull(), + "${message}${property} should match" + ) + + else -> assertEquals( + expectedValue.toString(), + json[property]?.toString()?.trim('"'), + "${message}${property} should match" + ) + } + } + + /** + * Asserts that a JSON property has the expected boolean value. + */ + fun assertJsonProperty(json: JsonObject, property: String, expectedValue: Boolean, message: String = "") { + assertEquals(expectedValue.toString(), json[property].toString(), "${message}${property} should match") + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/utils/myClient.ts b/src/jvmTest/kotlin/integration/utils/myClient.ts new file mode 100644 index 000000000..c6ebf3220 --- /dev/null +++ b/src/jvmTest/kotlin/integration/utils/myClient.ts @@ -0,0 +1,93 @@ +// @ts-ignore +import {Client} from "../../../resources/typescript-sdk/src/client"; +// @ts-ignore +import {StreamableHTTPClientTransport} from "../../../resources/typescript-sdk/src/client/streamableHttp.js"; + +const args = process.argv.slice(2); +const serverUrl = args[0] || 'http://localhost:3001/mcp'; +const toolName = args[1]; +const toolArgs = args.slice(2); +const PROTOCOL_VERSION = "2024-11-05"; + +async function main() { + if (!toolName) { + console.log('Usage: npx tsx myClient.ts [server-url] [tool-args...]'); + console.log('Using default server URL:', serverUrl); + console.log('Available utils will be listed after connection'); + } + + console.log(`Connecting to server at ${serverUrl}`); + if (toolName) { + console.log(`Will call tool: ${toolName} with args: ${toolArgs.join(', ')}`); + } + + const client = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + try { + await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); + console.log('Connected to server'); + + const toolsResult = await client.listTools(); + const tools = toolsResult.tools; + console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); + + if (!toolName) { + await client.close(); + return; + } + + const tool = tools.find((t: { name: string; }) => t.name === toolName); + if (!tool) { + console.error(`Tool "${toolName}" not found`); + process.exit(1); + } + + const toolArguments = {}; + + if (toolName === "greet" && toolArgs.length > 0) { + toolArguments["name"] = toolArgs[0]; + } else if (tool.input && tool.input.properties) { + const propNames = Object.keys(tool.input.properties); + if (propNames.length > 0 && toolArgs.length > 0) { + toolArguments[propNames[0]] = toolArgs[0]; + } + } + + console.log(`Calling tool ${toolName} with arguments:`, toolArguments); + + const result = await client.callTool({ + name: toolName, + arguments: toolArguments + }); + console.log('Tool result:', result); + + if (result.content) { + for (const content of result.content) { + if (content.type === 'text') { + console.log('Text content:', content.text); + } + } + } + + if (result.structuredContent) { + console.log('Structured content:', JSON.stringify(result.structuredContent, null, 2)); + } + + } catch (error) { + console.error('Error:', error); + process.exit(1); + } finally { + await client.close(); + console.log('Disconnected from server'); + } +} + +main().catch(error => { + console.error('Unhandled error:', error); + process.exit(1); +}); \ No newline at end of file From 39ccebe6602e8dcba27db39c78c1d1932ddaa2a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:37:08 +0000 Subject: [PATCH 02/24] Bump org.jetbrains.kotlinx.binary-compatibility-validator (#148) Bumps [org.jetbrains.kotlinx.binary-compatibility-validator](https://github.com/Kotlin/binary-compatibility-validator) from 0.18.0 to 0.18.1. - [Release notes](https://github.com/Kotlin/binary-compatibility-validator/releases) - [Commits](https://github.com/Kotlin/binary-compatibility-validator/compare/0.18.0...0.18.1) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx.binary-compatibility-validator dependency-version: 0.18.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1227aab64..201a74f83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ ktor = "3.2.1" mockk = "1.14.4" logging = "7.0.7" jreleaser = "1.19.0" -binaryCompatibilityValidatorPlugin = "0.18.0" +binaryCompatibilityValidatorPlugin = "0.18.1" slf4j = "2.0.17" kotest = "5.9.1" From 0f6504561d35f5799f56eb3f610245d80c1dbfa6 Mon Sep 17 00:00:00 2001 From: Sergei Dubov Date: Wed, 23 Jul 2025 16:55:08 +0200 Subject: [PATCH 03/24] Add optional title property for tool object (#191) (#192) --- api/kotlin-sdk.api | 18 +- .../kotlin/sdk/server/Server.kt | 4 +- .../modelcontextprotocol/kotlin/sdk/types.kt | 4 + .../kotlin/sdk/ToolSerializationTest.kt | 339 +++++++++++++++++- src/jvmTest/kotlin/client/ClientTest.kt | 1 + 5 files changed, 351 insertions(+), 15 deletions(-) diff --git a/api/kotlin-sdk.api b/api/kotlin-sdk.api index 8d356d5cb..ce39975a5 100644 --- a/api/kotlin-sdk.api +++ b/api/kotlin-sdk.api @@ -2558,20 +2558,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/TextResourceContents$Compa public final class io/modelcontextprotocol/kotlin/sdk/Tool { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Tool$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; - public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; - public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;)Lio/modelcontextprotocol/kotlin/sdk/Tool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/Tool; public fun equals (Ljava/lang/Object;)Z public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations; public final fun getDescription ()Ljava/lang/String; public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Input; public final fun getName ()Ljava/lang/String; public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/Tool$Output; + public final fun getTitle ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3045,8 +3047,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public final fun addResources (Ljava/util/List;)V public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public final fun addTools (Ljava/util/List;)V protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index ecfaeba17..1230b895c 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -206,6 +206,7 @@ public open class Server( * Registers a single tool. The client can then call this tool. * * @param name The name of the tool. + * @param title An optional human-readable name of the tool for display purposes. * @param description A human-readable description of what the tool does. * @param inputSchema The expected input schema for the tool. * @param outputSchema The optional expected output schema for the tool. @@ -217,11 +218,12 @@ public open class Server( name: String, description: String, inputSchema: Tool.Input = Tool.Input(), + title: String? = null, outputSchema: Tool.Output? = null, toolAnnotations: ToolAnnotations? = null, handler: suspend (CallToolRequest) -> CallToolResult ) { - val tool = Tool(name, description, inputSchema, outputSchema, toolAnnotations) + val tool = Tool(name, title, description, inputSchema, outputSchema, toolAnnotations) addTool(tool, handler) } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 130585bf0..d5b45f6ff 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -1110,6 +1110,10 @@ public data class Tool( * The name of the tool. */ val name: String, + /** + * The title of the tool. + */ + val title: String?, /** * A human-readable description of the tool. */ diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt index 6e2ac447f..0e1f704a6 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt @@ -15,6 +15,7 @@ class ToolSerializationTest { private val getWeatherToolJson = """ { "name": "get_weather", + "title": "Get weather", "description": "Get the current weather in a given location", "inputSchema": { "type": "object", @@ -49,6 +50,7 @@ class ToolSerializationTest { val getWeatherTool = Tool( name = "get_weather", + title = "Get weather", description = "Get the current weather in a given location", annotations = null, inputSchema = Tool.Input( @@ -79,17 +81,13 @@ class ToolSerializationTest { ) ) + //region Serialize + @Test fun `should serialize get_weather tool`() { McpJson.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson } - @Test - fun `should deserialize get_weather tool`() { - val tool = McpJson.decodeFromString(getWeatherToolJson) - assertEquals(expected = getWeatherTool, actual = tool) - } - @Test fun `should always serialize default value`() { val json = Json(from = McpJson) { @@ -97,4 +95,333 @@ class ToolSerializationTest { } json.encodeToString(getWeatherTool) shouldEqualJson getWeatherToolJson } + + @Test + fun `should serialize get_weather tool without optional properties`() { + val weatherTool = createWeatherTool(name = "get_weather") + val expectedJson = createWeatherToolJson(name = "get_weather") + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with title optional property specified`() { + val weatherTool = createWeatherTool(name = "get_weather", title = "Get weather") + val expectedJson = createWeatherToolJson(name = "get_weather", title = "Get weather") + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with outputSchema optional property specified`() { + val weatherTool = createWeatherTool( + name = "get_weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + val expectedJson = createWeatherToolJson(name = "get_weather", outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `should serialize get_weather tool with all properties specified`() { + val weatherTool = createWeatherTool( + name = "get_weather", + title = "Get weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + val expectedJson = createWeatherToolJson( + name = "get_weather", + title = "Get weather", + outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val actualJson = McpJson.encodeToString(weatherTool) + + actualJson shouldEqualJson expectedJson + } + + //endregion Serialize + + //region Deserialize + + @Test + fun `should deserialize get_weather tool`() { + val actualTool = McpJson.decodeFromString(getWeatherToolJson) + assertEquals(expected = getWeatherTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool without optional properties`() { + val toolJson = createWeatherToolJson(name = "get_weather") + val expectedTool = createWeatherTool(name = "get_weather") + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with title properties specified`() { + val toolJson = createWeatherToolJson(name = "get_weather", title = "Get weather") + val expectedTool = createWeatherTool(name = "get_weather", title = "Get weather") + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with outputSchema optional property specified`() { + val toolJson = createWeatherToolJson(name = "get_weather", outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val expectedTool = createWeatherTool( + name = "get_weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + @Test + fun `should deserialize get_weather tool with all properties specified`() { + val toolJson = createWeatherToolJson( + name = "get_weather", + title = "Get weather", + outputSchema = """ + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "description": "Temperature in celsius" + }, + "conditions": { + "type": "string", + "description": "Weather conditions description" + }, + "humidity": { + "type": "number", + "description": "Humidity percentage" + } + }, + "required": ["temperature", "conditions", "humidity"] + } + """.trimIndent()) + + val expectedTool = createWeatherTool( + name = "get_weather", + title = "Get weather", + outputSchema = Tool.Output( + properties = buildJsonObject { + put("temperature", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Temperature in celsius")) + }) + put("conditions", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("Weather conditions description")) + }) + put("humidity", buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("Humidity percentage")) + }) + }, + required = listOf("temperature", "conditions", "humidity") + ) + ) + + val actualTool = McpJson.decodeFromString(toolJson) + + assertEquals(expected = expectedTool, actual = actualTool) + } + + //endregion Deserialize + + //region Private Methods + + private fun createWeatherToolJson( + name: String = "get_weather", + title: String? = null, + outputSchema: String? = null + ): String { + + val stringBuilder = StringBuilder() + + stringBuilder + .appendLine("{") + .append(" \"name\": \"$name\"") + + if (title != null) { + stringBuilder + .appendLine(",") + .append(" \"title\": \"$title\"") + } + + stringBuilder + .appendLine(",") + .append(" \"description\": \"Get the current weather in a given location\"") + .appendLine(",") + .append(""" + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": ["location"] + } + """.trimIndent()) + + if (outputSchema != null) { + stringBuilder + .appendLine(",") + .append(""" + "outputSchema": $outputSchema + """.trimIndent()) + } + + stringBuilder + .appendLine() + .appendLine("}") + + + return stringBuilder.toString().trimIndent() + } + + private fun createWeatherTool( + name: String = "get_weather", + title: String? = null, + outputSchema: Tool.Output? = null + ): Tool { + return Tool( + name = name, + title = title, + description = "Get the current weather in a given location", + annotations = null, + inputSchema = Tool.Input( + properties = buildJsonObject { + put("location", buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("The city and state, e.g. San Francisco, CA")) + }) + }, + required = listOf("location") + ), + outputSchema = outputSchema + ) + } + + //endregion Private Methods } diff --git a/src/jvmTest/kotlin/client/ClientTest.kt b/src/jvmTest/kotlin/client/ClientTest.kt index 9e2ea0557..91bd12deb 100644 --- a/src/jvmTest/kotlin/client/ClientTest.kt +++ b/src/jvmTest/kotlin/client/ClientTest.kt @@ -585,6 +585,7 @@ class ClientTest { tools = listOf( Tool( name = "testTool", + title = "testTool title", description = "testTool description", annotations = null, inputSchema = Tool.Input(), From 36746f69323da14c27a2f14e8cf4ba4c3d20e132 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 24 Jul 2025 10:45:40 +0200 Subject: [PATCH 04/24] Update PR validation workflow (#177) * Update PR validation workflow * Update PR validation trigger configuration * Add concurrency control to PR validation workflow --- .github/workflows/validate-pr.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index b33803c76..0ab75a73a 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -3,7 +3,11 @@ name: Validate PR on: workflow_dispatch: pull_request: - types: [auto_merge_enabled] + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: validate-pr: @@ -17,7 +21,7 @@ jobs: java-version: '21' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 + uses: gradle/actions/setup-gradle@v4 - name: Clean Build with Gradle run: ./gradlew clean build From 9be5a14db092b44a91b2e0abd8a878f4df5f0b96 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 28 Jul 2025 11:20:55 +0200 Subject: [PATCH 05/24] Update notification schema and refactor tests package structure (#199) --- api/kotlin-sdk.api | 425 +++++++++++++++--- .../kotlin/sdk/server/Server.kt | 4 +- .../kotlin/sdk/shared/Protocol.kt | 17 +- .../modelcontextprotocol/kotlin/sdk/types.kt | 194 +++++--- .../sdk}/AudioContentSerializationTest.kt | 1 - .../kotlin/sdk}/CallToolResultUtilsTest.kt | 0 .../kotlin/sdk/TypesTest.kt | 378 ++++++++++++++++ .../kotlin/sdk/TypesUtilTest.kt | 252 +++++++++++ .../kotlin/sdk}/client/ClientTest.kt | 20 +- .../sdk}/client/StdioClientTransportTest.kt | 4 +- .../StreamableHttpClientTransportTest.kt | 3 +- .../kotlin/sdk}/server/ServerTest.kt | 4 +- .../sdk}/server/StdioServerTransportTest.kt | 19 +- 13 files changed, 1173 insertions(+), 148 deletions(-) rename src/commonTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/AudioContentSerializationTest.kt (95%) rename src/commonTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/CallToolResultUtilsTest.kt (100%) create mode 100644 src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt create mode 100644 src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt rename src/jvmTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/client/ClientTest.kt (98%) rename src/jvmTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/client/StdioClientTransportTest.kt (88%) rename src/jvmTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/client/StreamableHttpClientTransportTest.kt (99%) rename src/jvmTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/server/ServerTest.kt (99%) rename src/jvmTest/kotlin/{ => io/modelcontextprotocol/kotlin/sdk}/server/StdioServerTransportTest.kt (93%) diff --git a/api/kotlin-sdk.api b/api/kotlin-sdk.api index ce39975a5..f91d3f20d 100644 --- a/api/kotlin-sdk.api +++ b/api/kotlin-sdk.api @@ -133,17 +133,45 @@ public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Default public static fun isError (Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase;)Ljava/lang/Boolean; } -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; public fun equals (Ljava/lang/Object;)Z - public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; public final fun getReason ()Ljava/lang/String; public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; public fun get_meta ()Lkotlinx/serialization/json/JsonObject; @@ -151,18 +179,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification : io public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params;)V public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Companion { +public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification$Params$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -942,7 +970,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializeResult$Companion public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -960,6 +998,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Co public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/JSONRPCError : io/modelcontextprotocol/kotlin/sdk/JSONRPCMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/JSONRPCError$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V @@ -1386,22 +1453,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; public fun equals (Ljava/lang/Object;)Z - public final fun getData ()Lkotlinx/serialization/json/JsonObject; - public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun getLogger ()Ljava/lang/String; public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1421,6 +1482,40 @@ public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonElement; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Ljava/lang/String;Lkotlinx/serialization/json/JsonElement;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Lkotlinx/serialization/json/JsonElement; + public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; + public final fun getLogger ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest : io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification$SetLevelRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lkotlinx/serialization/json/JsonObject;)V @@ -1587,12 +1682,21 @@ public final class io/modelcontextprotocol/kotlin/sdk/ModelPreferences$Companion public abstract interface class io/modelcontextprotocol/kotlin/sdk/Notification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Notification$Companion; public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public abstract fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; } public final class io/modelcontextprotocol/kotlin/sdk/Notification$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/NotificationParams : io/modelcontextprotocol/kotlin/sdk/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/NotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/Request, io/modelcontextprotocol/kotlin/sdk/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PaginatedRequest$Companion; public abstract fun getCursor ()Ljava/lang/String; @@ -1635,10 +1739,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/PingRequest$Companion { public class io/modelcontextprotocol/kotlin/sdk/Progress : io/modelcontextprotocol/kotlin/sdk/ProgressBase { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Progress$Companion; - public synthetic fun (IILjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (ILjava/lang/Double;Ljava/lang/String;)V + public fun (DLjava/lang/Double;Ljava/lang/String;)V + public synthetic fun (IDLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public fun getMessage ()Ljava/lang/String; - public fun getProgress ()I + public fun getProgress ()D public fun getTotal ()Ljava/lang/Double; public static final synthetic fun write$Self (Lio/modelcontextprotocol/kotlin/sdk/Progress;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V } @@ -1661,7 +1765,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/Progress$Companion { public abstract interface class io/modelcontextprotocol/kotlin/sdk/ProgressBase { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion; public abstract fun getMessage ()Ljava/lang/String; - public abstract fun getProgress ()I + public abstract fun getProgress ()D public abstract fun getTotal ()Ljava/lang/Double; } @@ -1669,24 +1773,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ProgressBase, io/modelcontextprotocol/kotlin/sdk/ServerNotification { +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Companion; - public fun (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;)V - public synthetic fun (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()I - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Ljava/lang/Double; - public final fun component5 ()Ljava/lang/String; - public final fun copy (ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;ILio/modelcontextprotocol/kotlin/sdk/RequestId;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Double;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; public fun equals (Ljava/lang/Object;)Z - public fun getMessage ()Ljava/lang/String; public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public fun getProgress ()I - public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public fun getTotal ()Ljava/lang/Double; - public final fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1706,6 +1802,42 @@ public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Compa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams, io/modelcontextprotocol/kotlin/sdk/ProgressBase { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion; + public fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()D + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;DLio/modelcontextprotocol/kotlin/sdk/RequestId;Ljava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun getMessage ()Ljava/lang/String; + public fun getProgress ()D + public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/RequestId; + public fun getTotal ()Ljava/lang/Double; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/Prompt { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Prompt$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V @@ -1763,7 +1895,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/PromptArgument$Companion { public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -1781,6 +1923,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotificat public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/PromptMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/PromptMessage$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContent;)V @@ -2053,7 +2224,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceContents$Companion public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2071,6 +2252,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotific public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/ResourceReference : io/modelcontextprotocol/kotlin/sdk/Reference { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceReference$Companion; public static final field TYPE Ljava/lang/String; @@ -2133,18 +2343,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceTemplate$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/WithMeta { +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Companion; - public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public fun (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2164,6 +2372,36 @@ public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotificatio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion; + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public final fun getUri ()Ljava/lang/String; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/Role : java/lang/Enum { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Role$Companion; public static final field assistant Lio/modelcontextprotocol/kotlin/sdk/Role; @@ -2209,7 +2447,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/Root$Companion { public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2227,6 +2475,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotificati public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/SamplingMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/SamplingMessage$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/Role;Lio/modelcontextprotocol/kotlin/sdk/PromptMessageContentMultimodal;)V @@ -2696,7 +2973,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; + public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -2714,6 +3001,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotificatio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params : io/modelcontextprotocol/kotlin/sdk/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public fun equals (Ljava/lang/Object;)Z + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification$Params$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/TypesKt { public static final field JSONRPC_VERSION Ljava/lang/String; public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; @@ -2756,12 +3072,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent$Companion { public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification : io/modelcontextprotocol/kotlin/sdk/ClientNotification, io/modelcontextprotocol/kotlin/sdk/ClientRequest, io/modelcontextprotocol/kotlin/sdk/ServerNotification, io/modelcontextprotocol/kotlin/sdk/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;Lio/modelcontextprotocol/kotlin/sdk/Method;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification;Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/NotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; public fun equals (Ljava/lang/Object;)Z public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/NotificationParams; public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 1230b895c..f0655fd9e 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -558,7 +558,7 @@ public open class Server( * @param params The logging message notification parameters. */ public suspend fun sendLoggingMessage(params: LoggingMessageNotification) { - logger.trace { "Sending logging message: ${params.data}" } + logger.trace { "Sending logging message: ${params.params.data}" } notification(params) } @@ -568,7 +568,7 @@ public open class Server( * @param params Details of the updated resource. */ public suspend fun sendResourceUpdated(params: ResourceUpdatedNotification) { - logger.debug { "Sending resource updated notification for: ${params.uri}" } + logger.debug { "Sending resource updated notification for: ${params.params.uri}" } notification(params) } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 06ce6baa3..45c07b647 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -292,11 +292,11 @@ public abstract class Protocol( } private fun onProgress(notification: ProgressNotification) { - LOGGER.trace { "Received progress notification: token=${notification.progressToken}, progress=${notification.progress}/${notification.total}" } - val progress = notification.progress - val total = notification.total - val message = notification.message - val progressToken = notification.progressToken + LOGGER.trace { "Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" } + val progress = notification.params.progress + val total = notification.params.total + val message = notification.params.message + val progressToken = notification.params.progressToken val handler = _progressHandlers.value[progressToken] if (handler == null) { @@ -424,7 +424,12 @@ public abstract class Protocol( _responseHandlers.update { current -> current.remove(messageId) } _progressHandlers.update { current -> current.remove(messageId) } - val notification = CancelledNotification(requestId = messageId, reason = reason.message ?: "Unknown") + val notification = CancelledNotification( + params = CancelledNotification.Params( + requestId = messageId, + reason = reason.message ?: "Unknown" + ) + ) val serialized = JSONRPCNotification( notification.method.value, diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index d5b45f6ff..077b3f638 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonObject @@ -125,10 +126,11 @@ public sealed interface Request { * @return The JSON-RPC request representation. */ internal fun Request.toJSON(): JSONRPCRequest { - val encoded = JsonObject(McpJson.encodeToJsonElement(this).jsonObject.minus("method")) + val fullJson = McpJson.encodeToJsonElement(this).jsonObject + val params = JsonObject(fullJson.filterKeys { it != "method" }) return JSONRPCRequest( method = method.value, - params = encoded, + params = params, jsonrpc = JSONRPC_VERSION, ) } @@ -139,8 +141,7 @@ internal fun Request.toJSON(): JSONRPCRequest { * @return The decoded [Request] or null */ internal fun JSONRPCRequest.fromJSON(): Request { - val requestData = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method))) - + val requestData = JsonObject(params.jsonObject + ("method" to JsonPrimitive(method))) val deserializer = selectRequestDeserializer(method) return McpJson.decodeFromJsonElement(deserializer, requestData) } @@ -159,6 +160,7 @@ public open class CustomRequest(override val method: Method) : Request @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { public val method: Method + public val params: NotificationParams? } /** @@ -167,10 +169,9 @@ public sealed interface Notification { * @return The JSON-RPC notification representation. */ internal fun Notification.toJSON(): JSONRPCNotification { - val encoded = JsonObject(McpJson.encodeToJsonElement(this).jsonObject.minus("method")) return JSONRPCNotification( - method.value, - params = encoded + method = method.value, + params = McpJson.encodeToJsonElement(params), ) } @@ -180,7 +181,10 @@ internal fun Notification.toJSON(): JSONRPCNotification { * @return The decoded [Notification]. */ internal fun JSONRPCNotification.fromJSON(): Notification { - val data = JsonObject(params.jsonObject.plus("method" to JsonPrimitive(method))) + val data = buildJsonObject { + put("method", JsonPrimitive(method)) + put("params", params) + } return McpJson.decodeFromJsonElement(data) } @@ -295,6 +299,12 @@ public data class JSONRPCError( val data: JsonObject = EmptyJsonObject, ) : JSONRPCMessage +/** + * Base interface for notification parameters with optional metadata. + */ +@Serializable +public sealed interface NotificationParams : WithMeta + /* Cancellation */ /** * This notification can be sent by either side to indicate that it is cancelling a previously issued request. @@ -307,19 +317,24 @@ public data class JSONRPCError( */ @Serializable public data class CancelledNotification( - /** - * The ID of the request to cancel. - * - * It MUST correspond to the ID of a request previously issued in the same direction. - */ - val requestId: RequestId, - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - val reason: String?, - override val _meta: JsonObject = EmptyJsonObject, -) : ClientNotification, ServerNotification, WithMeta { + override val params: Params, +) : ClientNotification, ServerNotification { override val method: Method = Method.Defined.NotificationsCancelled + + @Serializable + public data class Params( + /** + * The ID of the request to cancel. + * + * It MUST correspond to the ID of a request previously issued in the same direction. + */ + val requestId: RequestId, + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + val reason: String? = null, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Initialization */ @@ -408,7 +423,7 @@ public sealed interface ServerResult : RequestResult */ @Serializable public data class UnknownMethodRequestOrNotification( - override val method: Method, + override val method: Method, override val params: NotificationParams? = null, ) : ClientNotification, ClientRequest, ServerNotification, ServerRequest /** @@ -506,8 +521,15 @@ public data class InitializeResult( * This notification is sent from the client to the server after initialization has finished. */ @Serializable -public class InitializedNotification : ClientNotification { +public data class InitializedNotification( + override val params: Params = Params(), +) : ClientNotification { override val method: Method = Method.Defined.NotificationsInitialized + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Ping */ @@ -528,7 +550,7 @@ public sealed interface ProgressBase { /** * The progress thus far. This should increase every time progress is made, even if the total is unknown. */ - public val progress: Int + public val progress: Double /** * Total number of items to a process (or total progress required), if known. @@ -553,7 +575,7 @@ public open class Progress( /** * The progress thus far. This should increase every time progress is made, even if the total is unknown. */ - override val progress: Int, + override val progress: Double, /** * Total number of items to a process (or total progress required), if known. @@ -571,18 +593,32 @@ public open class Progress( */ @Serializable public data class ProgressNotification( - override val progress: Int, - /** - * The progress token, - * which was given in the initial request, - * used to associate this notification with the request that is proceeding. - */ - public val progressToken: ProgressToken, - @Suppress("PropertyName") val _meta: JsonObject = EmptyJsonObject, - override val total: Double?, - override val message: String?, -) : ClientNotification, ServerNotification, ProgressBase { + override val params: Params, +) : ClientNotification, ServerNotification { override val method: Method = Method.Defined.NotificationsProgress + + @Serializable + public data class Params( + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + */ + override val progress: Double, + /** + * The progress token, + * which was given in the initial request, + * used to associate this notification with the request that is proceeding. + */ + val progressToken: ProgressToken, + /** + * Total number of items to process (or total progress required), if known. + */ + override val total: Double? = null, + /** + * An optional message describing the current progress. + */ + override val message: String? = null, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams, ProgressBase } /* Pagination */ @@ -784,8 +820,15 @@ public class ReadResourceResult( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class ResourceListChangedNotification : ServerNotification { +public data class ResourceListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsResourcesListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /** @@ -821,13 +864,18 @@ public data class UnsubscribeRequest( */ @Serializable public data class ResourceUpdatedNotification( - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - */ - val uri: String, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerNotification, WithMeta { + override val params: Params, +) : ServerNotification { override val method: Method = Method.Defined.NotificationsResourcesUpdated + + @Serializable + public data class Params( + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + */ + val uri: String, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Prompts */ @@ -1044,8 +1092,15 @@ public class GetPromptResult( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class PromptListChangedNotification : ServerNotification { +public data class PromptListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsPromptsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Tools */ @@ -1223,8 +1278,15 @@ public data class CallToolRequest( * Servers may issue this without any previous subscription from the client. */ @Serializable -public class ToolListChangedNotification : ServerNotification { +public data class ToolListChangedNotification( + override val params: Params = Params(), +) : ServerNotification { override val method: Method = Method.Defined.NotificationsToolsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /* Logging */ @@ -1252,22 +1314,27 @@ public enum class LoggingLevel { */ @Serializable public data class LoggingMessageNotification( - /** - * The severity of this log message. - */ - val level: LoggingLevel, + override val params: Params, +) : ServerNotification { + override val method: Method = Method.Defined.NotificationsMessage - /** - * An optional name of the logger issuing this message. - */ - val logger: String? = null, + @Serializable + public data class Params( + /** + * The severity of this log message. + */ + val level: LoggingLevel, + /** + * An optional name of the logger issuing this message. + */ + val logger: String? = null, + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + val data: JsonElement, + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams - /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. - */ - val data: JsonObject = EmptyJsonObject, - override val _meta: JsonObject = EmptyJsonObject, -) : ServerNotification, WithMeta { /** * A request from the client to the server to enable or adjust logging. */ @@ -1281,8 +1348,6 @@ public data class LoggingMessageNotification( ) : ClientRequest, WithMeta { override val method: Method = Method.Defined.LoggingSetLevel } - - override val method: Method = Method.Defined.NotificationsMessage } /* Sampling */ @@ -1578,8 +1643,15 @@ public class ListRootsResult( * A notification from the client to the server, informing it that the list of roots has changed. */ @Serializable -public class RootsListChangedNotification : ClientNotification { +public data class RootsListChangedNotification( + override val params: Params = Params(), +) : ClientNotification { override val method: Method = Method.Defined.NotificationsRootsListChanged + + @Serializable + public data class Params( + override val _meta: JsonObject = EmptyJsonObject, + ) : NotificationParams } /** diff --git a/src/commonTest/kotlin/AudioContentSerializationTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt similarity index 95% rename from src/commonTest/kotlin/AudioContentSerializationTest.kt rename to src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt index 6745972c7..5b2d8e2b3 100644 --- a/src/commonTest/kotlin/AudioContentSerializationTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt @@ -2,7 +2,6 @@ package io.modelcontextprotocol.kotlin.sdk import io.kotest.assertions.json.shouldEqualJson import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlinx.serialization.encodeToString import kotlin.test.Test import kotlin.test.assertEquals diff --git a/src/commonTest/kotlin/CallToolResultUtilsTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt similarity index 100% rename from src/commonTest/kotlin/CallToolResultUtilsTest.kt rename to src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt new file mode 100644 index 000000000..c87d2bde9 --- /dev/null +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt @@ -0,0 +1,378 @@ +package io.modelcontextprotocol.kotlin.sdk + +import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class TypesTest { + + @Test + fun `should have correct latest protocol version`() { + assertNotEquals("", LATEST_PROTOCOL_VERSION) + assertEquals("2024-11-05", LATEST_PROTOCOL_VERSION) + } + + @Test + fun `should have correct supported protocol versions`() { + assertIs>(SUPPORTED_PROTOCOL_VERSIONS) + assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains(LATEST_PROTOCOL_VERSION)) + assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-10-07")) + assertEquals(2, SUPPORTED_PROTOCOL_VERSIONS.size) + } + + @Test + fun `should validate JSONRPC version constant`() { + assertEquals("2.0", JSONRPC_VERSION) + } + + // Reference Tests + @Test + fun `should validate ResourceReference`() { + val resourceRef = ResourceReference(uri = "file:///path/to/file.txt") + + assertEquals("ref/resource", resourceRef.type) + assertEquals("file:///path/to/file.txt", resourceRef.uri) + } + + @Test + fun `should serialize and deserialize ResourceReference correctly`() { + val resourceRef = ResourceReference(uri = "https://example.com/resource") + + val json = McpJson.encodeToString(resourceRef) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/resource", decoded.type) + assertEquals("https://example.com/resource", decoded.uri) + } + + @Test + fun `should validate PromptReference`() { + val promptRef = PromptReference(name = "greeting") + + assertEquals("ref/prompt", promptRef.type) + assertEquals("greeting", promptRef.name) + } + + @Test + fun `should serialize and deserialize PromptReference correctly`() { + val promptRef = PromptReference(name = "test-prompt") + + val json = McpJson.encodeToString(promptRef) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/prompt", decoded.type) + assertEquals("test-prompt", decoded.name) + } + + @Test + fun `should handle UnknownReference for invalid type`() { + val invalidJson = """{"type": "invalid_type"}""" + + val decoded = McpJson.decodeFromString(invalidJson) + + assertIs(decoded) + assertEquals("invalid_type", decoded.type) + } + + // PromptMessageContent Tests + @Test + fun `should validate text content`() { + val textContent = TextContent(text = "Hello, world!") + + assertEquals("text", textContent.type) + assertEquals("Hello, world!", textContent.text) + } + + @Test + fun `should serialize and deserialize text content correctly`() { + val textContent = TextContent(text = "Test message") + + val json = McpJson.encodeToString(textContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("text", decoded.type) + assertEquals("Test message", decoded.text) + } + + @Test + fun `should validate image content`() { + val imageContent = ImageContent( + data = "aGVsbG8=", // base64 encoded "hello" + mimeType = "image/png" + ) + + assertEquals("image", imageContent.type) + assertEquals("aGVsbG8=", imageContent.data) + assertEquals("image/png", imageContent.mimeType) + } + + @Test + fun `should serialize and deserialize image content correctly`() { + val imageContent = ImageContent( + data = "dGVzdA==", // base64 encoded "test" + mimeType = "image/jpeg" + ) + + val json = McpJson.encodeToString(imageContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("image", decoded.type) + assertEquals("dGVzdA==", decoded.data) + assertEquals("image/jpeg", decoded.mimeType) + } + + @Test + fun `should validate audio content`() { + val audioContent = AudioContent( + data = "aGVsbG8=", // base64 encoded "hello" + mimeType = "audio/mp3" + ) + + assertEquals("audio", audioContent.type) + assertEquals("aGVsbG8=", audioContent.data) + assertEquals("audio/mp3", audioContent.mimeType) + } + + @Test + fun `should serialize and deserialize audio content correctly`() { + val audioContent = AudioContent( + data = "YXVkaW8=", // base64 encoded "audio" + mimeType = "audio/wav" + ) + + val json = McpJson.encodeToString(audioContent) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("audio", decoded.type) + assertEquals("YXVkaW8=", decoded.data) + assertEquals("audio/wav", decoded.mimeType) + } + + @Test + fun `should validate embedded resource content`() { + val resource = TextResourceContents( + text = "File contents", + uri = "file:///path/to/file.txt", + mimeType = "text/plain" + ) + val embeddedResource = EmbeddedResource(resource = resource) + + assertEquals("resource", embeddedResource.type) + assertEquals(resource, embeddedResource.resource) + } + + @Test + fun `should serialize and deserialize embedded resource content correctly`() { + val resource = BlobResourceContents( + blob = "YmluYXJ5ZGF0YQ==", + uri = "file:///path/to/binary.dat", + mimeType = "application/octet-stream" + ) + val embeddedResource = EmbeddedResource(resource = resource) + + val json = McpJson.encodeToString(embeddedResource) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("resource", decoded.type) + assertIs(decoded.resource) + val decodedBlob = decoded.resource + assertEquals("YmluYXJ5ZGF0YQ==", decodedBlob.blob) + assertEquals("file:///path/to/binary.dat", decodedBlob.uri) + assertEquals("application/octet-stream", decodedBlob.mimeType) + } + + @Test + fun `should handle unknown content type`() { + val unknownJson = """{"type": "unknown_type"}""" + + val decoded = McpJson.decodeFromString(unknownJson) + + assertIs(decoded) + assertEquals("unknown_type", decoded.type) + } + + // PromptMessage Tests + @Test + fun `should validate prompt message with text content`() { + val textContent = TextContent(text = "Hello, assistant!") + val promptMessage = PromptMessage( + role = Role.user, + content = textContent + ) + + assertEquals(Role.user, promptMessage.role) + assertEquals(textContent, promptMessage.content) + assertEquals("text", promptMessage.content.type) + } + + @Test + fun `should validate prompt message with embedded resource`() { + val resource = TextResourceContents( + text = "Primary application entry point", + uri = "file:///project/src/main.rs", + mimeType = "text/x-rust" + ) + val embeddedResource = EmbeddedResource(resource = resource) + val promptMessage = PromptMessage( + role = Role.assistant, + content = embeddedResource + ) + + assertEquals(Role.assistant, promptMessage.role) + assertEquals("resource", promptMessage.content.type) + val content = promptMessage.content as EmbeddedResource + val textResource = content.resource as TextResourceContents + assertEquals("Primary application entry point", textResource.text) + assertEquals("file:///project/src/main.rs", textResource.uri) + assertEquals("text/x-rust", textResource.mimeType) + } + + @Test + fun `should serialize and deserialize prompt message correctly`() { + val imageContent = ImageContent( + data = "aW1hZ2VkYXRh", // base64 encoded "imagedata" + mimeType = "image/png" + ) + val promptMessage = PromptMessage( + role = Role.assistant, + content = imageContent + ) + + val json = McpJson.encodeToString(promptMessage) + val decoded = McpJson.decodeFromString(json) + + assertEquals(Role.assistant, decoded.role) + assertIs(decoded.content) + val decodedContent = decoded.content + assertEquals("aW1hZ2VkYXRh", decodedContent.data) + assertEquals("image/png", decodedContent.mimeType) + } + + // CallToolResult Tests + @Test + fun `should validate tool result with multiple content types`() { + val toolResult = CallToolResult( + content = listOf( + TextContent(text = "Found the following files:"), + EmbeddedResource( + resource = TextResourceContents( + text = "fn main() {}", + uri = "file:///project/src/main.rs", + mimeType = "text/x-rust" + ) + ), + EmbeddedResource( + resource = TextResourceContents( + text = "pub mod lib;", + uri = "file:///project/src/lib.rs", + mimeType = "text/x-rust" + ) + ) + ) + ) + + assertEquals(3, toolResult.content.size) + assertEquals("text", toolResult.content[0].type) + assertEquals("resource", toolResult.content[1].type) + assertEquals("resource", toolResult.content[2].type) + assertEquals(false, toolResult.isError) + } + + @Test + fun `should validate empty content array with default`() { + val toolResult = CallToolResult(content = emptyList()) + + assertEquals(0, toolResult.content.size) + assertEquals(false, toolResult.isError) + } + + @Test + fun `should serialize and deserialize CallToolResult correctly`() { + val toolResult = CallToolResult( + content = listOf( + TextContent(text = "Operation completed"), + ImageContent(data = "aW1hZ2U=", mimeType = "image/png") + ), + isError = false + ) + + val json = McpJson.encodeToString(toolResult) + val decoded = McpJson.decodeFromString(json) + + assertEquals(2, decoded.content.size) + assertIs(decoded.content[0]) + assertIs(decoded.content[1]) + assertEquals(false, decoded.isError) + } + + // CompleteRequest Tests + @Test + fun `should validate CompleteRequest with prompt reference`() { + val request = CompleteRequest( + ref = PromptReference(name = "greeting"), + argument = CompleteRequest.Argument(name = "name", value = "A") + ) + + assertEquals("completion/complete", request.method.value) + assertIs(request.ref) + val promptRef = request.ref + assertEquals("greeting", promptRef.name) + assertEquals("name", request.argument.name) + assertEquals("A", request.argument.value) + } + + @Test + fun `should validate CompleteRequest with resource reference`() { + val request = CompleteRequest( + ref = ResourceReference(uri = "github://repos/{owner}/{repo}"), + argument = CompleteRequest.Argument(name = "repo", value = "t") + ) + + assertEquals("completion/complete", request.method.value) + assertIs(request.ref) + val resourceRef = request.ref + assertEquals("github://repos/{owner}/{repo}", resourceRef.uri) + assertEquals("repo", request.argument.name) + assertEquals("t", request.argument.value) + } + + @Test + fun `should serialize and deserialize CompleteRequest correctly`() { + val request = CompleteRequest( + ref = PromptReference(name = "test"), + argument = CompleteRequest.Argument(name = "arg", value = "") + ) + + val json = McpJson.encodeToString(request) + val decoded = McpJson.decodeFromString(json) + + assertEquals("completion/complete", decoded.method.value) + assertIs(decoded.ref) + val promptRef = decoded.ref + assertEquals("test", promptRef.name) + assertEquals("arg", decoded.argument.name) + assertEquals("", decoded.argument.value) + } + + @Test + fun `should validate CompleteRequest with complex URIs`() { + val request = CompleteRequest( + ref = ResourceReference(uri = "api://v1/{tenant}/{resource}/{id}"), + argument = CompleteRequest.Argument(name = "id", value = "123") + ) + + val resourceRef = request.ref as ResourceReference + assertEquals("api://v1/{tenant}/{resource}/{id}", resourceRef.uri) + assertEquals("id", request.argument.name) + assertEquals("123", request.argument.value) + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt new file mode 100644 index 000000000..444e41804 --- /dev/null +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt @@ -0,0 +1,252 @@ +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 TypesUtilTest { + + // ErrorCode Serializer Tests + @Test + fun `should serialize and deserialize ErrorCode Defined correctly`() { + val errorCode: ErrorCode = ErrorCode.Defined.InvalidRequest + + val json = McpJson.encodeToString(errorCode) + val decoded = McpJson.decodeFromString(json) + + assertEquals(ErrorCode.Defined.InvalidRequest, decoded) + assertEquals(-32600, decoded.code) + } + + @Test + fun `should serialize and deserialize ErrorCode Unknown correctly`() { + val errorCode: ErrorCode = ErrorCode.Unknown(1001) + + val json = McpJson.encodeToString(errorCode) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(1001, decoded.code) + } + + // Method Serializer Tests + @Test + fun `should serialize and deserialize Method Defined correctly`() { + val method: Method = Method.Defined.Initialize + + val json = McpJson.encodeToString(method) + val decoded = McpJson.decodeFromString(json) + + assertEquals(Method.Defined.Initialize, decoded) + assertEquals("initialize", decoded.value) + } + + @Test + fun `should serialize and deserialize Method Custom correctly`() { + val method: Method = Method.Custom("custom/method") + + val json = McpJson.encodeToString(method) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("custom/method", decoded.value) + } + + // StopReason Serializer Tests + @Test + fun `should serialize and deserialize StopReason EndTurn correctly`() { + val stopReason: StopReason = StopReason.EndTurn + + val json = McpJson.encodeToString(stopReason) + val decoded = McpJson.decodeFromString(json) + + assertEquals(StopReason.EndTurn, decoded) + assertEquals("endTurn", decoded.value) + } + + @Test + fun `should serialize and deserialize StopReason Other correctly`() { + val stopReason: StopReason = StopReason.Other("custom_reason") + + val json = McpJson.encodeToString(stopReason) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("custom_reason", decoded.value) + } + + // Reference Polymorphic Serializer Tests + @Test + fun `should deserialize ResourceReference polymorphically`() { + val json = """{"type": "ref/resource", "uri": "file:///test.txt"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/resource", decoded.type) + assertEquals("file:///test.txt", decoded.uri) + } + + @Test + fun `should deserialize PromptReference polymorphically`() { + val json = """{"type": "ref/prompt", "name": "test-prompt"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("ref/prompt", decoded.type) + assertEquals("test-prompt", decoded.name) + } + + @Test + fun `should deserialize UnknownReference for invalid type`() { + val json = """{"type": "unknown_ref", "data": "test"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("unknown_ref", decoded.type) + } + + // PromptMessageContent Polymorphic Serializer Tests + @Test + fun `should deserialize TextContent polymorphically`() { + val json = """{"type": "text", "text": "Hello world"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("text", decoded.type) + assertEquals("Hello world", decoded.text) + } + + @Test + fun `should deserialize ImageContent polymorphically`() { + val json = """{"type": "image", "data": "aW1hZ2U=", "mimeType": "image/png"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("image", decoded.type) + assertEquals("aW1hZ2U=", decoded.data) + assertEquals("image/png", decoded.mimeType) + } + + @Test + fun `should deserialize AudioContent polymorphically`() { + val json = """{"type": "audio", "data": "YXVkaW8=", "mimeType": "audio/mp3"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("audio", decoded.type) + assertEquals("YXVkaW8=", decoded.data) + assertEquals("audio/mp3", decoded.mimeType) + } + + @Test + fun `should deserialize EmbeddedResource polymorphically`() { + val json = + """{"type": "resource", "resource": {"uri": "file:///test.txt", "mimeType": "text/plain", "text": "content"}}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("resource", decoded.type) + assertIs(decoded.resource) + val textResource = decoded.resource + assertEquals("file:///test.txt", textResource.uri) + assertEquals("content", textResource.text) + } + + // ResourceContents Polymorphic Serializer Tests + @Test + fun `should deserialize TextResourceContents polymorphically`() { + val json = """{"uri": "file:///test.txt", "mimeType": "text/plain", "text": "file content"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///test.txt", decoded.uri) + assertEquals("file content", decoded.text) + assertEquals("text/plain", decoded.mimeType) + } + + @Test + fun `should deserialize BlobResourceContents polymorphically`() { + val json = """{"uri": "file:///binary.dat", "mimeType": "application/octet-stream", "blob": "YmluYXJ5"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///binary.dat", decoded.uri) + assertEquals("YmluYXJ5", decoded.blob) + assertEquals("application/octet-stream", decoded.mimeType) + } + + @Test + fun `should deserialize UnknownResourceContents for missing fields`() { + val json = """{"uri": "file:///unknown.dat", "mimeType": "unknown/type"}""" + + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("file:///unknown.dat", decoded.uri) + assertEquals("unknown/type", decoded.mimeType) + } + + // RequestId Serializer Tests + @Test + fun `should serialize and deserialize RequestId StringId correctly`() { + val requestId: RequestId = RequestId.StringId("test-id") + + val json = McpJson.encodeToString(requestId) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals("test-id", decoded.value) + } + + @Test + fun `should serialize and deserialize RequestId NumberId correctly`() { + val requestId: RequestId = RequestId.NumberId(42L) + + val json = McpJson.encodeToString(requestId) + val decoded = McpJson.decodeFromString(json) + + assertIs(decoded) + assertEquals(42L, decoded.value) + } + + // Utility Functions Tests + @Test + fun `should create CallToolResult ok correctly`() { + val result = CallToolResult.ok("Success message") + + assertEquals(listOf(TextContent("Success message")), result.content) + assertEquals(false, result.isError) + assertEquals(EmptyJsonObject, result._meta) + } + + @Test + fun `should create CallToolResult error correctly`() { + val result = CallToolResult.error("Error message") + + assertEquals(listOf(TextContent("Error message")), result.content) + assertEquals(true, result.isError) + assertEquals(EmptyJsonObject, result._meta) + } + + @Test + fun `should create CallToolResult with custom meta`() { + val meta = buildJsonObject { put("custom", "value") } + val result = CallToolResult.ok("Success", meta) + + assertEquals(listOf(TextContent("Success")), result.content) + assertEquals(false, result.isError) + assertEquals(meta, result._meta) + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/client/ClientTest.kt b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt similarity index 98% rename from src/jvmTest/kotlin/client/ClientTest.kt rename to src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 91bd12deb..1aadbc740 100644 --- a/src/jvmTest/kotlin/client/ClientTest.kt +++ b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1,4 +1,4 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client import io.mockk.coEvery import io.mockk.spyk @@ -31,17 +31,17 @@ import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.Tool -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject @@ -411,8 +411,10 @@ class ClientTest { } server.sendLoggingMessage( LoggingMessageNotification( - level = LoggingLevel.info, - data = jsonObject + params = LoggingMessageNotification.Params( + level = LoggingLevel.info, + data = jsonObject + ) ) ) server.sendResourceListChanged() @@ -439,7 +441,7 @@ class ClientTest { // Simulate delay def.complete(Unit) try { - kotlinx.coroutines.delay(1000) + delay(1000) } catch (e: CancellationException) { defTimeOut.complete(Unit) throw e @@ -494,9 +496,9 @@ class ClientTest { // Simulate a delayed response // Wait ~100ms unless canceled try { - kotlinx.coroutines.withTimeout(100L) { + withTimeout(100L) { // Just delay here, if timeout is 0 on the client side, this won't return in time - kotlinx.coroutines.delay(100) + delay(100) } } catch (_: Exception) { // If aborted, just rethrow or return early @@ -523,7 +525,7 @@ class ClientTest { // Request with 1 msec timeout should fail immediately val ex = assertFailsWith { - kotlinx.coroutines.withTimeout(1) { + withTimeout(1) { client.listResources() } } diff --git a/src/jvmTest/kotlin/client/StdioClientTransportTest.kt b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt similarity index 88% rename from src/jvmTest/kotlin/client/StdioClientTransportTest.kt rename to src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index 15defaede..d9f226a93 100644 --- a/src/jvmTest/kotlin/client/StdioClientTransportTest.kt +++ b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -1,8 +1,6 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client -import io.modelcontextprotocol.kotlin.sdk.client.BaseTransportTest import kotlinx.coroutines.test.runTest -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered diff --git a/src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt similarity index 99% rename from src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt rename to src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt index ca255f4a9..83d818bee 100644 --- a/src/jvmTest/kotlin/client/StreamableHttpClientTransportTest.kt +++ b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt @@ -1,4 +1,4 @@ -package client +package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine @@ -16,7 +16,6 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.RequestId -import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest diff --git a/src/jvmTest/kotlin/server/ServerTest.kt b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt similarity index 99% rename from src/jvmTest/kotlin/server/ServerTest.kt rename to src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt index 35e077414..8ba455dec 100644 --- a/src/jvmTest/kotlin/server/ServerTest.kt +++ b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt @@ -1,4 +1,4 @@ -package server +package io.modelcontextprotocol.kotlin.sdk.server import io.modelcontextprotocol.kotlin.sdk.CallToolResult import io.modelcontextprotocol.kotlin.sdk.GetPromptResult @@ -15,8 +15,6 @@ import io.modelcontextprotocol.kotlin.sdk.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest diff --git a/src/jvmTest/kotlin/server/StdioServerTransportTest.kt b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt similarity index 93% rename from src/jvmTest/kotlin/server/StdioServerTransportTest.kt rename to src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index 8c865aa21..2bbff14ae 100644 --- a/src/jvmTest/kotlin/server/StdioServerTransportTest.kt +++ b/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -1,23 +1,26 @@ -package server +package io.modelcontextprotocol.kotlin.sdk.server import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.PingRequest -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.runBlocking -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import java.io.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayOutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream class StdioServerTransportTest { private lateinit var input: PipedInputStream From ef35bedf41d27d31fdfab83650a7399b289e183e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:10:45 +0200 Subject: [PATCH 06/24] Bump io.mockk:mockk from 1.14.4 to 1.14.5 (#204) Bumps [io.mockk:mockk](https://github.com/mockk/mockk) from 1.14.4 to 1.14.5. - [Release notes](https://github.com/mockk/mockk/releases) - [Commits](https://github.com/mockk/mockk/compare/1.14.4...1.14.5) --- updated-dependencies: - dependency-name: io.mockk:mockk dependency-version: 1.14.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 201a74f83..590e1bcee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ serialization = "1.9.0" collections-immutable = "0.4.0" coroutines = "1.10.2" ktor = "3.2.1" -mockk = "1.14.4" +mockk = "1.14.5" logging = "7.0.7" jreleaser = "1.19.0" binaryCompatibilityValidatorPlugin = "0.18.1" From 150ed87a23fae3c7f512acd6e9a78a8dbc8aaecc Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 29 Jul 2025 11:26:11 +0200 Subject: [PATCH 07/24] Bump protocol version to `2025-03-26` and remove outdated test file (#202) --- .../modelcontextprotocol/kotlin/sdk/types.kt | 6 +-- .../kotlin/sdk/TypesTest.kt | 4 +- .../kotlin/sdk/client/TypesTest.kt | 49 ------------------- 3 files changed, 5 insertions(+), 54 deletions(-) delete mode 100644 src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 077b3f638..57a5f803f 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -18,11 +18,11 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.incrementAndFetch import kotlin.jvm.JvmInline -public const val LATEST_PROTOCOL_VERSION: String = "2024-11-05" +public const val LATEST_PROTOCOL_VERSION: String = "2025-03-26" public val SUPPORTED_PROTOCOL_VERSIONS: Array = arrayOf( LATEST_PROTOCOL_VERSION, - "2024-10-07", + "2024-11-05", ) public const val JSONRPC_VERSION: String = "2.0" @@ -1250,7 +1250,7 @@ public class CallToolResult( ) : CallToolResultBase /** - * [CallToolResult] extended with backwards compatibility to protocol version 2024-10-07. + * [CallToolResult] extended with backwards compatibility to protocol version 2024-11-05. */ @Serializable public class CompatibilityCallToolResult( diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt index c87d2bde9..819db328f 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt +++ b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt @@ -12,14 +12,14 @@ class TypesTest { @Test fun `should have correct latest protocol version`() { assertNotEquals("", LATEST_PROTOCOL_VERSION) - assertEquals("2024-11-05", LATEST_PROTOCOL_VERSION) + assertEquals("2025-03-26", LATEST_PROTOCOL_VERSION) } @Test fun `should have correct supported protocol versions`() { assertIs>(SUPPORTED_PROTOCOL_VERSIONS) assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains(LATEST_PROTOCOL_VERSION)) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-10-07")) + assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-11-05")) assertEquals(2, SUPPORTED_PROTOCOL_VERSIONS.size) } diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt b/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt deleted file mode 100644 index 1714ded67..000000000 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/TypesTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.Request -import io.modelcontextprotocol.kotlin.sdk.shared.McpJson -import kotlin.test.Test - -class TypesTest { - - @Test - fun testRequestResult() { - val message = - "{\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{\"listChanged\":true},\"resources\":{}},\"serverInfo\":{\"name\":\"jetbrains/proxy\",\"version\":\"0.1.0\"}},\"jsonrpc\":\"2.0\",\"id\":1}" - McpJson.decodeFromString(message) - } - - @Test - fun testRequestError() { - val message = - "{\"method\":\"initialize\", \"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"experimental\":{},\"sampling\":{}},\"clientInfo\":{\"name\":\"test client\",\"version\":\"1.0\"},\"_meta\":{}}" - McpJson.decodeFromString(message) - } - - @Test - fun testJSONRPCMessage() { - val line = "{\"result\":{\"content\":[{\"type\":\"text\"}],\"isError\":false},\"jsonrpc\":\"2.0\",\"id\":4}" - McpJson.decodeFromString(line) - } - - @Test - fun testJSONRPCMessageWithStringId() { - val line = """ - { - "jsonrpc": "2.0", - "method": "initialize", - "id": "ebf9f64a-0", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": { - "name": "mcp-java-client", - "version": "0.2.0" - } - } - } - """.trimIndent() - McpJson.decodeFromString(line) - } -} \ No newline at end of file From 7bc826b8af16bd2772fd0927229453956db8168f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:06:34 +0200 Subject: [PATCH 08/24] Bump ktor from 3.2.1 to 3.2.2 (#205) Bumps `ktor` from 3.2.1 to 3.2.2. Updates `io.ktor:ktor-client-cio` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) Updates `io.ktor:ktor-server-sse` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) Updates `io.ktor:ktor-server-websockets` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) Updates `io.ktor:ktor-server-cio` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) Updates `io.ktor:ktor-server-test-host` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) Updates `io.ktor:ktor-client-mock` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/3.2.1...3.2.2) --- updated-dependencies: - dependency-name: io.ktor:ktor-client-cio dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-server-sse dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-server-websockets dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-server-cio dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-server-test-host dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-mock dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 590e1bcee..cfaf9db12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ atomicfu = "0.29.0" serialization = "1.9.0" collections-immutable = "0.4.0" coroutines = "1.10.2" -ktor = "3.2.1" +ktor = "3.2.2" mockk = "1.14.5" logging = "7.0.7" jreleaser = "1.19.0" From 759754daec6cbff3b86084da40e03623282a49c4 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 30 Jul 2025 09:17:28 +0200 Subject: [PATCH 09/24] Add CodeQL workflow for Kotlin analysis (#200) --- .github/workflows/codeql.yml | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..721f5498f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,62 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '0 4 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + matrix: + language: [ java-kotlin ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: manual + + - uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Build Kotlin sources + run: | + ./gradlew \ + :build -Pkotlin.incremental=false \ + --no-daemon --stacktrace --parallel + + - name: Analyze + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{ matrix.language }}' From a895a298d52f6b090c0ff632a91bec57915d4d40 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 12:49:20 +0300 Subject: [PATCH 10/24] fixup! Introduce Kotlin integration tests --- .../TypeScriptClientKotlinServerTest.kt | 4 +++- .../utils/KotlinServerForTypeScriptClient.kt | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt b/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt index f9c740cc0..74942807f 100644 --- a/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -22,7 +22,9 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { killProcessOnPort(port) httpServer = KotlinServerForTypeScriptClient() httpServer?.start(port) - Thread.sleep(1000) + if (!waitForPort("localhost", port, 10)) { + throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") + } println("Kotlin server started on port $port") } diff --git a/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt b/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt index 891e9b42c..1006d413c 100644 --- a/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt +++ b/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt @@ -197,14 +197,14 @@ class KotlinServerForTypeScriptClient { val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" repeat(3) { index -> - server.sendLoggingMessage( - LoggingMessageNotification( - level = LoggingLevel.info, - data = buildJsonObject { - put("message", JsonPrimitive("Greeting notification #${index + 1} for $name")) - } - ) - ) + val notifJson = buildJsonObject { + put("level", JsonPrimitive("info")) + put("data", buildJsonObject { + put("message", JsonPrimitive("Greeting notification #${index + 1} for $name")) + }) + } + val notif = McpJson.decodeFromJsonElement(notifJson) + server.sendLoggingMessage(notif) } CallToolResult( @@ -232,7 +232,7 @@ class KotlinServerForTypeScriptClient { messages = listOf( PromptMessage( role = Role.user, - content = TextContent("Please greet ${(request.arguments?.get("name") as? JsonPrimitive)?.content ?: "someone"} in a friendly manner.") + content = TextContent("Please greet ${request.arguments?.get("name") ?: "someone"} in a friendly manner.") ) ) ) From 1c5536987b8644b9441ef98af06c5eca0d2d5f5d Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 13:00:20 +0300 Subject: [PATCH 11/24] fixup! Introduce Kotlin integration tests --- .../typescript/TypeScriptTestBase.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt b/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt index 1db55699e..5cd69a7ba 100644 --- a/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt +++ b/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt @@ -46,8 +46,13 @@ abstract class TypeScriptTestBase { @JvmStatic protected fun executeCommand(command: String, workingDir: File): String { + // Prefer running TypeScript via ts-node to avoid npx network delays on CI + val adjusted = if (command.contains("npx tsx ")) { + command.replaceFirst("npx tsx ", "node --loader ts-node/esm ") + } else command + val process = ProcessBuilder() - .command("bash", "-c", command) + .command("bash", "-c", adjusted) .directory(workingDir) .redirectErrorStream(true) .start() @@ -63,7 +68,7 @@ abstract class TypeScriptTestBase { val exitCode = process.waitFor() if (exitCode != 0) { - throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") + throw RuntimeException("Command execution failed with exit code $exitCode: $adjusted\nOutput:\n$output") } return output.toString() @@ -75,17 +80,10 @@ abstract class TypeScriptTestBase { } @JvmStatic - protected fun findFreePort(minPort: Int = 3000, maxPort: Int = 4000): Int { - for (port in minPort..maxPort) { - try { - ServerSocket(port).use { - return port - } - } catch (_: Exception) { - // port is in use, try the next one - } + protected fun findFreePort(): Int { + ServerSocket(0).use { socket -> + return socket.localPort } - throw RuntimeException("No free port found between $minPort and $maxPort") } } From 99383ae00f7d0b4b9ed3a0774bbb2d2dbe35b222 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 4 Aug 2025 11:53:38 +0200 Subject: [PATCH 12/24] modularize sdk (#208) * Modularize SDK: migrated client and server implementations to separate modules. Removed old configuration files and reorganized the source code structure * add support native targets and update js/wasm configurations * Refactor sample projects: add samples as composite builds, replace hardcoded dependencies with version catalog, remove obsolete gradle wrapper files * Update .gitignore: exclude SWE agent directories (.claude/ and .junie/) * Refine CodeQL workflow: update Kotlin build steps to target individual modules separately instead of building all at once --- .github/workflows/codeql.yml | 9 +- .gitignore | 4 + build.gradle.kts | 282 +----------------- buildSrc/build.gradle.kts | 16 + buildSrc/settings.gradle.kts | 7 + buildSrc/src/main/kotlin/mcp.dokka.gradle.kts | 23 ++ .../src/main/kotlin/mcp.jreleaser.gradle.kts | 73 +++++ .../main/kotlin/mcp.multiplatform.gradle.kts | 50 ++++ .../src/main/kotlin/mcp.publishing.gradle.kts | 66 ++++ gradle.properties | 8 +- gradle/libs.versions.toml | 33 +- kotlin-sdk-client/api/kotlin-sdk-client.api | 117 ++++++++ kotlin-sdk-client/build.gradle.kts | 43 +++ .../kotlin/sdk/client/Client.kt | 32 +- .../kotlin/sdk/client/KtorClient.kt | 0 .../kotlin/sdk/client/SSEClientTransport.kt | 0 .../kotlin/sdk/client/StdioClientTransport.kt | 0 .../client/StreamableHttpClientTransport.kt | 1 + .../StreamableHttpMcpKtorClientExtensions.kt | 0 .../sdk/client/WebSocketClientTransport.kt | 0 .../WebSocketMcpKtorClientExtensions.kt | 0 .../StreamableHttpClientTransportTest.kt | 9 +- .../api/kotlin-sdk-core.api | 254 +--------------- kotlin-sdk-core/build.gradle.kts | 45 +++ .../kotlin/sdk/internal/utils.kt | 2 +- .../kotlin/sdk/shared/Protocol.kt | 4 +- .../kotlin/sdk/shared/ReadBuffer.kt | 6 +- .../kotlin/sdk/shared/Transport.kt | 0 .../sdk/shared/WebSocketMcpTransport.kt | 3 +- .../modelcontextprotocol/kotlin/sdk/types.kt | 4 +- .../kotlin/sdk/types.util.kt | 2 +- .../sdk/AudioContentSerializationTest.kt | 0 .../kotlin/sdk/CallToolResultUtilsTest.kt | 0 .../kotlin/sdk/ToolSerializationTest.kt | 0 .../kotlin/sdk/TypesTest.kt | 0 .../kotlin/sdk/TypesUtilTest.kt | 0 .../kotlin/sdk/shared/ReadBufferTest.kt | 1 - .../kotlin/sdk/internal/utils.js.kt | 4 +- .../kotlin/sdk/internal/utils.jvm.kt | 4 +- .../kotlin/sdk/internal/utils.native.kt | 4 +- .../kotlin/sdk/internal/utils.wasmJs.kt | 4 +- kotlin-sdk-server/api/kotlin-sdk-server.api | 133 +++++++++ kotlin-sdk-server/build.gradle.kts | 28 ++ .../kotlin/sdk/server/KtorServer.kt | 2 +- .../kotlin/sdk/server/SSEServerTransport.kt | 1 - .../kotlin/sdk/server/Server.kt | 0 .../kotlin/sdk/server/StdioServerTransport.kt | 0 .../WebSocketMcpKtorServerExtensions.kt | 0 .../sdk/server/WebSocketMcpServerTransport.kt | 0 .../sdk/server/StdioServerTransportTest.kt | 105 ++++--- kotlin-sdk-test/build.gradle.kts | 16 + .../kotlin/sdk/client/ClientTest.kt | 88 +++--- .../kotlin/sdk/client/SseTransportTest.kt | 114 +++---- .../sdk/client/WebSocketTransportTest.kt | 1 + .../sdk/integration}/InMemoryTransportTest.kt | 4 +- .../sdk/integration/SseIntegrationTest.kt | 0 .../kotlin/sdk/shared}/BaseTransportTest.kt | 3 +- .../kotlin/sdk/shared}/InMemoryTransport.kt | 6 +- .../sdk/client/StdioClientTransportTest.kt | 1 + .../kotlin/sdk/server/ServerTest.kt | 25 +- kotlin-sdk/build.gradle.kts | 18 ++ samples/kotlin-mcp-client/.gitignore | 42 --- samples/kotlin-mcp-client/build.gradle.kts | 15 +- samples/kotlin-mcp-client/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - samples/kotlin-mcp-client/gradlew | 251 ---------------- samples/kotlin-mcp-client/gradlew.bat | 94 ------ samples/kotlin-mcp-client/settings.gradle.kts | 14 +- samples/kotlin-mcp-server/.gitignore | 45 --- samples/kotlin-mcp-server/.idea/.gitignore | 8 - samples/kotlin-mcp-server/.idea/gradle.xml | 17 -- samples/kotlin-mcp-server/.idea/kotlinc.xml | 6 - samples/kotlin-mcp-server/.idea/misc.xml | 10 - samples/kotlin-mcp-server/.idea/vcs.xml | 7 - samples/kotlin-mcp-server/build.gradle.kts | 8 +- samples/kotlin-mcp-server/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - samples/kotlin-mcp-server/gradlew | 234 --------------- samples/kotlin-mcp-server/gradlew.bat | 89 ------ samples/kotlin-mcp-server/settings.gradle.kts | 21 +- samples/weather-stdio-server/.gitignore | 42 --- samples/weather-stdio-server/build.gradle.kts | 24 +- .../weather-stdio-server/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - samples/weather-stdio-server/gradlew | 251 ---------------- samples/weather-stdio-server/gradlew.bat | 94 ------ .../weather-stdio-server/settings.gradle.kts | 14 +- settings.gradle.kts | 12 +- 91 files changed, 977 insertions(+), 2001 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/mcp.dokka.gradle.kts create mode 100644 buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts create mode 100644 buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts create mode 100644 buildSrc/src/main/kotlin/mcp.publishing.gradle.kts create mode 100644 kotlin-sdk-client/api/kotlin-sdk-client.api create mode 100644 kotlin-sdk-client/build.gradle.kts rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt (96%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt (100%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt (100%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt (100%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt (99%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt (100%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt (100%) rename {src => kotlin-sdk-client/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt (100%) rename {src/jvmTest => kotlin-sdk-client/src/commonTest}/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt (97%) rename api/kotlin-sdk.api => kotlin-sdk-core/api/kotlin-sdk-core.api (90%) create mode 100644 kotlin-sdk-core/build.gradle.kts rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt (65%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt (99%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt (88%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt (100%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt (97%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt (99%) rename {src => kotlin-sdk-core/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt (99%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt (100%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt (100%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt (100%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt (100%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt (100%) rename {src => kotlin-sdk-core/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt (97%) rename {src => kotlin-sdk-core/src}/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt (61%) rename {src/jvmMain/java => kotlin-sdk-core/src/jvmMain/kotlin}/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt (63%) rename src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt => kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt (67%) rename {src => kotlin-sdk-core/src}/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt (61%) create mode 100644 kotlin-sdk-server/api/kotlin-sdk-server.api create mode 100644 kotlin-sdk-server/build.gradle.kts rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt (98%) rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt (98%) rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt (100%) rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt (100%) rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt (100%) rename {src => kotlin-sdk-server/src}/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt (100%) rename {src => kotlin-sdk-server/src}/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt (54%) create mode 100644 kotlin-sdk-test/build.gradle.kts rename {src/jvmTest => kotlin-sdk-test/src/commonTest}/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt (93%) rename {src => kotlin-sdk-test/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt (52%) rename {src => kotlin-sdk-test/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt (95%) rename {src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client => kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration}/InMemoryTransportTest.kt (95%) rename {src => kotlin-sdk-test/src}/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt (100%) rename {src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client => kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared}/BaseTransportTest.kt (94%) rename {src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk => kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared}/InMemoryTransport.kt (93%) rename {src => kotlin-sdk-test/src}/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt (94%) rename {src => kotlin-sdk-test/src}/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt (96%) create mode 100644 kotlin-sdk/build.gradle.kts delete mode 100644 samples/kotlin-mcp-client/.gitignore delete mode 100644 samples/kotlin-mcp-client/gradle.properties delete mode 100644 samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/kotlin-mcp-client/gradlew delete mode 100644 samples/kotlin-mcp-client/gradlew.bat delete mode 100644 samples/kotlin-mcp-server/.gitignore delete mode 100644 samples/kotlin-mcp-server/.idea/.gitignore delete mode 100644 samples/kotlin-mcp-server/.idea/gradle.xml delete mode 100644 samples/kotlin-mcp-server/.idea/kotlinc.xml delete mode 100644 samples/kotlin-mcp-server/.idea/misc.xml delete mode 100644 samples/kotlin-mcp-server/.idea/vcs.xml delete mode 100644 samples/kotlin-mcp-server/gradle.properties delete mode 100644 samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/kotlin-mcp-server/gradlew delete mode 100644 samples/kotlin-mcp-server/gradlew.bat delete mode 100644 samples/weather-stdio-server/.gitignore delete mode 100644 samples/weather-stdio-server/gradle.properties delete mode 100644 samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar delete mode 100644 samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties delete mode 100755 samples/weather-stdio-server/gradlew delete mode 100644 samples/weather-stdio-server/gradlew.bat diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 721f5498f..ffd98374c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,8 +53,13 @@ jobs: - name: Build Kotlin sources run: | ./gradlew \ - :build -Pkotlin.incremental=false \ - --no-daemon --stacktrace --parallel + :kotlin-sdk-core:compileKotlinJvm \ + :kotlin-sdk-client:compileKotlinJvm \ + :kotlin-sdk-server:compileKotlinJvm \ + :kotlin-sdk:compileKotlinJvm \ + :kotlin-sdk-test:compileKotlinJvm \ + -Pkotlin.incremental=false \ + --no-daemon --stacktrace - name: Analyze uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index e650bb754..3d8dd1d96 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ bin/ ### Node.js ### node_modules dist + +### SWE agents ### +.claude/ +.junie/ diff --git a/build.gradle.kts b/build.gradle.kts index ec12f22f1..8672d45f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,278 +1,4 @@ -@file:OptIn(ExperimentalWasmDsl::class) - -import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jreleaser.model.Active - -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.kotlin.atomicfu) - alias(libs.plugins.dokka) - alias(libs.plugins.jreleaser) - `maven-publish` - signing - alias(libs.plugins.kotlinx.binary.compatibility.validator) -} - -group = "io.modelcontextprotocol" -version = "0.6.0" - -val javadocJar by tasks.registering(Jar::class) { - archiveClassifier.set("javadoc") -} - -publishing { - publications.withType(MavenPublication::class).all { - if (name.contains("jvm", ignoreCase = true)) { - artifact(javadocJar) - } - pom.configureMavenCentralMetadata() - signPublicationIfKeyPresent() - } - - repositories { - maven(url = layout.buildDirectory.dir("staging-deploy")) - } -} - -jreleaser { - gitRootSearch = true - strict = true - - signing { - active = Active.ALWAYS - armored = true - artifacts = true - files = true - } - - deploy { - active.set(Active.ALWAYS) - maven { - active.set(Active.ALWAYS) - mavenCentral { - val ossrh by creating { - active.set(Active.ALWAYS) - url.set("https://central.sonatype.com/api/v1/publisher") - applyMavenCentralRules = false - maxRetries = 240 - stagingRepository(layout.buildDirectory.dir("staging-deploy").get().asFile.path) - // workaround: https://github.com/jreleaser/jreleaser/issues/1784 - afterEvaluate { - publishing.publications.forEach { publication -> - if (publication is MavenPublication) { - val pubName = publication.name - - if (!pubName.contains("jvm", ignoreCase = true) - && !pubName.contains("metadata", ignoreCase = true) - && !pubName.contains("kotlinMultiplatform", ignoreCase = true) - ) { - - artifactOverride { - artifactId = when { - pubName.contains("wasm", ignoreCase = true) -> - "${project.name}-wasm-${pubName.lowercase().substringAfter("wasm")}" - - else -> "${project.name}-${pubName.lowercase()}" - } - jar = false - verifyPom = false - sourceJar = false - javadocJar = false - } - } - } - } - } - } - } - } - } - - release { - github { - changelog.enabled = false - skipRelease = true - skipTag = true - overwrite = false - token = "none" - } - } - - checksum { - individual = false - artifacts = false - files = false - } -} - -fun MavenPom.configureMavenCentralMetadata() { - name by project.name - description by "Kotlin implementation of the Model Context Protocol (MCP)" - url by "https://github.com/modelcontextprotocol/kotlin-sdk" - - licenses { - license { - name by "MIT License" - url by "https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/LICENSE" - distribution by "repo" - } - } - - developers { - developer { - id by "Anthropic" - name by "Anthropic Team" - organization by "Anthropic" - organizationUrl by "https://www.anthropic.com" - } - } - - scm { - url by "https://github.com/modelcontextprotocol/kotlin-sdk" - connection by "scm:git:git://github.com/modelcontextprotocol/kotlin-sdk.git" - developerConnection by "scm:git:git@github.com:modelcontextprotocol/kotlin-sdk.git" - } -} - -fun MavenPublication.signPublicationIfKeyPresent() { - val signingKey = project.getSensitiveProperty("GPG_SECRET_KEY") - val signingKeyPassphrase = project.getSensitiveProperty("SIGNING_PASSPHRASE") - - if (!signingKey.isNullOrBlank()) { - the().apply { - useInMemoryPgpKeys(signingKey, signingKeyPassphrase) - - sign(this@signPublicationIfKeyPresent) - } - } -} - -fun Project.getSensitiveProperty(name: String?): String? { - if (name == null) { - error("Expected not null property '$name' for publication repository config") - } - - return project.findProperty(name) as? String - ?: System.getenv(name) - ?: System.getProperty(name) -} - -infix fun Property.by(value: T) { - set(value) -} - -tasks.withType().configureEach { - useJUnitPlatform() -} - -abstract class GenerateLibVersionTask @Inject constructor( - @get:Input val libVersion: String, - @get:OutputDirectory val sourcesDir: File, -) : DefaultTask() { - @TaskAction - fun generate() { - val sourceFile = File(sourcesDir.resolve("io/modelcontextprotocol/kotlin/sdk"), "LibVersion.kt") - - if (!sourceFile.exists()) { - sourceFile.parentFile.mkdirs() - sourceFile.createNewFile() - } - - sourceFile.writeText( - """ - package io.modelcontextprotocol.kotlin.sdk - - public const val LIB_VERSION: String = "$libVersion" - - """.trimIndent() - ) - } -} - -dokka { - moduleName.set("MCP Kotlin SDK") - - dokkaSourceSets.configureEach { - sourceLink { - localDirectory.set(file("src/main/kotlin")) - remoteUrl("https://github.com/modelcontextprotocol/kotlin-sdk") - remoteLineSuffix.set("#L") - documentedVisibilities(VisibilityModifier.Public) - } - } - dokkaPublications.html { - outputDirectory.set(project.layout.projectDirectory.dir("docs")) - } -} - -val sourcesDir = File(project.layout.buildDirectory.asFile.get(), "generated-sources/libVersion") - -val generateLibVersionTask = - tasks.register("generateLibVersion", version.toString(), sourcesDir) - -kotlin { - jvm { - compilerOptions { - jvmTarget = JvmTarget.JVM_1_8 - } - } - - iosArm64() - iosX64() - iosSimulatorArm64() - - js(IR) { - nodejs { - testTask { - useMocha { - timeout = "30s" - } - } - } - } - - wasmJs { - nodejs() - } - - explicitApi = ExplicitApiMode.Strict - - jvmToolchain(21) - - sourceSets { - commonMain { - kotlin.srcDir(generateLibVersionTask.map { it.sourcesDir }) - dependencies { - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.collections.immutable) - api(libs.ktor.client.cio) - api(libs.ktor.server.cio) - api(libs.ktor.server.sse) - api(libs.ktor.server.websockets) - - implementation(libs.kotlin.logging) - } - } - - commonTest { - dependencies { - implementation(libs.kotlin.test) - implementation(libs.ktor.server.test.host) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotest.assertions.json) - } - } - - jvmTest { - dependencies { - implementation(libs.ktor.client.mock) - implementation(libs.mockk) - implementation(libs.slf4j.simple) - } - } - } -} +allprojects { + group = "io.modelcontextprotocol" + version = "0.6.0" +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..16c0eb9b3 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation(libs.kotlin.gradle) + implementation(libs.kotlin.serialization) + implementation(libs.kotlinx.atomicfu.gradle) + implementation(libs.dokka.gradle) + implementation(libs.jreleaser.gradle) +} \ No newline at end of file diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 000000000..fa8bc7492 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts b/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts new file mode 100644 index 000000000..8f912e9a7 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.dokka.gradle.kts @@ -0,0 +1,23 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier + +plugins { + id("org.jetbrains.dokka") +} + +dokka { + moduleName.set("MCP Kotlin SDK - ${project.name}") + + dokkaSourceSets.configureEach { + sourceLink { + localDirectory = projectDir.resolve("src") + remoteUrl("https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/${project.name}/src") + remoteLineSuffix = "#L" + } + + documentedVisibilities(VisibilityModifier.Public) + } + + dokkaPublications.html { + outputDirectory = rootProject.layout.projectDirectory.dir("docs/${project.name}") + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts new file mode 100644 index 000000000..7d288b84e --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts @@ -0,0 +1,73 @@ +import org.jreleaser.model.Active + +plugins { + id("org.jreleaser") + id("mcp.publishing") +} + +jreleaser { + gitRootSearch = true + strict = true + + signing { + active = Active.ALWAYS + armored = true + artifacts = true + files = true + } + + deploy { + active = Active.ALWAYS + maven { + active = Active.ALWAYS + mavenCentral.create("ossrh") { + active = Active.ALWAYS + url = "https://central.sonatype.com/api/v1/publisher" + applyMavenCentralRules = false + maxRetries = 240 + stagingRepository(layout.buildDirectory.dir("staging-deploy").get().asFile.path) + + // workaround: https://github.com/jreleaser/jreleaser/issues/1784 + afterEvaluate { + publishing.publications.forEach { publication -> + if (publication is MavenPublication) { + val pubName = publication.name + + if (!pubName.contains("jvm", ignoreCase = true) + && !pubName.contains("metadata", ignoreCase = true) + && !pubName.contains("kotlinMultiplatform", ignoreCase = true) + ) { + artifactOverride { + artifactId = when { + pubName.contains("wasm", ignoreCase = true) -> + "${project.name}-wasm-${pubName.lowercase().substringAfter("wasm")}" + + else -> "${project.name}-${pubName.lowercase()}" + } + jar = false + verifyPom = false + sourceJar = false + javadocJar = false + } + } + } + } + } + } + } + + release { + github { + enabled = false + skipRelease = true + skipTag = true + } + } + + checksum { + individual = false + artifacts = false + files = false + } + } +} diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts new file mode 100644 index 000000000..865698421 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -0,0 +1,50 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("org.jetbrains.kotlinx.atomicfu") +} + +// Generation library versions +val generateLibVersion by tasks.registering { + val outputDir = layout.buildDirectory.dir("generated-sources/libVersion") + outputs.dir(outputDir) + + doLast { + val sourceFile = outputDir.get().file("io/modelcontextprotocol/kotlin/sdk/LibVersion.kt").asFile + sourceFile.parentFile.mkdirs() + sourceFile.writeText( + """ + package io.modelcontextprotocol.kotlin.sdk + + public const val LIB_VERSION: String = "${project.version}" + + """.trimIndent() + ) + } +} + +kotlin { + jvm { + compilerOptions.jvmTarget = JvmTarget.JVM_1_8 + } + macosX64(); macosArm64() + linuxX64(); linuxArm64() + mingwX64() + js { nodejs() } + wasmJs { nodejs() } + + explicitApi = ExplicitApiMode.Strict + jvmToolchain(21) + + sourceSets { + commonMain { + kotlin.srcDir(generateLibVersion) + } + } +} diff --git a/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts new file mode 100644 index 000000000..bed170b10 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts @@ -0,0 +1,66 @@ +plugins { + `maven-publish` + signing +} + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} + +publishing { + publications.withType().configureEach { + if (name.contains("jvm", ignoreCase = true)) { + artifact(javadocJar) + } + + pom { + name = project.name + description = "Kotlin implementation of the Model Context Protocol (MCP)" + url = "https://github.com/modelcontextprotocol/kotlin-sdk" + + licenses { + license { + name = "MIT License" + url = "https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/LICENSE" + distribution = "repo" + } + } + + developers { + developer { + id = "Anthropic" + name = "Anthropic Team" + organization = "Anthropic" + organizationUrl = "https://www.anthropic.com" + } + } + + scm { + url = "https://github.com/modelcontextprotocol/kotlin-sdk" + connection = "scm:git:git://github.com/modelcontextprotocol/kotlin-sdk.git" + developerConnection = "scm:git:git@github.com:modelcontextprotocol/kotlin-sdk.git" + } + } + } + + repositories { + maven { + name = "staging" + url = uri(layout.buildDirectory.dir("staging-deploy")) + } + } +} + +signing { + val gpgKeyName = "GPG_SIGNING_KEY" + val gpgPassphraseName = "SIGNING_PASSPHRASE" + val signingKey = providers.environmentVariable(gpgKeyName) + .orElse(providers.gradleProperty(gpgKeyName)) + val signingPassphrase = providers.environmentVariable(gpgPassphraseName) + .orElse(providers.gradleProperty(gpgPassphraseName)) + + if (signingKey.isPresent) { + useInMemoryPgpKeys(signingKey.get(), signingPassphrase.get()) + sign(publishing.publications) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7a184e545..a868081a7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,11 @@ -kotlin.code.style=official org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true - +# Dokka org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true - +# Kotlin +kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx4G +# MPP +kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cfaf9db12..5afb3d7a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,17 +8,31 @@ atomicfu = "0.29.0" serialization = "1.9.0" collections-immutable = "0.4.0" coroutines = "1.10.2" +kotlinx-io = "0.8.0" ktor = "3.2.2" -mockk = "1.14.5" logging = "7.0.7" jreleaser = "1.19.0" binaryCompatibilityValidatorPlugin = "0.18.1" slf4j = "2.0.17" kotest = "5.9.1" +# Samples +mcp-kotlin = "0.6.0" +anthropic = "0.8.0" +shadow = "8.1.1" + [libraries] +# Plugins +kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } +kotlinx-atomicfu-gradle = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" } +dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +jreleaser-gradle = { module = "org.jreleaser:jreleaser-gradle-plugin", version.ref = "jreleaser" } + # Kotlinx libraries kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" } kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" } @@ -29,18 +43,23 @@ ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", v ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } # Testing -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } -mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" } +# Samples +mcp-kotlin = { group = "io.modelcontextprotocol", name = "kotlin-sdk", version.ref = "mcp-kotlin" } +anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } + [plugins] +kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } + +# Samples +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" } -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -jreleaser = { id = "org.jreleaser", version.ref = "jreleaser"} -kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api new file mode 100644 index 000000000..00d80eb43 --- /dev/null +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -0,0 +1,117 @@ +public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { + public static final field LIB_VERSION Ljava/lang/String; +} + +public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addRoot (Ljava/lang/String;Ljava/lang/String;)V + public final fun addRoots (Ljava/util/List;)V + protected final fun assertCapability (Ljava/lang/String;Ljava/lang/String;)V + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; + public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun ping (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun removeRoot (Ljava/lang/String;)Z + public final fun removeRoots (Ljava/util/List;)I + public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V + public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { + public static final fun mcpSse-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpSse-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpSseTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; + public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getProtocolVersion ()Ljava/lang/String; + public final fun getSessionId ()Ljava/lang/String; + public final fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun setProtocolVersion (Ljava/lang/String;)V + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun terminateSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError : java/lang/Exception { + public fun ()V + public fun (Ljava/lang/Integer;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCode ()Ljava/lang/Integer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { + public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; + public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { + public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensionsKt { + public static final fun mcpWebSocket (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpWebSocket$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun mcpWebSocketTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; +} + diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts new file mode 100644 index 000000000..ffc83ceea --- /dev/null +++ b/kotlin-sdk-client/build.gradle.kts @@ -0,0 +1,43 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + iosArm64(); iosX64(); iosSimulatorArm64() + watchosX64(); watchosArm64(); watchosSimulatorArm64() + tvosX64(); tvosArm64(); tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(libs.ktor.client.cio) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt similarity index 96% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 59abee5b0..75d0b221a 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -299,8 +299,8 @@ public open class Client( * @return The completion result returned by the server, or `null` if none. * @throws IllegalStateException If the server does not support prompts or completion. */ - public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult? { - return request(params, options) + public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult { + return request(params, options) } /** @@ -322,8 +322,8 @@ public open class Client( * @return The requested prompt details, or `null` if not found. * @throws IllegalStateException If the server does not support prompts. */ - public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult? { - return request(request, options) + public suspend fun getPrompt(request: GetPromptRequest, options: RequestOptions? = null): GetPromptResult { + return request(request, options) } /** @@ -337,8 +337,8 @@ public open class Client( public suspend fun listPrompts( request: ListPromptsRequest = ListPromptsRequest(), options: RequestOptions? = null, - ): ListPromptsResult? { - return request(request, options) + ): ListPromptsResult { + return request(request, options) } /** @@ -352,8 +352,8 @@ public open class Client( public suspend fun listResources( request: ListResourcesRequest = ListResourcesRequest(), options: RequestOptions? = null, - ): ListResourcesResult? { - return request(request, options) + ): ListResourcesResult { + return request(request, options) } /** @@ -367,8 +367,8 @@ public open class Client( public suspend fun listResourceTemplates( request: ListResourceTemplatesRequest, options: RequestOptions? = null, - ): ListResourceTemplatesResult? { - return request(request, options) + ): ListResourceTemplatesResult { + return request(request, options) } /** @@ -382,8 +382,8 @@ public open class Client( public suspend fun readResource( request: ReadResourceRequest, options: RequestOptions? = null, - ): ReadResourceResult? { - return request(request, options) + ): ReadResourceResult { + return request(request, options) } /** @@ -397,7 +397,7 @@ public open class Client( request: SubscribeRequest, options: RequestOptions? = null, ): EmptyRequestResult { - return request(request, options) + return request(request, options) } /** @@ -411,7 +411,7 @@ public open class Client( request: UnsubscribeRequest, options: RequestOptions? = null, ): EmptyRequestResult { - return request(request, options) + return request(request, options) } /** @@ -480,8 +480,8 @@ public open class Client( public suspend fun listTools( request: ListToolsRequest = ListToolsRequest(), options: RequestOptions? = null, - ): ListToolsResult? { - return request(request, options) + ): ListToolsResult { + return request(request, options) } /** diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 6584bc121..7b3656388 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -150,6 +150,7 @@ public class StreamableHttpClientTransport( response, onResumptionToken = onResumptionToken, replayMessageId = if (message is JSONRPCRequest) message.id else null ) + else -> { val body = response.bodyAsText() if (response.contentType() == null && body.isBlank()) return diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt rename to kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt diff --git a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt similarity index 97% rename from src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt rename to kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt index 83d818bee..c286eaab1 100644 --- a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject -import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -148,9 +147,7 @@ class StreamableHttpClientTransportTest { transport.start() // Should not throw for 405 - assertDoesNotThrow { - transport.terminateSession() - } + transport.terminateSession() // Session ID should still be cleared assertNull(transport.sessionId) @@ -173,7 +170,7 @@ class StreamableHttpClientTransportTest { transport.close() } - @Ignore("Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support") + @Ignore //Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support @Test fun testNotificationSchemaE2E() = runTest { val receivedMessages = mutableListOf() @@ -308,7 +305,7 @@ class StreamableHttpClientTransportTest { transport.close() } - @Ignore("Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support") + @Ignore // Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support @Test fun testNotificationWithResumptionToken() = runTest { var resumptionTokenReceived: String? = null diff --git a/api/kotlin-sdk.api b/kotlin-sdk-core/api/kotlin-sdk-core.api similarity index 90% rename from api/kotlin-sdk.api rename to kotlin-sdk-core/api/kotlin-sdk-core.api index f91d3f20d..569dfb3e7 100644 --- a/api/kotlin-sdk.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -3034,11 +3034,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/TypesKt { public static final field JSONRPC_VERSION Ljava/lang/String; public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()[Ljava/lang/String; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Notification;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCNotification; + public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/Request;)Lio/modelcontextprotocol/kotlin/sdk/JSONRPCRequest; } public final class io/modelcontextprotocol/kotlin/sdk/Types_utilKt { public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; + public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; public static final fun ok (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; public static synthetic fun ok$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; } @@ -3197,246 +3200,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/WithMeta$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/client/ClientOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addRoot (Ljava/lang/String;Ljava/lang/String;)V - public final fun addRoots (Ljava/util/List;)V - protected final fun assertCapability (Ljava/lang/String;Ljava/lang/String;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getPrompt (Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun getPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getServerCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun getServerVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun listPrompts (Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listPrompts$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResourceTemplates (Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResourceTemplates$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listResources (Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listResources$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun listTools (Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listTools$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun ping (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun readResource (Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun readResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun removeRoot (Ljava/lang/String;)Z - public final fun removeRoots (Ljava/util/List;)I - public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V - public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun subscribeResource (Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { - public static final fun mcpSse-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpSse-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpSseTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; - public static synthetic fun mcpSseTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/SseClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getProtocolVersion ()Ljava/lang/String; - public final fun getSessionId ()Ljava/lang/String; - public final fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun setProtocolVersion (Ljava/lang/String;)V - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun terminateSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError : java/lang/Exception { - public fun ()V - public fun (Ljava/lang/Integer;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCode ()Ljava/lang/Integer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { - public static final fun mcpStreamableHttp-BZiP2OM (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpStreamableHttp-BZiP2OM$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpStreamableHttpTransport-5_5nbZA (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; - public static synthetic fun mcpStreamableHttpTransport-5_5nbZA$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { - public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensionsKt { - public static final fun mcpWebSocket (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun mcpWebSocket$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun mcpWebSocketTransport (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { - public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function0;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; - public fun equals (Ljava/lang/Object;)Z - public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2; - public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; - public fun equals (Ljava/lang/Object;)Z - public final fun getReadHandler ()Lkotlin/jvm/functions/Function2; - public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool; - public final fun component2 ()Lkotlin/jvm/functions/Function2; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; - public fun equals (Ljava/lang/Object;)Z - public final fun getHandler ()Lkotlin/jvm/functions/Function2; - public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { - public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V - public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V - public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addPrompts (Ljava/util/List;)V - public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addResources (Ljava/util/List;)V - public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V - public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public final fun addTools (Ljava/util/List;)V - protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V - public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; - public final fun getPrompts ()Ljava/util/Map; - public final fun getResources ()Ljava/util/Map; - public final fun getTools ()Ljava/util/Map; - public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public fun onClose ()V - public final fun onClose (Lkotlin/jvm/functions/Function0;)V - public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V - public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun removePrompt (Ljava/lang/String;)Z - public final fun removePrompts (Ljava/util/List;)I - public final fun removeResource (Ljava/lang/String;)Z - public final fun removeResources (Ljava/util/List;)I - public final fun removeTool (Ljava/lang/String;)Z - public final fun removeTools (Ljava/util/List;)I - public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getSessionId ()Ljava/lang/String; - public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { - public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V - public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { - public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V - public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V - public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;)V - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { - public fun (Lio/ktor/server/websocket/WebSocketServerSession;)V - public synthetic fun getSession ()Lio/ktor/websocket/WebSocketSession; +public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { + public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; } public abstract class io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport : io/modelcontextprotocol/kotlin/sdk/shared/Transport { @@ -3478,6 +3243,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { } public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { + public static final field IMPLEMENTATION_NAME Ljava/lang/String; public static final fun getDEFAULT_REQUEST_TIMEOUT ()J public static final fun getMcpJson ()Lkotlinx/serialization/json/Json; } @@ -3498,6 +3264,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer { public final fun readMessage ()Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferKt { + public static final fun serializeMessage (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;)Ljava/lang/String; +} + public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra { public fun ()V } @@ -3534,3 +3304,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTran public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransportKt { + public static final field MCP_SUBPROTOCOL Ljava/lang/String; +} + diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts new file mode 100644 index 000000000..ee8c3477f --- /dev/null +++ b/kotlin-sdk-core/build.gradle.kts @@ -0,0 +1,45 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + iosArm64(); iosX64(); iosSimulatorArm64() + watchosX64(); watchosArm64(); watchosSimulatorArm64() + tvosX64(); tvosArm64(); tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.io.core) + api(libs.ktor.server.websockets) + api(libs.kotlinx.collections.immutable) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotest.assertions.json) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt similarity index 65% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt index 49436f934..d1cbe781f 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt @@ -2,4 +2,4 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher -internal expect val IODispatcher: CoroutineDispatcher \ No newline at end of file +public expect val IODispatcher: CoroutineDispatcher diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 45c07b647..b5f157519 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -36,6 +36,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.serializer +import kotlin.collections.get import kotlin.reflect.KType import kotlin.reflect.typeOf import kotlin.time.Duration @@ -43,7 +44,7 @@ import kotlin.time.Duration.Companion.milliseconds private val LOGGER = KotlinLogging.logger { } -internal const val IMPLEMENTATION_NAME = "mcp-ktor" +public const val IMPLEMENTATION_NAME: String = "mcp-ktor" /** * Callback for progress notifications. @@ -438,7 +439,6 @@ public abstract class Protocol( transport.send(serialized) result.completeExceptionally(reason) - Unit } val timeout = options?.timeout ?: DEFAULT_REQUEST_TIMEOUT diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt similarity index 88% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index ddffaa99a..c235e65b5 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -1,11 +1,9 @@ package io.modelcontextprotocol.kotlin.sdk.shared -import io.ktor.utils.io.core.writeFully import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import kotlinx.io.Buffer import kotlinx.io.indexOf import kotlinx.io.readString -import kotlinx.serialization.encodeToString /** * Buffers a continuous stdio stream into discrete JSON-RPC messages. @@ -14,7 +12,7 @@ public class ReadBuffer { private val buffer: Buffer = Buffer() public fun append(chunk: ByteArray) { - buffer.writeFully(chunk) + buffer.write(chunk) } public fun readMessage(): JSONRPCMessage? { @@ -50,7 +48,7 @@ internal fun deserializeMessage(line: String): JSONRPCMessage { return McpJson.decodeFromString(line) } -internal fun serializeMessage(message: JSONRPCMessage): String { +public fun serializeMessage(message: JSONRPCMessage): String { return McpJson.encodeToString(message) + "\n" } diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt similarity index 97% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index ff601ed39..29e7b8665 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -12,11 +12,10 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.job import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi -internal const val MCP_SUBPROTOCOL = "mcp" +public const val MCP_SUBPROTOCOL: String = "mcp" /** * Abstract class representing a WebSocket transport for the Model Context Protocol (MCP). diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 57a5f803f..8918f5c34 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -125,7 +125,7 @@ public sealed interface Request { * * @return The JSON-RPC request representation. */ -internal fun Request.toJSON(): JSONRPCRequest { +public fun Request.toJSON(): JSONRPCRequest { val fullJson = McpJson.encodeToJsonElement(this).jsonObject val params = JsonObject(fullJson.filterKeys { it != "method" }) return JSONRPCRequest( @@ -168,7 +168,7 @@ public sealed interface Notification { * * @return The JSON-RPC notification representation. */ -internal fun Notification.toJSON(): JSONRPCNotification { +public fun Notification.toJSON(): JSONRPCNotification { return JSONRPCNotification( method = method.value, params = McpJson.encodeToJsonElement(params), diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt similarity index 99% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt rename to kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt index 67b6b9f37..fad5f14a0 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt @@ -279,7 +279,7 @@ internal object JSONRPCMessagePolymorphicSerializer : } } -internal val EmptyJsonObject = JsonObject(emptyMap()) +public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) public class RequestIdSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("RequestId") diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/AudioContentSerializationTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/CallToolResultUtilsTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/ToolSerializationTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/TypesUtilTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt similarity index 97% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt rename to kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt index 6890aef64..8e6f4f659 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt @@ -4,7 +4,6 @@ import io.ktor.utils.io.charsets.Charsets import io.ktor.utils.io.core.toByteArray import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals diff --git a/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt similarity index 61% rename from src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt rename to kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt index 1ecad7715..27fee2f73 100644 --- a/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt +++ b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.Default \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.Default diff --git a/src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt similarity index 63% rename from src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt rename to kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt index 2c44eec8e..d00c1ab55 100644 --- a/src/jvmMain/java/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt +++ b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.IO \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.IO diff --git a/src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt similarity index 67% rename from src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt rename to kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt index 17f6555d1..470ae23de 100644 --- a/src/iosMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.ios.kt +++ b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt @@ -4,5 +4,5 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.IO \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.IO diff --git a/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt similarity index 61% rename from src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt rename to kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt index 1ecad7715..27fee2f73 100644 --- a/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt +++ b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt @@ -3,5 +3,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -internal actual val IODispatcher: CoroutineDispatcher - get() = Dispatchers.Default \ No newline at end of file +public actual val IODispatcher: CoroutineDispatcher + get() = Dispatchers.Default diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api new file mode 100644 index 000000000..cd64a75ed --- /dev/null +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -0,0 +1,133 @@ +public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { + public static final field LIB_VERSION Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { + public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V + public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function0;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2; + public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource; + public fun equals (Ljava/lang/Object;)Z + public final fun getReadHandler ()Lkotlin/jvm/functions/Function2; + public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool; + public final fun component2 ()Lkotlin/jvm/functions/Function2; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool; + public fun equals (Ljava/lang/Object;)Z + public final fun getHandler ()Lkotlin/jvm/functions/Function2; + public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { + public fun (Lio/modelcontextprotocol/kotlin/sdk/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;)V + public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V + public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addPrompts (Ljava/util/List;)V + public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addResources (Ljava/util/List;)V + public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V + public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Input;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/Tool$Output;Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public final fun addTools (Ljava/util/List;)V + protected fun assertCapabilityForMethod (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V + public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun getClientCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; + public final fun getClientVersion ()Lio/modelcontextprotocol/kotlin/sdk/Implementation; + public final fun getPrompts ()Ljava/util/Map; + public final fun getResources ()Ljava/util/Map; + public final fun getTools ()Ljava/util/Map; + public final fun listRoots (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun onClose ()V + public final fun onClose (Lkotlin/jvm/functions/Function0;)V + public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V + public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun removePrompt (Ljava/lang/String;)Z + public final fun removePrompts (Ljava/util/List;)I + public final fun removeResource (Ljava/lang/String;)Z + public final fun removeResources (Ljava/util/List;)I + public final fun removeTool (Ljava/lang/String;)Z + public final fun removeTools (Ljava/util/List;)I + public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Z)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getSessionId ()Ljava/lang/String; + public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { + public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V + public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V + public static final fun mcpWebSocket (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun mcpWebSocket$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V + public static final fun mcpWebSocketTransport (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;)V + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun mcpWebSocketTransport$default (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport { + public fun (Lio/ktor/server/websocket/WebSocketServerSession;)V + public synthetic fun getSession ()Lio/ktor/websocket/WebSocketSession; +} + diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts new file mode 100644 index 000000000..d107debfb --- /dev/null +++ b/kotlin-sdk-server/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") + alias(libs.plugins.kotlinx.binary.compatibility.validator) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(libs.ktor.server.cio) + api(libs.ktor.server.sse) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.slf4j.simple) + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt similarity index 98% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index f3683497d..056c78541 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -69,7 +69,7 @@ private suspend fun ServerSSESession.mcpSseEndpoint( transports: ConcurrentMap, block: () -> Server, ) { - val transport = mcpSseTransport(postEndpoint, transports) + val transport = mcpSseTransport(postEndpoint, transports) val server = block() diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt similarity index 98% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index c5b59702f..83defa2e8 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -12,7 +12,6 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson import kotlinx.coroutines.job -import kotlinx.serialization.encodeToString import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.uuid.ExperimentalUuidApi diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt similarity index 100% rename from src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt rename to kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt diff --git a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt similarity index 54% rename from src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt rename to kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index 2bbff14ae..be1e64e8a 100644 --- a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -8,19 +8,20 @@ import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.toJSON import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test import java.io.ByteArrayOutputStream import java.io.PipedInputStream import java.io.PipedOutputStream +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class StdioServerTransportTest { private lateinit var input: PipedInputStream @@ -32,7 +33,7 @@ class StdioServerTransportTest { private lateinit var bufferedInput: Source private lateinit var printOutput: Sink - @BeforeEach + @BeforeTest fun setUp() { // Simulate an input stream that we can push data into using inputWriter. input = PipedInputStream() @@ -75,70 +76,66 @@ class StdioServerTransportTest { } @Test - fun `should not read until started`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } + fun `should not read until started`() = runTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onError { error -> + throw error + } - var didRead = false - val readMessage = CompletableDeferred() + var didRead = false + val readMessage = CompletableDeferred() - server.onMessage { message -> - didRead = true - readMessage.complete(message) - } + server.onMessage { message -> + didRead = true + readMessage.complete(message) + } - val message = PingRequest().toJSON() + val message = PingRequest().toJSON() - // Push a message before the server started - val serialized = serializeMessage(message) - inputWriter.write(serialized) - inputWriter.flush() + // Push a message before the server started + val serialized = serializeMessage(message) + inputWriter.write(serialized) + inputWriter.flush() - assertFalse(didRead, "Should not have read message before start") + assertFalse(didRead, "Should not have read message before start") - server.start() - val received = readMessage.await() - assertEquals(message, received) - } + server.start() + val received = readMessage.await() + assertEquals(message, received) } @Test - fun `should read multiple messages`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } + fun `should read multiple messages`() = runTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onError { error -> + throw error + } - val messages = listOf( - PingRequest().toJSON(), - InitializedNotification().toJSON(), - ) + val messages = listOf( + PingRequest().toJSON(), + InitializedNotification().toJSON(), + ) - val readMessages = mutableListOf() - val finished = CompletableDeferred() + val readMessages = mutableListOf() + val finished = CompletableDeferred() - server.onMessage { message -> - readMessages.add(message) - if (message == messages[1]) { - finished.complete(Unit) - } + server.onMessage { message -> + readMessages.add(message) + if (message == messages[1]) { + finished.complete(Unit) } + } - // Push both messages before starting the server - for (m in messages) { - inputWriter.write(serializeMessage(m)) - } - inputWriter.flush() + // Push both messages before starting the server + for (m in messages) { + inputWriter.write(serializeMessage(m)) + } + inputWriter.flush() - server.start() - finished.await() + server.start() + finished.await() - assertEquals(messages, readMessages) - } + assertEquals(messages, readMessages) } } diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts new file mode 100644 index 000000000..7a5cabbda --- /dev/null +++ b/kotlin-sdk-test/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("mcp.multiplatform") +} + +kotlin { + sourceSets { + commonTest { + dependencies { + implementation(project(":kotlin-sdk")) + implementation(kotlin("test")) + implementation(libs.ktor.server.test.host) + implementation(libs.kotlinx.coroutines.test) + } + } + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt similarity index 93% rename from src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 1aadbc740..26330ce19 100644 --- a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1,7 +1,5 @@ package io.modelcontextprotocol.kotlin.sdk.client -import io.mockk.coEvery -import io.mockk.spyk import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult @@ -9,7 +7,6 @@ import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.InitializeResult import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage @@ -34,6 +31,7 @@ import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel @@ -45,12 +43,11 @@ import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertInstanceOf -import org.junit.jupiter.api.assertThrows import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertIs import kotlin.test.assertTrue import kotlin.test.fail @@ -200,28 +197,13 @@ class ClientTest { @Test fun `should reject due to non cancellation exception`() = runTest { var closed = false - val clientTransport = object : AbstractTransport() { + val failingTransport = object : AbstractTransport() { override suspend fun start() {} override suspend fun send(message: JSONRPCMessage) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) - - val result = InitializeResult( - protocolVersion = LATEST_PROTOCOL_VERSION, - capabilities = ServerCapabilities(), - serverInfo = Implementation( - name = "test", - version = "1.0" - ) - ) - - val response = JSONRPCResponse( - id = message.id, - result = result - ) - - _onMessage.invoke(response) + throw IllegalStateException("Test error") } override suspend fun close() { @@ -229,22 +211,16 @@ class ClientTest { } } - val mockClient = spyk( - Client( - clientInfo = Implementation( - name = "test client", - version = "1.0" - ), - options = ClientOptions() - ) + val client = Client( + clientInfo = Implementation( + name = "test client", + version = "1.0" + ), + options = ClientOptions() ) - coEvery{ - mockClient.request(any()) - } throws IllegalStateException("Test error") - val exception = assertFailsWith { - mockClient.connect(clientTransport) + client.connect(failingTransport) } assertEquals("Error connecting to transport: Test error", exception.message) @@ -265,7 +241,7 @@ class ClientTest { serverOptions ) - server.setRequestHandler(Method.Defined.Initialize) { request, _ -> + server.setRequestHandler(Method.Defined.Initialize) { _, _ -> InitializeResult( protocolVersion = LATEST_PROTOCOL_VERSION, capabilities = ServerCapabilities( @@ -276,11 +252,11 @@ class ClientTest { ) } - server.setRequestHandler(Method.Defined.ResourcesList) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> ListResourcesResult(resources = emptyList(), nextCursor = null) } - server.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> ListToolsResult(tools = emptyList(), nextCursor = null) } @@ -431,13 +407,18 @@ class ClientTest { val server = Server( Implementation(name = "test server", version = "1.0"), ServerOptions( - capabilities = ServerCapabilities(resources = ServerCapabilities.Resources(listChanged = null, subscribe = null)) + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null + ) + ) ) ) val def = CompletableDeferred() val defTimeOut = CompletableDeferred() - server.setRequestHandler(Method.Defined.ResourcesList) { _, extra -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> // Simulate delay def.complete(Unit) try { @@ -446,8 +427,8 @@ class ClientTest { defTimeOut.complete(Unit) throw e } - fail("Shouldn't have been called") ListResourcesResult(resources = emptyList()) + fail("Shouldn't have been called") } val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() @@ -488,11 +469,16 @@ class ClientTest { val server = Server( Implementation(name = "test server", version = "1.0"), ServerOptions( - capabilities = ServerCapabilities(resources = ServerCapabilities.Resources(listChanged = null, subscribe = null)) + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources( + listChanged = null, + subscribe = null + ) + ) ) ) - server.setRequestHandler(Method.Defined.ResourcesList) { _, extra -> + server.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> // Simulate a delayed response // Wait ~100ms unless canceled try { @@ -546,7 +532,7 @@ class ClientTest { ) ) - client.setRequestHandler(Method.Defined.SamplingCreateMessage) { request, _ -> + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> CreateMessageResult( model = "test-model", role = Role.assistant, @@ -573,7 +559,7 @@ class ClientTest { serverOptions ) - server.setRequestHandler(Method.Defined.Initialize) { request, _ -> + server.setRequestHandler(Method.Defined.Initialize) { _, _ -> InitializeResult( protocolVersion = LATEST_PROTOCOL_VERSION, capabilities = ServerCapabilities( @@ -596,7 +582,7 @@ class ClientTest { ), nextCursor = null ) - server.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + server.setRequestHandler(Method.Defined.ToolsList) { _, _ -> serverListToolsResult } @@ -631,7 +617,7 @@ class ClientTest { ) clientTransport.send(request) - assertInstanceOf(receivedMessage) + assertIs(receivedMessage) val receivedAsResponse = receivedMessage as JSONRPCResponse assertEquals(request.id, receivedAsResponse.id) assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc) @@ -688,7 +674,7 @@ class ClientTest { ) // Verify that adding a root throws an exception - val exception = assertThrows { + val exception = assertFailsWith { client.addRoot(uri = "file:///test-root1", name = "testRoot1") } assertEquals("Client does not support roots capability.", exception.message) @@ -704,7 +690,7 @@ class ClientTest { ) // Verify that removing a root throws an exception - val exception = assertThrows { + val exception = assertFailsWith { client.removeRoot(uri = "file:///test-root1") } assertEquals("Client does not support roots capability.", exception.message) @@ -828,7 +814,7 @@ class ClientTest { ).joinAll() // Verify that creating an elicitation throws an exception - val exception = assertThrows { + val exception = assertFailsWith { server.createElicitation( message = "Please provide your GitHub username", requestedSchema = CreateElicitationRequest.RequestedSchema( diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt similarity index 52% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index 1c63ff65f..23ddadf18 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -1,46 +1,54 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.ktor.client.HttpClient -import io.ktor.client.plugins.sse.SSE import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer -import io.ktor.server.routing.post -import io.ktor.server.routing.route import io.ktor.server.routing.routing -import io.ktor.server.sse.sse -import io.ktor.util.collections.ConcurrentMap -import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport -import io.modelcontextprotocol.kotlin.sdk.server.mcpPostEndpoint -import io.modelcontextprotocol.kotlin.sdk.server.mcpSseTransport +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test +import io.ktor.client.plugins.sse.SSE as ClientSSE +import io.ktor.server.sse.SSE as ServerSSE class SseTransportTest : BaseTransportTest() { private suspend fun EmbeddedServer<*, *>.actualPort() = engine.resolvedConnectors().first().port + private lateinit var mcpServer: Server + + @BeforeTest + fun setUp() { + mcpServer = Server( + serverInfo = Implementation( + name = "test-server", + version = "1.0" + ), + options = ServerOptions(ServerCapabilities()) + ) + } + @Test fun `should start then close cleanly`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - sse { - mcpSseTransport("", transports).start() - } - - post { - mcpPostEndpoint(transports) - } + mcp { mcpServer } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" @@ -55,32 +63,33 @@ class SseTransportTest : BaseTransportTest() { } } + @Ignore @Test fun `should read messages`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - sse { - mcpSseTransport("", transports).apply { - onMessage { - send(it) - } - - start() - } - } - - post { - mcpPostEndpoint(transports) - } + mcp { mcpServer } +// sse { +// mcpSseTransport("", transports).apply { +// onMessage { +// send(it) +// } +// +// start() +// } +// } +// +// post { +// mcpPostEndpoint(transports) +// } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" @@ -95,34 +104,35 @@ class SseTransportTest : BaseTransportTest() { } } + @Ignore @Test fun `test sse path not root path`() = runTest { val server = embeddedServer(CIO, port = 0) { - install(io.ktor.server.sse.SSE) - val transports = ConcurrentMap() + install(ServerSSE) routing { - route("/sse") { - sse { - mcpSseTransport("", transports).apply { - onMessage { - send(it) - } - - start() - } - } - - post { - mcpPostEndpoint(transports) - } - } + mcp("/sse") { mcpServer } +// route("/sse") { +// sse { +// mcpSseTransport("", transports).apply { +// onMessage { +// send(it) +// } +// +// start() +// } +// } +// +// post { +// mcpPostEndpoint(transports) +// } +// } } }.startSuspend(wait = false) val actualPort = server.actualPort() val client = HttpClient { - install(SSE) + install(ClientSSE) }.mcpSseTransport { url { host = "localhost" diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt similarity index 95% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt index 57423b501..a4bf7a3f2 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt @@ -4,6 +4,7 @@ import io.ktor.server.testing.testApplication import io.ktor.server.websocket.WebSockets import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocketTransport +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import kotlinx.coroutines.CompletableDeferred import kotlin.test.Ignore import kotlin.test.Test diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt similarity index 95% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt index 6ab3feafb..24e5261e1 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/InMemoryTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/InMemoryTransportTest.kt @@ -1,8 +1,8 @@ -package io.modelcontextprotocol.kotlin.sdk.client +package io.modelcontextprotocol.kotlin.sdk.integration -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.toJSON import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt similarity index 100% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt similarity index 94% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt index 2c82ff72d..6c69a135f 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/BaseTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt @@ -1,9 +1,8 @@ -package io.modelcontextprotocol.kotlin.sdk.client +package io.modelcontextprotocol.kotlin.sdk.shared import io.modelcontextprotocol.kotlin.sdk.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.PingRequest -import io.modelcontextprotocol.kotlin.sdk.shared.Transport import io.modelcontextprotocol.kotlin.sdk.toJSON import kotlinx.coroutines.CompletableDeferred import kotlin.test.assertEquals diff --git a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt similarity index 93% rename from src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt rename to kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt index cd17bfc7d..86dc706ab 100644 --- a/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/InMemoryTransport.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt @@ -1,6 +1,6 @@ -package io.modelcontextprotocol.kotlin.sdk +package io.modelcontextprotocol.kotlin.sdk.shared -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage /** * In-memory transport for creating clients and servers that talk to each other within the same process. @@ -44,4 +44,4 @@ class InMemoryTransport : AbstractTransport() { other._onMessage.invoke(message) } -} +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt similarity index 94% rename from src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index d9f226a93..a8007a287 100644 --- a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import kotlinx.coroutines.test.runTest import kotlinx.io.asSink import kotlinx.io.asSource diff --git a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt similarity index 96% rename from src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt index 8ba455dec..bc6ae014f 100644 --- a/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerTest.kt @@ -3,7 +3,6 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.modelcontextprotocol.kotlin.sdk.CallToolResult import io.modelcontextprotocol.kotlin.sdk.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.Method import io.modelcontextprotocol.kotlin.sdk.Prompt import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification @@ -15,6 +14,7 @@ import io.modelcontextprotocol.kotlin.sdk.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.Tool import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -40,7 +40,7 @@ class ServerTest { ) // Add a tool - server.addTool("test-tool", "Test Tool", Tool.Input()) { request -> + server.addTool("test-tool", "Test Tool", Tool.Input()) { CallToolResult(listOf(TextContent("Test result"))) } @@ -131,10 +131,10 @@ class ServerTest { ) // Add tools - server.addTool("test-tool-1", "Test Tool 1") { request -> + server.addTool("test-tool-1", "Test Tool 1") { CallToolResult(listOf(TextContent("Test result 1"))) } - server.addTool("test-tool-2", "Test Tool 2") { request -> + server.addTool("test-tool-2", "Test Tool 2") { CallToolResult(listOf(TextContent("Test result 2"))) } @@ -170,7 +170,7 @@ class ServerTest { // Add a prompt val testPrompt = Prompt("test-prompt", "Test Prompt", null) - server.addPrompt(testPrompt) { request -> + server.addPrompt(testPrompt) { GetPromptResult( description = "Test prompt description", messages = listOf() @@ -210,13 +210,13 @@ class ServerTest { // Add prompts val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null) val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null) - server.addPrompt(testPrompt1) { request -> + server.addPrompt(testPrompt1) { GetPromptResult( description = "Test prompt description 1", messages = listOf() ) } - server.addPrompt(testPrompt2) { request -> + server.addPrompt(testPrompt2) { GetPromptResult( description = "Test prompt description 2", messages = listOf() @@ -260,7 +260,7 @@ class ServerTest { name = "Test Resource", description = "A test resource", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -310,7 +310,7 @@ class ServerTest { name = "Test Resource 1", description = "A test resource 1", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -326,7 +326,7 @@ class ServerTest { name = "Test Resource 2", description = "A test resource 2", mimeType = "text/plain" - ) { request -> + ) { ReadResourceResult( contents = listOf( TextResourceContents( @@ -446,7 +446,10 @@ class ServerTest { // Verify the result assertFalse(result, "Removing non-existent resource should return false") - assertFalse(resourceListChangedNotificationReceived, "No notification should be sent when resource doesn't exist") + assertFalse( + resourceListChangedNotificationReceived, + "No notification should be sent when resource doesn't exist" + ) } @Test diff --git a/kotlin-sdk/build.gradle.kts b/kotlin-sdk/build.gradle.kts new file mode 100644 index 000000000..37680171d --- /dev/null +++ b/kotlin-sdk/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + id("mcp.jreleaser") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":kotlin-sdk-core")) + api(project(":kotlin-sdk-client")) + api(project(":kotlin-sdk-server")) + } + } + } +} diff --git a/samples/kotlin-mcp-client/.gitignore b/samples/kotlin-mcp-client/.gitignore deleted file mode 100644 index 108d5673c..000000000 --- a/samples/kotlin-mcp-client/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/kotlin-mcp-client/build.gradle.kts b/samples/kotlin-mcp-client/build.gradle.kts index 6c7f1bbe7..582540022 100644 --- a/samples/kotlin-mcp-client/build.gradle.kts +++ b/samples/kotlin-mcp-client/build.gradle.kts @@ -1,25 +1,20 @@ plugins { - kotlin("jvm") version "2.1.10" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.shadow) application - id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.modelcontextprotocol.sample.client.MainKt") } - group = "org.example" version = "0.1.0" -val mcpVersion = "0.6.0" -val slf4jVersion = "2.0.9" -val anthropicVersion = "0.8.0" - dependencies { - implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") - implementation("org.slf4j:slf4j-nop:$slf4jVersion") - implementation("com.anthropic:anthropic-java:$anthropicVersion") + implementation(libs.mcp.kotlin) + implementation(libs.slf4j.simple) + implementation(libs.anthropic.java) } tasks.test { diff --git a/samples/kotlin-mcp-client/gradle.properties b/samples/kotlin-mcp-client/gradle.properties deleted file mode 100644 index 7fc6f1ff2..000000000 --- a/samples/kotlin-mcp-client/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.code.style=official diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c742b298b441bfb90dbc124400a3751b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43705 zcma&Obx`DOvL%eWOXJW;V64viP??$)@wHcsJ68)>bJS6*&iHnskXE8MjvIPVl|FrmV}Npeql07fCw6`pw`0s zGauF(<*@v{3t!qoUU*=j)6;|-(yg@jvDx&fV^trtZt27?4Tkn729qrItVh@PMwG5$ z+oXHSPM??iHZ!cVP~gYact-CwV`}~Q+R}PPNRy+T-geK+>fHrijpllon_F4N{@b-} z1M0=a!VbVmJM8Xk@NRv)m&aRYN}FSJ{LS;}2ArQ5baSjfy40l@T5)1r-^0fAU6f_} zzScst%$Nd-^ElV~H0TetQhMc%S{}Q4lssln=|;LG?Ulo}*mhg8YvBAUY7YFdXs~vv zv~{duzVw%C#GxkBwX=TYp1Dh*Uaum2?RmsvPaLlzO^fIJ`L?&OV?Y&kKj~^kWC`Ly zfL-}J^4a0Ojuz9O{jUbIS;^JatJ5+YNNHe}6nG9Yd6P-lJiK2ms)A^xq^H2fKrTF) zp!6=`Ece~57>^9(RA4OB9;f1FAhV%zVss%#rDq$9ZW3N2cXC7dMz;|UcRFecBm`DA z1pCO!#6zKp#@mx{2>Qcme8y$Qg_gnA%(`Vtg3ccwgb~D(&@y8#Jg8nNYW*-P{_M#E zZ|wCsQoO1(iIKd-2B9xzI}?l#Q@G5d$m1Lfh0q;iS5FDQ&9_2X-H)VDKA*fa{b(sV zL--krNCXibi1+*C2;4qVjb0KWUVGjjRT{A}Q*!cFmj0tRip2ra>WYJ>ZK4C|V~RYs z6;~+*)5F^x^aQqk9tjh)L;DOLlD8j+0<>kHc8MN|68PxQV`tJFbgxSfq-}b(_h`luA0&;Vk<@51i0 z_cu6{_*=vlvYbKjDawLw+t^H?OV00_73Cn3goU5?})UYFuoSX6Xqw;TKcrsc|r# z$sMWYl@cs#SVopO$hpHZ)cdU-+Ui%z&Sa#lMI~zWW@vE%QDh@bTe0&V9nL>4Et9`N zGT8(X{l@A~loDx}BDz`m6@tLv@$mTlVJ;4MGuj!;9Y=%;;_kj#o8n5tX%@M)2I@}u z_{I!^7N1BxW9`g&Z+K#lZ@7_dXdsqp{W9_`)zgZ=sD~%WS5s$`7z#XR!Lfy(4se(m zR@a3twgMs19!-c4jh`PfpJOSU;vShBKD|I0@rmv_x|+ogqslnLLOepJpPMOxhRb*i zGHkwf#?ylQ@k9QJL?!}MY4i7joSzMcEhrDKJH&?2v{-tgCqJe+Y0njl7HYff z{&~M;JUXVR$qM1FPucIEY(IBAuCHC@^~QG6O!dAjzQBxDOR~lJEr4KS9R*idQ^p{D zS#%NQADGbAH~6wAt}(1=Uff-1O#ITe)31zCL$e9~{w)gx)g>?zFE{Bc9nJT6xR!i8 z)l)~9&~zSZTHk{?iQL^MQo$wLi}`B*qnvUy+Y*jEraZMnEhuj`Fu+>b5xD1_Tp z)8|wedv42#3AZUL7x&G@p@&zcUvPkvg=YJS6?1B7ZEXr4b>M+9Gli$gK-Sgh{O@>q7TUg+H zNJj`6q#O@>4HpPJEHvNij`sYW&u%#=215HKNg;C!0#hH1vlO5+dFq9& zS)8{5_%hz?#D#wn&nm@aB?1_|@kpA@{%jYcs{K%$a4W{k@F zPyTav?jb;F(|GaZhm6&M#g|`ckO+|mCtAU)5_(hn&Ogd z9Ku}orOMu@K^Ac>eRh3+0-y^F`j^noa*OkS3p^tLV`TY$F$cPXZJ48!xz1d7%vfA( zUx2+sDPqHfiD-_wJDb38K^LtpN2B0w=$A10z%F9f_P2aDX63w7zDG5CekVQJGy18I zB!tI`6rZr7TK10L(8bpiaQ>S@b7r_u@lh^vakd0e6USWw7W%d_Ob%M!a`K>#I3r-w zo2^+9Y)Sb?P9)x0iA#^ns+Kp{JFF|$09jb6ZS2}_<-=$?^#IUo5;g`4ICZknr!_aJ zd73%QP^e-$%Xjt|28xM}ftD|V@76V_qvNu#?Mt*A-OV{E4_zC4Ymo|(cb+w^`Wv== z>)c%_U0w`d$^`lZQp@midD89ta_qTJW~5lRrIVwjRG_9aRiQGug%f3p@;*%Y@J5uQ|#dJ+P{Omc`d2VR)DXM*=ukjVqIpkb<9gn9{*+&#p)Ek zN=4zwNWHF~=GqcLkd!q0p(S2_K=Q`$whZ}r@ec_cb9hhg9a z6CE=1n8Q;hC?;ujo0numJBSYY6)GTq^=kB~`-qE*h%*V6-ip=c4+Yqs*7C@@b4YAi zuLjsmD!5M7r7d5ZPe>4$;iv|zq=9=;B$lI|xuAJwi~j~^Wuv!Qj2iEPWjh9Z&#+G>lZQpZ@(xfBrhc{rlLwOC;optJZDj4Xfu3$u6rt_=YY0~lxoy~fq=*L_&RmD7dZWBUmY&12S;(Ui^y zBpHR0?Gk|`U&CooNm_(kkO~pK+cC%uVh^cnNn)MZjF@l{_bvn4`Jc}8QwC5_)k$zs zM2qW1Zda%bIgY^3NcfL)9ug`05r5c%8ck)J6{fluBQhVE>h+IA&Kb}~$55m-^c1S3 zJMXGlOk+01qTQUFlh5Jc3xq|7McY$nCs$5=`8Y;|il#Ypb{O9}GJZD8!kYh{TKqs@ z-mQn1K4q$yGeyMcryHQgD6Ra<6^5V(>6_qg`3uxbl|T&cJVA*M_+OC#>w(xL`RoPQ zf1ZCI3G%;o-x>RzO!mc}K!XX{1rih0$~9XeczHgHdPfL}4IPi~5EV#ZcT9 zdgkB3+NPbybS-d;{8%bZW^U+x@Ak+uw;a5JrZH!WbNvl!b~r4*vs#he^bqz`W93PkZna2oYO9dBrKh2QCWt{dGOw)%Su%1bIjtp4dKjZ^ zWfhb$M0MQiDa4)9rkip9DaH0_tv=XxNm>6MKeWv>`KNk@QVkp$Lhq_~>M6S$oliq2 zU6i7bK;TY)m>-}X7hDTie>cc$J|`*}t=MAMfWIALRh2=O{L57{#fA_9LMnrV(HrN6 zG0K_P5^#$eKt{J|#l~U0WN_3)p^LLY(XEqes0OvI?3)GTNY&S13X+9`6PLVFRf8K) z9x@c|2T72+-KOm|kZ@j4EDDec>03FdgQlJ!&FbUQQH+nU^=U3Jyrgu97&#-W4C*;_ z(WacjhBDp@&Yon<9(BWPb;Q?Kc0gR5ZH~aRNkPAWbDY!FiYVSu!~Ss^9067|JCrZk z-{Rn2KEBR|Wti_iy) zXnh2wiU5Yz2L!W{{_#LwNWXeNPHkF=jjXmHC@n*oiz zIoM~Wvo^T@@t!QQW?Ujql-GBOlnB|HjN@x~K8z)c(X}%%5Zcux09vC8=@tvgY>czq z3D(U&FiETaN9aP}FDP3ZSIXIffq>M3{~eTB{uauL07oYiM=~K(XA{SN!rJLyXeC+Y zOdeebgHOc2aCIgC=8>-Q>zfuXV*=a&gp{l#E@K|{qft@YtO>xaF>O7sZz%8);e86? z+jJlFB{0fu6%8ew^_<+v>>%6eB8|t*_v7gb{x=vLLQYJKo;p7^o9!9A1)fZZ8i#ZU z<|E?bZakjkEV8xGi?n+{Xh3EgFKdM^;4D;5fHmc04PI>6oU>>WuLy6jgpPhf8$K4M zjJo*MbN0rZbZ!5DmoC^@hbqXiP^1l7I5;Wtp2i9Jkh+KtDJoXP0O8qmN;Sp(+%upX zAxXs*qlr(ck+-QG_mMx?hQNXVV~LT{$Q$ShX+&x?Q7v z@8t|UDylH6@RZ?WsMVd3B0z5zf50BP6U<&X_}+y3uJ0c5OD}+J&2T8}A%2Hu#Nt_4 zoOoTI$A!hQ<2pk5wfZDv+7Z{yo+Etqry=$!*pvYyS+kA4xnJ~3b~TBmA8Qd){w_bE zqDaLIjnU8m$wG#&T!}{e0qmHHipA{$j`%KN{&#_Kmjd&#X-hQN+ju$5Ms$iHj4r?) z&5m8tI}L$ih&95AjQ9EDfPKSmMj-@j?Q+h~C3<|Lg2zVtfKz=ft{YaQ1i6Om&EMll zzov%MsjSg=u^%EfnO+W}@)O6u0LwoX709h3Cxdc2Rwgjd%LLTChQvHZ+y<1q6kbJXj3_pq1&MBE{8 zd;aFotyW>4WHB{JSD8Z9M@jBitC1RF;!B8;Rf-B4nOiVbGlh9w51(8WjL&e{_iXN( zAvuMDIm_>L?rJPxc>S`bqC|W$njA0MKWa?V$u6mN@PLKYqak!bR!b%c^ze(M`ec(x zv500337YCT4gO3+9>oVIJLv$pkf`01S(DUM+4u!HQob|IFHJHm#>eb#eB1X5;bMc| z>QA4Zv}$S?fWg~31?Lr(C>MKhZg>gplRm`2WZ--iw%&&YlneQYY|PXl;_4*>vkp;I z$VYTZq|B*(3(y17#@ud@o)XUZPYN*rStQg5U1Sm2gM}7hf_G<>*T%6ebK*tF(kbJc zNPH4*xMnJNgw!ff{YXrhL&V$6`ylY={qT_xg9znQWw9>PlG~IbhnpsG_94Kk_(V-o&v7#F znra%uD-}KOX2dkak**hJnZZQyp#ERyyV^lNe!Qrg=VHiyr7*%j#PMvZMuYNE8o;JM zGrnDWmGGy)(UX{rLzJ*QEBd(VwMBXnJ@>*F8eOFy|FK*Vi0tYDw;#E zu#6eS;%Nm2KY+7dHGT3m{TM7sl=z8|V0e!DzEkY-RG8vTWDdSQFE|?+&FYA146@|y zV(JP>LWL;TSL6rao@W5fWqM1-xr$gRci#RQV2DX-x4@`w{uEUgoH4G|`J%H!N?*Qn zy~rjzuf(E7E!A9R2bSF|{{U(zO+;e29K_dGmC^p7MCP!=Bzq@}&AdF5=rtCwka zTT1A?5o}i*sXCsRXBt)`?nOL$zxuP3i*rm3Gmbmr6}9HCLvL*45d|(zP;q&(v%}S5yBmRVdYQQ24zh z6qL2<2>StU$_Ft29IyF!6=!@;tW=o8vNzVy*hh}XhZhUbxa&;9~woye<_YmkUZ)S?PW{7t; zmr%({tBlRLx=ffLd60`e{PQR3NUniWN2W^~7Sy~MPJ>A#!6PLnlw7O0(`=PgA}JLZ ztqhiNcKvobCcBel2 z-N82?4-()eGOisnWcQ9Wp23|ybG?*g!2j#>m3~0__IX1o%dG4b;VF@^B+mRgKx|ij zWr5G4jiRy}5n*(qu!W`y54Y*t8g`$YrjSunUmOsqykYB4-D(*(A~?QpuFWh;)A;5= zPl|=x+-w&H9B7EZGjUMqXT}MkcSfF}bHeRFLttu!vHD{Aq)3HVhvtZY^&-lxYb2%` zDXk7>V#WzPfJs6u{?ZhXpsMdm3kZscOc<^P&e&684Rc1-d=+=VOB)NR;{?0NjTl~D z1MXak$#X4{VNJyD$b;U~Q@;zlGoPc@ny!u7Pe;N2l4;i8Q=8>R3H{>HU(z z%hV2?rSinAg6&wuv1DmXok`5@a3@H0BrqsF~L$pRYHNEXXuRIWom0l zR9hrZpn1LoYc+G@q@VsFyMDNX;>_Vf%4>6$Y@j;KSK#g)TZRmjJxB!_NmUMTY(cAV zmewn7H{z`M3^Z& z2O$pWlDuZHAQJ{xjA}B;fuojAj8WxhO}_9>qd0|p0nBXS6IIRMX|8Qa!YDD{9NYYK z%JZrk2!Ss(Ra@NRW<7U#%8SZdWMFDU@;q<}%F{|6n#Y|?FaBgV$7!@|=NSVoxlJI4G-G(rn}bh|?mKkaBF$-Yr zA;t0r?^5Nz;u6gwxURapQ0$(-su(S+24Ffmx-aP(@8d>GhMtC5x*iEXIKthE*mk$` zOj!Uri|EAb4>03C1xaC#(q_I<;t}U7;1JqISVHz3tO{) zD(Yu@=>I9FDmDtUiWt81;BeaU{_=es^#QI7>uYl@e$$lGeZ~Q(f$?^3>$<<{n`Bn$ zn8bamZlL@6r^RZHV_c5WV7m2(G6X|OI!+04eAnNA5=0v1Z3lxml2#p~Zo57ri;4>;#16sSXXEK#QlH>=b$inEH0`G#<_ zvp;{+iY)BgX$R!`HmB{S&1TrS=V;*5SB$7*&%4rf_2wQS2ed2E%Wtz@y$4ecq4w<) z-?1vz_&u>s?BMrCQG6t9;t&gvYz;@K@$k!Zi=`tgpw*v-#U1Pxy%S9%52`uf$XMv~ zU}7FR5L4F<#9i%$P=t29nX9VBVv)-y7S$ZW;gmMVBvT$BT8d}B#XV^@;wXErJ-W2A zA=JftQRL>vNO(!n4mcd3O27bHYZD!a0kI)6b4hzzL9)l-OqWn)a~{VP;=Uo|D~?AY z#8grAAASNOkFMbRDdlqVUfB;GIS-B-_YXNlT_8~a|LvRMVXf!<^uy;)d$^OR(u)!) zHHH=FqJF-*BXif9uP~`SXlt0pYx|W&7jQnCbjy|8b-i>NWb@!6bx;1L&$v&+!%9BZ z0nN-l`&}xvv|wwxmC-ZmoFT_B#BzgQZxtm|4N+|;+(YW&Jtj^g!)iqPG++Z%x0LmqnF875%Ry&2QcCamx!T@FgE@H zN39P6e#I5y6Yl&K4eUP{^biV`u9{&CiCG#U6xgGRQr)zew;Z%x+ z-gC>y%gvx|dM=OrO`N@P+h2klPtbYvjS!mNnk4yE0+I&YrSRi?F^plh}hIp_+OKd#o7ID;b;%*c0ES z!J))9D&YufGIvNVwT|qsGWiZAwFODugFQ$VsNS%gMi8OJ#i${a4!E3<-4Jj<9SdSY z&xe|D0V1c`dZv+$8>(}RE|zL{E3 z-$5Anhp#7}oO(xm#}tF+W=KE*3(xxKxhBt-uuJP}`_K#0A< zE%rhMg?=b$ot^i@BhE3&)bNBpt1V*O`g?8hhcsV-n#=|9wGCOYt8`^#T&H7{U`yt2 z{l9Xl5CVsE=`)w4A^%PbIR6uG_5Ww9k`=q<@t9Bu662;o{8PTjDBzzbY#tL;$wrpjONqZ{^Ds4oanFm~uyPm#y1Ll3(H57YDWk9TlC zq;kebC!e=`FU&q2ojmz~GeLxaJHfs0#F%c(i+~gg$#$XOHIi@1mA72g2pFEdZSvp}m0zgQb5u2?tSRp#oo!bp`FP}< zaK4iuMpH+Jg{bb7n9N6eR*NZfgL7QiLxI zk6{uKr>xxJ42sR%bJ%m8QgrL|fzo9@?9eQiMW8O`j3teoO_R8cXPe_XiLnlYkE3U4 zN!^F)Z4ZWcA8gekEPLtFqX-Q~)te`LZnJK_pgdKs)Dp50 zdUq)JjlJeELskKg^6KY!sIou-HUnSFRsqG^lsHuRs`Z{f(Ti9eyd3cwu*Kxp?Ws7l z3cN>hGPXTnQK@qBgqz(n*qdJ2wbafELi?b90fK~+#XIkFGU4+HihnWq;{{)1J zv*Txl@GlnIMOjzjA1z%g?GsB2(6Zb-8fooT*8b0KF2CdsIw}~Hir$d3TdVHRx1m3c z4C3#h@1Xi@{t4zge-#B6jo*ChO%s-R%+9%-E|y<*4;L>$766RiygaLR?X%izyqMXA zb|N=Z-0PSFeH;W6aQ3(5VZWVC>5Ibgi&cj*c%_3=o#VyUJv* zM&bjyFOzlaFq;ZW(q?|yyi|_zS%oIuH^T*MZ6NNXBj;&yM3eQ7!CqXY?`7+*+GN47 zNR#%*ZH<^x{(0@hS8l{seisY~IE*)BD+R6^OJX}<2HRzo^fC$n>#yTOAZbk4%=Bei=JEe=o$jm`or0YDw*G?d> z=i$eEL7^}_?UI^9$;1Tn9b>$KOM@NAnvWrcru)r`?LodV%lz55O3y(%FqN;cKgj7t zlJ7BmLTQ*NDX#uelGbCY>k+&H*iSK?x-{w;f5G%%!^e4QT9z<_0vHbXW^MLR} zeC*jezrU|{*_F`I0mi)9=sUj^G03i@MjXx@ePv@(Udt2CCXVOJhRh4yp~fpn>ssHZ z?k(C>2uOMWKW5FVsBo#Nk!oqYbL`?#i~#!{3w^qmCto05uS|hKkT+iPrC-}hU_nbL zO622#mJupB21nChpime}&M1+whF2XM?prT-Vv)|EjWYK(yGYwJLRRMCkx;nMSpu?0 zNwa*{0n+Yg6=SR3-S&;vq=-lRqN`s9~#)OOaIcy3GZ&~l4g@2h| zThAN#=dh{3UN7Xil;nb8@%)wx5t!l z0RSe_yJQ+_y#qEYy$B)m2yDlul^|m9V2Ia$1CKi6Q19~GTbzqk*{y4;ew=_B4V8zw zScDH&QedBl&M*-S+bH}@IZUSkUfleyM45G>CnYY{hx8J9q}ME?Iv%XK`#DJRNmAYt zk2uY?A*uyBA=nlYjkcNPMGi*552=*Q>%l?gDK_XYh*Rya_c)ve{=ps`QYE0n!n!)_$TrGi_}J|>1v}(VE7I~aP-wns#?>Y zu+O7`5kq32zM4mAQpJ50vJsUDT_^s&^k-llQMy9!@wRnxw@~kXV6{;z_wLu3i=F3m z&eVsJmuauY)8(<=pNUM5!!fQ4uA6hBkJoElL1asWNkYE#qaP?a+biwWw~vB48PRS7 zY;DSHvgbIB$)!uJU)xA!yLE*kP0owzYo`v@wfdux#~f!dv#uNc_$SF@Qq9#3q5R zfuQnPPN_(z;#X#nRHTV>TWL_Q%}5N-a=PhkQ^GL+$=QYfoDr2JO-zo#j;mCsZVUQ) zJ96e^OqdLW6b-T@CW@eQg)EgIS9*k`xr$1yDa1NWqQ|gF^2pn#dP}3NjfRYx$pTrb zwGrf8=bQAjXx*8?du*?rlH2x~^pXjiEmj^XwQo{`NMonBN=Q@Y21!H)D( zA~%|VhiTjaRQ%|#Q9d*K4j~JDXOa4wmHb0L)hn*;Eq#*GI}@#ux4}bt+olS(M4$>c z=v8x74V_5~xH$sP+LZCTrMxi)VC%(Dg!2)KvW|Wwj@pwmH6%8zd*x0rUUe$e(Z%AW z@Q{4LL9#(A-9QaY2*+q8Yq2P`pbk3!V3mJkh3uH~uN)+p?67d(r|Vo0CebgR#u}i? zBxa^w%U|7QytN%L9bKaeYhwdg7(z=AoMeP0)M3XZA)NnyqL%D_x-(jXp&tp*`%Qsx z6}=lGr;^m1<{;e=QQZ!FNxvLcvJVGPkJ63at5%*`W?46!6|5FHYV0qhizSMT>Zoe8 zsJ48kb2@=*txGRe;?~KhZgr-ZZ&c0rNV7eK+h$I-UvQ=552@psVrvj#Ys@EU4p8`3 zsNqJu-o=#@9N!Pq`}<=|((u)>^r0k^*%r<{YTMm+mOPL>EoSREuQc-e2~C#ZQ&Xve zZ}OUzmE4{N-7cqhJiUoO_V#(nHX11fdfVZJT>|6CJGX5RQ+Ng$Nq9xs-C86-)~`>p zW--X53J`O~vS{WWjsAuGq{K#8f#2iz` zzSSNIf6;?5sXrHig%X(}0q^Y=eYwvh{TWK-fT>($8Ex>!vo_oGFw#ncr{vmERi^m7lRi%8Imph})ZopLoIWt*eFWSPuBK zu>;Pu2B#+e_W|IZ0_Q9E9(s@0>C*1ft`V{*UWz^K<0Ispxi@4umgGXW!j%7n+NC~* zBDhZ~k6sS44(G}*zg||X#9Weto;u*Ty;fP!+v*7be%cYG|yEOBomch#m8Np!Sw`L)q+T` zmrTMf2^}7j=RPwgpO9@eXfb{Q>GW#{X=+xt`AwTl!=TgYm)aS2x5*`FSUaaP_I{Xi zA#irF%G33Bw>t?^1YqX%czv|JF0+@Pzi%!KJ?z!u$A`Catug*tYPO`_Zho5iip0@! z;`rR0-|Ao!YUO3yaujlSQ+j-@*{m9dHLtve!sY1Xq_T2L3&=8N;n!!Eb8P0Z^p4PL zQDdZ?An2uzbIakOpC|d@=xEA}v-srucnX3Ym{~I#Ghl~JZU(a~Ppo9Gy1oZH&Wh%y zI=KH_s!Lm%lAY&`_KGm*Ht)j*C{-t}Nn71drvS!o|I|g>ZKjE3&Mq0TCs6}W;p>%M zQ(e!h*U~b;rsZ1OPigud>ej=&hRzs@b>>sq6@Yjhnw?M26YLnDH_Wt#*7S$-BtL08 zVyIKBm$}^vp?ILpIJetMkW1VtIc&7P3z0M|{y5gA!Yi5x4}UNz5C0Wdh02!h zNS>923}vrkzl07CX`hi)nj-B?#n?BJ2Vk0zOGsF<~{Fo7OMCN_85daxhk*pO}x_8;-h>}pcw26V6CqR-=x2vRL?GB#y%tYqi;J}kvxaz}*iFO6YO0ha6!fHU9#UI2Nv z_(`F#QU1B+P;E!t#Lb)^KaQYYSewj4L!_w$RH%@IL-M($?DV@lGj%3ZgVdHe^q>n(x zyd5PDpGbvR-&p*eU9$#e5#g3-W_Z@loCSz}f~{94>k6VRG`e5lI=SE0AJ7Z_+=nnE zTuHEW)W|a8{fJS>2TaX zuRoa=LCP~kP)kx4L+OqTjtJOtXiF=y;*eUFgCn^Y@`gtyp?n14PvWF=zhNGGsM{R- z^DsGxtoDtx+g^hZi@E2Y(msb-hm{dWiHdoQvdX88EdM>^DS#f}&kCGpPFDu*KjEpv$FZtLpeT>@)mf|z#ZWEsueeW~hF78Hu zfY9a+Gp?<)s{Poh_qdcSATV2oZJo$OH~K@QzE2kCADZ@xX(; z)0i=kcAi%nvlsYagvUp(z0>3`39iKG9WBDu3z)h38p|hLGdD+Khk394PF3qkX!02H z#rNE`T~P9vwNQ_pNe0toMCRCBHuJUmNUl)KFn6Gu2je+p>{<9^oZ4Gfb!)rLZ3CR3 z-o&b;Bh>51JOt=)$-9+Z!P}c@cKev_4F1ZZGs$I(A{*PoK!6j@ZJrAt zv2LxN#p1z2_0Ox|Q8PVblp9N${kXkpsNVa^tNWhof)8x8&VxywcJz#7&P&d8vvxn` zt75mu>yV=Dl#SuiV!^1BPh5R)`}k@Nr2+s8VGp?%Le>+fa{3&(XYi~{k{ z-u4#CgYIdhp~GxLC+_wT%I*)tm4=w;ErgmAt<5i6c~)7JD2olIaK8by{u-!tZWT#RQddptXRfEZxmfpt|@bs<*uh?Y_< zD>W09Iy4iM@@80&!e^~gj!N`3lZwosC!!ydvJtc0nH==K)v#ta_I}4Tar|;TLb|+) zSF(;=?$Z0?ZFdG6>Qz)6oPM}y1&zx_Mf`A&chb znSERvt9%wdPDBIU(07X+CY74u`J{@SSgesGy~)!Mqr#yV6$=w-dO;C`JDmv=YciTH zvcrN1kVvq|(3O)NNdth>X?ftc`W2X|FGnWV%s})+uV*bw>aoJ#0|$pIqK6K0Lw!@- z3pkPbzd`ljS=H2Bt0NYe)u+%kU%DWwWa>^vKo=lzDZHr>ruL5Ky&#q7davj-_$C6J z>V8D-XJ}0cL$8}Xud{T_{19#W5y}D9HT~$&YY-@=Th219U+#nT{tu=d|B)3K`pL53 zf7`I*|L@^dPEIDJkI3_oA9vsH7n7O}JaR{G~8 zfi$?kmKvu20(l`dV7=0S43VwVKvtF!7njv1Q{Ju#ysj=|dASq&iTE8ZTbd-iiu|2& zmll%Ee1|M?n9pf~?_tdQ<7%JA53!ulo1b^h#s|Su2S4r{TH7BRB3iIOiX5|vc^;5( zKfE1+ah18YA9o1EPT(AhBtve5(%GMbspXV)|1wf5VdvzeYt8GVGt0e*3|ELBhwRaO zE|yMhl;Bm?8Ju3-;DNnxM3Roelg`^!S%e({t)jvYtJCKPqN`LmMg^V&S z$9OIFLF$%Py~{l?#ReyMzpWixvm(n(Y^Am*#>atEZ8#YD&?>NUU=zLxOdSh0m6mL? z_twklB0SjM!3+7U^>-vV=KyQZI-6<(EZiwmNBzGy;Sjc#hQk%D;bay$v#zczt%mFCHL*817X4R;E$~N5(N$1Tv{VZh7d4mhu?HgkE>O+^-C*R@ zR0ima8PsEV*WFvz`NaB+lhX3&LUZcWWJJrG7ZjQrOWD%_jxv=)`cbCk zMgelcftZ%1-p9u!I-Zf_LLz{hcn5NRbxkWby@sj2XmYfAV?iw^0?hM<$&ZDctdC`; zsL|C-7d;w$z2Gt0@hsltNlytoPnK&$>ksr(=>!7}Vk#;)Hp)LuA7(2(Hh(y3LcxRY zim!`~j6`~B+sRBv4 z<#B{@38kH;sLB4eH2+8IPWklhd25r5j2VR}YK$lpZ%7eVF5CBr#~=kUp`i zlb+>Z%i%BJH}5dmfg1>h7U5Q(-F{1d=aHDbMv9TugohX5lq#szPAvPE|HaokMQIi_ zTcTNsO53(oX=hg2w!XA&+qP}nwr$(C)pgG8emS@Mf7m0&*kiA!wPLS`88c=aD$niJ zp?3j%NI^uy|5*MzF`k4hFbsyQZ@wu!*IY+U&&9PwumdmyfL(S0#!2RFfmtzD3m9V7 zsNOw9RQofl-XBfKBF^~~{oUVouka#r3EqRf=SnleD=r1Hm@~`y8U7R)w16fgHvK-6?-TFth)f3WlklbZh+}0 zx*}7oDF4U^1tX4^$qd%987I}g;+o0*$Gsd=J>~Uae~XY6UtbdF)J8TzJXoSrqHVC) zJ@pMgE#;zmuz?N2MIC+{&)tx=7A%$yq-{GAzyz zLzZLf=%2Jqy8wGHD;>^x57VG)sDZxU+EMfe0L{@1DtxrFOp)=zKY1i%HUf~Dro#8} zUw_Mj10K7iDsX}+fThqhb@&GI7PwONx!5z;`yLmB_92z0sBd#HiqTzDvAsTdx+%W{ z2YL#U=9r!@3pNXMp_nvximh+@HV3psUaVa-lOBekVuMf1RUd26~P*|MLouQrb}XM-bEw(UgQxMI6M&l3Nha z{MBcV=tl(b_4}oFdAo}WX$~$Mj-z70FowdoB{TN|h2BdYs?$imcj{IQpEf9q z)rzpttc0?iwopSmEoB&V!1aoZqEWEeO-MKMx(4iK7&Fhc(94c zdy}SOnSCOHX+A8q@i>gB@mQ~Anv|yiUsW!bO9hb&5JqTfDit9X6xDEz*mQEiNu$ay zwqkTV%WLat|Ar+xCOfYs0UQNM`sdsnn*zJr>5T=qOU4#Z(d90!IL76DaHIZeWKyE1 zqwN%9+~lPf2d7)vN2*Q?En?DEPcM+GQwvA<#;X3v=fqsxmjYtLJpc3)A8~*g(KqFx zZEnqqruFDnEagXUM>TC7ngwKMjc2Gx%#Ll#=N4qkOuK|;>4%=0Xl7k`E69@QJ-*Vq zk9p5!+Ek#bjuPa<@Xv7ku4uiWo|_wy)6tIr`aO!)h>m5zaMS-@{HGIXJ0UilA7*I} z?|NZ!Tp8@o-lnyde*H+@8IHME8VTQOGh96&XX3E+}OB zA>VLAGW+urF&J{H{9Gj3&u+Gyn?JAVW84_XBeGs1;mm?2SQm9^!3UE@(_FiMwgkJI zZ*caE={wMm`7>9R?z3Ewg!{PdFDrbzCmz=RF<@(yQJ_A6?PCd_MdUf5vv6G#9Mf)i#G z($OxDT~8RNZ>1R-vw|nN699a}MQN4gJE_9gA-0%>a?Q<9;f3ymgoi$OI!=aE6Elw z2I`l!qe-1J$T$X&x9Zz#;3!P$I);jdOgYY1nqny-k=4|Q4F!mkqACSN`blRji>z1` zc8M57`~1lgL+Ha%@V9_G($HFBXH%k;Swyr>EsQvg%6rNi){Tr&+NAMga2;@85531V z_h+h{jdB&-l+%aY{$oy2hQfx`d{&?#psJ78iXrhrO)McOFt-o80(W^LKM{Zw93O}m z;}G!51qE?hi=Gk2VRUL2kYOBRuAzktql%_KYF4>944&lJKfbr+uo@)hklCHkC=i)E zE*%WbWr@9zoNjumq|kT<9Hm*%&ahcQ)|TCjp@uymEU!&mqqgS;d|v)QlBsE0Jw|+^ zFi9xty2hOk?rlGYT3)Q7i4k65@$RJ-d<38o<`}3KsOR}t8sAShiVWevR8z^Si4>dS z)$&ILfZ9?H#H&lumngpj7`|rKQQ`|tmMmFR+y-9PP`;-425w+#PRKKnx7o-Rw8;}*Ctyw zKh~1oJ5+0hNZ79!1fb(t7IqD8*O1I_hM;o*V~vd_LKqu7c_thyLalEF8Y3oAV=ODv z$F_m(Z>ucO(@?+g_vZ`S9+=~Msu6W-V5I-V6h7->50nQ@+TELlpl{SIfYYNvS6T6D z`9cq=at#zEZUmTfTiM3*vUamr!OB~g$#?9$&QiwDMbSaEmciWf3O2E8?oE0ApScg38hb&iN%K+kvRt#d))-tr^ zD+%!d`i!OOE3in0Q_HzNXE!JcZ<0;cu6P_@;_TIyMZ@Wv!J z)HSXAYKE%-oBk`Ye@W3ShYu-bfCAZ}1|J16hFnLy z?Bmg2_kLhlZ*?`5R8(1%Y?{O?xT)IMv{-)VWa9#1pKH|oVRm4!lLmls=u}Lxs44@g^Zwa0Z_h>Rk<(_mHN47=Id4oba zQ-=qXGz^cNX(b*=NT0<^23+hpS&#OXzzVO@$Z2)D`@oS=#(s+eQ@+FSQcpXD@9npp zlxNC&q-PFU6|!;RiM`?o&Sj&)<4xG3#ozRyQxcW4=EE;E)wcZ&zUG*5elg;{9!j}I z9slay#_bb<)N!IKO16`n3^@w=Y%duKA-{8q``*!w9SW|SRbxcNl50{k&CsV@b`5Xg zWGZ1lX)zs_M65Yt&lO%mG0^IFxzE_CL_6$rDFc&#xX5EXEKbV8E2FOAt>Ka@e0aHQ zMBf>J$FLrCGL@$VgPKSbRkkqo>sOXmU!Yx+Dp7E3SRfT`v~!mjU3qj-*!!YjgI*^) z+*05x78FVnVwSGKr^A|FW*0B|HYgc{c;e3Ld}z4rMI7hVBKaiJRL_e$rxDW^8!nGLdJ<7ex9dFoyj|EkODflJ#Xl`j&bTO%=$v)c+gJsLK_%H3}A_} z6%rfG?a7+k7Bl(HW;wQ7BwY=YFMSR3J43?!;#~E&)-RV_L!|S%XEPYl&#`s!LcF>l zn&K8eemu&CJp2hOHJKaYU#hxEutr+O161ze&=j3w12)UKS%+LAwbjqR8sDoZHnD=m0(p62!zg zxt!Sj65S?6WPmm zL&U9c`6G}T`irf=NcOiZ!V)qhnvMNOPjVkyO2^CGJ+dKTnNAPa?!AxZEpO7yL_LkB zWpolpaDfSaO-&Uv=dj7`03^BT3_HJOAjn~X;wz-}03kNs@D^()_{*BD|0mII!J>5p z1h06PTyM#3BWzAz1FPewjtrQfvecWhkRR=^gKeFDe$rmaYAo!np6iuio3>$w?az$E zwGH|zy@OgvuXok}C)o1_&N6B3P7ZX&-yimXc1hAbXr!K&vclCL%hjVF$yHpK6i_Wa z*CMg1RAH1(EuuA01@lA$sMfe*s@9- z$jNWqM;a%d3?(>Hzp*MiOUM*?8eJ$=(0fYFis!YA;0m8s^Q=M0Hx4ai3eLn%CBm14 zOb8lfI!^UAu_RkuHmKA-8gx8Z;##oCpZV{{NlNSe<i;9!MfIN!&;JI-{|n{(A19|s z9oiGesENcLf@NN^9R0uIrgg(46r%kjR{0SbnjBqPq()wDJ@LC2{kUu_j$VR=l`#RdaRe zxx;b7bu+@IntWaV$si1_nrQpo*IWGLBhhMS13qH zTy4NpK<-3aVc;M)5v(8JeksSAGQJ%6(PXGnQ-g^GQPh|xCop?zVXlFz>42%rbP@jg z)n)% zM9anq5(R=uo4tq~W7wES$g|Ko z1iNIw@-{x@xKxSXAuTx@SEcw(%E49+JJCpT(y=d+n9PO0Gv1SmHkYbcxPgDHF}4iY zkXU4rkqkwVBz<{mcv~A0K|{zpX}aJcty9s(u-$je2&=1u(e#Q~UA{gA!f;0EAaDzdQ=}x7g(9gWrWYe~ zV98=VkHbI!5Rr;+SM;*#tOgYNlfr7;nLU~MD^jSdSpn@gYOa$TQPv+e8DyJ&>aInB zDk>JmjH=}<4H4N4z&QeFx>1VPY8GU&^1c&71T*@2#dINft%ibtY(bAm%<2YwPL?J0Mt{ z7l7BR718o5=v|jB!<7PDBafdL>?cCdVmKC;)MCOobo5edt%RTWiReAMaIU5X9h`@El0sR&Z z7Ed+FiyA+QAyWn zf7=%(8XpcS*C4^-L24TBUu%0;@s!Nzy{e95qjgkzElf0#ou`sYng<}wG1M|L? zKl6ITA1X9mt6o@S(#R3B{uwJI8O$&<3{+A?T~t>Kapx6#QJDol6%?i-{b1aRu?&9B z*W@$T*o&IQ&5Kc*4LK_)MK-f&Ys^OJ9FfE?0SDbAPd(RB)Oju#S(LK)?EVandS1qb#KR;OP|86J?;TqI%E8`vszd&-kS%&~;1Als=NaLzRNnj4q=+ zu5H#z)BDKHo1EJTC?Cd_oq0qEqNAF8PwU7fK!-WwVEp4~4g z3SEmE3-$ddli))xY9KN$lxEIfyLzup@utHn=Q{OCoz9?>u%L^JjClW$M8OB`txg4r6Q-6UlVx3tR%%Z!VMb6#|BKRL`I))#g zij8#9gk|p&Iwv+4s+=XRDW7VQrI(+9>DikEq!_6vIX8$>poDjSYIPcju%=qluSS&j zI-~+ztl1f71O-B+s7Hf>AZ#}DNSf`7C7*)%(Xzf|ps6Dr7IOGSR417xsU=Rxb z1pgk9vv${17h7mZ{)*R{mc%R=!i}8EFV9pl8V=nXCZruBff`$cqN3tpB&RK^$yH!A8RL zJ5KltH$&5%xC7pLZD}6wjD2-uq3&XL8CM$@V9jqalF{mvZ)c4Vn?xXbvkB(q%xbSdjoXJXanVN@I;8I`)XlBX@6BjuQKD28Jrg05} z^ImmK-Ux*QMn_A|1ionE#AurP8Vi?x)7jG?v#YyVe_9^up@6^t_Zy^T1yKW*t* z&Z0+0Eo(==98ig=^`he&G^K$I!F~1l~gq}%o5#pR6?T+ zLmZu&_ekx%^nys<^tC@)s$kD`^r8)1^tUazRkWEYPw0P)=%cqnyeFo3nW zyV$^0DXPKn5^QiOtOi4MIX^#3wBPJjenU#2OIAgCHPKXv$OY=e;yf7+_vI7KcjKq% z?RVzC24ekYp2lEhIE^J$l&wNX0<}1Poir8PjM`m#zwk-AL0w6WvltT}*JN8WFmtP_ z6#rK7$6S!nS!}PSFTG6AF7giGJw5%A%14ECde3x95(%>&W3zUF!8x5%*h-zk8b@Bz zh`7@ixoCVCZ&$$*YUJpur90Yg0X-P82>c~NMzDy7@Ed|6(#`;{)%t7#Yb>*DBiXC3 zUFq(UDFjrgOsc%0KJ_L;WQKF0q!MINpQzSsqwv?#Wg+-NO; z84#4nk$+3C{2f#}TrRhin=Erdfs77TqBSvmxm0P?01Tn@V(}gI_ltHRzQKPyvQ2=M zX#i1-a(>FPaESNx+wZ6J{^m_q3i})1n~JG80c<%-Ky!ZdTs8cn{qWY%x%X^27-Or_ z`KjiUE$OG9K4lWS16+?aak__C*)XA{ z6HmS*8#t_3dl}4;7ZZgn4|Tyy1lOEM1~6Qgl(|BgfQF{Mfjktch zB5kc~4NeehRYO%)3Z!FFHhUVVcV@uEX$eft5Qn&V3g;}hScW_d)K_h5i)vxjKCxcf zL>XlZ^*pQNuX*RJQn)b6;blT3<7@Ap)55)aK3n-H08GIx65W zO9B%gE%`!fyT`)hKjm-&=on)l&!i-QH+mXQ&lbXg0d|F{Ac#U;6b$pqQcpqWSgAPo zmr$gOoE*0r#7J=cu1$5YZE%uylM!i3L{;GW{ae9uy)+EaV>GqW6QJ)*B2)-W`|kLL z)EeeBtpgm;79U_1;Ni5!c^0RbG8yZ0W98JiG~TC8rjFRjGc6Zi8BtoC);q1@8h7UV zFa&LRzYsq%6d!o5-yrqyjXi>jg&c8bu}{Bz9F2D(B%nnuVAz74zmBGv)PAdFXS2(A z=Z?uupM2f-ar0!A)C6l2o8a|+uT*~huH)!h3i!&$ zr>76mt|lwexD(W_+5R{e@2SwR15lGxsnEy|gbS-s5?U}l*kcfQlfnQKo5=LZXizrL zM=0ty+$#f_qGGri-*t@LfGS?%7&LigUIU#JXvwEdJZvIgPCWFBTPT`@Re5z%%tRDO zkMlJCoqf2A=hkU7Ih=IxmPF~fEL90)u76nfFRQwe{m7b&Ww$pnk~$4Lx#s9|($Cvt ze|p{Xozhb^g1MNh-PqS_dLY|Fex4|rhM#lmzq&mhebD$5P>M$eqLoV|z=VQY{)7&sR#tW zl(S1i!!Rrg7kv+V@EL51PGpm511he%MbX2-Jl+DtyYA(0gZyZQjPZP@`SAH{n&25@ zd)emg(p2T3$A!Nmzo|%=z%AhLX)W4hsZNFhmd4<1l6?b3&Fg)G(Zh%J{Cf8Q;?_++ zgO7O<(-)H|Es@QqUgcXNJEfC-BCB~#dhi6ADVZtL!)Mx|u7>ukD052z!QZ5UC-+rd zYXWNRpCmdM{&?M9OMa;OiN{Y#0+F>lBQ=W@M;OXq;-7v3niC$pM8p!agNmq7F04;| z@s-_98JJB&s`Pr6o$KZ=8}qO*7m6SMp7kVmmh$jfnG{r@O(auI7Z^jj!x}NTLS9>k zdo}&Qc2m4Ws3)5qFw#<$h=g%+QUKiYog33bE)e4*H~6tfd42q+|FT5+vmr6Y$6HGC zV!!q>B`1Ho|6E|D<2tYE;4`8WRfm2#AVBBn%_W)mi(~x@g;uyQV3_)~!#A6kmFy0p zY~#!R1%h5E{5;rehP%-#kjMLt*{g((o@0-9*8lKVu+t~CtnOxuaMgo2ssI6@kX09{ zkn~q8Gx<6T)l}7tWYS#q0&~x|-3ho@l}qIr79qOJQcm&Kfr7H54=BQto0)vd1A_*V z)8b2{xa5O^u95~TS=HcJF5b9gMV%&M6uaj<>E zPNM~qGjJ~xbg%QTy#(hPtfc46^nN=Y_GmPYY_hTL{q`W3NedZyRL^kgU@Q$_KMAjEzz*eip`3u6AhPDcWXzR=Io5EtZRPme>#K9 z4lN&87i%YYjoCKN_z9YK+{fJu{yrriba#oGM|2l$ir017UH86Eoig3x+;bz32R*;n zt)Eyg#PhQbbGr^naCv0?H<=@+Poz)Xw*3Gn00qdSL|zGiyYKOA0CP%qk=rBAlt~hr zEvd3Z4nfW%g|c`_sfK$z8fWsXTQm@@eI-FpLGrW<^PIjYw)XC-xFk+M<6>MfG;WJr zuN}7b;p^`uc0j(73^=XJcw;|D4B(`)Flm|qEbB?>qBBv2V?`mWA?Q3yRdLkK7b}y& z+!3!JBI{+&`~;%Pj#n&&y+<;IQzw5SvqlbC+V=kLZLAHOQb zS{{8E&JXy1p|B&$K!T*GKtSV^{|Uk;`oE*F;?@q1dX|>|KWb@|Dy*lbGV0Gx;gpA$ z*N16`v*gQ?6Skw(f^|SL;;^ox6jf2AQ$Zl?gvEV&H|-ep*hIS@0TmGu1X1ZmEPY&f zKCrV{UgRAiNU*=+Uw%gjIQhTAC@67m)6(_D+N>)(^gK74F%M2NUpWpho}aq|Kxh$3 zz#DWOmQV4Lg&}`XTU41Z|P~5;wN2c?2L{a=)Xi~!m#*=22c~&AW zgG#yc!_p##fI&E{xQD9l#^x|9`wSyCMxXe<3^kDIkS0N>=oAz7b`@M>aT?e$IGZR; zS;I{gnr4cS^u$#>D(sjkh^T6_$s=*o%vNLC5+6J=HA$&0v6(Y1lm|RDn&v|^CTV{= zjVrg_S}WZ|k=zzp>DX08AtfT@LhW&}!rv^);ds7|mKc5^zge_Li>FTNFoA8dbk@K$ zuuzmDQRL1leikp%m}2_`A7*7=1p2!HBlj0KjPC|WT?5{_aa%}rQ+9MqcfXI0NtjvXz1U)|H>0{6^JpHspI4MfXjV%1Tc1O!tdvd{!IpO+@ z!nh()i-J3`AXow^MP!oVLVhVW&!CDaQxlD9b|Zsc%IzsZ@d~OfMvTFXoEQg9Nj|_L zI+^=(GK9!FGck+y8!KF!nzw8ZCX>?kQr=p@7EL_^;2Mlu1e7@ixfZQ#pqpyCJ```(m;la2NpJNoLQR};i4E;hd+|QBL@GdQy(Cc zTSgZ)4O~hXj86x<7&ho5ePzDrVD`XL7{7PjjNM1|6d5>*1hFPY!E(XDMA+AS;_%E~ z(dOs)vy29&I`5_yEw0x{8Adg%wvmoW&Q;x?5`HJFB@KtmS+o0ZFkE@f)v>YYh-z&m z#>ze?@JK4oE7kFRFD%MPC@x$^p{aW}*CH9Y_(oJ~St#(2)4e-b34D>VG6giMGFA83 zpZTHM2I*c8HE}5G;?Y7RXMA2k{Y?RxHb2 zZFQv?!*Kr_q;jt3`{?B5Wf}_a7`roT&m1BN9{;5Vqo6JPh*gnN(gj}#=A$-F(SRJj zUih_ce0f%K19VLXi5(VBGOFbc(YF zLvvOJl+W<}>_6_4O?LhD>MRGlrk;~J{S#Q;Q9F^;Cu@>EgZAH=-5fp02(VND(v#7n zK-`CfxEdonk!!65?3Ry(s$=|CvNV}u$5YpUf?9kZl8h@M!AMR7RG<9#=`_@qF@})d ztJDH>=F!5I+h!4#^DN6C$pd6^)_;0Bz7|#^edb9_qFg&eI}x{Roovml5^Yf5;=ehZ zGqz-x{I`J$ejkmGTFipKrUbv-+1S_Yga=)I2ZsO16_ye@!%&Op^6;#*Bm;=I^#F;? z27Sz-pXm4x-ykSW*3`)y4$89wy6dNOP$(@VYuPfb97XPDTY2FE{Z+{6=}LLA23mAc zskjZJ05>b)I7^SfVc)LnKW(&*(kP*jBnj>jtph`ZD@&30362cnQpZW8juUWcDnghc zy|tN1T6m?R7E8iyrL%)53`ymXX~_;#r${G`4Q(&7=m7b#jN%wdLlS0lb~r9RMdSuU zJ{~>>zGA5N`^QmrzaqDJ(=9y*?@HZyE!yLFONJO!8q5Up#2v>fR6CkquE$PEcvw5q zC8FZX!15JgSn{Gqft&>A9r0e#be^C<%)psE*nyW^e>tsc8s4Q}OIm})rOhuc{3o)g1r>Q^w5mas) zDlZQyjQefhl0PmH%cK05*&v{-M1QCiK=rAP%c#pdCq_StgDW}mmw$S&K6ASE=`u4+ z5wcmtrP27nAlQCc4qazffZoFV7*l2=Va}SVJD6CgRY^=5Ul=VYLGqR7H^LHA;H^1g}ekn=4K8SPRCT+pel*@jUXnLz+AIePjz@mUsslCN2 z({jl?BWf&DS+FlE5Xwp%5zXC7{!C=k9oQLP5B;sLQxd`pg+B@qPRqZ6FU(k~QkQu{ zF~5P=kLhs+D}8qqa|CQo2=cv$wkqAzBRmz_HL9(HRBj&73T@+B{(zZahlkkJ>EQmQ zenp59dy+L;sSWYde!z_W+I~-+2Xnm;c;wI_wH=RTgxpMlCW@;Us*0}L74J#E z8XbDWJGpBscw?W$&ZxZNxUq(*DKDwNzW7_}AIw$HF6Ix|;AJ3t6lN=v(c9=?n9;Y0 zK9A0uW4Ib9|Mp-itnzS#5in=Ny+XhGO8#(1_H4%Z6yEBciBiHfn*h;^r9gWb^$UB4 zJtN8^++GfT`1!WfQt#3sXGi-p<~gIVdMM<#ZZ0e_kdPG%Q5s20NNt3Jj^t$(?5cJ$ zGZ#FT(Lt>-0fP4b5V3az4_byF12k%}Spc$WsRydi&H|9H5u1RbfPC#lq=z#a9W(r1 z!*}KST!Yhsem0tO#r!z`znSL-=NnP~f(pw-sE+Z$e7i7t9nBP^5ts1~WFmW+j+<@7 zIh@^zKO{1%Lpx^$w8-S+T_59v;%N;EZtJzcfN%&@(Ux5 z@YzX^MwbbXESD*d(&qT7-eOHD6iaH-^N>p2sVdq&(`C$;?#mgBANIc5$r| z^A$r)@c{Z}N%sbfo?T`tTHz9-YpiMW?6>kr&W9t$Cuk{q^g1<$I~L zo++o2!!$;|U93cI#p4hyc!_Mv2QKXxv419}Ej#w#%N+YIBDdnn8;35!f2QZkUG?8O zpP47Wf9rnoI^^!9!dy~XsZ&!DU4bVTAi3Fc<9$_krGR&3TI=Az9uMgYU5dd~ksx+} zP+bs9y+NgEL>c@l>H1R%@>5SWg2k&@QZL(qNUI4XwDl6(=!Q^U%o984{|0e|mR$p+ z9BcwttR#7?As?@Q{+j?K6H7R71PuiA^Dl$=f47nUKL|koCwutc_P<-m{|Al3C~o7w z=4S=}s5LcJFT1zjS)+10X_r$74`K78pz!nGGH%JV%w75!YSIt#hT7}}K>+@{{a+Im z5p#6%^X*txY?}|T17xWW*sa^?G2QHt#@tlcw0GIcy;|NR2vaCBDvn=`h)1il7E5Rx z%)mA4$`$OZx)NF5vXZnaJ1)*cA6ryx6Ll~t!LzhxvcTedxT;>JS&e=?-&DXUPaQ2~ zH*69ezE`hgV{K-|0z|m~ld}=X^-Ob={wpex&}*+Rz{gx)G}gn!C_VN{UN=>^EV=Xc zr$-HO09cW&p4^M}V3yBjTP_xrVcc8iU_^Y-JD~(bgw*@GXGB1gYKz5DWO+O`>})|N zWrC)MR93yA)3{&27-M)TJB6Ml3~?zZg#mYsF=#OSTaw&K z@hBftpt+2l@)YK@|3DvTjl(8wZtpLp9Ik!6G$CSL_idZ$Ti?R)4toe8bb)l|)lNb}?K;O2K9vyn1QG zd=v#y-Ld49UVkmfRU>Egc+(Y$^-;6vW;3Lcu*6~etz}0|@+b|+!UCal)DEYGLbHWJ zll5Wi^$Y<6@S%^y%hdjRh6&{!z1Py|lZ|q&Wub3l41uN2zEF8E&5H5?PL*&V}?*a}Lp% zCYi{ghjpRNT^^B+_U59No50Ghih5qn(W5`RkrsDWr{~A1dgtv{sRkH4RU2^A{jb&0 zxVRnrm|u<;$iI;M6A>$POP)TWGU-gSjAERk*EGmVT(aw$!XUSe~7Ql-oRA54^4V(JWS6Q1mG?!vZ zx+pE!FEtvqr|Xrcb3oR`%LHFLmU_&{=p%mGy6MRe2Yz_5WJ8p@IgU2 zdVvvhhQtiQkChK%*&PsiPCBL9oDOoJX8!$S(V>R}+1M}wzK*U*A{KJ`r=lM;mPrKU zQDqqN(W*u-5-?$(SIk<6A0E}34y&@-IVC%S!a1F4kz<3bIKjlyD)ooO_7ftl%S_(6w`!vX&1PZ!K`@D@L6JR)6zO@Dl!YF{RY}d3HZ7?Q5E>w=$ ze)H_)48Ds*Ov4?zoGb2fe3}{!5Ooc|KCIni1o)(Gj+CO?`*7jsV`hIv@8J(22o4Q? zu?Bvi)zDG(me?7XKeL|iF9ZRgZdT*}Ffsl62Cu;{Gv9j6dO zPt*H2GqC)-C`V`ceuu=tM{7!2yTEj=*5+T~5DYiZ)Hy)*PARYI6R2lZXoOj;v8M4W z*O-NX(7_~Q&A3>Oaw&1lBH_H%SwmISX-i3)HfHvBOeVwTT{LUM3}ZuZmg<(>)KE;d zbs2!0v6>J;1nQ0UJkUxnkE@Ibi~Q}M=-=Rk;hcOnxO$luOKEVxZc|!XECgex(2`}T z3Y;Q_6rL)e+SrOZhQj5_e}Lv>w7n*Pep$yWZNQl>ubBgb_NIWWDn3kNpn+MPQXV;8 zV|_Ba5jsQ(w&Ey^IM|@|y!AqcJ#3m0#Q6_qvgCG~eoF#mnGmbO(;DP+bW%_aOs1R_ z@9p#7X2UA^--#Nwx_Hvk2l1`eO{P*#j@q2UELtH|Uh6hxR`h_847wIJo0=5CQQ`6it|%a-I$^&a@we1rc&*;QIu5Ck^?) zx*5eSd*mG#=6Hi(5!;5uUi&{HfnT1S8X-)?gE5CZ6KWoqM5|CyrULmuFBKOU8SOp* z{IB1$OCcq`S-k*xs;4fmhKsIGZ;GYAY*%(@875NxhMq|j*m4CNLI(Vho|N|F);!E0cS5y^$H^Izje?z}oTgyr`9x9G&rlJZw&uqIoBMtz zzhU0(9;w02?m#0!)cFi*r+8YvooQ;(s2lLVvyLqAE%Xqe!vtWbIs!l1Bpp(FIht-Z zPn#CN-2C|J*GhA2fuHqYQ2mJiXlGTzD}mkr2;ia8Wp}h^;OS7+N^Mw|en!1${vN6 z-x{8N*4UekA~`IV2&K-GzhAqau|}d*pEQ$1MH$cFi03OG^1NetZ_jW^STaEzr&Xho zB452St%v3ez2#TFm~`gZh$vi=in+y2d!z<{OZ~Kty-5bQ;0O=k_ESi8Nx9{*T`LJy6jqR>&|+>OZ;+=0hA04 zE25t^sE9HG)3^KKR_A5WDkqispweP9!I-@dCO&N!JrD@i{WBHnfQ z95o8;d$`AFnca3;N-0iX-CmbbAp5yQ!GoH;h7Cn?m{ammZJI8igP{U73lFnl2&gCs zqJ4(Vo~^j`{zOAzScL5B_Sm?Mjtek1d(A6X5ObcZi$;aOYy|g$}BY z$GEP3#i60Ju_&3SHzryH!gUFwC9-295u??cf+aYRQ1$+!rc#42YNattd6mZEFI@?C zqFM>6+zxEunIHDZ>{Z15u##>N(28Dw!>G(k*dB{NHvip@aP}f`@=Q;!o;zRMWo{Cx zo?kyzh8n7#f1g0&g>Cd>O-2g?uPwy8sy8hZbHSsXPmU;@l=HL=zm7mN(=@*|D$i+u zs~TllkCTvD$f&-#b9B?}#Lg*-ibK13R_a$RyoN3m5`10tdhAq{+VW)K#Bht-ra1*J z+n$N%V>u0rVtx`aKJDwXXrxaD7nS<>$=c82v7@KVx^S@vT;h=SZE37K>iahpx3;VDzEr9GY=2(%uaqM;^76eSP0QLzo4sI z>p_Eei*T$K;|qK`sq;?Hesp}(@VvX2Q4sAMYAJ}b&d$htDMC{FG-$o4k9ApECi1$a zXdamjiOGKHBh(4M<3(2x6n-CrmZMCknkQxdSS!qlis#I}btfX;J`JU3RlvtLdrymP zG0ZzrsGXVFiq+Wk1=BFay&9ZiCE#(`h~CL+c-Hs@iGTU@YxM%vlg;)`Tf~IknA^02 zXkN#Txo6aR{j$wP5T#|UH#5AP2{rSY8p?jKFv zG3kn3y`FaV!*Jq%m39_TQEhD>M@l*bhEPGe1{ft3q#K5AknT=F2_=T^l#ou5ln@D# z5Tzs(kRG@qNDa~HLNvfv7Z0g=bSlb?`QAx|Gfoni|iHJ%K0cy z;~Nsaa+{8HP_qrb{nj+xzkdYhSI@W4N_1`z(eSGIkbDP)!Ko|M%}Rqp(~KI2hl~eE zvJ!j4m6iwMgKy>fkCLC)`M$z9EV}B+sq1}}kVf$(ig0pWTY?rHz1Sm=4srTGNb^JG z=2$9wz-C@aZZZ2!HY#HNejqZRmE=pN(D$Kui$NpfhU`!y_s{@MIxiJdHb1|{6xb`> zE74_@QtgtG{4=3P1$^vn&m}7Aw8!1DnT$2thO#~44wl(N#ao8S0@t@m+Z!KD2CfK; z)n5DAPKV_etmH1aLDK$?`;sL91iVt$D z*SG}=-LIAg(*+JON!-5ivqOMQ1S!OQUgHglDsKik&Mwg;vva523`JwQH6SRz9eTY# zTIi23145~kc3r1mSWC_RzD%hs$S#!pkI9!BU80jJCJcwo*FZolQG$q`8C1d9pP@ND zG^&-ZraIvhg_FDVSfKGwkcI=avIan%2sK4coUs~Nr8jC*&!G0#?}_^s3r-c}-uAqi zM-Lw>Y}I``T;IS%Y|qH;s{F*ZefM!4{I5awr!K+T@uPd*Vu*iPWI}>(-D{zxsN>LG z=@747a_Rb2>q?y8xYf?dq2HM5tFO8Y5e4N;Y=xy8yAhI zsm>oy%R5;7)7T3V_b2%`aH^tNlsQpFxIFW#iV#8?{6{^cGr{A0@1bA)|K z>MMTuZD(pd2t|7vmHtywGXb%%=)S<`OG~}U+jm#xd%H8 z$v8-C%F?ah3$;hn?{G3(LT!SgvCVi$vwsZssAQvUwT`Q%qSw!LSd!(I!64w1=%Sc1Mck)q1@pZ@)=SY zoX}d+L3-RA|c?G3_BQNm&( z!i$AZ7cI(z7q|e9VM##6T3Xorj1JG(9os$;(I$y%mBy(#8{|3l4|x*oBAQL^XhZ0g zy1FR1teRrpKq{uLAibTLx#n({qwjlkOvR{OdSAeT5ah4-sNN)n4Clg1T9lzF)&yj; zyal1%+s4n1IG;^VPWJ;#olpk8Z42Gj-tjFeQ&PlxB)`oCNoUYKj4U$AeG8rYiD{pK zndDf&2;2;)D|KvOZP+e7fcPU9k4M2sfhr@vC~Ly0?S-4dz)ZGAYpCsAhChgbxLd4g zhTrbIPkO5SEp_kD>Ha0m12h5n3s;mE8kn515&nzSf+^D= zyE{JnJ;43l&BH55CL<=W%CF;6iUI)V5C*6!`**KqvzR2=Fj*3Y4`HYwx}TYD445(K z-QtXwtL?m*(F=LVH*H4oM>dXHBW=38q_dZ-_Vr&qpEPxd9Fs95P5W~@Z|Rt+WZP6l zPSQ}~Dh4V?Pp1g&Hk*Px?lm16C@X6M29Vrk%Rw@E||E-v~$ zb_E~{z<}#8i`Mx9mkqtd#Z1lZ-E_J8I+2oumc#x1)jdvh{W76NKm6x-RYpM~v!P8$ zw3e|YVf|}Hse9~oC@N7^j}Fi$hNpyaYnu1}bdXsD=^oI*%WKvbme|BI}$G3>smu#6y)ls|j? zF7Bhu9Z)j)C;3cZb+I>0stSK^WLOYV^U{pUYkgv>?+Nt^5j*CUB=eGw-CvU&40>y~ zGoHLXxY^7k5Xgv62{iQy|5jJQuq0|LU`}lE@flQ2Z*Zn*VWcQjm4FTb>LSVox^S4q zLn`LfS@mrjKCmg$nb^af?d?0&$aX6#2u(JyzIJvuJ*lwPrh|0~aEnSACCTezSdG%h zmSQg`17j@$Iq)r1&?+eR@1nlX|H`<}_!?BQSF&N+QQnvEAqZe+mIFui!0V49R?|9*$ zv!K1A01{8xq;L()Tv*Qk0-$Oj6+vCT*TUD{HvxO@3JjxBwM!4g3ydy&eaJw4CoQBF zJtULJ!YxgNR7_Ls%LmogyI7uIs=!B&?=MYY^yX+v;j@D_xGeZg>eZk0C;4e|HRNSi z6KlD9>q=3v-$4Zik&^ZDhNm1X)+7LCH1k!s+T3tn zUn@={1U&NJLq@K?~w|(=Y<4W{ucX}FdRr6pLw(l2$iK)At%t3gYBMlJz#(K0Nqm;=KAML!&MMSNz=%k=j*zh77r34Rs37iCY` z=_kva_41bdrj(b=4Wc5MO0~q^z#pIWJ>)vDSgIQF=3JVJe1iDy%h)8oNy{s_r&;m` zL{DYKSB_5xRb9xKNOS{qAY3qv5sSXVrrf%~*q5HO|CQ&lbKMePa$M5D{vlJcoGrCZ zD?fKbZN$6rWwz)w7`9h4DAmh1ij2}EO|bO#A9L0_RW6l*$sPPUJrUbhLC75L9%W5iO$Iw5~Yut-qBeu~hF|xD7-eQ%l z412vpq_;t%^F*pYDk%Q35c-erK|6Ve=FxQbAv~ikZ4c9$Y4;ee#ciOD9{yRqf55Qk zumv}#+JciT|Gj$uFOxBUze)=?l{B}qaC0_7m`t82<$K53!4Xvi9Tr)ADp3Off?O8o zVDG0Yx|tfn@r((m?Nxrh(b0DGjg)$;DfO&$6uY;4&F!4jnxkhP}Y3x zS?WFFt>=HWzqlQhffVfvM$Ta8Sg*r3j!Eo&rUOW7SCL2~lG7<+XZ;+{&8h5g8ElI+P>>yR2U%S93NN!Xhm|C682t6ysH-=o1=Bd*N*VlnG%l+KZFtjG`UkL;%65qn0UYQ`h zh0{9jDQx(`aBe7J0Aj3Z)4}`A|4OMM0a;?{j}qkYwi)~O8$9D}ITiMH2buiU>ixYp zhL${nwj6X($*OwmpVG`y5b6v45tX*J8?og}Qju6eJ9H}`X87iEd%BUo7<`2q(HJx+ zMR}d-J4oAf{V1W^a2~`M-YAdZ81dd4o6NPO{cmZaAS@RS4ir#Sr zfFZO-VIL|VN<%nEXr2` z$0FK2L#8O_f1w~c@G70JrB@N}r(gJ!Vmkk6{r68w!o$qO?HrFcjeU0_3F5;*!E2%( zTx>4?gP8w z1B?3UVZmz^%d_dIps>>0{cB~mp3{9UoPR6uQFecVq&} zY{ebB?AlPAD_}(ll{fK99;Wh1cgRbnw)maD^F>*J!R}eHM*W0VYN1TADWMy9H=$00 z5bHY${oDgwX7(W9LZw?}{!8(_{JB~Xkje6{0x4fgC4kUmpfJ+LT1DYD*TWu4#h{Y7 zFLronmc=hS=W=j1ar3r1JNjQoWo2hMWsqW*e?TF%#&{GpsaLp}iN~$)ar+7Ti}E&X z-nq~+Gkp(`qF0F_4A22>VZn-x>I$?PDZSeG8h_ifoWf^DxIb5%T7UytYo3}F|4#RC zUHpg$=)qVqD~=m(!~?XwocuxU1u}9qhhM7d^eqmJPi_e-!IO`*{u7A zbu*?L$Mbj-X9n3G2>+Kc#l`@d8}Xb9{l*IN{#M*d;s+3Pdr8FO$EBELR=8{ zd?LJbSv9fI`{OqTH)5{b?WulgMb)psp+W|@cSp=jtl-&5C}9lw@*0H+gEW(}mAWNz zf{~U;;N}|wdSaphgqnH{FWUy!{y3^=AC*c?RJ5Eb<^ zCgH_v7^axIUVmHSFL^zlj2R$zow$|y#7>%#U7d#Vp_ezcp3lefMyd5ES=q$>4pWyA zp_Zso^^NP~lu2=S6nD(3Z5u=Uy&B&F1i$J*3;3KhEkD_lgscHGR*;T;U!9vgQa(hI}oh9IzEf_PU_8F+i77t-~gDX z490Sb)LyVZmf18N6w{+37$aO<2!Av0 ztLaPOv^J<2@p{WnMiDudoghX_`luFZt_4eNU}*~cF5i%eEcNLs;D>QVIwr8mH;=dc z09`}JV;aaF;13@&iS(w>Jc=k~|d_1hcpM(l|O zu>!@}me%isTT$xT#hNUvh(ATd0wT4fbv=6htcHNEZIw9%E6wlYmwfu2{j0kh1y=$;Yf!|NldgB9ul zB{dbE&LfRnr8ITm@;-68wo#VV?8lG3ed&9k1}QBS3}WGV9%26?A1rBkkDR9Z3o+g+ z)eQg8BY3y(Dh5&z?VLLNdDV`C=muUvCPpGg!oYxIgOI3^%4>5d7jTh~ni!Fg2;fhx z(*c%H6Je84kmQh;5tC3*l~7khLxK-e|Cz?FLh!yYe7g|*LwqU?2wv^_ZyKT$fYVkGJo@AK0$+ml?}zJeB~deT2WL1vz}dxB z)y??t!}%M@)u$_IyW~)6u1SttJ!awd6N5lx|xBrmyrBh>tb&D*=C+Z3nPfq$1%WgY0bY*?PZ#Hk|=xn zGM#0*w4CaB^y0G(J4q=;5NeM@m-P}#mv7QZNF)M!dK^w{mk_!n0`+Y3PQutu-%NBt zzgPXug?JLEbUL{e_dk;Vd896&yPe(hliVK!lj%5+@BKdcrEZ2Nc_*i@ve*2lB>u~{ zFozd2FM|_0+nAGR4TLNHanQn_Oeb!JrUcvzJ?7p9TTNB}ocO3j$7ij!li8#k6 z@2tSd1>K03K9A#_-MIq)S;T#oE^;>U$)&}okIvDf3lm?kI{d80$>~xKUoS!%q1Pi?WpsUUt(tI ztjNjY*y&Rm9(S(DC2GuPHBJs@5M{RGm`c1z<6nwyN^)rMo-AS{M2$oM9|y%fM|}G~ DHx0+F diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b1c..000000000 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/kotlin-mcp-client/gradlew b/samples/kotlin-mcp-client/gradlew deleted file mode 100755 index faf93008b..000000000 --- a/samples/kotlin-mcp-client/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat deleted file mode 100644 index 9d21a2183..000000000 --- a/samples/kotlin-mcp-client/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/kotlin-mcp-client/settings.gradle.kts b/samples/kotlin-mcp-client/settings.gradle.kts index 39d0cc293..6e2f160db 100644 --- a/samples/kotlin-mcp-client/settings.gradle.kts +++ b/samples/kotlin-mcp-client/settings.gradle.kts @@ -1,11 +1,19 @@ rootProject.name = "kotlin-mcp-client" -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } } dependencyResolutionManagement { repositories { mavenCentral() } -} + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.gitignore b/samples/kotlin-mcp-server/.gitignore deleted file mode 100644 index b1dff0dd9..000000000 --- a/samples/kotlin-mcp-server/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/.gitignore b/samples/kotlin-mcp-server/.idea/.gitignore deleted file mode 100644 index 13566b81b..000000000 --- a/samples/kotlin-mcp-server/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/samples/kotlin-mcp-server/.idea/gradle.xml b/samples/kotlin-mcp-server/.idea/gradle.xml deleted file mode 100644 index 2a65317ef..000000000 --- a/samples/kotlin-mcp-server/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/kotlinc.xml b/samples/kotlin-mcp-server/.idea/kotlinc.xml deleted file mode 100644 index bb4493707..000000000 --- a/samples/kotlin-mcp-server/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/misc.xml b/samples/kotlin-mcp-server/.idea/misc.xml deleted file mode 100644 index f16dea79b..000000000 --- a/samples/kotlin-mcp-server/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/.idea/vcs.xml b/samples/kotlin-mcp-server/.idea/vcs.xml deleted file mode 100644 index fdf1fc87c..000000000 --- a/samples/kotlin-mcp-server/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/samples/kotlin-mcp-server/build.gradle.kts b/samples/kotlin-mcp-server/build.gradle.kts index 10520c202..5ecbc43c1 100644 --- a/samples/kotlin-mcp-server/build.gradle.kts +++ b/samples/kotlin-mcp-server/build.gradle.kts @@ -4,8 +4,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - kotlin("multiplatform") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) } group = "org.example" @@ -43,10 +43,10 @@ kotlin { sourceSets { commonMain.dependencies { - implementation("io.modelcontextprotocol:kotlin-sdk:0.6.0") + implementation(libs.mcp.kotlin) } jvmMain.dependencies { - implementation("org.slf4j:slf4j-nop:2.0.9") + implementation(libs.slf4j.simple) } wasmJsMain.dependencies {} } diff --git a/samples/kotlin-mcp-server/gradle.properties b/samples/kotlin-mcp-server/gradle.properties deleted file mode 100644 index a39b70007..000000000 --- a/samples/kotlin-mcp-server/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -kotlin.code.style=official - -kotlin.daemon.jvmargs=-Xmx2G diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832f090a2944b7473328c07c9755baa3196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat deleted file mode 100644 index ac1b06f93..000000000 --- a/samples/kotlin-mcp-server/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/kotlin-mcp-server/settings.gradle.kts b/samples/kotlin-mcp-server/settings.gradle.kts index 6efbc10a2..080398449 100644 --- a/samples/kotlin-mcp-server/settings.gradle.kts +++ b/samples/kotlin-mcp-server/settings.gradle.kts @@ -1,4 +1,19 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} rootProject.name = "kotlin-mcp-server" + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/samples/weather-stdio-server/.gitignore b/samples/weather-stdio-server/.gitignore deleted file mode 100644 index 108d5673c..000000000 --- a/samples/weather-stdio-server/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Kotlin ### -.kotlin - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index bd599728b..1b019d278 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -1,7 +1,7 @@ plugins { - kotlin("jvm") version "2.1.10" - kotlin("plugin.serialization") version "2.1.10" - id("com.github.johnrengelman.shadow") version "8.1.1" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) application } @@ -9,21 +9,21 @@ application { mainClass.set("io.modelcontextprotocol.sample.server.MainKt") } +repositories { + mavenCentral() +} + group = "org.example" version = "0.1.0" -val mcpVersion = "0.6.0" -val slf4jVersion = "2.0.9" -val ktorVersion = "3.1.1" - dependencies { - implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion") - implementation("org.slf4j:slf4j-nop:$slf4jVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation(libs.mcp.kotlin) + implementation(libs.slf4j.simple) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) testImplementation(kotlin("test")) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1") + testImplementation(libs.kotlinx.coroutines.test) } tasks.test { diff --git a/samples/weather-stdio-server/gradle.properties b/samples/weather-stdio-server/gradle.properties deleted file mode 100644 index 7fc6f1ff2..000000000 --- a/samples/weather-stdio-server/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.code.style=official diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c742b298b441bfb90dbc124400a3751b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43705 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 DHx0+F diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b1c..000000000 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/samples/weather-stdio-server/gradlew b/samples/weather-stdio-server/gradlew deleted file mode 100755 index faf93008b..000000000 --- a/samples/weather-stdio-server/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat deleted file mode 100644 index 9d21a2183..000000000 --- a/samples/weather-stdio-server/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/weather-stdio-server/settings.gradle.kts b/samples/weather-stdio-server/settings.gradle.kts index 015fa3f2a..1121bf5ff 100644 --- a/samples/weather-stdio-server/settings.gradle.kts +++ b/samples/weather-stdio-server/settings.gradle.kts @@ -1,11 +1,19 @@ rootProject.name = "weather-stdio-server" -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } } dependencyResolutionManagement { repositories { mavenCentral() } -} + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c0ad75186..aa90c769d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +rootProject.name = "kotlin-sdk" + pluginManagement { repositories { mavenCentral() @@ -15,5 +17,13 @@ dependencyResolutionManagement { } } -rootProject.name = "kotlin-sdk" +include(":kotlin-sdk-core") +include(":kotlin-sdk-client") +include(":kotlin-sdk-server") +include(":kotlin-sdk") +include(":kotlin-sdk-test") +// Include sample projects as composite builds +includeBuild("samples/kotlin-mcp-client") +includeBuild("samples/kotlin-mcp-server") +includeBuild("samples/weather-stdio-server") From a8c193efb43bb949e705ae4ef37b0f52d90bb50f Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 17:14:09 +0300 Subject: [PATCH 13/24] fixup! Introduce Kotlin integration tests --- kotlin-sdk-test/build.gradle.kts | 13 +++++ .../sdk}/integration/kotlin/KotlinTestBase.kt | 2 +- .../integration/kotlin/PromptEdgeCasesTest.kt | 4 +- .../kotlin/PromptIntegrationTest.kt | 51 +++++++++---------- .../kotlin/ResourceEdgeCasesTest.kt | 4 +- .../kotlin/ResourceIntegrationTest.kt | 4 +- .../integration/kotlin/ToolEdgeCasesTest.kt | 10 ++-- .../integration/kotlin/ToolIntegrationTest.kt | 14 +++-- ...tlinClientTypeScriptServerEdgeCasesTest.kt | 2 +- .../KotlinClientTypeScriptServerTest.kt | 2 +- .../TypeScriptClientKotlinServerTest.kt | 12 ++--- .../typescript/TypeScriptEdgeCasesTest.kt | 14 ++--- .../typescript/TypeScriptTestBase.kt | 13 ++--- .../utils/KotlinServerForTypeScriptClient.kt | 13 +---- .../sdk}/integration/utils/TestUtils.kt | 2 +- .../kotlin/sdk}/integration/utils/myClient.ts | 4 +- 16 files changed, 81 insertions(+), 83 deletions(-) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/KotlinTestBase.kt (97%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/PromptEdgeCasesTest.kt (99%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/PromptIntegrationTest.kt (89%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/ResourceEdgeCasesTest.kt (98%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/ResourceIntegrationTest.kt (95%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/ToolEdgeCasesTest.kt (97%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/kotlin/ToolIntegrationTest.kt (97%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt (99%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/typescript/KotlinClientTypeScriptServerTest.kt (99%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/typescript/TypeScriptClientKotlinServerTest.kt (92%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/typescript/TypeScriptEdgeCasesTest.kt (90%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/typescript/TypeScriptTestBase.kt (91%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/utils/KotlinServerForTypeScriptClient.kt (96%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/utils/TestUtils.kt (98%) rename {src/jvmTest/kotlin => kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk}/integration/utils/myClient.ts (93%) diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 7a5cabbda..3300e53b6 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -3,6 +3,11 @@ plugins { } kotlin { + jvm { + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } sourceSets { commonTest { dependencies { @@ -12,5 +17,13 @@ kotlin { implementation(libs.kotlinx.coroutines.test) } } + jvmTest { + dependencies { + implementation(kotlin("test-junit5")) + implementation(libs.kotlin.logging) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.client.cio) + } + } } } \ No newline at end of file diff --git a/src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt similarity index 97% rename from src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt index 3eaef18c6..dd6a56546 100644 --- a/src/jvmTest/kotlin/integration/kotlin/KotlinTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -1,4 +1,4 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin import io.ktor.client.* import io.ktor.client.engine.cio.* diff --git a/src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt similarity index 99% rename from src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt index 336d8c188..f23ffda78 100644 --- a/src/jvmTest/kotlin/integration/kotlin/PromptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt @@ -1,6 +1,6 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking diff --git a/src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt similarity index 89% rename from src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt index 8d96eb4e9..889d41570 100644 --- a/src/jvmTest/kotlin/integration/kotlin/PromptIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt @@ -1,6 +1,6 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test @@ -77,7 +77,7 @@ class PromptIntegrationTest : KotlinTestBase() { ) ) { request -> val topic = request.arguments?.get("topic") ?: "general knowledge" - val includeImage = request.arguments?.get("includeImage")?.toString()?.toBoolean() ?: true + val includeImage = request.arguments?.get("includeImage")?.toBoolean() ?: true val messages = mutableListOf() @@ -153,7 +153,7 @@ class PromptIntegrationTest : KotlinTestBase() { PromptMessage( role = Role.assistant, content = TextContent(text = "You're welcome! Let me know if you have more questions about $topic."), - ) + ), ) ) } @@ -177,14 +177,13 @@ class PromptIntegrationTest : KotlinTestBase() { name = "optionalArg", description = "Optional argument", required = false, - ) + ), ) ) { request -> - val arg1 = request.arguments?.get("requiredArg1") - ?: throw IllegalArgumentException("Missing required argument: requiredArg1") - val arg2 = request.arguments["requiredArg2"] - ?: throw IllegalArgumentException("Missing required argument: requiredArg2") - val optArg = request.arguments["optionalArg"] ?: "default" + val args = request.arguments ?: emptyMap() + val arg1 = args["requiredArg1"] ?: throw IllegalArgumentException("Missing required argument: requiredArg1") + val arg2 = args["requiredArg2"] ?: throw IllegalArgumentException("Missing required argument: requiredArg2") + val optArg = args["optionalArg"] ?: "default" GetPromptResult( description = strictPromptDescription, @@ -196,7 +195,7 @@ class PromptIntegrationTest : KotlinTestBase() { PromptMessage( role = Role.assistant, content = TextContent(text = "I received your arguments: $arg1, $arg2, and $optArg"), - ) + ), ) ) } @@ -213,10 +212,10 @@ class PromptIntegrationTest : KotlinTestBase() { assertNotNull(testPrompt, "Test prompt should be in the list") assertEquals(testPromptDescription, testPrompt.description, "Prompt description should match") - assertNotNull(testPrompt.arguments, "Prompt arguments should not be null") - assertTrue(testPrompt.arguments.isNotEmpty(), "Prompt arguments should not be empty") + val arguments = testPrompt.arguments ?: error("Prompt arguments should not be null") + assertTrue(arguments.isNotEmpty(), "Prompt arguments should not be empty") - val nameArg = testPrompt.arguments.find { it.name == "name" } + val nameArg = arguments.find { it.name == "name" } assertNotNull(nameArg, "Name argument should be in the list") assertEquals("The name to greet", nameArg.description, "Argument description should match") assertEquals(true, nameArg.required, "Argument required flag should match") @@ -264,8 +263,8 @@ class PromptIntegrationTest : KotlinTestBase() { val strictPrompt = promptsList.prompts.find { it.name == strictPromptName } assertNotNull(strictPrompt, "Strict prompt should be in the list") - assertNotNull(strictPrompt.arguments, "Prompt arguments should not be null") - val requiredArgs = strictPrompt.arguments.filter { it.required == true } + val argsDef = strictPrompt.arguments ?: error("Prompt arguments should not be null") + val requiredArgs = argsDef.filter { it.required == true } assertEquals(2, requiredArgs.size, "Strict prompt should have 2 required arguments") // test missing required arg @@ -320,9 +319,9 @@ class PromptIntegrationTest : KotlinTestBase() { assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") - assertNotNull(userContent.text, "User message text should not be null") - assertTrue(userContent.text.contains("value1"), "Message should contain first argument") - assertTrue(userContent.text.contains("value2"), "Message should contain second argument") + val userText = requireNotNull(userContent.text) + assertTrue(userText.contains("value1"), "Message should contain first argument") + assertTrue(userText.contains("value2"), "Message should contain second argument") } @Test @@ -348,15 +347,15 @@ class PromptIntegrationTest : KotlinTestBase() { assertNotNull(userMessage, "User message should be in the list") val userContent = userMessage.content as? TextContent assertNotNull(userContent, "User message content should be TextContent") - assertNotNull(userContent.text, "User message text should not be null") - assertTrue(userContent.text.contains(topic), "User message should contain the topic") + val userText2 = requireNotNull(userContent.text) + assertTrue(userText2.contains(topic), "User message should contain the topic") val assistantMessage = result.messages.find { it.role == Role.assistant } assertNotNull(assistantMessage, "Assistant message should be in the list") val assistantContent = assistantMessage.content as? TextContent assertNotNull(assistantContent, "Assistant message content should be TextContent") - assertNotNull(assistantContent.text, "Assistant message text should not be null") - assertTrue(assistantContent.text.contains(topic), "Assistant message should contain the topic") + val assistantText = requireNotNull(assistantContent.text) + assertTrue(assistantText.contains(topic), "Assistant message should contain the topic") val resultNoImage = client.getPrompt( GetPromptRequest( @@ -402,11 +401,11 @@ class PromptIntegrationTest : KotlinTestBase() { for (message in result.messages) { val content = message.content as? TextContent assertNotNull(content, "Message content should be TextContent") - assertNotNull(content.text, "Message text should not be null") + val text = requireNotNull(content.text) // Either the message contains the topic or it's a generic conversation message - val containsTopic = content.text.contains(topic) - val isGenericMessage = content.text.contains("thank you") || content.text.contains("welcome") + val containsTopic = text.contains(topic) + val isGenericMessage = text.contains("thank you") || text.contains("welcome") assertTrue( containsTopic || isGenericMessage, diff --git a/src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt similarity index 98% rename from src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt index ec4995539..4a721e8bb 100644 --- a/src/jvmTest/kotlin/integration/kotlin/ResourceEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt @@ -1,6 +1,6 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking diff --git a/src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt similarity index 95% rename from src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt index 11ebedb9c..53b08b0d1 100644 --- a/src/jvmTest/kotlin/integration/kotlin/ResourceIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt @@ -1,6 +1,6 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import org.junit.jupiter.api.Test import kotlin.test.assertEquals diff --git a/src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt similarity index 97% rename from src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt index 4d15c3acc..b8f7b581e 100644 --- a/src/jvmTest/kotlin/integration/kotlin/ToolEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt @@ -1,9 +1,9 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.assertCallToolResult -import integration.utils.TestUtils.assertJsonProperty -import integration.utils.TestUtils.assertTextContent -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt similarity index 97% rename from src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt index dd971efa5..7ad160bdd 100644 --- a/src/jvmTest/kotlin/integration/kotlin/ToolIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt @@ -1,9 +1,9 @@ -package integration.kotlin +package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import integration.utils.TestUtils.assertCallToolResult -import integration.utils.TestUtils.assertJsonProperty -import integration.utils.TestUtils.assertTextContent -import integration.utils.TestUtils.runTest +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.* @@ -293,9 +293,7 @@ class ToolIntegrationTest : KotlinTestBase() { val content = toolResult.content.firstOrNull() as? TextContent assertNotNull(content, "Tool result content should be TextContent") - assertNotNull(content.text, "Text content should not be null") - val contentText = content.text - + val contentText = requireNotNull(content.text) assertTrue(contentText.contains("Operation"), "Result should contain operation") assertTrue(contentText.contains("multiply"), "Result should contain multiply operation") diff --git a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt similarity index 99% rename from src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt index a2a2c9ec7..2d33052aa 100644 --- a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt @@ -1,4 +1,4 @@ -package integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript import io.ktor.client.* import io.ktor.client.engine.cio.* diff --git a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt similarity index 99% rename from src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt index 1aaaeb81b..3bae5ac22 100644 --- a/src/jvmTest/kotlin/integration/typescript/KotlinClientTypeScriptServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt @@ -1,4 +1,4 @@ -package integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript import io.ktor.client.* import io.ktor.client.engine.cio.* diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt similarity index 92% rename from src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index 74942807f..3651401bf 100644 --- a/src/jvmTest/kotlin/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -1,6 +1,6 @@ -package integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript -import integration.utils.KotlinServerForTypeScriptClient +import io.modelcontextprotocol.kotlin.sdk.integration.utils.KotlinServerForTypeScriptClient import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -42,7 +42,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Timeout(20, unit = TimeUnit.SECONDS) fun testToolCall() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") // call the "greet" tool val testName = "TestUser" @@ -66,7 +66,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Timeout(20, unit = TimeUnit.SECONDS) fun testToolCallWithSessionManagement() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val testName = "SessionTest" val command = "npx tsx myClient.ts $serverUrl greet $testName" @@ -96,7 +96,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleClientSequence() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val testName1 = "FirstClient" val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" @@ -131,7 +131,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleClientParallel() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val clientCount = 3 val clients = listOf( diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt similarity index 90% rename from src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index 81da42db9..c64515c3b 100644 --- a/src/jvmTest/kotlin/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -1,6 +1,6 @@ -package integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript -import integration.utils.KotlinServerForTypeScriptClient +import io.modelcontextprotocol.kotlin.sdk.integration.utils.KotlinServerForTypeScriptClient import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -43,7 +43,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Timeout(20, unit = TimeUnit.SECONDS) fun testErrorHandling() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, clientDir) @@ -66,7 +66,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Timeout(20, unit = TimeUnit.SECONDS) fun testSpecialCharacters() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val specialChars = "!@#$+-[].,?" @@ -92,7 +92,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Timeout(30, unit = TimeUnit.SECONDS) fun testLargePayload() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val largeName = "A".repeat(10 * 1024) @@ -118,7 +118,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Timeout(60, unit = TimeUnit.SECONDS) fun testComplexConcurrentRequests() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val commands = listOf( "npx tsx myClient.ts $serverUrl greet \"Client1\"", @@ -169,7 +169,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Timeout(30, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() { val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/integration/utils") + val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") val outputs = (1..10).map { i -> val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" diff --git a/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt similarity index 91% rename from src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt index 5cd69a7ba..7fc888323 100644 --- a/src/jvmTest/kotlin/integration/typescript/TypeScriptTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -1,4 +1,4 @@ -package integration.typescript +package io.modelcontextprotocol.kotlin.sdk.integration.typescript import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll @@ -6,6 +6,7 @@ import java.io.BufferedReader import java.io.File import java.io.InputStreamReader import java.net.ServerSocket +import java.net.Socket import java.util.concurrent.TimeUnit abstract class TypeScriptTestBase { @@ -47,12 +48,8 @@ abstract class TypeScriptTestBase { @JvmStatic protected fun executeCommand(command: String, workingDir: File): String { // Prefer running TypeScript via ts-node to avoid npx network delays on CI - val adjusted = if (command.contains("npx tsx ")) { - command.replaceFirst("npx tsx ", "node --loader ts-node/esm ") - } else command - val process = ProcessBuilder() - .command("bash", "-c", adjusted) + .command("bash", "-c", command) .directory(workingDir) .redirectErrorStream(true) .start() @@ -68,7 +65,7 @@ abstract class TypeScriptTestBase { val exitCode = process.waitFor() if (exitCode != 0) { - throw RuntimeException("Command execution failed with exit code $exitCode: $adjusted\nOutput:\n$output") + throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") } return output.toString() @@ -116,7 +113,7 @@ abstract class TypeScriptTestBase { val deadline = System.currentTimeMillis() + timeoutSeconds * 1000 while (System.currentTimeMillis() < deadline) { try { - java.net.Socket(host, port).use { return true } + Socket(host, port).use { return true } } catch (_: Exception) { Thread.sleep(100) } diff --git a/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt similarity index 96% rename from src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt index 1006d413c..9029d233a 100644 --- a/src/jvmTest/kotlin/integration/utils/KotlinServerForTypeScriptClient.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/KotlinServerForTypeScriptClient.kt @@ -1,4 +1,4 @@ -package integration.utils +package io.modelcontextprotocol.kotlin.sdk.integration.utils import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.* @@ -196,16 +196,7 @@ class KotlinServerForTypeScriptClient { ) { request -> val name = (request.arguments["name"] as? JsonPrimitive)?.content ?: "World" - repeat(3) { index -> - val notifJson = buildJsonObject { - put("level", JsonPrimitive("info")) - put("data", buildJsonObject { - put("message", JsonPrimitive("Greeting notification #${index + 1} for $name")) - }) - } - val notif = McpJson.decodeFromJsonElement(notifJson) - server.sendLoggingMessage(notif) - } + // Notifications disabled for test stability CallToolResult( content = listOf(TextContent("Multiple greetings sent to $name!")), diff --git a/src/jvmTest/kotlin/integration/utils/TestUtils.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt similarity index 98% rename from src/jvmTest/kotlin/integration/utils/TestUtils.kt rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt index 08458b766..0c8b4b2f6 100644 --- a/src/jvmTest/kotlin/integration/utils/TestUtils.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/TestUtils.kt @@ -1,4 +1,4 @@ -package integration.utils +package io.modelcontextprotocol.kotlin.sdk.integration.utils import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent diff --git a/src/jvmTest/kotlin/integration/utils/myClient.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts similarity index 93% rename from src/jvmTest/kotlin/integration/utils/myClient.ts rename to kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts index c6ebf3220..47d005b71 100644 --- a/src/jvmTest/kotlin/integration/utils/myClient.ts +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts @@ -1,7 +1,7 @@ // @ts-ignore -import {Client} from "../../../resources/typescript-sdk/src/client"; +import {Client} from "../../../../../../../resources/typescript-sdk/src/client"; // @ts-ignore -import {StreamableHTTPClientTransport} from "../../../resources/typescript-sdk/src/client/streamableHttp.js"; +import {StreamableHTTPClientTransport} from "../../../../../../../resources/typescript-sdk/src/client/streamableHttp.js"; const args = process.argv.slice(2); const serverUrl = args[0] || 'http://localhost:3001/mcp'; From b8b0406ce0ed65213f1614aded1b815b9d346347 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 17:45:32 +0300 Subject: [PATCH 14/24] fixup! Introduce Kotlin integration tests --- .../sdk/integration/typescript/TypeScriptEdgeCasesTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index c64515c3b..a476d5966 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -166,7 +166,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { } @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(120, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() { val projectRoot = File(System.getProperty("user.dir")) val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") From c75b799f6a576a12838c890fdebf541a29bc1a6f Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 18:02:30 +0300 Subject: [PATCH 15/24] fixup! Introduce Kotlin integration tests --- .../TypeScriptClientKotlinServerTest.kt | 26 +++------- .../typescript/TypeScriptEdgeCasesTest.kt | 50 +++---------------- .../typescript/TypeScriptTestBase.kt | 22 ++++++++ 3 files changed, 37 insertions(+), 61 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index 3651401bf..a6950ac42 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -41,13 +41,10 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Test @Timeout(20, unit = TimeUnit.SECONDS) fun testToolCall() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - // call the "greet" tool val testName = "TestUser" val command = "npx tsx myClient.ts $serverUrl greet $testName" - val output = executeCommand(command, clientDir) + val output = executeCommand(command, tsClientDir) assertTrue( output.contains("Hello, $testName!"), @@ -65,12 +62,9 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Test @Timeout(20, unit = TimeUnit.SECONDS) fun testToolCallWithSessionManagement() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val testName = "SessionTest" val command = "npx tsx myClient.ts $serverUrl greet $testName" - val output = executeCommand(command, clientDir) + val output = executeCommand(command, tsClientDir) assertTrue(output.contains("Connected to server"), "Client should connect to server") assertTrue( @@ -82,7 +76,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { val multiGreetName = "NotificationTest" val multiGreetCommand = "npx tsx myClient.ts $serverUrl multi-greet $multiGreetName" - val multiGreetOutput = executeCommand(multiGreetCommand, clientDir) + val multiGreetOutput = executeCommand(multiGreetCommand, tsClientDir) assertTrue(multiGreetOutput.contains("Connected to server"), "Client should connect to server") assertTrue( @@ -95,12 +89,9 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleClientSequence() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val testName1 = "FirstClient" val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" - val output1 = executeCommand(command1, clientDir) + val output1 = executeCommand(command1, tsClientDir) assertTrue(output1.contains("Connected to server"), "First client should connect to server") assertTrue(output1.contains("Hello, $testName1!"), "Tool response should contain the greeting for first client") @@ -108,7 +99,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { val testName2 = "SecondClient" val command2 = "npx tsx myClient.ts $serverUrl multi-greet $testName2" - val output2 = executeCommand(command2, clientDir) + val output2 = executeCommand(command2, tsClientDir) assertTrue(output2.contains("Connected to server"), "Second client should connect to server") assertTrue( @@ -118,7 +109,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { assertTrue(output2.contains("Disconnected from server"), "Second client should disconnect cleanly") val command3 = "npx tsx myClient.ts $serverUrl" - val output3 = executeCommand(command3, clientDir) + val output3 = executeCommand(command3, tsClientDir) assertTrue(output3.contains("Connected to server"), "Third client should connect to server") assertTrue(output3.contains("Available utils:"), "Third client should list available utils") @@ -130,9 +121,6 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleClientParallel() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val clientCount = 3 val clients = listOf( "FirstClient" to "greet", @@ -154,7 +142,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { "npx tsx myClient.ts $serverUrl $toolName $clientName" } - val output = executeCommand(command, clientDir) + val output = executeCommand(command, tsClientDir) synchronized(outputs) { outputs.add(i to output) } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index a476d5966..704c09a76 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -42,11 +42,8 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Test @Timeout(20, unit = TimeUnit.SECONDS) fun testErrorHandling() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" - val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, clientDir) + val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, tsClientDir) assertTrue( nonExistentToolOutput.contains("Tool \"non-existent-tool\" not found"), @@ -54,7 +51,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { ) val invalidUrlCommand = "npx tsx myClient.ts http://localhost:${port + 1000}/mcp greet TestUser" - val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, clientDir) + val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, tsClientDir) assertTrue( invalidUrlOutput.contains("Error:") || invalidUrlOutput.contains("ECONNREFUSED"), @@ -65,16 +62,13 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Test @Timeout(20, unit = TimeUnit.SECONDS) fun testSpecialCharacters() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val specialChars = "!@#$+-[].,?" - val tempFile = File(clientDir, "special_chars.txt") + val tempFile = File(tsClientDir, "special_chars.txt") tempFile.writeText(specialChars) val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat special_chars.txt)\"" - val specialCharsOutput = executeCommand(specialCharsCommand, clientDir) + val specialCharsOutput = executeCommand(specialCharsCommand, tsClientDir) tempFile.delete() @@ -91,16 +85,13 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testLargePayload() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val largeName = "A".repeat(10 * 1024) - val tempFile = File(clientDir, "large_name.txt") + val tempFile = File(tsClientDir, "large_name.txt") tempFile.writeText(largeName) val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat large_name.txt)\"" - val largePayloadOutput = executeCommand(largePayloadCommand, clientDir) + val largePayloadOutput = executeCommand(largePayloadCommand, tsClientDir) tempFile.delete() @@ -117,9 +108,6 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Test @Timeout(60, unit = TimeUnit.SECONDS) fun testComplexConcurrentRequests() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val commands = listOf( "npx tsx myClient.ts $serverUrl greet \"Client1\"", "npx tsx myClient.ts $serverUrl multi-greet \"Client2\"", @@ -131,7 +119,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { val threads = commands.mapIndexed { index, command -> Thread { println("Starting client $index") - val output = executeCommand(command, clientDir) + val output = executeCommand(command, tsClientDir) println("Client $index completed") assertTrue( @@ -168,12 +156,9 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { @Test @Timeout(120, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() { - val projectRoot = File(System.getProperty("user.dir")) - val clientDir = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") - val outputs = (1..10).map { i -> val command = "npx tsx myClient.ts $serverUrl greet \"RapidClient$i\"" - val output = executeCommand(command, clientDir) + val output = executeCommand(command, tsClientDir) assertTrue( output.contains("Connected to server"), @@ -193,23 +178,4 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { assertEquals(10, outputs.size, "All 10 rapid requests should complete successfully") } - - private fun executeCommandAllowingFailure(command: String, workingDir: File): String { - val process = ProcessBuilder() - .command("bash", "-c", command) - .directory(workingDir) - .redirectErrorStream(true) - .start() - - val output = StringBuilder() - process.inputStream.bufferedReader().useLines { lines -> - for (line in lines) { - println(line) - output.append(line).append("\n") - } - } - - process.waitFor(20, TimeUnit.SECONDS) - return output.toString() - } } \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt index 7fc888323..03639c8dc 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -11,6 +11,9 @@ import java.util.concurrent.TimeUnit abstract class TypeScriptTestBase { + protected val projectRoot: File get() = File(System.getProperty("user.dir")) + protected val tsClientDir: File get() = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") + companion object { @JvmStatic protected val sdkDir = File("src/jvmTest/resources/typescript-sdk") @@ -120,4 +123,23 @@ abstract class TypeScriptTestBase { } return false } + + protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String { + val process = ProcessBuilder() + .command("bash", "-c", command) + .directory(workingDir) + .redirectErrorStream(true) + .start() + + val output = StringBuilder() + process.inputStream.bufferedReader().useLines { lines -> + for (line in lines) { + println(line) + output.append(line).append("\n") + } + } + + process.waitFor(timeoutSeconds, TimeUnit.SECONDS) + return output.toString() + } } \ No newline at end of file From 2d1887eb2ae619fe93919f020539fc28591041c1 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 18:12:21 +0300 Subject: [PATCH 16/24] fixup! Introduce Kotlin integration tests --- .../typescript/TypeScriptEdgeCasesTest.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index 704c09a76..c2ee4d389 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -64,14 +64,14 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { fun testSpecialCharacters() { val specialChars = "!@#$+-[].,?" - val tempFile = File(tsClientDir, "special_chars.txt") + val tempFile = File.createTempFile("special_chars", ".txt") tempFile.writeText(specialChars) + tempFile.deleteOnExit() - val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat special_chars.txt)\"" + val specialCharsContent = tempFile.readText() + val specialCharsCommand = "npx tsx myClient.ts $serverUrl greet \"$specialCharsContent\"" val specialCharsOutput = executeCommand(specialCharsCommand, tsClientDir) - tempFile.delete() - assertTrue( specialCharsOutput.contains("Hello, $specialChars!"), "Tool should handle special characters in arguments" @@ -81,16 +81,17 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { "Client should disconnect cleanly after handling special characters" ) } - @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testLargePayload() { val largeName = "A".repeat(10 * 1024) - val tempFile = File(tsClientDir, "large_name.txt") + val tempFile = File.createTempFile("large_name", ".txt") tempFile.writeText(largeName) + tempFile.deleteOnExit() - val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$(cat large_name.txt)\"" + val largeNameContent = tempFile.readText() + val largePayloadCommand = "npx tsx myClient.ts $serverUrl greet \"$largeNameContent\"" val largePayloadOutput = executeCommand(largePayloadCommand, tsClientDir) tempFile.delete() From b6472d4d5a97a04fe61c9116fad8da85fbafa613 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 18:50:23 +0300 Subject: [PATCH 17/24] fixup! Introduce Kotlin integration tests Signed-off-by: Sergey Karpov --- .../integration/typescript/TypeScriptClientKotlinServerTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index a6950ac42..cdbb2b817 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout -import java.io.File import java.util.concurrent.TimeUnit import kotlin.test.assertTrue @@ -87,7 +86,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { } @Test - @Timeout(30, unit = TimeUnit.SECONDS) + @Timeout(120, unit = TimeUnit.SECONDS) fun testMultipleClientSequence() { val testName1 = "FirstClient" val command1 = "npx tsx myClient.ts $serverUrl greet $testName1" From 9c45ca75880218a1325ca855acaece71a4232f02 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Mon, 4 Aug 2025 19:11:22 +0300 Subject: [PATCH 18/24] fixup! Introduce Kotlin integration tests Signed-off-by: Sergey Karpov --- .gitignore | 1 - .../typescript/TypeScriptTestBase.kt | 19 +++++++-------- .../kotlin/sdk/integration/utils/myClient.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 3d8dd1d96..4db438146 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -/src/jvmTest/resources/typescript-sdk/ ### IntelliJ IDEA ### .idea/modules.xml diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt index 03639c8dc..a5f6cf662 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -1,6 +1,5 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript -import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import java.io.BufferedReader import java.io.File @@ -16,7 +15,11 @@ abstract class TypeScriptTestBase { companion object { @JvmStatic - protected val sdkDir = File("src/jvmTest/resources/typescript-sdk") + private val tempRootDir: File = + java.nio.file.Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } + + @JvmStatic + protected val sdkDir: File = File(tempRootDir, "typescript-sdk") /** * clone TypeScript SDK and install dependencies @@ -27,7 +30,7 @@ abstract class TypeScriptTestBase { println("Cloning TypeScript SDK repository") if (!sdkDir.exists()) { val cloneCommand = - "git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk.git src/jvmTest/resources/typescript-sdk" + "git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk.git ${sdkDir.absolutePath}" val process = ProcessBuilder() .command("bash", "-c", cloneCommand) .redirectErrorStream(true) @@ -42,17 +45,11 @@ abstract class TypeScriptTestBase { executeCommand("npm install", sdkDir) } - @JvmStatic - @AfterAll - fun removeTypeScriptSDK() { - sdkDir.deleteRecursively() - } - @JvmStatic protected fun executeCommand(command: String, workingDir: File): String { // Prefer running TypeScript via ts-node to avoid npx network delays on CI val process = ProcessBuilder() - .command("bash", "-c", command) + .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") .directory(workingDir) .redirectErrorStream(true) .start() @@ -126,7 +123,7 @@ abstract class TypeScriptTestBase { protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String { val process = ProcessBuilder() - .command("bash", "-c", command) + .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") .directory(workingDir) .redirectErrorStream(true) .start() diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts index 47d005b71..3b5ea75ca 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/myClient.ts @@ -1,15 +1,27 @@ // @ts-ignore -import {Client} from "../../../../../../../resources/typescript-sdk/src/client"; -// @ts-ignore -import {StreamableHTTPClientTransport} from "../../../../../../../resources/typescript-sdk/src/client/streamableHttp.js"; - const args = process.argv.slice(2); const serverUrl = args[0] || 'http://localhost:3001/mcp'; const toolName = args[1]; const toolArgs = args.slice(2); const PROTOCOL_VERSION = "2024-11-05"; +// @ts-ignore async function main() { + // @ts-ignore + const sdkDir = process.env.TYPESCRIPT_SDK_DIR; + let Client: any; + let StreamableHTTPClientTransport: any; + if (sdkDir) { + // @ts-ignore + ({Client} = await import(`${sdkDir}/src/client`)); + // @ts-ignore + ({StreamableHTTPClientTransport} = await import(`${sdkDir}/src/client/streamableHttp.js`)); + } else { + // @ts-ignore + ({Client} = await import("../../../../../../../resources/typescript-sdk/src/client")); + // @ts-ignore + ({StreamableHTTPClientTransport} = await import("../../../../../../../resources/typescript-sdk/src/client/streamableHttp.js")); + } if (!toolName) { console.log('Usage: npx tsx myClient.ts [server-url] [tool-args...]'); console.log('Using default server URL:', serverUrl); @@ -44,6 +56,7 @@ async function main() { const tool = tools.find((t: { name: string; }) => t.name === toolName); if (!tool) { console.error(`Tool "${toolName}" not found`); + // @ts-ignore process.exit(1); } @@ -80,6 +93,7 @@ async function main() { } catch (error) { console.error('Error:', error); + // @ts-ignore process.exit(1); } finally { await client.close(); @@ -89,5 +103,6 @@ async function main() { main().catch(error => { console.error('Unhandled error:', error); + // @ts-ignore process.exit(1); }); \ No newline at end of file From d977a5c2484452f676309b52c260ad68107b2036 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 4 Aug 2025 18:41:33 +0200 Subject: [PATCH 19/24] Refactor `SseIntegrationTest` to use dynamic port assignment and simplify client/server initialization. (#212) --- .../sdk/integration/SseIntegrationTest.kt | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt index 19d84589a..720525181 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/SseIntegrationTest.kt @@ -1,7 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.integration import io.ktor.client.HttpClient -import io.ktor.client.plugins.sse.SSE import io.ktor.server.application.install import io.ktor.server.cio.CIOApplicationEngine import io.ktor.server.engine.EmbeddedServer @@ -20,36 +19,32 @@ import kotlinx.coroutines.withContext import kotlin.test.Test import kotlin.test.fail import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.client.plugins.sse.SSE as ClientSSE import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +private const val URL = "127.0.0.1" class SseIntegrationTest { @Test fun `client should be able to connect to sse server`() = runTest { val serverEngine = initServer() - var client: Client? = null try { withContext(Dispatchers.Default) { - assertDoesNotThrow { client = initClient() } + val port = serverEngine.engine.resolvedConnectors().first().port + val client = initClient(port) + client.close() } } catch (e: Exception) { fail("Failed to connect client: $e") } finally { - client?.close() // Make sure to stop the server serverEngine.stopSuspend(1000, 2000) } } - private inline fun assertDoesNotThrow(block: () -> T): T { - return try { - block() - } catch (e: Throwable) { - fail("Expected no exception, but got: $e") - } - } - - private suspend fun initClient(): Client { - return HttpClient(ClientCIO) { install(SSE) }.mcpSse("http://$URL:$PORT") + private suspend fun initClient(port: Int): Client { + return HttpClient(ClientCIO) { install(ClientSSE) }.mcpSse("http://$URL:$port") } private suspend fun initServer(): EmbeddedServer { @@ -58,16 +53,11 @@ class SseIntegrationTest { ServerOptions(capabilities = ServerCapabilities()), ) - return embeddedServer(ServerCIO, host = URL, port = PORT) { - install(io.ktor.server.sse.SSE) + return embeddedServer(ServerCIO, host = URL, port = 0) { + install(ServerSSE) routing { mcp { server } } }.startSuspend(wait = false) } - - companion object { - private const val PORT = 3001 - private const val URL = "localhost" - } } \ No newline at end of file From 26b20b680b3c5c4310db47478ece8d114b0bba22 Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Tue, 5 Aug 2025 10:58:40 +0300 Subject: [PATCH 20/24] fixup! Introduce Kotlin integration tests Signed-off-by: Sergey Karpov --- .../sdk/integration/kotlin/KotlinTestBase.kt | 2 + ...tlinClientTypeScriptServerEdgeCasesTest.kt | 25 +---- .../KotlinClientTypeScriptServerTest.kt | 25 +---- .../TypeScriptClientKotlinServerTest.kt | 2 +- .../typescript/TypeScriptEdgeCasesTest.kt | 2 +- .../typescript/TypeScriptTestBase.kt | 99 +++++++++++-------- .../kotlin/sdk/integration/utils/Retry.kt | 89 +++++++++++++++++ 7 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt index dd6a56546..06621a92a 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcp @@ -21,6 +22,7 @@ import kotlin.time.Duration.Companion.seconds import io.ktor.server.cio.CIO as ServerCIO import io.ktor.server.sse.SSE as ServerSSE +@Retry(times = 3) abstract class KotlinTestBase { protected val host = "localhost" diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt index 2d33052aa..5b72b7dad 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerEdgeCasesTest.kt @@ -30,27 +30,12 @@ class KotlinClientTypeScriptServerEdgeCasesTest : TypeScriptTestBase() { fun setUp() { port = findFreePort() serverUrl = "http://$host:$port/mcp" - killProcessOnPort(port) - println("Starting TypeScript server on port $port") - val processBuilder = ProcessBuilder() - .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") - .directory(sdkDir) - .redirectErrorStream(true) - - tsServerProcess = processBuilder.start() - if (!waitForPort(host, port, 10)) { - throw IllegalStateException("TypeScript server did not become ready on $host:$port within timeout") - } + tsServerProcess = startTypeScriptServer(port) println("TypeScript server started on port $port") - - // print TypeScript server process output - val outputReader = createProcessOutputReader(tsServerProcess, "TS-SERVER") - outputReader.start() } @AfterEach fun tearDown() { - // close the client if (::client.isInitialized) { try { runBlocking { @@ -63,16 +48,10 @@ class KotlinClientTypeScriptServerEdgeCasesTest : TypeScriptTestBase() { } } - // terminate TypeScript server if (::tsServerProcess.isInitialized) { try { println("Stopping TypeScript server") - tsServerProcess.destroy() - if (waitForProcessTermination(tsServerProcess, 3)) { - println("TypeScript server stopped gracefully") - } else { - println("TypeScript server did not stop gracefully, forced termination") - } + stopProcess(tsServerProcess) } catch (e: Exception) { println("Warning: Error during TypeScript server stop: ${e.message}") } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt index 3bae5ac22..b021a86e5 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/KotlinClientTypeScriptServerTest.kt @@ -35,27 +35,12 @@ class KotlinClientTypeScriptServerTest : TypeScriptTestBase() { fun setUp() { port = findFreePort() serverUrl = "http://$host:$port/mcp" - killProcessOnPort(port) - println("Starting TypeScript server on port $port") - val processBuilder = ProcessBuilder() - .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") - .directory(sdkDir) - .redirectErrorStream(true) - - tsServerProcess = processBuilder.start() - if (!waitForPort(host, port, 10)) { - throw IllegalStateException("TypeScript server did not become ready on $host:$port within timeout") - } + tsServerProcess = startTypeScriptServer(port) println("TypeScript server started on port $port") - - // print TypeScript server process output - val outputReader = createProcessOutputReader(tsServerProcess, "TS-SERVER") - outputReader.start() } @AfterEach fun tearDown() { - // close the client if (::client.isInitialized) { try { runBlocking { @@ -68,16 +53,10 @@ class KotlinClientTypeScriptServerTest : TypeScriptTestBase() { } } - // terminate TypeScript server if (::tsServerProcess.isInitialized) { try { println("Stopping TypeScript server") - tsServerProcess.destroy() - if (waitForProcessTermination(tsServerProcess, 3)) { - println("TypeScript server stopped gracefully") - } else { - println("TypeScript server did not stop gracefully, forced termination") - } + stopProcess(tsServerProcess) } catch (e: Exception) { println("Warning: Error during TypeScript server stop: ${e.message}") } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index cdbb2b817..1b81dad22 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -21,7 +21,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { killProcessOnPort(port) httpServer = KotlinServerForTypeScriptClient() httpServer?.start(port) - if (!waitForPort("localhost", port, 10)) { + if (!waitForPort(port = port)) { throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") } println("Kotlin server started on port $port") diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index c2ee4d389..7c5d37d6c 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -23,7 +23,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { killProcessOnPort(port) httpServer = KotlinServerForTypeScriptClient() httpServer?.start(port) - if (!waitForPort("localhost", port, 10)) { + if (!waitForPort(port = port)) { throw IllegalStateException("Kotlin test server did not become ready on localhost:$port within timeout") } println("Kotlin server started on port $port") diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt index a5f6cf662..37a162ce3 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt @@ -1,29 +1,32 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript +import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry import org.junit.jupiter.api.BeforeAll import java.io.BufferedReader import java.io.File import java.io.InputStreamReader import java.net.ServerSocket import java.net.Socket +import java.nio.file.Files import java.util.concurrent.TimeUnit +@Retry(times = 3) abstract class TypeScriptTestBase { protected val projectRoot: File get() = File(System.getProperty("user.dir")) - protected val tsClientDir: File get() = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils") + protected val tsClientDir: File + get() = File( + projectRoot, + "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils" + ) companion object { @JvmStatic - private val tempRootDir: File = - java.nio.file.Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } + private val tempRootDir: File = Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } @JvmStatic protected val sdkDir: File = File(tempRootDir, "typescript-sdk") - /** - * clone TypeScript SDK and install dependencies - */ @JvmStatic @BeforeAll fun setupTypeScriptSdk() { @@ -46,8 +49,27 @@ abstract class TypeScriptTestBase { } @JvmStatic - protected fun executeCommand(command: String, workingDir: File): String { - // Prefer running TypeScript via ts-node to avoid npx network delays on CI + protected fun executeCommand(command: String, workingDir: File): String = + runCommand(command, workingDir, allowFailure = false, timeoutSeconds = null) + + @JvmStatic + protected fun killProcessOnPort(port: Int) { + executeCommand("lsof -ti:$port | xargs kill -9 2>/dev/null || true", File(".")) + } + + @JvmStatic + protected fun findFreePort(): Int { + ServerSocket(0).use { socket -> + return socket.localPort + } + } + + private fun runCommand( + command: String, + workingDir: File, + allowFailure: Boolean, + timeoutSeconds: Long? + ): String { val process = ProcessBuilder() .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") .directory(workingDir) @@ -63,25 +85,17 @@ abstract class TypeScriptTestBase { } } - val exitCode = process.waitFor() - if (exitCode != 0) { - throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") + if (timeoutSeconds == null) { + val exitCode = process.waitFor() + if (!allowFailure && exitCode != 0) { + throw RuntimeException("Command execution failed with exit code $exitCode: $command\nOutput:\n$output") + } + } else { + process.waitFor(timeoutSeconds, TimeUnit.SECONDS) } return output.toString() } - - @JvmStatic - protected fun killProcessOnPort(port: Int) { - executeCommand("lsof -ti:$port | xargs kill -9 2>/dev/null || true", File(".")) - } - - @JvmStatic - protected fun findFreePort(): Int { - ServerSocket(0).use { socket -> - return socket.localPort - } - } } protected fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { @@ -93,7 +107,7 @@ abstract class TypeScriptTestBase { return true } - protected fun createProcessOutputReader(process: Process, prefix: String): Thread { + protected fun createProcessOutputReader(process: Process, prefix: String = "TS-SERVER"): Thread { val outputReader = Thread { try { process.inputStream.bufferedReader().useLines { lines -> @@ -109,7 +123,7 @@ abstract class TypeScriptTestBase { return outputReader } - protected fun waitForPort(host: String, port: Int, timeoutSeconds: Long = 10): Boolean { + protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean { val deadline = System.currentTimeMillis() + timeoutSeconds * 1000 while (System.currentTimeMillis() < deadline) { try { @@ -121,22 +135,29 @@ abstract class TypeScriptTestBase { return false } - protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String { - val process = ProcessBuilder() - .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") - .directory(workingDir) - .redirectErrorStream(true) - .start() + protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = + runCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) - val output = StringBuilder() - process.inputStream.bufferedReader().useLines { lines -> - for (line in lines) { - println(line) - output.append(line).append("\n") - } + protected fun startTypeScriptServer(port: Int): Process { + killProcessOnPort(port) + val processBuilder = ProcessBuilder() + .command("bash", "-c", "MCP_PORT=$port npx tsx src/examples/server/simpleStreamableHttp.ts") + .directory(sdkDir) + .redirectErrorStream(true) + val process = processBuilder.start() + if (!waitForPort(port = port)) { + throw IllegalStateException("TypeScript server did not become ready on localhost:$port within timeout") } + createProcessOutputReader(process).start() + return process + } - process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - return output.toString() + protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "TypeScript server") { + process.destroy() + if (waitForProcessTermination(process, waitSeconds)) { + println("$name stopped gracefully") + } else { + println("$name did not stop gracefully, forced termination") + } } } \ No newline at end of file diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt new file mode 100644 index 000000000..c39f321a9 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/Retry.kt @@ -0,0 +1,89 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.InvocationInterceptor +import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation +import org.junit.jupiter.api.extension.ReflectiveInvocationContext +import org.opentest4j.TestAbortedException +import java.lang.reflect.AnnotatedElement +import java.lang.reflect.Method +import java.util.* + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(RetryExtension::class) +annotation class Retry( + val times: Int = 3, + val delayMs: Long = 1000, +) + +class RetryExtension : InvocationInterceptor { + override fun interceptTestMethod( + invocation: Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext + ) { + executeWithRetry(invocation, extensionContext) + } + + private fun resolveRetryAnnotation(extensionContext: ExtensionContext): Retry? { + val classAnn = extensionContext.testClass.flatMap { findRetry(it) } + return classAnn.orElse(null) + } + + private fun findRetry(element: AnnotatedElement): Optional { + return Optional.ofNullable(element.getAnnotation(Retry::class.java)) + } + + private fun executeWithRetry(invocation: Invocation, extensionContext: ExtensionContext) { + val retry = resolveRetryAnnotation(extensionContext) + if (retry == null || retry.times <= 1) { + invocation.proceed() + return + } + + val maxAttempts = retry.times + val delay = retry.delayMs + var lastError: Throwable? = null + + for (attempt in 1..maxAttempts) { + if (attempt > 1 && delay > 0) { + try { + Thread.sleep(delay) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + break + } + } + + try { + if (attempt == 1) { + invocation.proceed() + } else { + val instance = extensionContext.requiredTestInstance + val testMethod = extensionContext.requiredTestMethod + testMethod.isAccessible = true + testMethod.invoke(instance) + } + return + } catch (t: Throwable) { + if (t is TestAbortedException) throw t + lastError = if (t is java.lang.reflect.InvocationTargetException) t.targetException ?: t else t + if (attempt == maxAttempts) { + println("[Retry] Giving up after $attempt attempts for ${describeTest(extensionContext)}: ${lastError.message}") + throw lastError + } + println("[Retry] Failure on attempt $attempt/${maxAttempts} for ${describeTest(extensionContext)}: ${lastError.message}") + } + } + + throw lastError ?: IllegalStateException("Unexpected state in retry logic") + } + + private fun describeTest(ctx: ExtensionContext): String { + val methodName = ctx.testMethod.map(Method::getName).orElse("") + val className = ctx.testClass.map { it.name }.orElse("") + return "$className#$methodName" + } +} \ No newline at end of file From c24c6d9a2bd0f99865cfab721bab3bb5b25f453b Mon Sep 17 00:00:00 2001 From: Artem Bukhonov Date: Mon, 4 Aug 2025 20:13:21 +0200 Subject: [PATCH 21/24] Add ServerSSESession as a receiver for mcp {} dsl (#190) * Add ServerSSESession as a receiver for mcp {} dsl * update api --------- Co-authored-by: devcrocod --- kotlin-sdk-server/api/kotlin-sdk-server.api | 8 ++++---- .../kotlin/sdk/server/KtorServer.kt | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index cd64a75ed..7e2ed4e19 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -3,10 +3,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { } public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { - public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V - public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function0;)V + public static final fun MCP (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function1;)V } public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index 056c78541..260ef9676 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -19,7 +19,7 @@ import io.ktor.utils.io.KtorDsl private val logger = KotlinLogging.logger {} @KtorDsl -public fun Routing.mcp(path: String, block: () -> Server) { +public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) { route(path) { mcp(block) } @@ -29,7 +29,7 @@ public fun Routing.mcp(path: String, block: () -> Server) { * Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE). */ @KtorDsl -public fun Routing.mcp(block: () -> Server) { +public fun Routing.mcp(block: ServerSSESession.() -> Server) { val transports = ConcurrentMap() sse { @@ -43,12 +43,12 @@ public fun Routing.mcp(block: () -> Server) { @Suppress("FunctionName") @Deprecated("Use mcp() instead", ReplaceWith("mcp(block)"), DeprecationLevel.WARNING) -public fun Application.MCP(block: () -> Server) { +public fun Application.MCP(block: ServerSSESession.() -> Server) { mcp(block) } @KtorDsl -public fun Application.mcp(block: () -> Server) { +public fun Application.mcp(block: ServerSSESession.() -> Server) { val transports = ConcurrentMap() install(SSE) @@ -67,7 +67,7 @@ public fun Application.mcp(block: () -> Server) { private suspend fun ServerSSESession.mcpSseEndpoint( postEndpoint: String, transports: ConcurrentMap, - block: () -> Server, + block: ServerSSESession.() -> Server, ) { val transport = mcpSseTransport(postEndpoint, transports) From 47cbec153bbe30ddc825ac5e10fed7363e8df710 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 5 Aug 2025 12:00:21 +0200 Subject: [PATCH 22/24] Resolve conflicting publisher settings (#201) --- buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts | 9 +-------- buildSrc/src/main/kotlin/mcp.publishing.gradle.kts | 13 +++++++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts index 7d288b84e..95c641ec2 100644 --- a/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.jreleaser.gradle.kts @@ -22,6 +22,7 @@ jreleaser { active = Active.ALWAYS mavenCentral.create("ossrh") { active = Active.ALWAYS + sign = true url = "https://central.sonatype.com/api/v1/publisher" applyMavenCentralRules = false maxRetries = 240 @@ -56,14 +57,6 @@ jreleaser { } } - release { - github { - enabled = false - skipRelease = true - skipTag = true - } - } - checksum { individual = false artifacts = false diff --git a/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts index bed170b10..bef396ded 100644 --- a/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.publishing.gradle.kts @@ -26,12 +26,17 @@ publishing { } } + organization { + name = "Anthropic" + url = "https://www.anthropic.com" + } + developers { developer { - id = "Anthropic" - name = "Anthropic Team" - organization = "Anthropic" - organizationUrl = "https://www.anthropic.com" + id = "JetBrains" + name = "JetBrains Team" + organization = "JetBrains" + organizationUrl = "https://www.jetbrains.com" } } From e2a70a9f8d1a8b12c3542ea5baa3eaec3cdc113d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 5 Aug 2025 12:51:51 +0200 Subject: [PATCH 23/24] Fix incorrect deserializer mapping for tool and prompt list notifications (#195) --- .../kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fad5f14a0..570b376a0 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 @@ -178,8 +178,8 @@ internal fun selectServerNotificationDeserializer(element: JsonElement): Deseria Method.Defined.NotificationsMessage.value -> LoggingMessageNotification.serializer() Method.Defined.NotificationsResourcesUpdated.value -> ResourceUpdatedNotification.serializer() Method.Defined.NotificationsResourcesListChanged.value -> ResourceListChangedNotification.serializer() - Method.Defined.ToolsList.value -> ToolListChangedNotification.serializer() - Method.Defined.PromptsList.value -> PromptListChangedNotification.serializer() + Method.Defined.NotificationsToolsListChanged.value -> ToolListChangedNotification.serializer() + Method.Defined.NotificationsPromptsListChanged.value -> PromptListChangedNotification.serializer() else -> null } } From 54d8b17d0f2077f28b1877ca65481f94fb275b3d Mon Sep 17 00:00:00 2001 From: Sergey Karpov Date: Tue, 5 Aug 2025 16:54:56 +0300 Subject: [PATCH 24/24] fixup! Introduce Kotlin integration tests Signed-off-by: Sergey Karpov --- .../sdk/integration/kotlin/PromptIntegrationTest.kt | 12 +++++------- .../sdk/integration/kotlin/ResourceEdgeCasesTest.kt | 6 +++--- .../integration/kotlin/ResourceIntegrationTest.kt | 6 +++--- .../sdk/integration/kotlin/ToolEdgeCasesTest.kt | 4 ++-- .../sdk/integration/kotlin/ToolIntegrationTest.kt | 7 ++++--- .../typescript/TypeScriptClientKotlinServerTest.kt | 5 ++--- .../typescript/TypeScriptEdgeCasesTest.kt | 4 ++-- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt index 889d41570..786169794 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt @@ -1,7 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -279,10 +279,7 @@ class PromptIntegrationTest : KotlinTestBase() { } } - assertTrue( - exception.message?.contains("requiredArg2") == true, - "Exception should mention the missing argument" - ) + assertEquals(exception.message?.contains("requiredArg2"), true, "Exception should mention the missing argument") // test with no args val exception2 = assertThrows { @@ -296,8 +293,9 @@ class PromptIntegrationTest : KotlinTestBase() { } } - assertTrue( - exception2.message?.contains("requiredArg") == true, + assertEquals( + exception2.message?.contains("requiredArg"), + true, "Exception should mention a missing required argument" ) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt index 4a721e8bb..b37487361 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt @@ -1,7 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test @@ -113,11 +113,11 @@ class ResourceEdgeCasesTest : KotlinTestBase() { ) } - server.setRequestHandler(Method.Defined.ResourcesSubscribe) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { _, _ -> EmptyRequestResult() } - server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { _, _ -> EmptyRequestResult() } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt index 53b08b0d1..5c5c0569f 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt @@ -1,7 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin -import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -42,11 +42,11 @@ class ResourceIntegrationTest : KotlinTestBase() { ) } - server.setRequestHandler(Method.Defined.ResourcesSubscribe) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesSubscribe) { _, _ -> EmptyRequestResult() } - server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { request, _ -> + server.setRequestHandler(Method.Defined.ResourcesUnsubscribe) { _, _ -> EmptyRequestResult() } } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt index b8f7b581e..eb8f90f5c 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolEdgeCasesTest.kt @@ -1,10 +1,10 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin +import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest -import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -159,7 +159,7 @@ class ToolEdgeCasesTest : KotlinTestBase() { ) ) { request -> val size = (request.arguments["size"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 1 - val content = largeToolContent.substring(0, largeToolContent.length.coerceAtMost(size * 1000)) + val content = largeToolContent.take(largeToolContent.length.coerceAtMost(size * 1000)) CallToolResult( content = listOf(TextContent(text = content)), diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt index 7ad160bdd..a8ba56cea 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ToolIntegrationTest.kt @@ -1,10 +1,10 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin +import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertCallToolResult import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertJsonProperty import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.assertTextContent import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest -import io.modelcontextprotocol.kotlin.sdk.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.* import org.junit.jupiter.api.Test @@ -352,8 +352,9 @@ class ToolIntegrationTest : KotlinTestBase() { } } - assertTrue( - exception.message?.contains("Exception message") == true, + assertEquals( + exception.message?.contains("Exception message"), + true, "Exception message should contain 'Exception message'" ) } diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt index 1b81dad22..638e288c4 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt @@ -38,9 +38,8 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { } @Test - @Timeout(20, unit = TimeUnit.SECONDS) + @Timeout(30, unit = TimeUnit.SECONDS) fun testToolCall() { - // call the "greet" tool val testName = "TestUser" val command = "npx tsx myClient.ts $serverUrl greet $testName" val output = executeCommand(command, tsClientDir) @@ -59,7 +58,7 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() { } @Test - @Timeout(20, unit = TimeUnit.SECONDS) + @Timeout(30, unit = TimeUnit.SECONDS) fun testToolCallWithSessionManagement() { val testName = "SessionTest" val command = "npx tsx myClient.ts $serverUrl greet $testName" diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt index 7c5d37d6c..a418021bc 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptEdgeCasesTest.kt @@ -40,7 +40,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { } @Test - @Timeout(20, unit = TimeUnit.SECONDS) + @Timeout(30, unit = TimeUnit.SECONDS) fun testErrorHandling() { val nonExistentToolCommand = "npx tsx myClient.ts $serverUrl non-existent-tool" val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, tsClientDir) @@ -60,7 +60,7 @@ class TypeScriptEdgeCasesTest : TypeScriptTestBase() { } @Test - @Timeout(20, unit = TimeUnit.SECONDS) + @Timeout(30, unit = TimeUnit.SECONDS) fun testSpecialCharacters() { val specialChars = "!@#$+-[].,?"